Cut a release candidate #19
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: Cut a release candidate | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| bump: | |
| description: "Semver bump type from current package.json version" | |
| required: true | |
| type: choice | |
| options: [patch, minor, major] | |
| default: minor | |
| rc_number: | |
| description: "Pre-release counter (rc.1, rc.2, ...)" | |
| required: true | |
| type: number | |
| default: 1 | |
| target_version: | |
| description: "Override the auto-bumped next version (e.g. 2.0.0). Leave empty to bump from package.json." | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: prerelease-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| prerelease: | |
| name: Cut pre-release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: ./.github/actions/setup | |
| - name: Calculate next version and RC tag | |
| id: version | |
| env: | |
| BUMP: ${{ inputs.bump }} | |
| RC_NUMBER: ${{ inputs.rc_number }} | |
| TARGET_VERSION: ${{ inputs.target_version }} | |
| run: | | |
| set -euo pipefail | |
| CURRENT=$(node -p "require('./package.json').version") | |
| if [[ -n "$TARGET_VERSION" ]]; then | |
| NEXT="$TARGET_VERSION" | |
| else | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" | |
| case "$BUMP" in | |
| patch) PATCH=$((PATCH + 1)) ;; | |
| minor) MINOR=$((MINOR + 1)); PATCH=0 ;; | |
| major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; | |
| esac | |
| NEXT="${MAJOR}.${MINOR}.${PATCH}" | |
| fi | |
| PRERELEASE_VERSION="${NEXT}-rc.${RC_NUMBER}" | |
| RC_TAG="v${PRERELEASE_VERSION}" | |
| echo "current=$CURRENT" >> "$GITHUB_OUTPUT" | |
| echo "next=$NEXT" >> "$GITHUB_OUTPUT" | |
| echo "prerelease=$PRERELEASE_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "rc_tag=$RC_TAG" >> "$GITHUB_OUTPUT" | |
| echo "Will cut ${RC_TAG} from ${CURRENT}" | |
| - name: Migrate issues from "Next Release" to versioned milestone | |
| env: | |
| TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| NEXT: ${{ steps.version.outputs.next }} | |
| run: node .github/scripts/release-milestone-migrate.mjs | |
| - name: Bump package.json to pre-release version | |
| env: | |
| PRERELEASE: ${{ steps.version.outputs.prerelease }} | |
| run: | | |
| set -euo pipefail | |
| sed -i -E "s|(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1${PRERELEASE}\2|" package.json | |
| echo "package.json version:" | |
| grep '"version"' package.json | |
| - name: Commit package.json bump on a release branch | |
| env: | |
| TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| PRERELEASE: ${{ steps.version.outputs.prerelease }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # The main-protection ruleset forbids direct pushes to main from workflow tokens. | |
| # Push to a release branch and merge via PR instead (merge via PAT bypasses as EtienneLescot). | |
| BRANCH="release/v${PRERELEASE}" | |
| # Delete remote branch first so the push below is always fast-forward (idempotent on rerun). | |
| git push "https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git" ":${BRANCH}" 2>/dev/null || true | |
| git checkout -b "$BRANCH" | |
| git add package.json | |
| git commit -m "chore(release): bump to ${PRERELEASE} [skip ci]" | |
| git push "https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$BRANCH" | |
| - name: Open PR and rebase-merge into main | |
| env: | |
| # Org policy blocks GITHUB_TOKEN write, so we use the PAT for both PR create and merge. | |
| # PAT (EtienneLescot) is a ruleset bypass actor -> the merge doesn't need approval. | |
| GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| PRERELEASE: ${{ steps.version.outputs.prerelease }} | |
| run: | | |
| set -euo pipefail | |
| BRANCH="release/v${PRERELEASE}" | |
| gh pr create \ | |
| --base main \ | |
| --head "$BRANCH" \ | |
| --title "chore(release): bump to ${PRERELEASE}" \ | |
| --body "Automated version bump from the prerelease workflow. Rebase-merged via PAT; bypass applies because EtienneLescot is a ruleset bypass actor." \ | |
| --repo "$GITHUB_REPOSITORY" | |
| PR_NUMBER=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number' --repo "$GITHUB_REPOSITORY") | |
| gh pr merge "$PR_NUMBER" --rebase --delete-branch --admin \ | |
| --repo "$GITHUB_REPOSITORY" | |
| - name: Push RC tag | |
| 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. | |
| RC_TAG: ${{ steps.version.outputs.rc_tag }} | |
| run: | | |
| set -euo pipefail | |
| # After gh pr merge --rebase, the bump commit is on origin/main. Refresh the | |
| # local main and tag the resulting HEAD so the tag points at the merged commit. | |
| git fetch origin main | |
| git checkout main | |
| git reset --hard origin/main | |
| # Delete remote tag first (idempotent on rerun) and any local tag. | |
| git push origin ":${RC_TAG}" 2>/dev/null || true | |
| git tag -d "$RC_TAG" 2>/dev/null || true | |
| git tag "$RC_TAG" | |
| git push origin "$RC_TAG" | |
| - name: Trigger build workflow | |
| env: | |
| GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| RC_TAG: ${{ steps.version.outputs.rc_tag }} | |
| run: | | |
| set -euo pipefail | |
| # GITHUB_TOKEN tag pushes don't fire the build.yml trigger in this setup, | |
| # so dispatch it explicitly. The PAT ensures the build's release creation | |
| # propagates to Tier 3 (homebrew/winget/nix/aur) via release: published. | |
| gh workflow run build.yml \ | |
| -f release_tag="${RC_TAG}" \ | |
| -f arch=both \ | |
| --repo "$GITHUB_REPOSITORY" | |
| - name: Announce RC on Discord (#rc-testing) | |
| if: success() | |
| env: | |
| DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} | |
| DISCORD_RC_TESTING_CHANNEL_ID: ${{ vars.DISCORD_RC_TESTING_CHANNEL_ID }} | |
| GITHUB_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| STABLE_TAG: ${{ steps.version.outputs.rc_tag }} | |
| KIND: rc | |
| run: node .github/scripts/discord-release-announce.mjs | |
| - name: Workflow summary | |
| run: | | |
| { | |
| echo "## Pre-release cut" | |
| echo "" | |
| echo "- RC tag: \`${{ steps.version.outputs.rc_tag }}\`" | |
| echo "- Stable target: \`v${{ steps.version.outputs.next }}\`" | |
| echo "- Build workflow triggered by the tag push will publish the GitHub pre-release." | |
| echo "- Announce in #rc-testing on Discord, then run \`Promote RC to stable\` when QA is green." | |
| } >> "$GITHUB_STEP_SUMMARY" |