Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions .github/workflows/nightly-release-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ jobs:
echo "has_tag=false" >> $GITHUB_OUTPUT
echo "tag=" >> $GITHUB_OUTPUT
fi

- name: Check whether a tagged commit still needs publication
id: publication_check
env:
GH_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
run: |
# A release-bookkeeping commit can advance master after the release tag.
# Select only active-series tags that are ancestors of this checkout so
# a failed publication can still be recovered on the following run.
tag=$(git tag --merged HEAD --list "v3.0.0-alpha2.*" "v3.0.0-beta.*" "v3.0.0-rc.*" | sort -V | tail -1)
echo "tag=$tag" >> "$GITHUB_OUTPUT"
if [ -z "$tag" ]; then
echo "needs_recovery=false" >> "$GITHUB_OUTPUT"
exit 0
fi

api_status=0
response=$(gh api --include --silent "repos/$GITHUB_REPOSITORY/releases/tags/$tag" 2>&1) || api_status=$?
if [ "$api_status" -eq 0 ]; then
echo "needs_recovery=false" >> "$GITHUB_OUTPUT"
elif grep -Eq '^HTTP/[0-9.]+ 404 ' <<< "$response"; then
echo "needs_recovery=true" >> "$GITHUB_OUTPUT"
echo "Release $tag is missing; the desktop artifact workflow will be re-dispatched."
else
printf '%s\n' "$response" >&2
echo "::error::Unable to check publication state for $tag (gh api exit $api_status)."
exit "$api_status"
fi
Comment thread
taliesin-ai marked this conversation as resolved.

- name: Check for unreleased changelog content
id: changelog_check
Expand Down Expand Up @@ -102,7 +130,7 @@ jobs:
# Only consider the active release series (alpha2/beta/rc): they
# outrank legacy alpha tags, and explicit patterns avoid stray tags
# such as v3.0.0-alpha.98-tui being selected as the baseline.
LATEST_TAG=$(git tag --list "v3.0.0-alpha2.*" "v3.0.0-beta.*" "v3.0.0-rc.*" | sort -V | tail -1)
LATEST_TAG=$(git tag --merged HEAD --list "v3.0.0-alpha2.*" "v3.0.0-beta.*" "v3.0.0-rc.*" | sort -V | tail -1)
fi

if [ -z "$LATEST_TAG" ]; then
Expand Down Expand Up @@ -138,6 +166,7 @@ jobs:
- name: Early exit - No changes detected
if: |
steps.quick_check.outputs.should_continue == 'false' &&
steps.publication_check.outputs.needs_recovery != 'true' &&
github.event.inputs.force_release != 'true'
run: |
echo "🛑 EARLY EXIT: ${{ steps.quick_check.outputs.reason }}"
Expand All @@ -155,8 +184,9 @@ jobs:

- name: Continue with release process
if: |
steps.quick_check.outputs.should_continue == 'true' ||
github.event.inputs.force_release == 'true'
(steps.quick_check.outputs.should_continue == 'true' ||
github.event.inputs.force_release == 'true') &&
steps.publication_check.outputs.needs_recovery != 'true'
run: |
echo "✅ Proceeding with release process..."
if [ "${{ github.event.inputs.force_release }}" == "true" ]; then
Expand All @@ -166,8 +196,9 @@ jobs:
- name: Run release script
id: release
if: |
steps.quick_check.outputs.should_continue == 'true' ||
github.event.inputs.force_release == 'true'
(steps.quick_check.outputs.should_continue == 'true' ||
github.event.inputs.force_release == 'true') &&
steps.publication_check.outputs.needs_recovery != 'true'
env:
# Keep these DISTINCT: the release script validates WAILS_REPO_TOKEN
# against the API before pushing anything and falls back to
Expand All @@ -182,31 +213,81 @@ jobs:
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
ARGS+=(--dry-run)
fi
# Release v3 must create the GitHub release with its artifacts in the
# same operation. Publishing here first makes the release immutable and
# prevents the artifact workflow from uploading its outputs.
ARGS+=(--defer-github-release)
go run release.go "${ARGS[@]}"

