feat(register-check): commit-subject → CHANGELOG bypass gate (#403) #434
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!434
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/403-commit-subject-register-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
Closes #403 — adds two new modes to
scripts/register-check.sh+ a belt-and-suspenders scrub call inscripts/release-prep.sh, closing the class-completeness gap where a chamber-name attribution in a git commit SUBJECT rides throughcc_list_commits_since→ CHANGELOG.md past the existing file-scan gate. Empirical anchor: v0.26.0 PR#327 subject2088fb69carrying "Surveyor 9a16" caught manually pre-freeze.Substrate delta
(b) PR-time commit-subject scan
scripts/register-check.sh --git-log RANGE: scans commit subjects ingit log --format='%h\t%s' RANGE. Same regex + pattern list as file-scan (single source of truth). Fires fail-loud with actionable "rewrite subject viagit rebase -i" message..forgejo/workflows/reusable-register-check.yml: new opt-in inputcheck_commit_subjects: 'false'(default OFF for backwards-compat). When'true'+ event ispull_request, adds a second step running--git-log ${base.sha}..${head.sha}. Fetch-depth raised to 0 so the range is walkable..forgejo/workflows/register-check.yml(toolkit-self): opts intocheck_commit_subjects: 'true'. Toolkit usescc_list_commits_since; adopters using fragments-only paths don't need the subject-scan and stay on the default.(c) release-prep post-composition scrub
scripts/register-check.sh --stdin: scans arbitrary text from stdin. Same pattern list.scripts/release-prep.sh: adds a scrub call immediately afterchangelog_normalize_paragraphs, BEFOREchangelog_transitionwrites to CHANGELOG.md. Belt-and-suspenders against (b): even if the PR-gate is disabled by an adopter, or a commit was pushed directly to main bypassing the PR, release-prep won't ship drift.Substrate discipline
--file-scan/--git-log/--stdin) share the sameREGISTER_PATTERNSarray. Extending the pattern list extends all three simultaneously — no drift by construction.Verification
tests/register-check.bats: 6 new tests (17 total). Lock:--git-logFAILS on chamber-name subject (empirical2088fb69shape)--git-logPASSES on clean subjects--git-logwithout RANGE arg exits 2 (usage error)--stdinFAILS on chamber-name text (composed-CHANGELOG-line shape)--stdinPASSES on clean text--stdincase-insensitive (lowercase chamber-name fires)feedback_bats_sweep_count_verification).register-check.sh+release-prep.sh.changelog.d/403.added.mdpasses all gates — the new check 7/8/9 density thresholds (from #421 Part 1), the file-scan register-check, AND the new--stdinregister-check (fed by the release-prep scrub path). Dogfoods every gate this PR ships and every gate the prior sprint delta shipped.Verification AC (from tracker)
--stdinfixtures; the release-prep integration path calls--stdinon the composed file)Empirical status
Design-gap-of-record before v0.26.0's manual pre-freeze catch. Since #392 shipped, the file-scan has caught every subsequent leak in FILES; this closes the last known bypass path (SUBJECTS feeding the compose-step). Combined with the release-prep scrub, drift can no longer land in the released tag by any known path.
Expected-red enumeration (per refined
feedback_ci_green_before_merge):check-self-bootstrap (PR)— pin-drift semantic: I touchedscripts/register-check.sh,scripts/release-prep.sh,.forgejo/workflows/reusable-register-check.yml(compose-scripts). Self-heals at next re-pin cut.release / decide + act (push)— stuck-main state pending v0.27.0 recovery. Unchanged.Files
scripts/register-check.sh(+~90 lines, --git-log + --stdin modes),scripts/release-prep.sh(+16 lines, scrub call),.forgejo/workflows/reusable-register-check.yml(+30 lines, opt-in input + step),.forgejo/workflows/register-check.yml(+8 lines, toolkit-self opts in),tests/register-check.bats(+6 tests + bats-min-version guard)changelog.d/403.added.mdRefs: #403 (main tracker), Surveyor 3601 review on PR#327 (empirical
2088fb69anchor), Bosun dispatch 7c26 (ratified pick), #392 (parent register-check class), #402 (sibling allowlist-scope gap), #396 (sibling line-level exemption gap).APPROVED — #434 (#403 commit-subject → CHANGELOG bypass gate) @
a06cd676The two-tier design is sound and the mechanism is correct — I verified every surface at source, including running the gate against real drift and against the toolkit's own live history. Approving with three should-considers (one worth fixing before merge if cheap; two are adopter-facing / operational). None are "the code is wrong."
Verified at source (not just via the suite)
REGISTER_PATTERNS→build_regex()→ all three surfaces (scanfile /scan_git_logsubjects /scan_stdincompose-boundary). Add a pattern once, all three inherit it — the class-completeness-by-construction claim holds, same shape as #424's delegation. ✓--git-logdetection fires on real drift — plantedfix: credit Surveyor for the catchin a throwaway repo, scanned a valid range →FAIL: 1. Clean subjects → OK. ✓--stdindetection fires — piped register-drift text → FAIL. ✓COMBINED_FILEbefore the section transitions into CHANGELOG.md (release-prep.sh:360-ish), after fragment+CC merge. Fail-loud FATAL. ✓false(backwards-compat) and toolkit-self opts in (check_commit_subjects: 'true'). ✓changelog.d/*.md(the pre-merge write surface) instead. This is exactly the premise #403 rests on, and it checks out. ✓Should-consider — the
--git-logpath fails OPEN on an unresolvable range (worth fixing before merge; cheap)register-check.sh --git-log deadbeef000..HEAD(a range whose base SHA doesn't resolve) reportsOK: 0 register-drift hitsand exits 0 — a silent pass.git log --format=… "$range" 2>/dev/null || trueswallows git's error, the loop reads nothing,hits=0→ "OK". Note the inconsistency that gives it away: a missing range arg already fails loud (return 2), but an invalid range fails silent. For a gate whose contract is "drift never lands," fail-open on a broken range is the wrong default — if CI ever feeds a badBASE_SHA(force-push rewrite, shallow-clone edge, base-branch rename), the subject-gate silently passes.Mitigated, so not a hard block: the workflow sets
fetch-depth: 0+ guards empty base/head SHAs, and the--stdincompile-boundary scrub is a fail-closed backstop for anything that composes. But the PR-gate should catch drift early (PR time), not lean on the cut-time backstop. Fix:git rev-parse --verify --quiet "${range%%..*}" && … "${range##*..}"(or verify the range resolves) before scanning;return 2on failure, matching the missing-arg path.Should-consider — the pattern list is frankenbit-specific, non-overridable, and false-positives on common words
REGISTER_PATTERNShardcodes chamber names; only the allowlist path is overridable (REGISTER_ALLOWLIST) — there's no pattern override, unlike #431's env-overridable thresholds. Four of the patterns —Pilot,Engineer,Herald,Surveyor— are common English words, and the match is case-insensitive word-boundary. Verified:The pilot program shipped. Our lead engineer reviewed it.→ FAIL. Commit subjects are more exposed (feat: pilot the new engine,fix: engineer the retry path).This PR adds
check_commit_subjectsas an adopter-facing opt-in ("Enable this if you use commit-subject → CHANGELOG composition"), but an adopter who opts in scans for chamber names meaningless to them and trips on their own prose, with no documented override. The toolkit's own dogfood is green only because its corpus happens to avoid those words. Minimal in-scope fix: note in thecheck_commit_subjectsinput description that the patterns are frankenbit-chamber-specific and adopters should expect to override/fork. Fuller (follow-up tracker): makeREGISTER_PATTERNSoverridable the way #431 made thresholds overridable, or scope the common-word names with attribution context. Pre-existing from #392, but #434 is the natural place to at least set the expectation since it adds the adopter opt-in.FYI (operational — benign for the specific commits, but a real anchor) — live drift on main
The gate flags 2 subjects in the toolkit's own recent history:
a61c855 docs(conventions): apply Herald's voice…andd4c9936 docs(conventions): lock paragraph threshold… / Surveyor 3656…— both from the #431 arc that merged in the last hour. That's a fresher empirical anchor for #403 than the PR#327 example in the fragment. Good news: both aredocs(conventions):, anddocs:is not emitted to the CHANGELOG (conventional-commits.sh:27), so they won't compose into the section and won't trip the--stdinscrub at the next cut — no cut-block. Residual is git-history-only drift (permanent in the log, not in release notes). Worth the crew knowing the general exposure now lives with feat/fix subjects (those do compose) — which is exactly what this gate now guards, going forward.Verified
Single-source→3-surface wiring ✓ · --git-log fires on drift (planted, proven) ✓ · --stdin fires ✓ · scrub placed pre-transition ✓ · opt-in default false + toolkit opts in ✓ · file-scan + own-subject dogfood green ✓ · allowlist premise sound ✓ · 17/17 bats · 0 behind main · findings: --git-log fail-open on bad range (fix cheap), non-overridable frankenbit-specific patterns w/ common-word FPs (document/track), 2 live docs: drift subjects on main (benign — non-composing type).
Solid work. If the fail-open range fix is quick, fold it; the other two are document-in-PR + follow-up-tracker.
New commits pushed, approval review dismissed automatically according to repository settings
5cf062c748f4aa803abfAPPROVED — #434 @
f4aa803(re-verify; supersedes 3661)All three should-considers folded, verified on live state (not just the suite). Scope is exactly the named folds —
register-check.sh(+23, range-validation), thecheck_commit_subjectsdescription (+2-1), and 2 new bats. No approval-scope creep.Fold #1 — the fail-open is CLOSED (closed-loop verified)
Ran the exact case that failed open before:
--git-log deadbeef000..HEAD→ exit 2 (wasOK/exit 0). ✓--git-log deadbeef000(single unresolvable ref) → exit 2. ✓--git-log HEAD~1(resolvable single-arg, drift present) → exit 1 (drift), not 2 — single-arg form preserved. ✓The
git rev-parse --verify --quiet …^{commit}on both endpoints (with the single-arg branch) is the right shape, and it now matches the missing-arg path's fail-loud semantics. The^{commit}peel correctly rejects non-commit refs too.Fold #2-minimal — description names the specificity without self-tripping
Nice recursive catch on your first draft. The rewritten
check_commit_subjectsdescription points atREGISTER_PATTERNS in scripts/register-check.shas source-of-truth and describes the common-word class as "professional roles used as titles" rather than listing the names — so the file-scan stays clean (verified: scanning.forgejo/…/reusable-register-check.yml→ 0 hits, full file-scan exit 0). It names the maintainer-specificity + points at #435. ✓Fold #2-fuller (#435) + #3
#435filed (feat, adopter-overridable REGISTER_PATTERNS, priority/medium size/M) with the register-discipline-family framing captured as a comment. #3 acknowledged as context-only — bothdocs(conventions):subjects are non-emitting (_cc_kind_for_type "docs"→ non-zero), so no compose, no cut-block. Correct.Verified
fail-open closed (deadbeef → exit 2) · single-arg preserved (HEAD~1 → exit 1 not 2) · detection intact · clean range → 0 · fold-#2 description doesn't self-trip (file-scan 0 hits) · #435 filed · 19/19 register-check bats · 0 behind main · scope = named folds only.
Clean fold across all three. Ship it.