From 85808d4f1ed4ef2e8f6b61611405742c2681d709 Mon Sep 17 00:00:00 2001 From: CI Date: Fri, 24 Jul 2026 12:13:32 -0400 Subject: [PATCH 1/2] ci: land release-notes regen on main via PR instead of committing to release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stable-release.yml's final step committed the regenerated docs/release-notes.md directly to the release branch, making release diverge from main and permanently tripping the workflow's own fast-forward guard on every cut after the first. Regenerate after publish as before (the notes must include the just-cut release), but land the commit on main via a short-lived branch + PR with best-effort auto-merge (the ardd-badge.yml idiom — main's protection blocks direct pushes). release is now only ever pushed main SHAs, so the guard cannot trip again; its copy of the notes trails by one cut (cosmetic — the docs site builds from main). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DPyUE4w2CWGbtyafYfhhxw --- .github/workflows/stable-release.yml | 50 ++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index 1f4ca7c..d6cd9f7 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -15,8 +15,14 @@ # `gh release create` (GitHub API -> web-flow Verified; no CI # signing keys); # 4. regenerate docs/release-notes.md from the real release history -# (scripts/release-notes.sh) and commit+push it to `release` if it -# changed — keeps the changelog page current without a manual step. +# (scripts/release-notes.sh) and, if it changed, land the commit on +# `main` via a short-lived branch + PR with best-effort auto-merge +# (the badge-sync idiom — main's branch protection blocks direct +# pushes). Never committed to `release`: a release-only commit made +# the branch diverge from main and permanently tripped step 2's +# fast-forward guard on every later cut. `release`'s copy of the +# notes now trails by one cut, which is cosmetic — the docs site +# builds from main. # # All version logic lives in scripts/next-version.sh (fixture-tested); # this YAML is thin glue, per the plan's Complexity Tracking. @@ -108,15 +114,39 @@ jobs: --target "$GITHUB_SHA" \ --title "${{ steps.version.outputs.tag }}" - - name: Regenerate and publish docs/release-notes.md + - name: Regenerate docs/release-notes.md and PR it to main env: GH_TOKEN: ${{ github.token }} run: | + set -e sh scripts/release-notes.sh - git config user.email "ci@example.com" - git config user.name "CI" - # Note: pushes HEAD (the new docs commit), not $GITHUB_SHA — unlike - # the fast-forward step above, this step creates a new commit on - # top of $GITHUB_SHA, so the ref pushed to refs/heads/release must - # be the commit that actually carries the regenerated file. - git diff --quiet -- docs/release-notes.md || (git add docs/release-notes.md && git commit -m "docs: regenerate release-notes.md" && git push origin "HEAD:refs/heads/release") + if git diff --quiet -- docs/release-notes.md; then + echo "No change to docs/release-notes.md — skipping commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Never commit to `release`: a release-only commit diverges the + # branch from main and permanently trips the fast-forward guard + # above on every later cut. main has branch protection, so land + # the commit there the same way ardd-badge.yml does: short-lived + # branch, PR, best-effort auto-merge. + branch="release-notes-${{ github.run_id }}" + git checkout -b "$branch" + git add docs/release-notes.md + git commit -m "docs: regenerate release-notes.md for ${{ steps.version.outputs.tag }} [skip ci]" + git push origin "$branch" + + pr_url="$(gh pr create --base main --head "$branch" \ + --title "docs: regenerate release-notes.md for ${{ steps.version.outputs.tag }}" \ + --body "Automated regeneration of docs/release-notes.md from the GitHub release history, triggered by the stable-release.yml workflow after publishing ${{ steps.version.outputs.tag }}.")" + echo "Opened $pr_url" + + # Best-effort auto-merge: never fail the release over the + # changelog page — the release itself is already published. + if gh pr merge --auto --squash "$pr_url"; then + echo "Auto-merge enabled for $pr_url" + else + echo "Could not enable auto-merge for $pr_url — PR is open and awaiting manual approval/merge." + fi From f77a0d5585214bd1bc32ee931ccd5f8cc57b171e Mon Sep 17 00:00:00 2001 From: CI Date: Fri, 24 Jul 2026 12:37:50 -0400 Subject: [PATCH 2/2] ci: suffix release-notes branch with run_attempt for rerun idempotency Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DPyUE4w2CWGbtyafYfhhxw --- .github/workflows/stable-release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index d6cd9f7..9ed27a1 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -132,7 +132,10 @@ jobs: # above on every later cut. main has branch protection, so land # the commit there the same way ardd-badge.yml does: short-lived # branch, PR, best-effort auto-merge. - branch="release-notes-${{ github.run_id }}" + # run_attempt makes reruns idempotent: run_id is stable across + # reruns, so a retry after a partial failure would otherwise + # collide with the branch the failed attempt already pushed. + branch="release-notes-${{ github.run_id }}-${{ github.run_attempt }}" git checkout -b "$branch" git add docs/release-notes.md git commit -m "docs: regenerate release-notes.md for ${{ steps.version.outputs.tag }} [skip ci]"