# Tags created with GITHUB_TOKEN do not emit a push event that can trigger
# Release v3. Dispatch it explicitly so each published nightly also gets
# the platform binaries, checksums, and provenance attached to its release.
- name: Build and attach desktop artifacts
# Release v3. Dispatch it explicitly; this workflow is the sole publisher
# so the immutable release is created with its platform binaries,
# checksums, and provenance already attached.
- name: Build and publish release with desktop artifacts
id: desktop_artifacts
if: steps.release.outcome == 'success' && steps.release.outputs.release_dry_run != 'true'
if: |
github.event.inputs.dry_run != 'true' &&
((steps.release.outputs.release_tag != '' && steps.release.outputs.release_dry_run != 'true') ||
steps.publication_check.outputs.needs_recovery == 'true')
env:
GH_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
RELEASE_NOTES: ${{ steps.release.outputs.release_notes }}
RELEASE_TAG: ${{ steps.release.outputs.release_tag }}
RECOVERY_TAG: ${{ steps.publication_check.outputs.tag }}
run: |
tag="${{ steps.release.outputs.release_tag }}"
tag="${RELEASE_TAG:-$RECOVERY_TAG}"
if [ -z "$tag" ]; then
echo "::error::No release tag was produced or found for recovery."
exit 1
fi
caller="$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
run_title="Release v3 $tag ($caller)"
pre_release=false
if [[ "$tag" == *-* ]]; then
pre_release=true
fi

INPUTS=(
-f tag="$tag"
-f pre_release="$pre_release"
-f draft=false
-f orchestrator_run="$caller"
)
if [ -n "$RELEASE_NOTES" ]; then
INPUTS+=(-f release_notes="$RELEASE_NOTES")
fi

gh workflow run release-v3.yml \
--repo "${{ github.repository }}" \
--ref master \
-f tag="$tag" \
-f pre_release="$pre_release" \
-f draft=false
"${INPUTS[@]}"
echo "dispatched=true" >> "$GITHUB_OUTPUT"
echo "Dispatched Release v3 for $tag"
echo "Dispatched Release v3 to publish $tag with desktop artifacts"

run_id=""
for _ in $(seq 1 30); do
run_id=$(gh run list \
--repo "${{ github.repository }}" \
--workflow release-v3.yml \
--event workflow_dispatch \
--limit 20 \
--json databaseId,displayTitle \
--jq ".[] | select(.displayTitle == \"$run_title\") | .databaseId" \
| head -n 1)
if [ -n "$run_id" ]; then
break
fi
sleep 2
done
if [ -z "$run_id" ]; then
echo "::error::Unable to identify the dispatched Release v3 run for $tag."
exit 1
fi

echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
echo "Waiting for Release v3 run $run_id"
gh run watch "$run_id" --repo "${{ github.repository }}" --exit-status
echo "published=true" >> "$GITHUB_OUTPUT"

- name: Summary
if: always()
Expand All @@ -226,9 +307,6 @@ jobs:
if [ -n "${{ steps.release.outputs.release_url }}" ]; then
echo "- **Release URL:** ${{ steps.release.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ steps.desktop_artifacts.outputs.dispatched }}" == "true" ]; then
echo "- **Desktop artifacts:** Release v3 dispatched" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Changelog" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then
Expand All @@ -239,4 +317,9 @@ jobs:
else
echo "- Release script did not run (skipped or failed before execution)." >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ steps.desktop_artifacts.outputs.published }}" == "true" ]; then
echo "- **Publication:** Release v3 completed with desktop artifacts" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.desktop_artifacts.outputs.dispatched }}" == "true" ]; then
echo "- **Publication:** Release v3 run ${{ steps.desktop_artifacts.outputs.run_id }} failed or could not be tracked" >> $GITHUB_STEP_SUMMARY
fi

16 changes: 15 additions & 1 deletion .github/workflows/release-v3.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: Release v3
run-name: Release v3 ${{ inputs.tag || github.ref_name }} (${{ inputs.orchestrator_run || 'direct' }})

on:
push:
Expand All @@ -20,6 +21,14 @@ on:
required: false
default: false
type: boolean
release_notes:
description: 'Optional curated release notes supplied by the release orchestrator'
required: false
type: string
orchestrator_run:
description: 'Optional caller run identifier used to track this workflow'
required: false
type: string

env:
GO_VERSION: '1.25'
Expand Down Expand Up @@ -241,12 +250,17 @@ jobs:
TAG: ${{ needs.preflight.outputs.tag }}
PRE: ${{ needs.preflight.outputs.pre_release }}
DRAFT: ${{ inputs.draft }}
RELEASE_NOTES: ${{ inputs.release_notes }}
run: |
FLAGS=(
--repo "${{ github.repository }}"
--title "Wails $TAG"
--generate-notes
)
if [ -n "$RELEASE_NOTES" ]; then
FLAGS+=(--notes "$RELEASE_NOTES")
else
FLAGS+=(--generate-notes)
fi
[[ "$PRE" == "true" ]] && FLAGS+=(--prerelease)
# A draft release is visible only to maintainers and can be deleted,
# which is what makes rehearsing the whole path possible without
Expand Down
1 change: 1 addition & 0 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file

