fix: the pre-push hook checked the wrong branch, and my first fix was wrong too (#437 review) - #438
fix: the pre-push hook checked the wrong branch, and my first fix was wrong too (#437 review)#438Polichinel wants to merge 1 commit into
Conversation
…oo (#437 review) /code-review medium found two real defects, in opposite directions, and drilling my fix for one of them found a third. DEFECT 1 — it checked the wrong branch entirely. Git hands a pre-push hook the refs being pushed, one per line on stdin. The hook instead read `git rev-parse HEAD`. So `git push origin feature-a:feature-a` with `main` checked out took the main fast-path and never consulted feature-a — silently allowing the exact push the hook exists to refuse. Reproduced. DEFECT 2 — it matched on branch NAME. Names get reused here: docs/roadmap-plan-v11 spans PRs #50-54, feat/acled-phase2 spans #35-36. A fresh branch reusing a name was permanently refused, no recourse but --no-verify. Reproduced against the live repo. A hook that blocks legitimate work is worse than the bug it prevents. DEFECT 3 — my fix for 2 was also wrong, and only the drill showed it. It asked whether the merged PR's head was an ANCESTOR of what was being pushed. That holds for a squash merge; for a merge-commit merge the head enters the base branch's history permanently, so it is an ancestor of every branch cut from it afterwards. PR #54's head is an ancestor of development today, so every future branch would still have been refused. rc=1 where rc=0 was required. The signal that distinguishes the cases was on stdin all along: remote_sha. If the remote ref sits exactly on the merged PR's head, this push adds to merged work (the #416 shape). Zeros — branch being created — or any other value means it does not. Seven cases drilled against the real repository: reused name, creating branch rc=0 reused name, remote at another sha rc=0 remote sitting on merged PR #54's head rc=1 remote sitting on merged PR #436's head rc=1 branch delete rc=0 tag push rc=0 development rc=0 The review's third finding was that the guard only grepped its own source and would have caught none of this. Correct. The tests now EXECUTE the hook — 11 of them, covering deletes, tags, long-lived branches, empty stdin, the gh-absent fail-open path, that stdin is read at all, and the reused-name case (which short-circuits before gh, so it asserts the real decision rather than an offline fallback). That harness was wrong first too: PATH=/nonexistent-bin hid bash from the test runner. Replaced with a sandbox holding git, sed and bash but not gh, with gh's absence asserted before the run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing unmerged. The hook is being abandoned, not fixed — see the multi-expert review below. Four versions, four different environment properties, all defeating it:
Plus the test suite was vacuous: reconstructing the v1 hook and running all seven behavioural tests — every one passed. The framing was the error. A client-side check is racing GitHub's asynchronous branch deletion; that is a property of the system, not a bug to iterate out. Each failure came from a different environment property, which is the signature of inferring state you cannot see. The orphaned state is trivially observable after the fact, which is what this should have been from the start: Replaced by a detector in
|
Third review round. Corrections only — no behaviour change. At the ritual's 3-iteration cap; remaining findings are reported, not fixed. "BOTH RECOVERED BY CHERRY-PICK INSIDE THE HOUR" IS FALSE The sole quantitative basis for abandoning the pre-push hook, cited to Beck, repeated in the register, the changelog twice, and the guide. Measured: #416 merged 2026-08-03T11:24:10Z #417 opened 2026-08-04T01:08:49Z -> 13h 44m, not "inside the hour" #438 CLOSED, never merged -> that recovery never landed It cuts both ways and both are now stated: 14 hours to notice makes a daily detector comparable rather than clearly worse, AND it removes the "cheap to recover" premise the abandonment argument leaned on. C-351's DATES WERE WRONG, AND THE ROOT CAUSE WITH THEM Said "failed since at least 2026-08-08 — dead for five days", implying a regression. `gh run list --workflow=serving-freshness.yml` returns 10 runs, 10 failures, ZERO successes, earliest 2026-08-03 — the day it was added. It did not regress: it shipped broken and was never verified, which makes it C-350's sibling rather than C-338's. THE DOCUMENTED WORKAROUND FOR C-350 DID NOT WORK `gh workflow run release-topology.yml` with no --ref dispatches the DEFAULT branch's copy — main, which has no detector — and the run goes green having executed none of it, which reads exactly like a clean result. Both of my own dispatches used --ref, so I never exercised the form I wrote down. Now `--ref development`, with the trap spelled out. THE CHANGELOG'S "WHAT IS NOT FIXED" LISTED NINE DEFECTS THAT NO LONGER EXIST 3e1fa37 deleted the code seven of them lived in; two were fixed. Left standing with its correction rather than rewritten, because #428 closes the epic from this record and would otherwise schedule work against defects nobody can reproduce (C-336). validate_docs PASSED | ruff clean | PYTEST_EXIT=0 | FAILED=0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recovers work orphaned on
ops/pre-push-hook-and-automergeafter #437 auto-merged. Part of #421, Story 5 follow-up.This is C-340 mechanism 2, in the PR that fixes C-340 mechanism 2
#437 auto-merged at 15:25:52 carrying the broken v1 hook. The review fixes were pushed to that branch afterwards, landed on a branch with no open PR, and
git pushreported success. Exactly #416's shape, in the change built to prevent it.The hook that would have refused that push was not installed in this clone —
core.hooksPathis per-clone config git does not version, which is precisely the gap the story's own documentation warns about. Now installed here.developmentcurrently carries a hook that is worse than none, which is why this PR matters: it checks the wrong branch, and it refuses legitimate work.Three defects, in order
1. It checked the wrong branch entirely. Git hands a pre-push hook the refs being pushed, one per line on stdin. v1 read
git rev-parse HEADinstead. Sogit push origin feature-a:feature-awithmainchecked out took themainfast-path and never consultedfeature-a— silently allowing the exact push the hook exists to refuse.2. It matched on branch NAME. Names get reused here:
docs/roadmap-plan-v11spans PRs #50–54,feat/acled-phase2spans #35–36. A fresh branch reusing a name was permanently refused, with no recourse but--no-verify. Reproduced against the live repo. A hook that blocks legitimate work is worse than the bug it prevents — it gets uninstalled.3. My fix for (2) was also wrong, and only the drill showed it. It asked whether the merged PR's head was an ancestor of what was being pushed. That holds for a squash merge. For a merge-commit merge the head enters the base branch's history permanently, so it is an ancestor of every branch cut from it afterwards — PR #54's head is an ancestor of
developmenttoday. Every future branch would still have been refused. The drill returnedrc=1whererc=0was required.The fix
The distinguishing signal was on stdin all along:
remote_sha. If the remote ref sits exactly on the merged PR's head, this push adds to merged work. Forty zeros (the branch is being created) or any other value means it does not.The guard now executes the hook
The review's third finding was that
tests/test_git_hooks.pyonly grepped its own source and would have caught none of this. Correct — every text assertion passed straight over all three defects.11 tests now run the hook: deletes, tags, long-lived branches, empty stdin, the
gh-absent fail-open path, that stdin is read at all, and the reused-name case — which short-circuits beforeghis consulted, so it asserts the real decision rather than an offline fallback.That harness was wrong first too:
PATH=/nonexistent-binhidbashfrom the test runner itself. Replaced with a sandbox holdinggit,sedandbashbut notgh, withgh's absence asserted before the run.Verification
bash -n scripts/git-hooks/pre-push→ OKruff check .→ All checks passedpytest tests/test_git_hooks.py→ 11 passed🤖 Generated with Claude Code