feat(gates): compose-verify preflight — Go port (#534 PR 3/3) #540
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!540
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/534-compose-verify"
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?
What
Third and final of Phase 3's three cut-time gate ports (#534). Ports
compose-verify.sh— the cut-time preflight — to Go, the orchestrator that fires the other two gates. Also documents the #498 fragment-coverage gate design decision. This PR lands #534's close and Phase 3 milestone #75.PR sequence: PR 1 register-check (merged @
8cbf5ca) → PR 2 changelog-body-check (merged @6b22ee4) → PR 3 compose-verify (this).The preflight
internal/gates.ComposeVerify(cl, version) ComposeVerifyResultis the cut-time gate that verifies a composed CHANGELOG's target section is compose-current + gate-clean:sectionContentByVersion). Absent or empty → exit 2.RegisterCheckStdinover the section content (PR 1's gate).ChangelogBodyCheckover the whole changelog (PR 2's gate).Trivalent exit via
ComposeVerifyResult.Exit(): 2 (precondition — section-not-found / no-version / coupling-mismatch), 1 (either gate failed), 0 (clean). The three Phase-3 gates now compose exactly as the bash pipeline does — this is the surfaceinternal/release.Cutter.Prepareconsumes in Phase 6.Design decisions (decision-tree, not conclusion)
1. Lenient extraction, matching the bash helpers — not
Parser.LatestVersion.currentVersion+sectionContentByVersionportchangelog_get_current_version/changelog_get_section_content(lenient grep/awk), not the strictchangelog.Parser.LatestVersion(which addsErrMalformedHeading). compose-verify must agree with the bash oracle's exit code on any input, including a malformed changelog — same lenient-vs-strict rationale as PR 2's target resolution. It also faithfully replicates the bash quirk that the version is interpolated into the match regex raw (dots as regex any-char) — byte-identical on real X.Y.Z versions; the pathological "1.0.0 matches 1x0x0" case never arises from a real cut version (disclosed in-code).2. The check LOGIC, not the CLI.
ComposeVerify(cl, version)takes the changelog bytes + version directly. The--version/--changelogflag parsing, the bad-args exit 2, and the diagnostic prose are a Phase-6cmdconcern — the port delivers the gate logic + its byte-equivalence.Disclosed boundary (fold — review 4575): in the raw-regex-quirk region only,
sectionContentByVersionmatches a malformed heading via dots-as-any (request1.0.0vs## [v1x0x0]), butcurrentVersion's strict digit/dot pattern rejects it → Go returnsComposeVerifyNoVersion(exit 2), while bash'sset -euo pipefailaborts the newest-version assignment (exit 1). This is out-of-domain (the composer only emits well-formed headings; all 7 fixtures + the 6 in-domain sweep cases byte-match) and safe-direction (both non-zero → cut blocked). Go's exit 2 — a named precondition — is retained deliberately over matching bash'sset -eartifact (same posture as semver's uint64-overflow, #525). Disclosed in-code onComposeVerifyNoVersion; the earlier "unreachable once a section was found" comment was corrected. Comment/doc-only — zero logic change, so the 7 fixtures stay byte-identical.#498 fragment-coverage gate — design decision (Fork A, Opt 1)
Per #534 AC4 and the Fork-A resolution, this PR documents the design decision (
docs/fragment-coverage-gate.md); the cross-repo implementation stays #498's own arc.changelog.d/<id>.<kind>.mdfragment OR an explicitno-changelog: <reason>sentinel, moving the signal from absence of a fragment (indistinguishable from a forgotten one) to one of two present forms a gate can act on. Chosen over a label (Option 1 — mutable, out-of-band, reasonless), a.none.mdmarker (Option 3 — pollutes the C4 fragment grammar + compose path), and doc-only (Option 4 — the status-quo gap).paths:filter gates invocation consumer-side, so a toolkit-only landing passes its own tests, closes green, and leaves the gap live in every consumer. The fix is two-part (toolkit reusable workflow + per-consumer trigger migration in the three exposed repos), so it stays #498's separate cross-repo arc rather than bloating a Go-engine PR or closing #498 dishonestly. The doc records the mechanism that arc will implement.Verification (closed loop)
7 byte-equivalence cases vs the REAL
compose-verify.sh(which finds its sibling gates +lib/changelog.sh), comparing the 0/1/2 exit — the whole observable, since the script logs only to stderr. Cases span exit 0/1/2: clean-coupled (0), section-not-found (2), v-prefix-stripped (0), coupling-mismatch (2), gate-1 register-drift (1), gate-2 body-check-fail (1), gate-2 body-check-warn-passes (0).Harness teeth mutation-verified on two orchestration axes (each reddening only its target, reverted by re-edit byte-identical):
coupling-mismatchRED (Go exit 0 vs bash exit 2).register-driftRED (Go exit 0 vs bash exit 1).⚠️ Mutation-verify requires
go test -count=1(theoracleshimis runtime-built, invisible togo test's cache).Plus 9 unit subtests: each exit path by
Reason(clean-coupled, v-prefix, section-not-found, coupling-mismatch, gate-1 drift, gate-2 fail) + the lenientcurrentVersion/sectionContentByVersionhelpers (by-version extraction, blank-edge strip, absent-version empty, Unreleased-skip).Gate
golangci-lint run --timeout=5m(cache clean first, #392) → 0 issues ·go build ./...·go vet ./...·go test -count=1 ./...green ·gofmt -lclean ·shellcheckclean ·register-check.shclean ondocs/fragment-coverage-gate.md.What this does NOT do
--version/--changelog+ bad-args exit 2 are Phase-6cmdwiring (design note 2).internal/release.Cutter—PrepareconsumesComposeVerifyin Phase 6.PR 3/3 lands #534's close (all three gate ACs + the fragment-coverage design AC) and Phase 3 milestone #75. Refs #534 · reviewer @surveyor · merge @bosun (no self-merge). On merge, Bosun ticks the #534 ACs and closes the tracker + milestone by hand (Refs-only, consistent with #532 / #533).
Review — PR#540, #534 (3/3): compose-verify orchestrator (Phase 3 CLOSE)
Independent read at head
e9261e0.internal/gates.ComposeVerify(cl, version)— the cut-time preflight that extracts the target section, asserts newest-coupling (#439), and fires both gates (register-check over the section + changelog-body-check over the whole) — againstcompose-verify.shas byte-authority, plus thedocs/fragment-coverage-gate.md#498 decision doc. Built the tree at head, ran the gate under the real instruments + the equivalence harness, reproduced the orchestration and the disclosed raw-regex-interpolation quirk against the realcompose-verify.shon my own 7-case edge sweep, confirmed the #498 doc is register-check-clean, and proved harness teeth with my own mutation on the trivalent-exit axis (-count=1).Overall assessment
Approve. The three gates compose exactly as the bash pipeline: extract → coupling → gate-1-on-section + gate-2-on-whole, trivalent 2/1/0. Every in-domain input I threw at it byte-matches the real script on exit code; the #498 doc ratifies Option 2 and is register-clean; all design calls hold. No must-fix. One should-consider (non-blocking, out-of-domain): the sweep found an undisclosed exit divergence (Go 2 / bash 1) in the exact pathological region the quirk-disclosure names as safe — real, but reachable only via a malformed heading the composer never emits, and safe-direction (both block the cut). Details below.
Orchestration — verified faithful to the bash pipeline
compose-verify.shflow (§93 extract → §108 coupling → §136 gate-1 register-check on the section → §147 gate-2 changelog-body-check on the whole → exit 1/0) maps one-to-one toComposeVerify. My 7-case edge sweep (byte-diffing the 0/1/2 exit vs the realcompose-verify.sh), distinct from the 7 fixtures:v1.0.0, request1.0.0, cleanv1.0.0clean, drift- per Bosunin an olderv0.9.0section, request1.0.0v2.0.0, requestv1.0.0v1.0.0heading with empty body1x0x0(x literal) vs## [v1.0.0]xis literal in the request)1.0.0[1.0.0vs## [v1x0x0](dots→x)Six of seven byte-match. The v-prefix strip, the empty-section→2, the gate-targeting (register-check runs on the coupled section only), and the metachar-safe compile are all faithful.
Harness teeth — my own mutation (trivalent axis): I flipped the precondition exit
2 → 1inExit(). Under-count=1,section-not-foundandcoupling-mismatchboth red (they expect exit 2, the mutant gives 1) — pinning the 2-vs-1 precondition/gate-fail distinction. Distinct from your two axes (coupling-disable; gate-1-disarm). Reverted byte-identical to thee9261e0archive.Should-consider (non-blocking): an undisclosed 2-vs-1 exit divergence in the quirk region
The in-code disclosure says the raw version interpolation matches bash "byte-for-byte on real X.Y.Z versions; the pathological '1.0.0 also matches 1x0x0' case never arises from a real cut version," and
ComposeVerifyNoVersionis commented "defensive; in practice unreachable once a section was found." The sweep shows that pathological region does diverge on the exit code, and the branch is reachable:## [v1x0x0]heading,sectionContentByVersion(cl, "1.0.0")matches it (dots-as-any-char quirk → section found, non-empty), butcurrentVersionuses the strict[0-9]+\.[0-9]+\.[0-9]+matcher and finds nothing → Go hitsComposeVerifyNoVersion→ exit 2.changelog_get_current_versionreturns non-zero, and undercompose-verify.sh'sset -euo pipefailthe bareNEWEST_VERSION=$(…)assignment aborts the script → exit 1 (verified:bash -c 'set -e; f(){ return 1;}; X=$(f); echo after'prints nothing, exits 1). The stderr confirms it — only the "verifying section" line, no FATAL, no gate log.So
ComposeVerifyNoVersionis reachable (the "in practice unreachable" comment is false in the quirk region), and Go/bash disagree there (2 vs 1).Why it's a should-consider, not a must-fix:
v1x0x0,v1_0_0) — the composer emits only well-formed## [vX.Y.Z], so a real composed CHANGELOG.md can't reach it. All 6 in-domain sweep cases + all 7 fixtures byte-match.Disposition — your call (I lean (a), matching this arc's disclosed-boundary discipline):
ComposeVerifyNoVersion"in practice unreachable" comment is reachable via the quirk; note that in that pathological region the exit diverges 2-vs-1 (both non-zero, cut blocked). One or two comment lines. Matches how #535/#538/#539 disclosed their boundaries.set -eaccident, so matching bash exactly would be reproducing an accident. Acceptable, but leaves the "unreachable" comment factually wrong.Either way the check logic needs no change. A cv-quirk fixture pinning the current (divergent) behavior would also document it in the harness.
Verification ledger (built / executed / reproduced — not read)
e9261e0; basev2/next@6b22ee4= current tip (#539 merged);merge_base==base(clean-ff); open, unmerged, mergeable/commits/e9261e0/status→state=success,total_count=1,status=success(actions run 2022)golangci-lint run ./...→ 0 issues;go build/vet/gofmt -l/go test -count=1 ./...all cleangates-oracle.shexecsRT_COMPOSE_VERIFY_SH --version … --changelog …(real script), propagates exit; oracleshim exec'd directlyCouplingMismatch)no-changelog: <reason>sentinel), decided #534 / impl deferred cross-repo #498; register-check--stdinon the doc → exit 0 (clean, docs/ is scanned)TestComposeVerify_Pathspins each path by Reason+Exit;TestSectionContentByVersion/TestCurrentVersiondirect — real assertionsDesign calls
currentVersion/sectionContentByVersion(not strictParser.LatestVersion) — ✅ endorse. Correct to match the bash oracle's exit on any input; the raw-interpolation quirk is faithful in-domain (with the out-of-domain divergence above flagged).Must-fix
None.
Stamp: APPROVED, head-pinned at
e9261e0. The three gates compose byte-faithfully tocompose-verify.sh— extract → coupling(#439) → gate-1-on-section + gate-2-on-whole, trivalent 2/1/0 — verified by the 7-fixture equivalence harness (real script) plus my own 7-case edge sweep (gate targeting, v-prefix, empty-section, metachar-safe all faithful); harness teeth proven by my own trivalent-exit mutation; the #498 doc ratifies Option 2 and is register-clean; gate green under real golangci + full module tests. One non-blocking should-consider: the sweep found an undisclosed 2-vs-1 exit divergence in the malformed-heading quirk region (out-of-domain — composer emits only well-formed headings — and safe-direction; both block the cut), with theComposeVerifyNoVersion"unreachable" comment falsified there. Disclose-or-leave is your call; the logic needs no change. Yours to land — this closes #534 + milestone #75.— Surveyor
e9261e01ee5b56d25debRe-stamp — APPROVED, head-pinned at
5b56d25. (4575 @e9261e0staled on the head move.)This folds the one should-consider from 4575: the undisclosed 2-vs-1 exit divergence in the malformed-heading quirk region, taken as disclose + correct the false comment, keeping Go's principled exit 2 (not matching bash's
set -eartifact).Narrow-delta verification (built / diffed — not read):
e9261e0 → 5b56d25: exactly one file differs —internal/gates/compose_verify.go. Everything else byte-identical to thee9261e0tree I fully reviewed at 4575.//comments from both revisions ofcompose_verify.goyields identical executable text (diffclean). The delta is purely two comment blocks — noconst, no function body, no regex changed. So all 7 fixtures + my own 7-case edge sweep carry forward byte-identical; no behavior re-run needed.ComposeVerifyNoVersioncomment drops the false "in practice unreachable once a section was found" and states the accurate reachability (the dots-as-any quirk on a malformed heading) + the 2-vs-1 disclosure (Go exit 2 vs bashset -eexit 1, OUT-OF-DOMAIN + SAFE-DIRECTION, exit 2 retained deliberately, anchored review 4575);sectionContentByVersioncross-refs it.compose_verify.gowhere the false comment lived — the quirk is a compose-verify exit boundary, not a fragment-coverage concern, so the #498 doc correctly stays untouched.5b56d25:golangci-lint ./internal/gates/...→ 0 issues,gofmtclean, compose-verify tests green. CI green (total=1, status=success).Phase 3 closes clean. Yours to land — #534 + milestone #75. Fragment-side #442 (register-check gating fragment content at cut) is now delivered by #538's gate; the remaining forward pieces are the Phase-6 adopter-override wiring (#435/#442/note-4) I've carried across #537/#538/#539.
— Surveyor