From c6d20a6fe146e4fcf887ab36c85718954eed396c Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 15 Jul 2026 17:27:29 +0200 Subject: [PATCH 1/2] fix(release): pin build.yml dispatch to the release tag ref prerelease.yml triggered build.yml without --ref, so the build ran on the default branch (main), where package.json is still the previous stable version. The RC version bump lives only on the release branch / RC tag, so build.yml's publish-release guard rejected it ("package.json version 1.6.0 does not match 1.7.0-rc.1 from tag v1.7.0-rc.1") and no release was created. Pass --ref so the build checks out the tag's tree (bumped package.json + frozen code). Apply the same pin in promote.yml for the stable build, which previously only worked because the main-merge happened to leave main at the stable version first. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/prerelease.yml | 7 +++++++ .github/workflows/promote.yml | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index a592488c7..f9e8d2c73 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -133,7 +133,14 @@ jobs: # 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. + # + # --ref is REQUIRED: the RC version bump lives ONLY on the release branch / + # RC tag, never on the default branch. Without --ref the build runs on main + # (still the previous stable version), so build.yml's publish-release step + # fails its guard ("package.json version X does not match from tag"). + # Pin the build to the RC tag so checkout gets the bumped package.json + code. gh workflow run build.yml \ + --ref "${RC_TAG}" \ -f release_tag="${RC_TAG}" \ -f arch=both \ --repo "$GITHUB_REPOSITORY" diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index ac4cfc079..3e8214b9c 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -142,7 +142,13 @@ jobs: 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" From 1d6a8a116a7a42e76b28e9ce46e031cc9d9a0a3c Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 16 Jul 2026 17:12:57 +0200 Subject: [PATCH 2/2] fix(release): name the release branch per stable version, reuse it across RCs prerelease.yml created release/v${PRERELEASE} (e.g. release/v1.7.0-rc.1) but promote.yml resolves release/v${STABLE_VERSION} (release/v1.7.0), so promote would die at `git fetch origin release/v1.7.0`. v1.7.0-rc.1 is the first RC cut under the release-branch freeze, so this path had never run. Worse, each re-cut created a *new* branch off main and force-deleted the remote branch first, so cutting rc.2 would both sweep in whatever had landed on main and destroy the cherry-picks on the rc.1 branch. That silently defeats the freeze the contract is supposed to guarantee. Use one branch per stable version (release/vX.Y.Z), created at rc.1 and reused for later RCs: fetch and check it out when it exists, branch from the dispatched ref when it doesn't, and never delete it. Fold the package.json bump into the same step so the version is written after the correct branch is checked out. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/prerelease.yml | 53 +++++++++++++++++++------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index f9e8d2c73..85b0f63af 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -75,34 +75,43 @@ jobs: 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 + - name: Create or reuse the release branch and bump package.json env: TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} PRERELEASE: ${{ steps.version.outputs.prerelease }} + NEXT: ${{ steps.version.outputs.next }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - # The release branch is FROZEN between RC cut and stable promotion. The bump - # commit lives on release/v${PRERELEASE} only; nothing is merged into main - # until promote.yml publishes the stable tag, so any features merged into - # main after this step are NOT in the RC build. - 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" + # ONE release branch per STABLE version (release/vX.Y.Z), created at rc.1 and + # FROZEN until promote publishes the stable tag. Later RCs (rc.2, rc.3, ...) are + # cut from this same branch, so they carry the rc.1 snapshot plus any + # cherry-picked bugfixes and NOT whatever has landed on main since. + # + # The name must stay in sync with promote.yml, which resolves + # release/v${STABLE_VERSION}. Naming this branch release/v${PRERELEASE} + # (with the -rc.N suffix) breaks promote and makes every re-cut a fresh + # branch off main, which silently defeats the freeze. + BRANCH="release/v${NEXT}" + REMOTE="https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + # Re-cut (rc.2+): build on the frozen branch. Never delete it — it carries the + # cherry-picks that make this RC differ from the previous one. + echo "Reusing frozen release branch ${BRANCH}" + git fetch origin "$BRANCH" + git checkout -B "$BRANCH" "origin/${BRANCH}" + else + # First cut (rc.1): branch from the dispatched ref (main). + echo "Creating release branch ${BRANCH} from ${GITHUB_REF_NAME}" + git checkout -b "$BRANCH" + fi + sed -i -E "s|(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1${PRERELEASE}\2|" package.json + echo "package.json version:" + grep '"version"' package.json 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" + git commit -m "chore(release): bump to ${PRERELEASE} [skip ci]" || echo "(version already at ${PRERELEASE})" + git push "$REMOTE" "$BRANCH" - name: Push RC tag on the release branch env: @@ -111,10 +120,10 @@ jobs: # 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 }} - PRERELEASE: ${{ steps.version.outputs.prerelease }} + NEXT: ${{ steps.version.outputs.next }} run: | set -euo pipefail - BRANCH="release/v${PRERELEASE}" + BRANCH="release/v${NEXT}" git fetch origin "$BRANCH" git checkout "$BRANCH" git reset --hard "origin/${BRANCH}"