From 3b1fc2939770c85d3fb18e8e9fb6ddd8b5d0a6ed Mon Sep 17 00:00:00 2001 From: William Aaron Cheung Date: Wed, 9 Sep 2026 14:42:24 +0800 Subject: [PATCH] fix(release-settle): supersede a previous settle PR instead of force-pushing its branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-running settle on a moved release tip force-pushed the existing chore/release-settle-vX.Y.Z branch to refresh the open PR. A repository ruleset that bans force pushes on every branch (stateless-validator has one) rejects that, and the run fails after the guard. The settle PR is a statement about one commit, so a new commit now gets a new PR: the open one is closed with the reason, the branch is dropped, and the new branch is pushed fresh — no force push anywhere. Co-Authored-By: Claude Fable 5.1 --- .github/actions/README.md | 2 +- .github/actions/release-settle/action.yml | 25 +++++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/actions/README.md b/.github/actions/README.md index b5df344..45645c8 100644 --- a/.github/actions/README.md +++ b/.github/actions/README.md @@ -365,7 +365,7 @@ by the GitHub Release that `release-publish` creates. |---|---|---| | `release-candidate` `stage: propose` | `workflow_dispatch` on the default branch | bumps `version_file`, runs `bump_command` (lockfile, path-dep versions), drafts this release's changelog entry under `## vX.Y.Z` (dated at settle), syncs the previous release's entry from its tag, opens `chore/release-candidate-vX.Y.Z` PR | | `release-candidate` `stage: cut` | that PR merging | creates `release-vX.Y.Z` at the merge commit | -| `release-settle` | `workflow_dispatch` with version + tip SHA | guards, regenerates the entry up to the tip and stamps the date, opens `chore/release-settle-vX.Y.Z` PR onto the release branch — or, with `settle_mode: direct` and an authorised actor (`settlers`), commits it straight to the branch and publishes at once | +| `release-settle` | `workflow_dispatch` with version + tip SHA | guards, warns if the tip lacks a workflow the default branch has, regenerates the entry up to the tip and stamps the date, opens `chore/release-settle-vX.Y.Z` PR onto the release branch (a re-run on a new tip closes the previous settle PR and opens a fresh one; nothing is ever force-pushed) — or, with `settle_mode: direct` and an authorised actor (`settlers`), commits it straight to the branch and publishes at once | | `release-publish` | the settle PR merging | annotated tag at the merge commit (refuses if it exists or the branch drifted), GitHub Release with the entry as notes | The changelog has one owner per phase and is never back-merged: the candidate diff --git a/.github/actions/release-settle/action.yml b/.github/actions/release-settle/action.yml index 0af975a..4b572d0 100644 --- a/.github/actions/release-settle/action.yml +++ b/.github/actions/release-settle/action.yml @@ -257,24 +257,31 @@ runs: fi git add "$CHANGELOG" git commit --quiet -m "chore(release): settle v$VERSION" -m "Changelog entry $what for v$VERSION at $COMMIT." - git push --quiet --force origin "$head" + # A previous settle branch (the tip moved, or settlement was re-run) + # is superseded, never force-pushed: a no-force-push ruleset rejects + # the rewrite, and a settle PR is a statement about one commit, so a + # new commit gets a new PR. Close its PR first so the reason is on + # record, then drop the branch and push this one fresh. + if git ls-remote --exit-code --heads origin "refs/heads/$head" >/dev/null; then + for n in $(gh pr list --head "$head" --base "$BRANCH" --state open --json number -q '.[].number'); do + gh pr close "$n" --comment "Superseded: settlement of v$VERSION was re-run (now at \`$COMMIT\`); a fresh settle PR follows." >/dev/null + echo "closed superseded settle PR #$n" + done + gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/$head" >/dev/null + echo "dropped previous settle branch $head" + fi + git push --quiet origin "$head" body="$RUNNER_TEMP/settle-pr-body.md" { echo "Settle **v$VERSION** at \`$COMMIT\` (tip of \`$BRANCH\`)." echo - echo "Merging this PR is the settlement decision: \`release-publish\` will tag the merge commit as \`v$VERSION\` and publish the GitHub Release with the notes below. The default branch picks this entry up in the next release candidate PR. If more commits land on \`$BRANCH\` first, this PR goes stale and must be regenerated." + echo "Merging this PR is the settlement decision: \`release-publish\` will tag the merge commit as \`v$VERSION\` and publish the GitHub Release with the notes below. The default branch picks this entry up in the next release candidate PR. If more commits land on \`$BRANCH\` first, this PR goes stale: re-run settle on the new tip, which closes this PR and opens a fresh one." echo echo "" echo cat "$NOTES" } > "$body" - existing="$(gh pr list --head "$head" --base "$BRANCH" --state open --json url -q '.[0].url')" - if [[ -n "$existing" ]]; then - gh pr edit "$existing" --body-file "$body" >/dev/null - url="$existing" - else - url="$(gh pr create --base "$BRANCH" --head "$head" --title "chore(release): settle v$VERSION" --body-file "$body" "${label_args[@]}")" - fi + url="$(gh pr create --base "$BRANCH" --head "$head" --title "chore(release): settle v$VERSION" --body-file "$body" "${label_args[@]}")" echo "pr_url=$url" >> "$GITHUB_OUTPUT" echo "settle PR: $url"