Skip to content

fix: the pre-push hook checked the wrong branch, and my first fix was wrong too (#437 review) - #438

Closed
Polichinel wants to merge 1 commit into
developmentfrom
fix/pre-push-hook-review-findings
Closed

fix: the pre-push hook checked the wrong branch, and my first fix was wrong too (#437 review)#438
Polichinel wants to merge 1 commit into
developmentfrom
fix/pre-push-hook-review-findings

Conversation

@Polichinel

Copy link
Copy Markdown
Contributor

Recovers work orphaned on ops/pre-push-hook-and-automerge after #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 push reported 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 clonecore.hooksPath is per-clone config git does not version, which is precisely the gap the story's own documentation warns about. Now installed here.

development currently 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 HEAD instead. 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.

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, 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 development today. Every future branch would still have been refused. The drill returned rc=1 where rc=0 was 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.

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 guard now executes the hook

The review's third finding was that tests/test_git_hooks.py only 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 before gh is consulted, 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 itself. Replaced with a sandbox holding git, sed and bash but not gh, with gh's absence asserted before the run.

Verification

🤖 Generated with Claude Code

…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>
@Polichinel

Copy link
Copy Markdown
Contributor Author

Closing unmerged. The hook is being abandoned, not fixed — see the multi-expert review below.

Four versions, four different environment properties, all defeating it:

approach defeated by
v1 HEAD + branch name checked the wrong branch; permanently refused reused names
v2 merged head is an ancestor merge-commit merges put it in development forever
v3 remote_sha equals merged head delete_branch_on_merge makes it 0000… — proved with a two-clone experiment
v4 ancestor AND not in base never shipped

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:

remote branch                              in development?      open PR?
fix/pre-push-hook-review-findings          NOT in development   438
ops/pre-push-hook-and-automerge            NOT in development   none   <- the orphan

Replaced by a detector in release-topology.yml, which already has fetch-depth: 0, issues: write, a daily cron, and one-reusable-issue machinery. It cannot block work, cannot be uninstalled, needs no core.hooksPath, and would have caught tonight's incident.

scripts/arm_automerge.sh (mechanism 1) is kept — it works and is drilled.

@Polichinel Polichinel closed this Aug 12, 2026
@Polichinel
Polichinel deleted the fix/pre-push-hook-review-findings branch August 12, 2026 10:10
Polichinel added a commit that referenced this pull request Aug 12, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant