Skip to content

Create Feature Release #672

Create Feature Release

Create Feature Release #672

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