Create Feature Release #672
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Create Feature Release | |
| permissions: | |
| contents: write | |
| actions: read # guard-ci-passed reads CI workflow runs via the GitHub API | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| branch: | |
| description: 'Branch to create release from' | |
| required: true | |
| type: string | |
| action: | |
| description: 'Action' | |
| required: true | |
| type: choice | |
| options: | |
| - new | |
| - override | |
| default: 'new' | |
| skip_ci_check: | |
| description: 'Skip the CI-passed pre-check (emergency only)' | |
| required: false | |
| type: boolean | |
| default: false | |
| jobs: | |
| # Override deletes-and-recreates the tag at v$(package.json version), which | |
| # reattaches it to a new commit and lets electron-builder rewrite the | |
| # corresponding GitHub Release. If that release was already published it | |
| # would be silently overwritten — including its Latest marker. The guard | |
| # always runs so downstream jobs don't need `always()` (which propagates | |
| # "skipped ancestor" status and silently skips dependents); the actual | |
| # check is gated on action=override at the step level. | |
| guard-release-not-published: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - if: inputs.action == 'override' | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.branch }} | |
| - if: inputs.action == 'override' | |
| name: Fail if release tag is already published | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | |
| RELEASE_TAG="v${PACKAGE_VERSION}" | |
| # Capture stdout and stderr separately so we can distinguish | |
| # "release not found" from transient gh errors (auth, network, rate limit). | |
| set +e | |
| STDOUT=$(gh release view "$RELEASE_TAG" \ | |
| --repo "${{ github.repository }}" \ | |
| --json isDraft \ | |
| --jq '.isDraft' 2> /tmp/gh_stderr) | |
| RC=$? | |
| STDERR=$(cat /tmp/gh_stderr) | |
| set -e | |
| if [ $RC -eq 0 ]; then | |
| if [ "$STDOUT" = "false" ]; then | |
| echo "::error::Release $RELEASE_TAG is already published. Refusing to overwrite — that would replace the existing GitHub Release and could strip its Latest marker." | |
| echo "::error::Bump the version before re-running." | |
| exit 1 | |
| fi | |
| echo "Release $RELEASE_TAG exists as a draft — safe to overwrite." | |
| exit 0 | |
| fi | |
| # Match only the specific "release not found" wording from `gh` | |
| # so unrelated 404-style errors (auth, repo not found, rate limit) | |
| # don't get treated as "safe to create". | |
| if echo "$STDERR" | grep -qi 'release not found'; then | |
| echo "No existing release for $RELEASE_TAG — safe to create." | |
| exit 0 | |
| fi | |
| echo "::error::gh release view failed for $RELEASE_TAG (exit $RC):" | |
| echo "$STDERR" | |
| exit 1 | |
| # Refuse to release a commit whose CI did not pass. The release builds from | |
| # the branch HEAD as it stands now, before calculate-and-update-version pushes | |
| # the version-bump commit — so we check that exact pre-bump commit. head_sha | |
| # for a pull_request CI run is the PR branch head, which equals the branch | |
| # HEAD resolved here. | |
| guard-ci-passed: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - if: ${{ !inputs.skip_ci_check }} | |
| name: Verify CI passed on the release commit | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BRANCH: ${{ inputs.branch }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| SHA=$(gh api "repos/${REPO}/commits/${BRANCH}" --jq '.sha') | |
| echo "Release branch '${BRANCH}' HEAD: $SHA" | |
| RUNS=$(gh api \ | |
| "repos/${REPO}/actions/workflows/ci.yml/runs?head_sha=${SHA}&per_page=100" \ | |
| --jq '[.workflow_runs[] | {status, conclusion, created_at, html_url}] | sort_by(.created_at) | reverse') | |
| if [ "$(echo "$RUNS" | jq 'length')" -eq 0 ]; then | |
| echo "::error::No CI run found for commit $SHA on branch '${BRANCH}'. CI must run and pass before releasing." | |
| exit 1 | |
| fi | |
| LATEST=$(echo "$RUNS" | jq -c '.[0]') | |
| STATUS=$(echo "$LATEST" | jq -r '.status') | |
| CONCLUSION=$(echo "$LATEST" | jq -r '.conclusion') | |
| URL=$(echo "$LATEST" | jq -r '.html_url') | |
| echo "Latest CI run for $SHA: status=$STATUS conclusion=$CONCLUSION ($URL)" | |
| if [ "$STATUS" != "completed" ]; then | |
| echo "::error::Latest CI run for $SHA is not finished (status=$STATUS). Wait for CI to complete before releasing: $URL" | |
| exit 1 | |
| fi | |
| if [ "$CONCLUSION" != "success" ]; then | |
| echo "::error::Latest CI run for $SHA did not succeed (conclusion=$CONCLUSION). Fix CI before releasing: $URL" | |
| exit 1 | |
| fi | |
| echo "CI passed for $SHA — safe to release." | |
| - if: ${{ inputs.skip_ci_check }} | |
| run: echo "::warning::skip_ci_check enabled — bypassing the CI-passed gate." | |
| calculate-version: | |
| needs: [guard-release-not-published, guard-ci-passed] | |
| uses: ./.github/workflows/calculate-and-update-version.yml | |
| with: | |
| branch: ${{ inputs.branch }} | |
| version_strategy: ${{ inputs.action == 'new' && 'increment' || 'override' }} | |
| secrets: inherit | |
| ensure-draft-release: | |
| needs: calculate-version | |
| uses: ./.github/workflows/ensure-draft-release.yml | |
| with: | |
| release_tag: ${{ needs.calculate-version.outputs.new_tag }} | |
| secrets: inherit | |
| run-release: | |
| needs: [calculate-version, ensure-draft-release] | |
| uses: ./.github/workflows/release_mac.yml | |
| with: | |
| tag: ${{ needs.calculate-version.outputs.new_tag }} | |
| secrets: inherit | |
| run-release-win: | |
| needs: [calculate-version, ensure-draft-release] | |
| uses: ./.github/workflows/release_win.yml | |
| with: | |
| tag: ${{ needs.calculate-version.outputs.new_tag }} | |
| secrets: inherit | |
| run-release-linux: | |
| needs: [calculate-version, ensure-draft-release] | |
| uses: ./.github/workflows/release_linux.yml | |
| with: | |
| tag: ${{ needs.calculate-version.outputs.new_tag }} | |
| secrets: inherit | |
| merge-latest-mac-yml: | |
| needs: [calculate-version, run-release] | |
| uses: ./.github/workflows/merge-latest-mac-yml.yml | |
| with: | |
| tag: ${{ needs.calculate-version.outputs.new_tag }} | |
| secrets: inherit | |
| summary: | |
| needs: [calculate-version, run-release, run-release-win, run-release-linux, merge-latest-mac-yml] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Create summary | |
| run: | | |
| echo "## Release Created Successfully :rocket:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Tag:** ${{ needs.calculate-version.outputs.new_tag }}" >> $GITHUB_STEP_SUMMARY |