Skip to content

Commit e30c07c

Browse files
johanzanderclaude
andauthored
docs: record the release-* ruleset, which now exists (#641)
The `Protect stable hotfix branches` ruleset was created after #635 merged (id 20972632: `refs/heads/release-*`, non_fast_forward, enforcement=active, empty bypass_actors). CLAUDE.md and quality-check.sh still carried the warning that `release-X.Y` was unguarded at both layers, which is no longer true -- and a doc asserting a gap that has been closed misleads exactly as much as one asserting a guard that does not exist. Adds the row to the ruleset table and replaces the warning with the reason for the one asymmetry in it: `release-*` blocks non_fast_forward and deliberately NOT deletion. The branch is pushed and tagged, so rewriting it must fail; deleting it once the release is out is ordinary cleanup and must not. The tag ruleset one row up is what makes dropping the deletion guard safe -- the published tag pins the released commit. Careful with the summary line above the table: `--delete release-X.Y` is still permitted, so "refuse every line above" would be false. It refuses every REWRITE. Written that way once in this commit's first draft and corrected before pushing. Claude-Session: https://claude.ai/code/session_016tGBHZEAfKzP9qoJU4NdNY Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent cd9a08f commit e30c07c

2 files changed

Lines changed: 28 additions & 29 deletions

File tree

CLAUDE.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -330,30 +330,28 @@ git push origin v9.9.0 # moves a published release tag
330330

331331
The glob was never what made those safe — it was a blunt instrument
332332
compensating for having no guard at the only layer that can see a *ref update*
333-
rather than a command string. That layer now exists. Four **GitHub rulesets**,
334-
all `enforcement=active` with an **empty `bypass_actors` list**, refuse three
335-
of the four lines above — see the gap called out immediately after the table:
333+
rather than a command string. That layer now exists. Five **GitHub rulesets**,
334+
all `enforcement=active` with an **empty `bypass_actors` list**. They refuse
335+
every *rewrite* above; the one thing they still permit is deleting a spent
336+
`release-*` branch, which is explained under the table:
336337

337338
| Repo | Ruleset | Applies to | Rules |
338339
|---|---|---|---|
339340
| `bess-manager` | Protect Main Branch | `~DEFAULT_BRANCH` | deletion, non_fast_forward, pull_request |
340341
| `bess-manager` | Protect beta release branches | `beta-release-*` | deletion, non_fast_forward |
341342
| `bess-manager` | Protect release tags | `~ALL` tags | deletion, non_fast_forward |
343+
| `bess-manager` | Protect stable hotfix branches | `release-*` | non_fast_forward |
342344
| `bess-manager-beta` | Protect beta main (fast-forward only) | `~DEFAULT_BRANCH` | deletion, non_fast_forward |
343345

344-
⚠️ **`release-X.Y` is not in that table, and `--delete release-X.Y` is
345-
therefore unguarded at BOTH layers.** That is the one line of the four above
346-
which nothing currently refuses. It is the short-lived stable hotfix branch the
347-
`release` skill creates (steps 2–6), and it *is* pushed and tagged, so it is a
348-
shared ref by the standard used everywhere else here. The protected tag
349-
preserves the released commit, which makes the branch recoverable after
350-
tagging but not before.
351-
352-
Left open knowingly rather than by oversight — closing it means adding a
353-
`release-*` ruleset (`non_fast_forward`, and deliberately **not** `deletion`,
354-
since the branch is meant to be cleaned up after the release). Do **not** close
355-
it by restoring a `Bash(git push*)` ask: that guards every push to fix one
356-
branch pattern, and `quality-check.sh` fails on it.
346+
**`release-*` carries `non_fast_forward` and deliberately NOT `deletion`**,
347+
which is the one asymmetry in the table and the one line of the four above that
348+
is still permitted. It is the short-lived stable hotfix branch the `release`
349+
skill creates (steps 2–6): it is pushed and tagged, so *rewriting* it must
350+
fail, while deleting it once the release is out is ordinary cleanup and must
351+
not. What makes dropping the deletion guard safe is the tag ruleset one row up
352+
— the published tag pins the released commit, so a deleted `release-*` branch
353+
costs nothing after tagging. Before tagging it is still recoverable only from
354+
a local reflog, so delete it after, not during.
357355

358356
The empty bypass list is the load-bearing part: **local pushes authenticate as
359357
the repo owner**, not as `bess-agent` (the credential helper is osxkeychain and

scripts/quality-check.sh

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ import json, re, sys
187187
#
188188
# git push origin main --force -> non_fast_forward on ~DEFAULT_BRANCH
189189
# git push origin +beta-release-9.9 -> non_fast_forward on beta-release-*
190+
# git push origin +release-9.9 -> non_fast_forward on release-*
190191
# git push origin --delete <ref> -> deletion on both of the above
191192
# git push origin v9.9.0 (force/move) -> non_fast_forward on ~ALL tags
192193
#
@@ -201,11 +202,11 @@ import json, re, sys
201202
# gh api repos/johanzander/bess-manager-beta/rulesets
202203
#
203204
# What this deliberately does NOT cover: force-pushing or deleting a FEATURE
204-
# branch (fix/**, feat/**) on origin, nor `release-X.Y` (see MUST_NOT_BE_GUARDED
205-
# below). Accepted residuals -- but NOT because "the damage is bounded to your
206-
# own unmerged branch". ~20 worktrees push in parallel as the same identity, so
207-
# a misaimed --force destroys another agent's commits and closes its PR, with
208-
# the recovering reflog sitting in a different worktree.
205+
# branch (fix/**, feat/**) on origin. An accepted residual -- but NOT because
206+
# "the damage is bounded to your own unmerged branch". ~20 worktrees push in
207+
# parallel as the same identity, so a misaimed --force destroys another agent's
208+
# commits and closes its PR, with the recovering reflog sitting in a different
209+
# worktree.
209210
#
210211
# Every entry below is a rule whose deletion is the exact regression this gate
211212
# was written for -- the GitHub-reaching and history-destroying guards. Keep
@@ -339,15 +340,15 @@ MUST_NOT_BE_GUARDED = [
339340
# "all of them" -- do not read this list as a protection matrix:
340341
#
341342
# main --force, +beta-release-*, v9.9.0 (tag) -> refused by a ruleset
342-
# --delete release-X.Y -> NOT refused; no ruleset
343-
# covers `release-*`
343+
# +release-X.Y (rewrite) -> refused by a ruleset
344+
# --delete release-X.Y -> ALLOWED, deliberately
344345
#
345-
# That last line is a real residual, not an oversight to "fix" by putting
346-
# the prompt back: `release-X.Y` is the short-lived hotfix branch created
347-
# by the release skill (steps 2-6), it is pushed and tagged, and nothing
348-
# currently protects it at either layer. The protected TAG preserves the
349-
# released commit, so the branch is recoverable after tagging but not
350-
# before. Closing it means adding a `release-*` ruleset, not an ask rule.
346+
# That last line is a deliberate asymmetry, not a residual and not an
347+
# oversight to "fix" by putting the prompt back. `release-X.Y` is the
348+
# short-lived hotfix branch created by the release skill (steps 2-6);
349+
# rewriting it is refused by the `release-*` ruleset, while deleting it
350+
# once the release is out is ordinary cleanup. The protected TAG pins the
351+
# released commit, so a spent branch costs nothing to lose.
351352
"git push", "git push -u origin main",
352353
"git push origin main --force",
353354
"git push origin +beta-release-9.9",

0 commit comments

Comments
 (0)