## Fixed
<!-- Bug fixes -->
- Prevent nightly releases from becoming immutable before desktop binaries, checksums, and provenance are attached (#5876)
- macOS app activation now respects activation policy for regular apps only in [PR](https://github.com/wailsapp/wails/pull/5897) by @julianstorer
- Guard uninitialized GTK windows in Linux builds in [PR](https://github.com/wailsapp/wails/pull/5898) by @julianstorer
- Set explicit opaque background color for Linux WebKit windows before URL load in [PR](https://github.com/wailsapp/wails/pull/5899) by @julianstorer
Expand Down
53 changes: 45 additions & 8 deletions v3/tasks/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ var (
)

type releaseOptions struct {
version string
dryRun bool
branch string
target string
version string
dryRun bool
deferGitHubRelease bool
branch string
target string
}

var errNoUnreleasedContent = errors.New("No unreleased changelog content found.")
Expand Down Expand Up @@ -453,6 +454,7 @@ func parseReleaseArgs(args []string) (releaseOptions, error) {
fs.SetOutput(io.Discard)

dryRun := fs.Bool("dry-run", false, "simulate the release without pushing changes or creating a GitHub release")
deferGitHubRelease := fs.Bool("defer-github-release", false, "push the release commit and tag, but let the artifact workflow create the GitHub release")
branch := fs.String("branch", defaultReleaseBranch, "git branch to push release changes to")
target := fs.String("target", defaultReleaseTarget, "target reference for the GitHub release (usually the same as branch)")
versionFlag := fs.String("version", "", "explicit release version (overrides automatic increment)")
Expand All @@ -473,10 +475,11 @@ func parseReleaseArgs(args []string) (releaseOptions, error) {
}

return releaseOptions{
version: version,
dryRun: *dryRun,
branch: *branch,
target: *target,
version: version,
dryRun: *dryRun,
deferGitHubRelease: *deferGitHubRelease,
branch: *branch,
target: *target,
}, nil
}

Expand Down Expand Up @@ -562,6 +565,7 @@ func runRelease(opts releaseOptions) error {
writeGitHubOutput("release_version", newVersion)
writeGitHubOutput("release_tag", newVersion)
writeGitHubOutput("release_target", opts.target)
writeGitHubMultilineOutput("release_notes", releaseBody)

if opts.dryRun {
writeGitHubOutput("release_dry_run", "true")
Expand Down Expand Up @@ -612,6 +616,17 @@ func runRelease(opts releaseOptions) error {
return err
}

// The nightly workflow delegates publication to release-v3.yml so that
// GitHub receives the binaries, checksums, and provenance before the
// release becomes immutable. Publishing here first would leave the
// artifact workflow unable to attach anything to the release.
if opts.deferGitHubRelease {
writeGitHubOutput("release_deferred", "true")
writeGitHubOutput("release_outcome", "success")
fmt.Println("📦 GitHub release creation deferred to the desktop artifact workflow.")
return nil
}

releaseTitle := fmt.Sprintf(defaultReleaseTitle, newVersion)
releaseInfo, err := createGitHubRelease(token, repoSlug, opts.target, newVersion, releaseTitle, releaseBody)
if err != nil {
Expand Down Expand Up @@ -1094,3 +1109,25 @@ func writeGitHubOutput(key, value string) {
fmt.Printf("Warning: unable to persist %s to GITHUB_OUTPUT: %v\n", key, err)
}
}

func writeGitHubMultilineOutput(key, value string) {
outputPath := strings.TrimSpace(os.Getenv("GITHUB_OUTPUT"))
if outputPath == "" || key == "" || value == "" {
return
}

delimiter := "WAILS_RELEASE_NOTES_EOF"
for strings.Contains(value, delimiter) {
delimiter += "_X"
}

f, err := os.OpenFile(outputPath, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
fmt.Printf("Warning: unable to write %s to GITHUB_OUTPUT: %v\n", key, err)
return
}
defer f.Close()
if _, err := fmt.Fprintf(f, "%s<<%s\n%s\n%s\n", key, delimiter, value, delimiter); err != nil {
fmt.Printf("Warning: unable to persist %s to GITHUB_OUTPUT: %v\n", key, err)
}
}
Loading
Loading