ops: a hook that cannot be forgotten, and a readback that cannot be skipped (C-340, #426) - #437
Conversation
…kipped (C-340, #426) Two mechanisms, two different fixes, because they fail differently. MECHANISM 2 gets a git hook. #416 merged the instant CI went green; a follow-up commit was pushed to that branch, which by then had no open PR, and two pieces of work were simply not on development. `git push` reported success. scripts/git-hooks/pre-push refuses that push, names the PR and when it merged, and says what to do instead. It is a hook rather than a checklist item deliberately. Everything else in this epic runs whether or not anyone remembers; a step someone has to recall would be the same class of defect the epic exists to remove. It ALLOWS the push whenever it cannot answer — gh absent, unauthenticated, offline — and says why. A hook that blocks work when it does not know gets uninstalled within a day and then guards nothing. Same idiom as the deploy gates (C-320): skip where the environment cannot answer. MECHANISM 1 gets a helper that reads the value back. `gh pr merge --auto --<method>` on an already-armed PR silently refuses to change the method — prints nothing, exits 0. scripts/arm_automerge.sh uses the GraphQL disable/enable pair, which does honour it, then reads auto_merge.merge_method back and exits non-zero on mismatch. Reading back is the point; arming is the easy part. Seven drills, all against the real repository: merged PR (#436) REFUSE exit 1, names the PR and merge time branch with no PR ALLOW exit 0 development ALLOW exit 0 gh absent from PATH ALLOW exit 0 with the reason gh unauthenticated ALLOW exit 0 with the reason arm on a MERGED pr exit 1, refuses to mutate arm with bad arguments exit 2 The gh-absent drill was wrong first: PATH=/usr/bin:/bin still found gh at ~/.local/bin, so it tested nothing. Redone with a sandbox PATH containing git and bash but not gh, with the absence verified before the run. tests/test_git_hooks.py guards the three things that fail silently: the executable bit (git ignores a non-executable hook with no warning), the fail-open paths, and the documented install step. Drilled by breaking each — three mutations, three catches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mechanism 1, reproduced live on this PR#426 called this the single most important drill in the story. Run against this pull request:
The GraphQL disable/enable pair does change it, and the script then reads the value back rather than trusting the call. Worth being precise about what this drill does and does not show: it demonstrates the silent no-op and the fix. It does not demonstrate the mismatch branch of the script — the path that exits non-zero when the readback disagrees — because that path only fires if GitHub accepts the mutation and then reports something else, which cannot be produced on demand. That branch is exercised by inspection only, and this comment says so rather than implying full coverage. |
…340) (#441) `development` has been carrying a pre-push hook that does not work, and `publishing_to_pypi.md` told you to install it — twice. Anyone running `git config core.hooksPath scripts/git-hooks` got a hook that checks the WRONG BRANCH (it read `git rev-parse HEAD` instead of the refs git supplies on stdin) and permanently refuses branch names reused from old pull requests. That is a trap, and it shipped in #437. Four hook versions and three detector versions were written to close C-340 mechanism 2; each was defeated by a different property of git, gh or GitHub. The harm being guarded against is two orphaned commits in ~440 pull requests, both recovered by cherry-pick. The guards cost far more than the failures, so there is no guard — and the guide now says so plainly instead of pointing at one. removed scripts/git-hooks/pre-push removed tests/test_git_hooks.py (vacuous: reconstructing the broken v1 hook passed all seven of its behavioural tests) guide install instructions replaced by what to do if it happens Mechanism 1 is unaffected: scripts/arm_automerge.sh stays, works, and was drilled live. Closes the last of epic #421, which is closed as a whole. validate_docs PASSED | ruff clean | PYTEST_EXIT=0 | FAILED=0 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #426. Part of #421, Story 5.
Two mechanisms, two different fixes, because they fail differently.
Mechanism 2 — a git hook
#416 merged the instant CI went green. A follow-up commit was pushed to that branch, which by then had no open PR, and two pieces of work were simply not on
development.git pushreported success; the only signal was a PR showing one commit when two had been pushed.scripts/git-hooks/pre-pushrefuses that push, names the PR and when it merged, and says what to do instead.It is a hook rather than a checklist item deliberately. Everything else in this epic runs whether or not anyone remembers. A step someone has to recall would be the same class of defect the epic exists to remove.
It allows the push whenever it cannot answer —
ghabsent, unauthenticated, offline — and says why. A hook that blocks work when it does not know gets uninstalled within a day, and then it guards nothing. Same idiom as the deploy gates (C-320).Mechanism 1 — a readback
gh pr merge --auto --<method>on an already-armed PR silently refuses to change the method: prints nothing, exits 0.scripts/arm_automerge.shuses the GraphQL disable/enable pair, which does honour it, then readsauto_merge.mergeMethodback and exits non-zero on mismatch. Reading back is the point; arming is the easy part.Drills — all against the real repository
The gh-absent drill was wrong first.
PATH=/usr/bin:/binstill foundghat~/.local/bin, so it tested nothing and reported a refusal as though that were the expected result. Redone with a sandboxPATHcontaininggitandbashbut notgh, with the absence verified before the run. Sixth time this week a drill's setup silently did not happen.Mechanism 1's decisive drill — done on this PR
Per #426 this is the most important one: arm one method, ask for another, confirm the readback catches what
gh pr merge --autosilently would not. Recorded in a comment below, on this PR itself.Guard
tests/test_git_hooks.pycovers the three things that fail silently:core.hooksPathis per-clone config git does not version, so an undocumented hook is a file nobody installsDrilled by breaking each: three mutations, three catches, baseline green.
Residue — C-340 will be narrowed, not closed
As #426 predicted. The hook covers mechanism 2 completely. Mechanism 1 now has a helper that works, but nothing forces its use —
gh pr merge --autois still one keystroke away, and "read the value back rather than trusting the command" is a general habit no hook enforces. Story 7 (#428) should say that plainly rather than claim a clean resolution.Verification
bash -non both scripts → OK (shellchecknot installed here)ruff check .→ All checks passedpytest→ PYTEST_EXIT=0,FAILED: 0docs/validate_docs.sh→ PASSED🤖 Generated with Claude Code