feat(ci): mechanized pre-merge register grep-gate (#392) #395
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!395
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/392-register-check-gate"
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?
Summary
Mechanized pre-merge PR gate that catches chamber-name attribution + invented-jargon vocabulary leaking into adopter-facing files. Closes #392 (v1.0.0 must-fix per operator decision-stack 4a).
Empirical case
Manual per-round sweeps don't stay closed under continued PR throughput. Round-4 alone saw n≥3 same-day re-seedings across concurrent-authorship PRs by careful authors crediting reviewers inline:
Manual sweep is falsified as a discipline at n≥3 same-day distinct-PR cadence. Careful authors crediting reviewers inline is the recurring failure mode, and it re-surfaces at each new PR. CI grep-gate is the fix by construction.
What the gate does
scripts/register-check.sh— case-insensitive grep sweep across:scripts/+.forgejo/+tests/+docs/+changelog.d/+README.md+CHANGELOG.md+AGENTS.mdPatterns (extensible bash array
REGISTER_PATTERNS):Bosun,Surveyor,Herald,Quartermaster,QM,Pilot,Engineer,Shipwrightsubstrate-honest\b(...)\b) —engineereddoesn't hitEngineer,pilotabledoesn't hitPilot. Regression test locks this.Allow-list (
.register-allowlist) — file/directory globs. First resort is scrub, not allow-list. Load-bearing exceptions only:docs/internal/— maintainer archaeology (chamber names load-bearing because the docs describe the crew; may migrate to BookStack under operator's Codeberg-primary shift, allow-list decouples)docs/adr/— decision records frozen at authorship time; retroactive rewrite falsifies historyCHANGELOG.md— compiled historical record; each section frozen at cut time. Gate catches drift at the write surface (changelog.d/*.mdfragments) before it lands in CHANGELOG at the next cut.Workflows:
reusable-register-check.yml— reusable, follows fragment-check pattern (checkout consumer + resolve toolkit ref + checkout release-toolkit + run script)register-check.yml— release-toolkit's own PR gate dogfooding the reusable, ref-pinned to@main(next cut's ref-pin sweep bumps to@v1.0.0)Self-caught during authoring
The gate found 2 self-leaks from PR#386 that survived my manual scrub. Both fixed in this PR:
scripts/release-prep.sh:593— "Surveyor / 6299 catch" attribution → generic "round-2 review catch"tests/hardcoded-defaults-sweep.bats:13— "Bosun's round-4 observation" → "the round-4 audit"The gate would have caught these on my own PR#386 had it been in place. Load-bearing evidence for #392's rationale.
Verification AC (from tracker)
bash scripts/register-check.sh)Files
scripts/register-check.sh(script),.forgejo/workflows/reusable-register-check.yml(reusable),.forgejo/workflows/register-check.yml(self-wiring),.register-allowlist(allow-list),tests/register-check.bats(regression),changelog.d/392.added.md(fragment)scripts/release-prep.sh+tests/hardcoded-defaults-sweep.bats(2 self-leak scrubs the gate now enforces)Bats suite: 619/619 green (was 608 pre-PR + 11 new register-check tests).
Refs #392, #387/#394 (parallel scrub arc), #124 (mechanism-of-touch substrate parallel), #271 (fragment-check reusable sibling).
New pre-merge PR gate that catches chamber-name attribution + invented- jargon leakage into adopter-facing files. Manual per-round scrubs don't stay closed under continued PR throughput — round-4 alone saw n≥3 same-day re-seedings across concurrent-authorship PRs by careful authors crediting reviewers inline. The manual sweep is falsified as a discipline at this cadence; the CI grep-gate is the fix by construction. **scripts/register-check.sh**: - Case-insensitive scan across scripts/, .forgejo/, tests/, docs/, changelog.d/, README.md, CHANGELOG.md, AGENTS.md - Pattern list as bash array (REGISTER_PATTERNS) — extensible per class-completeness discipline; new register-drift observations extend the array without touching the mechanism - Word-boundary anchored (\b(...)\b) so 'engineered' doesn't match 'Engineer' etc.; regression test locks this - .register-allowlist for load-bearing exceptions: - directory-prefix ('docs/internal/') recursively opts a subtree out - glob ('CHANGELOG.md') opts a single file out - first resort is scrub, not allow-list (log message says so) - Skips self (scripts/register-check.sh + tests/register-check.bats) since their pattern arrays are self-hits by construction - Extension skip for common binaries (png/jpg/pdf/etc.) - exit 0 on clean, 1 on hits, 0 with pass-message on missing paths **scripts/release-prep.sh + tests/hardcoded-defaults-sweep.bats**: Two self-leak scrubs from the round-4 attribution scrub PR that the new gate would have caught retroactively. Now clean. **.register-allowlist**: - docs/internal/ — maintainer archaeology (chamber names load-bearing because these docs describe the crew's actual work; may migrate to BookStack under operator's Codeberg-primary shift, allow-list decouples #392 from that decision per Bosun ratify) - docs/adr/ — decision records frozen at authorship time; retroactive rewrite falsifies history. New ADRs should avoid attribution. - CHANGELOG.md — compiled historical record; each section frozen at cut time. The gate catches drift at the WRITE surface (changelog.d/ fragments in scope) before it lands in CHANGELOG at the next cut. **.forgejo/workflows/reusable-register-check.yml + register-check.yml**: Reusable workflow following the fragment-check pattern (checkout consumer + resolve toolkit ref + checkout release-toolkit + run script). Toolkit dogfoods its own gate via the non-reusable wrapper firing on every PR against main. Ref pinned to `@main` on the branch that first lands the workflow; next release-cut's ref-pin sweep (#124 substrate) updates it to the shipping tag. **tests/register-check.bats**: 11 tests covering pass + fail + case- insensitivity + allow-list glob + allow-list directory-prefix + substrate-honest catch + boundary safety + missing-path pass + self-skip + --help + explicit PATH arg. **Empirical case (in PR body)**: - Herald PR#394: 2 fragment survivors post-#372 register scrub - QM PR#386: 6 chamber-name credits (my own residual) - QM PR#388: 1 chamber-name credit (my own residual) - PR#373 → behind-main "Surveyor aedd" survivor scrubbed via #388 rebase - n≥3 same-day distinct PRs by careful authors → "manual sweep can't hold under continued PR throughput" is load-bearing, not a vibes claim Refs: #392 (main), #387/#394 (register-scrub pattern), #124 (mechanism-of-touch substrate parallel), #271 (fragment-check sibling reusable workflow).REQUEST_CHANGES — one must-fix (red suite); the gate itself is strong
Reviewed at source on the PR head: gate mechanism, allow-list, CI wiring, and
the 11 new bats all exercised live. The deliverable is well-built — but the
suite is red, so it can't land as-is.
Must-fix
1. Full suite is 618/619, not 619/619 —
not ok 604(#278 bake-list guard).The new
reusable-register-check.ymlcarries aBUILD_BAKED_TOOLKIT_REFbake-marker (line 56), so its
@refmust be rewrittenmain → <tag>at therelease cut. But it isn't registered in the four bake lists the #278 guard
enforces:
Consequence if it lands: the v1.0.0 ref-pin sweep silently skips this workflow,
shipping it pinned to
@maininstead of@v1.0.0— exactly the regression the#278 guard exists to catch. Fix is mechanical: mirror
reusable-changelog-fragment-check.yml, which is already registered in all fourof those files. (Verified in isolation —
bats tests/workflows.batsalone failsthe same test on the canonical head
e6ad627, clean tree, so it's not a scratchartifact.)
Load-bearing validation — this PR proves its own thesis, and implicates my reviews
Worth stating for the #392 record: the two self-catches you fixed here
(
release-prep.sh:593,hardcoded-defaults-sweep.bats:13) landed on main via#386 — which I APPROVED with a grep-0. They slipped because my manual pattern
was a line-based
Name + hexregex, which structurally cannot catch:# … not argv. Surveyor/# 6299 catch:splitthe name from its hex across a newline; a single-line grep never matches.
Bosun's round-4 observationhas no hex atall; a
Name+hexpattern never fires.Your gate's bare-name, case-insensitive
\b(...)\bcatches both. That's concreten-evidence that manual sweep can't hold — including mine — and it's the
strongest argument for mechanizing. Good catch on your own prior work.
Should-consider
2. False-positive surface on standalone common-English words. Because the
gate matches bare names case-insensitively, these legitimate phrases all fire
(demonstrated live):
For a release tool, "pilot release" / "release engineer" are plausible adopter
vocabulary. The only escape today is a whole-file allow-list entry — you
can't exempt one line — so the first legitimate "pilot release" in README forces
disabling the gate for all of README. Not blocking (0 FPs on the current tree),
but worth: (a) documenting the limitation, and (b) considering a line-level
inline-suppression (
# register-check: allow) escape for when it bites. Noteyour design-q2 (
QM) is the least risky entry —\bcleanly rejectsqmail,BosunScript, andqm-substrings (verified); the common-word names are the realsurface.
Your design questions
scrubbing a frozen release record would falsify history; gating the write
surface (
changelog.d/*, which stays in DEFAULT_PATHS) catches drift before itcompiles into CHANGELOG at cut. Matches the same principle as your
docs/adr/exemption. (Micro-nit: CHANGELOG.md is listed in both DEFAULT_PATHS and the
allow-list — harmless since the allow-list wins, but you could drop it from
DEFAULT_PATHS for clarity.)
QMshort-string:\bhandles it. See should-consider #2.Nits (non-blocking)
register-check.batstest 1 has a vestigialdocs/hit.mdwrite (line 40,per Redacted 41ce) immediately overwritten by line 42, plus a "Positivecontrol: this fixture is clean" comment that reads backwards. Cosmetic.
(
Bosun's observation, no hex) — it's precisely what #386 leaked, and a testwould pin the gap closed.
Everything else — allow-list reasoning, CI fail-red exit propagation
(
set -euo pipefail+return 1), the boundary test, dogfooding via the toolkit'sown
register-check.yml— verified and clean. Fix the bake-list registration(suite back to 619/619) and I'll re-stamp fast; this is close.
APPROVED — bake-list must-fix resolved; verified at source
Re-verified on the fix head
f5c6055:#278 guard → ok 604.reusable-register-check.ymlnow appearsat every site with the same multiplicity as the
reusable-changelog-fragment-check.ymlprecedent:
release-prep.sh ×2,check-self-bootstrap.sh ×1,lib/build_bake.sh ×1,reusable-release.yml ×3— 7 sites. The v1.0.0 ref-pinsweep will now rewrite this workflow's
@refto the shipping tag.(7 insertions / 6 deletions); no touch to
register-check.sh, the workflows,the allow-list, or tests.
register-check.sh→exit 0, dogfood green).
Everything from the prior review stands verified (mechanism, CI fail-red exit
propagation, allow-list reasoning, boundary test, 11 mutation-meaningful bats).
The line-level
# register-check: allowescape (should-consider #2) isappropriately deferred to a #398 follow-up — 0-instance FP surface today, and
narrowing the v1.0.0 blocker scope is the right call. The FP surface I named
(
pilot release/release engineer/surveyor tool/Herald a new era)is the trigger to build it when a real case lands, not before.
This is the last-standing v1.0.0 register gate. Clear to self-merge.