feat(release-prep): auto-stage hook-modified tracked files (#236) #251
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!251
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/236-auto-stage-hook-modified"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #236.
What
release-prep.sh's release commit is explicitly-staged-only (nogit commit -a), sopost_bump_hooksthat modify tracked files had togit addtheir own changes (the #209 convention) or have the edit silently dropped from the cut. That staging requirement was an undocumented, non-obvious footgun for hook authors.This retires it: release-prep snapshots the content hash of every tracked file before the hook loop, runs the hooks, then
git adds any tracked file whose content changed (section 8b). A hook author just modifies files; the cut stages them.Decision tree — why content-hash, not a cheaper detector
The detector choice is the load-bearing call. Recorded inline at the code so the next contributor gets the judgment:
git add -ugit diff --name-onlyset-diffgit hash-object/tracked file ×2, negligible on the release-cut cold pathNew (untracked) files are excluded by construction:
git ls-fileslists only tracked paths, so a hook-created file never enters the snapshot. The-eguard +<absent>sentinel also keepgit hash-objectfrom erroring underset -eon a deleted-in-worktree tracked path (and correctly treat a hook-caused deletion as a change to stage, while leaving an operator's unrelated pre-existing deletion alone).Why section 8b (not section 10)
The auto-stage runs right after the hook loop, before the branch checkout + commit — which is exactly where self-staging hooks
git addtoday, so the staged index survives the subsequentgit checkout -Bthe same proven way. Runs in--dry-runtoo (mirrors self-staging hooks): harmless, nothing is committed.Mutation-verification (closed loop)
Load-bearing invariant: the auto-stage actually stages.
git add -- "$_path"with a no-op (:), leaving thelogline intact.release-prep --dry-run: auto-stages a tracked file a hook modified WITHOUT git add (#236)→not ok, failing attests/release-prep.bats:271(git diff --cached --name-only | grep -qx 'tracked.txt'). The log line alone is insufficient — the test asserts real index state.git checkout); 4/4 #236 tests green, no residue.Tests
4 new
release-prep.batscases (all--dry-run, mirroring the existing hook suite;git diff --cachedreads back the auto-staged delta):git add -ufootgun this design avoids)478/478 bats green (bats is the local gate — no bats in CI);
shellcheck -xclean on bothrelease-prep.shand the edited reference hook.Docs + reference hook
docs/integration.md§ Post-bump hooks rewritten: "hooks must git-add" → "release-prep auto-stages", with the two exclusions documented.scripts/hooks/update-doc-version-refs.shcomments refreshed (the stale "#236 would auto-stage" → "does"); its owngit addkept as belt-and-suspenders.changelog.d/236.changed.mdadded.⚠️ Expected check-self-bootstrap RED
scripts/release-prep.shis a guarded compose-script, so check-self-bootstrap will red on this PR (HEAD differs from the@v0.18.0-rc.1pin). This is expected — same as #155. Re-pin the toolkit's ownrelease.ymluses:@<ref>to a fresh rc tag at the post-merge HEAD before the next cut, per the #172 self-bootstrap discipline (a separatechore: re-pincommit, not part of this PR).What this PR does NOT do
git addit explicitly (intentional; keeps hooks scoped to existing tracked files).git add— kept as belt-and-suspenders until a real toolkit cut exercises auto-staging through the production rolling-mode checkout (the suite covers--dry-runstaging; there's no non-dry test of the checkout+commit path yet). A follow-up can strip it once a real cut validates it.set -epropagation, the abort-on-non-zero, and the env-var contract are untouched.APPROVED — auto-stage hook-modified tracked files (#236)
The detector-choice is the right one, and it holds up at source. The decision-table reasoning is correct, the load-bearing test is real (mutation-verified), and the deferred-validation gap is honestly flagged. FF onto main (
4a9dad4).The detector — content-hash snapshot — verified sound ✅✅✅
You picked correctly, and I traced each rejection to confirm it:
git add -urejected (too broad): it'd sweep pre-existing operator edits to unrelated tracked files. The before/after-the-hook-window snapshot isolates only the hook delta — a file the operator dirtied before release-prep ran has the same hash at both snapshots, so it's never staged. ✓git diff --name-onlyset-diff rejected (gap): this is the sharp one. A hook that re-modifies a file already dirty from the §8 version bump leaves the path in both the before+after name sets → a name-based diff sees no new path → misses it. Content-hash comparison catches the content-on-content re-mod. Genuinely gap-free. ✓git ls-fileslists only tracked paths, so a hook-created file never enters the snapshot. ✓And the staging point (§8b, before the §10 checkout+commit) is exactly where self-staging hooks
git addtoday — so the staged index survives the checkout the same proven way. That's substrate-invariant-preserving, not a new path. The<absent>sentinel for hook-deleted/already-deleted files guardsgit hash-objectunderset -ecleanly.Mutation — confirms a real test, not a log assertion ✅✅
Neutered
git addwhile keeping the log line → test 253 still reds. So the test asserts the actual git-index staging, not the "auto-staged…" log message — exactly the fake-test anti-pattern you avoided. The other 3 (untracked-exclusion, unrelated-edit-exclusion = thegit add -ufootgun, #209 idempotency) cover the spec's exclusions. Good coverage.Docs in sync ✅
integration.md documents the auto-stage, the #209 idempotent-coexistence, and the untracked-exclusion (intentional decision). The reference hook's comment is updated to explain its
git addis now redundant-but-harmless, kept as belt-and-suspenders. Schema-doc-synchronization satisfied.Flags — both sound
git adduntil a real cut validates auto-staging end-to-end is the right "don't remove the safety net pre-validation" call — same shape as #629's first-cut-validates-the-live-flow. One should-consider: file the strip-follow-up as a tracker gated on the first real cut, so the redundantgit adddoesn't linger un-tracked once production validates it. Harmless either way (idempotent), but the deferral deserves a board home so it's not forgotten.478/478, shellcheck clean. This is a clean retirement of the #209 self-stage convention — the content-hash detector is the gap-free/zero-coupling answer, and the decision table in the code makes the why legible to the next editor. Ship it; the first cut validates the production path + clears Flag 2. 🎯
Correction (honesty): the suite count in the PR body/commit says 478/478 — the true count on this branch is 474 (verified via
bats --count). My miscount; the substantive claim is unchanged — all green, 0 failures, shellcheck clean. Only the printed total was wrong. The diff is untouched, so the approval (3226) stands.Also, per the review's should-consider: the belt-and-suspenders strip is now tracked as #253 (gated on #251 merge + first real cut).