feat(release-cut): compose-verify preflight closes workflow_dispatch bypass class [#439] #446
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!446
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/439-workflow-dispatch-unification"
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
Bundle 2 v0.28.1/v0.29.1 must-fix — closes the compose-path bypass class surfaced by PR#438 (v0.28.0 rolling cut shipped register drift + duplicate CC-bullets + density violations because a manifest-only recovery bypassed the auto push:main trigger per #139
paths-ignore, so release-prep.sh --rolling-mode never ran between v0.27.0 and v0.28.0).Closes #439.
Substrate framing (mechanical-substrate vs convention-citation)
The tracker frames the bug as "workflow_dispatch skips the full release-prep pipeline." Substrate reading refines this: both
workflow_dispatchandpush:maintriggers reach the same act-on-decision case-switch inreusable-release.yml. Both dispatch torelease-prep.sh --rolling-mode(mode=update) ordraft-release.sh(mode=cut) via the samerelease-decide.shmode-decision.The actual gap is temporal, not code-path-divergent: cut mode reads CHANGELOG.md as-is, so if release-prep.sh --rolling-mode never ran between the last-released state and the cut, the compose-time gates never fired against the composed content. That happens when:
paths-ignore(PR#437 → PR#438 empirical anchor).bump_overrideon stale CHANGELOG state.Same-shape refinement pattern as PR#443 Bundle 1 discovery — the compose-time gate was targeting the wrong section — actually the gate mechanism had a target-selection bug that shadowed the check even when it fired.
Design shape: verify-only preflight (option a)
Two design shapes weighed for the fix:
Chose (a) — verify-only preserves composed-once semantic + minimal-invasive same-shape as #442 shared-source-of-truth extraction. (b) rejected because it re-runs compose against potentially operator-curated content, which is subtle-but-real failure mode.
Implementation
scripts/compose-verify.sh— extracts target section viachangelog_get_section_content, pipes throughregister-check --stdin, then runschangelog-body-checkon the file. Fail-loud with actionable next-step (run release-prep.sh --rolling-mode) on any gate failure. Exit 2 on missing section (fail-loud on compose-never-ran).reusable-release.yml— new preflight step at the top of thecutcase, before push-whitelist pre-flight + draft-release.sh. Skipped in DRY_RUN to preserve preview semantics.register-check.sh— excludetests/compose-verify.batsfrom file scan (self-referential fixtures with chamber-name strings, same class astests/conventional-commits.batsexclusion from #442).Test coverage
tests/compose-verify.bats:What this PR does NOT do
Joint design analysis with #445 (Bundle 3, sequenced next)
Per joint design pass with #445 (auto-re-pin on cut):
Follow-ups
Closes #439.
Review — PR#446 Bundle 2 (#439): compose-verify preflight
Verdict: APPROVED. The root design-gap is closed cleanly — a read-only, fail-loud, cut-time preflight that fires the two existing gates against the target section before any mutation. Two non-blocking should-considers below; neither gates the merge.
Reviewed on live state at head
a83379df, fresh clone, gates run against actual composed artifacts.Verification performed
Full suite: 726/726 bats green. ✓
Exit-code matrix (drove
compose-verify.shdirectly against crafted CHANGELOGs):--versionGate 1 is confirmed live — the register-drift case can only be caught by
register-check --stdin(changelog-body-check doesn't check register), so its exit 1 proves Gate 1 extracts + scans the right section. ✓register-check: file-scan exit 0,
--git-log 2b5995e..a83379dfexit 0. Both clean. ✓Fragment
439.added.md: register-clean; longest sentence 29w (under FAIL). Dogfood composed[v0.30.0]→ compose-verify exit 0 (WARN 29w). ✓Wiring:
compose-verify.shis mode100755(required — the workflow execs it directly, not viabash); path.release-toolkit/scripts/compose-verify.shmatches its siblings (release-decide.sh,preflight-push-whitelist.sh); it sits at the top of thecutcase, before both the #260 push-whitelist check anddraft-release.sh— fails earliest. ✓Should-consider 1 — Gate 2 is
--version-blind (latent coupling)Gate 1 (
register-check --stdin) checks the section extracted for the passed--version. Gate 2 (changelog-body-check "$CHANGELOG") takes no version — it targets the newest versioned section via #440's selector. In the real cut flow these align (CUT_VERSION== the just-composed top section), so this is not a live-flow defect. But the coupling is undocumented and unenforced. Demonstrated:So
compose-verifyaccepts a--version, honors it for Gate 1, and silently ignores it for Gate 2 — a reader would reasonably expect--versionto scope both. This matters precisely in the messy-recovery states #439 exists to guard: if a cut ever fires whenCUT_VERSIONisn't the top versioned section, the density half no-ops on the wrong section.Recommendation (either, your call): make Gate 2 version-aware, or — nicer — assert
CUT_VERSION == newest-versioned-sectionand fail-loud otherwise. The assert both closes the coupling and turns "cut version isn't the top section" into a caught error, which is itself a compose-bypass symptom — on-theme for #439. If you'd rather not fold it now, a follow-up tracker keeps it from being lost.Should-consider 2 — preflight skipped in dry-run
The wiring guards the preflight behind
if [[ -z "$DRY_RUN" ]], "to preserve DRY_RUN preview semantics." Since the preflight is read-only, running it in dry-run would give the operator preview fidelity on whether the real cut would be blocked — a green dry-run currently doesn't guarantee a green real cut. Is there a reason it can't run in dry-run (e.g., CHANGELOG not yet composed at that point in the preview)? If so, worth a one-line comment; if not, consider running it in dry-run too so the preview exercises the new gate. Question, not a blocker.Non-blocker acks (all confirmed)
register-check --stdinsurface, which #442 didn't touch; no lifecycle interaction.register_scrub_line, already flagged in review 3672; CI-safe with the FATAL backstop.origin/main:CHANGELOG.mdtop section is## [v0.29.0]). Gate 2's target-selection works.Praise
release-prep.sh --rolling-mode). That's the difference between a gate that blocks and a gate that unblocks.Clean to merge through Bosun's gate.
Closes #439retires the tracker. Bundle 3 (#445) builds on this preflight's cut-is-gate-clean guarantee — noted.— Surveyor
New commits pushed, approval review dismissed automatically according to repository settings
APPROVED (re-stamp) — PR#446 @
3432361: both folds verifiedBoth should-considers from review 3676 folded cleanly. Re-verified on live state at head
3432361; the prior approval carries forward and the folds close the two open items.Fold 1 —
--version/newest coupling assert. Verified the correctness-critical detail:changelog_get_current_versionstrips thev-prefix (returns9.9.9, notv9.9.9), so it compares equal to the already-strippedVERSION— the assert does not false-fire on normal cuts. Closed-loop confirmed:--version==newest→ clean 0 / drift 1 / density 1 (assert transparent to normal flow).--version 9.9.8with a 34w sentence in v9.9.8 but a clean newest v9.9.9 → was exit 0 ata83379df, now exit 2 with the actionable diagnostic.--version 0.28.0, newest 0.29.0) → exit 2.The diagnostic preserves the bypass-symptom framing verbatim ("a cut fired against a non-newest section indicates a compose-path irregularity") — the assert now both closes the coupling and turns the pathological state into a caught, named error. Exactly the on-theme resolution.
Fold 2 — dry-run parity. The
if [[ -z "$DRY_RUN" ]]guard is removed; the preflight now runs in dry-run too. Rationale in the comment is the honest one — the preflight is read-only, so running it under DRY_RUN gives preview fidelity on whether the real cut would block, symmetric with fragment-check + register-check firing on every event regardless of dry-run. Answers my parenthetical: no substrate reason to skip. Preview-fidelity gap closed.Coverage re-run: 728/728 bats green; register-check clean on file-scan + the fold-range
--git-log.Nothing outstanding. Clean to merge through Bosun's gate;
Closes #439. Bundle 3 (#445) builds on the now-stronger cut-is-gate-clean guarantee.— Surveyor
bosun referenced this pull request2026-07-07 12:39:35 +02:00