Promote RC to stable release #2
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: Promote RC to stable release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| rc_tag: | |
| description: "RC tag to promote (e.g. v1.5.0-rc.2)" | |
| required: true | |
| type: string | |
| release_notes_extra: | |
| description: "Optional message to prepend to auto-generated release notes" | |
| required: false | |
| type: string | |
| default: "" | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: promote-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| promote: | |
| name: Promote RC to stable | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: ./.github/actions/setup | |
| - name: Validate RC tag and compute stable version | |
| id: version | |
| env: | |
| RC_TAG: ${{ inputs.rc_tag }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${RC_TAG}" | |
| if [[ ! "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-(rc|beta|alpha)\.[0-9]+$ ]]; then | |
| echo "::error::Tag must look like v1.5.0-rc.1; got '${TAG}'" | |
| exit 1 | |
| fi | |
| STABLE_VERSION="${BASH_REMATCH[1]}" | |
| STABLE_TAG="v${STABLE_VERSION}" | |
| echo "rc_tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "stable_version=$STABLE_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "stable_tag=$STABLE_TAG" >> "$GITHUB_OUTPUT" | |
| echo "Promoting ${TAG} -> ${STABLE_TAG}" | |
| - name: Close versioned milestone | |
| env: | |
| TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| STABLE_VERSION: ${{ steps.version.outputs.stable_version }} | |
| run: node .github/scripts/release-milestone-close.mjs | |
| - name: Bump package.json to stable version on the release branch | |
| env: | |
| TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| STABLE_VERSION: ${{ steps.version.outputs.stable_version }} | |
| run: | | |
| set -euo pipefail | |
| # Promote checks out the FROZEN release branch (created by prerelease.yml) and | |
| # rewrites package.json there. This guarantees the stable tag points at the | |
| # same code that was tested as the RC plus any cherry-picked bugfixes. | |
| BRANCH="release/v${STABLE_VERSION}" | |
| git fetch origin "$BRANCH" | |
| git checkout "$BRANCH" | |
| git reset --hard "origin/${BRANCH}" | |
| sed -i -E "s|(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1${STABLE_VERSION}\2|" package.json | |
| echo "package.json version:" | |
| grep '"version"' package.json | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add package.json | |
| git commit --allow-empty -m "chore(release): bump to ${STABLE_VERSION} [skip ci]" || true | |
| git push "https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$BRANCH" | |
| - name: Push stable tag on the release branch tip | |
| env: | |
| # Use GITHUB_TOKEN for the tag push: a tag is a ref, not a file change, so | |
| # the workflows:write permission isn't needed. | |
| # Note: GITHUB_TOKEN tag pushes do NOT trigger build.yml in this org's setup, | |
| # so we explicitly trigger it via gh workflow run right after. | |
| STABLE_TAG: ${{ steps.version.outputs.stable_tag }} | |
| STABLE_VERSION: ${{ steps.version.outputs.stable_version }} | |
| run: | | |
| set -euo pipefail | |
| BRANCH="release/v${STABLE_VERSION}" | |
| git fetch origin "$BRANCH" | |
| git checkout "$BRANCH" | |
| git reset --hard "origin/${BRANCH}" | |
| # Delete remote tag first (idempotent on rerun) and any local tag. | |
| git push origin ":${STABLE_TAG}" 2>/dev/null || true | |
| git tag -d "$STABLE_TAG" 2>/dev/null || true | |
| git tag "$STABLE_TAG" | |
| git push origin "$STABLE_TAG" | |
| - name: Merge release branch into main | |
| env: | |
| GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| STABLE_VERSION: ${{ steps.version.outputs.stable_version }} | |
| run: | | |
| set -euo pipefail | |
| # After the stable tag is published, sync main with the released snapshot via | |
| # a rebase PR (PAT is a ruleset bypass actor so no approval is needed). | |
| BRANCH="release/v${STABLE_VERSION}" | |
| # If main already contains the release branch (clean fast-forward), there's | |
| # nothing to merge — just make sure the branch is tracked locally. | |
| git fetch origin main "$BRANCH" | |
| if git merge-base --is-ancestor "origin/${BRANCH}" origin/main; then | |
| echo "origin/${BRANCH} is already an ancestor of origin/main — nothing to merge." | |
| exit 0 | |
| fi | |
| # Push the release branch's commits onto main as a fresh branch and PR it. | |
| git checkout -b "${BRANCH}-sync" "origin/${BRANCH}" | |
| git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "${BRANCH}-sync" | |
| gh pr create \ | |
| --base main \ | |
| --head "${BRANCH}-sync" \ | |
| --title "chore(release): release v${STABLE_VERSION} into main" \ | |
| --body "Sync main with the released snapshot (RC + cherry-picked bugfixes + version bump). Rebase-merged via PAT; bypass applies because EtienneLescot is a ruleset bypass actor." \ | |
| --repo "$GITHUB_REPOSITORY" || echo "(PR already exists — skipping)" | |
| PR_NUMBER=$(gh pr list --head "${BRANCH}-sync" --state open --json number -q '.[0].number' --repo "$GITHUB_REPOSITORY") | |
| if [[ -n "$PR_NUMBER" ]]; then | |
| gh pr merge "$PR_NUMBER" --rebase --delete-branch --admin \ | |
| --repo "$GITHUB_REPOSITORY" | |
| fi | |
| # The release branch itself was already used to publish and contains frozen | |
| # history — leave it in place for forensics. A v1.6.0 release branch should | |
| # never be deleted until the next major cuts over. | |
| - name: Trigger build workflow | |
| env: | |
| GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| STABLE_TAG: ${{ steps.version.outputs.stable_tag }} | |
| run: | | |
| set -euo pipefail | |
| # GITHUB_TOKEN tag pushes don't fire build.yml in this setup, dispatch it. | |
| # | |
| # --ref pins the build to the stable tag (the frozen release-branch tip). The | |
| # prior main-merge step usually leaves main at the stable version already, but | |
| # relying on that is fragile; building the tag guarantees checkout has the | |
| # matching package.json + code and enables signing/notarization (tag has no '-'). | |
| gh workflow run build.yml \ | |
| --ref "${STABLE_TAG}" \ | |
| -f release_tag="${STABLE_TAG}" \ | |
| -f arch=both \ | |
| --repo "$GITHUB_REPOSITORY" | |
| - name: Announce stable on Discord | |
| if: success() | |
| env: | |
| DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} | |
| DISCORD_RELEASE_CHANNEL_ID: ${{ vars.DISCORD_RELEASE_CHANNEL_ID }} | |
| GITHUB_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| STABLE_TAG: ${{ steps.version.outputs.stable_tag }} | |
| RC_TAG: ${{ steps.version.outputs.rc_tag }} | |
| EXTRA: ${{ inputs.release_notes_extra }} | |
| KIND: stable | |
| run: node .github/scripts/discord-release-announce.mjs | |
| - name: Workflow summary | |
| run: | | |
| { | |
| echo "## Release promoted" | |
| echo "" | |
| echo "- Stable tag: \`${{ steps.version.outputs.stable_tag }}\`" | |
| echo "- Promoted from: \`${{ steps.version.outputs.rc_tag }}\`" | |
| echo "- Tier 3 (homebrew/winget/nix/aur) will fire on the published release via OPENSCREEN_RELEASE_TOKEN." | |
| } >> "$GITHUB_STEP_SUMMARY" |