diff --git a/CLAUDE.md b/CLAUDE.md index 03fb5ab4..aa4a8e58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -330,30 +330,28 @@ git push origin v9.9.0 # moves a published release tag The glob was never what made those safe — it was a blunt instrument compensating for having no guard at the only layer that can see a *ref update* -rather than a command string. That layer now exists. Four **GitHub rulesets**, -all `enforcement=active` with an **empty `bypass_actors` list**, refuse three -of the four lines above — see the gap called out immediately after the table: +rather than a command string. That layer now exists. Five **GitHub rulesets**, +all `enforcement=active` with an **empty `bypass_actors` list**. They refuse +every *rewrite* above; the one thing they still permit is deleting a spent +`release-*` branch, which is explained under the table: | Repo | Ruleset | Applies to | Rules | |---|---|---|---| | `bess-manager` | Protect Main Branch | `~DEFAULT_BRANCH` | deletion, non_fast_forward, pull_request | | `bess-manager` | Protect beta release branches | `beta-release-*` | deletion, non_fast_forward | | `bess-manager` | Protect release tags | `~ALL` tags | deletion, non_fast_forward | +| `bess-manager` | Protect stable hotfix branches | `release-*` | non_fast_forward | | `bess-manager-beta` | Protect beta main (fast-forward only) | `~DEFAULT_BRANCH` | deletion, non_fast_forward | -⚠️ **`release-X.Y` is not in that table, and `--delete release-X.Y` is -therefore unguarded at BOTH layers.** That is the one line of the four above -which nothing currently refuses. It is the short-lived stable hotfix branch the -`release` skill creates (steps 2–6), and it *is* pushed and tagged, so it is a -shared ref by the standard used everywhere else here. The protected tag -preserves the released commit, which makes the branch recoverable after -tagging but not before. - -Left open knowingly rather than by oversight — closing it means adding a -`release-*` ruleset (`non_fast_forward`, and deliberately **not** `deletion`, -since the branch is meant to be cleaned up after the release). Do **not** close -it by restoring a `Bash(git push*)` ask: that guards every push to fix one -branch pattern, and `quality-check.sh` fails on it. +**`release-*` carries `non_fast_forward` and deliberately NOT `deletion`**, +which is the one asymmetry in the table and the one line of the four above that +is still permitted. It is the short-lived stable hotfix branch the `release` +skill creates (steps 2–6): it is pushed and tagged, so *rewriting* it must +fail, while deleting it once the release is out is ordinary cleanup and must +not. What makes dropping the deletion guard safe is the tag ruleset one row up +— the published tag pins the released commit, so a deleted `release-*` branch +costs nothing after tagging. Before tagging it is still recoverable only from +a local reflog, so delete it after, not during. The empty bypass list is the load-bearing part: **local pushes authenticate as the repo owner**, not as `bess-agent` (the credential helper is osxkeychain and diff --git a/scripts/quality-check.sh b/scripts/quality-check.sh index 41b980b8..210d13a0 100755 --- a/scripts/quality-check.sh +++ b/scripts/quality-check.sh @@ -187,6 +187,7 @@ import json, re, sys # # git push origin main --force -> non_fast_forward on ~DEFAULT_BRANCH # git push origin +beta-release-9.9 -> non_fast_forward on beta-release-* +# git push origin +release-9.9 -> non_fast_forward on release-* # git push origin --delete -> deletion on both of the above # git push origin v9.9.0 (force/move) -> non_fast_forward on ~ALL tags # @@ -201,11 +202,11 @@ import json, re, sys # gh api repos/johanzander/bess-manager-beta/rulesets # # What this deliberately does NOT cover: force-pushing or deleting a FEATURE -# branch (fix/**, feat/**) on origin, nor `release-X.Y` (see MUST_NOT_BE_GUARDED -# below). Accepted residuals -- but NOT because "the damage is bounded to your -# own unmerged branch". ~20 worktrees push in parallel as the same identity, so -# a misaimed --force destroys another agent's commits and closes its PR, with -# the recovering reflog sitting in a different worktree. +# branch (fix/**, feat/**) on origin. An accepted residual -- but NOT because +# "the damage is bounded to your own unmerged branch". ~20 worktrees push in +# parallel as the same identity, so a misaimed --force destroys another agent's +# commits and closes its PR, with the recovering reflog sitting in a different +# worktree. # # Every entry below is a rule whose deletion is the exact regression this gate # was written for -- the GitHub-reaching and history-destroying guards. Keep @@ -339,15 +340,15 @@ MUST_NOT_BE_GUARDED = [ # "all of them" -- do not read this list as a protection matrix: # # main --force, +beta-release-*, v9.9.0 (tag) -> refused by a ruleset - # --delete release-X.Y -> NOT refused; no ruleset - # covers `release-*` + # +release-X.Y (rewrite) -> refused by a ruleset + # --delete release-X.Y -> ALLOWED, deliberately # - # That last line is a real residual, not an oversight to "fix" by putting - # the prompt back: `release-X.Y` is the short-lived hotfix branch created - # by the release skill (steps 2-6), it is pushed and tagged, and nothing - # currently protects it at either layer. The protected TAG preserves the - # released commit, so the branch is recoverable after tagging but not - # before. Closing it means adding a `release-*` ruleset, not an ask rule. + # That last line is a deliberate asymmetry, not a residual and not an + # oversight to "fix" by putting the prompt back. `release-X.Y` is the + # short-lived hotfix branch created by the release skill (steps 2-6); + # rewriting it is refused by the `release-*` ruleset, while deleting it + # once the release is out is ordinary cleanup. The protected TAG pins the + # released commit, so a spent branch costs nothing to lose. "git push", "git push -u origin main", "git push origin main --force", "git push origin +beta-release-9.9",