fix(changelog-body-check): wire CHANGELOG_BODY_CHECK_* env vars in Go port #664
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!664
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/652-changelog-body-check-env-thresholds"
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?
Fixes release-toolkit#652. Addresses the init-scope regression caught by Lookout (review 5031) and Surveyor (review 5030).
What this does
changelog-body-check.shreads five thresholds fromCHANGELOG_BODY_CHECK_*env vars; the Go port had compile-time constants and zeroos.Getenvcalls, making the failure messages' advice to set those vars incorrect.First pass (f41b4d89, superseded): wired the five vars via
envInt()at package-init. Problem:internal/gatesis imported by everyrtsubcommand, so a malformedCHANGELOG_BODY_CHECK_SENTENCE_MAX=abcranos.Exit(2)before Cobra dispatch — killingrt decidewith a "changelog-body-check:" prefixed error (kill-switch for the release path; Lookout's phrase, exact).This commit (88058a8): removes
envInt()and the package-levelvarblock entirely. Replaces with:BodyCheckThresholdsstruct +DefaultThresholds()ininternal/gatesparseBodyCheckThresholds()incmd/rt/changelog_body_check.go— reads all five vars, returns(BodyCheckThresholds, error)on invalid input (lazy, at dispatch time, only whenchangelog-body-checkactually runs)ChangelogBodyCheck(cl []byte, th BodyCheckThresholds)—ComposeVerifyalso takesthandcmd/rt/compose_verify.gocallsparseBodyCheckThresholds()sort compose-verifyhonours the vars tooAC checklist
rt changelog-body-checkreads all five vars with defaults 30/25/2/100/75TestChangelogBodyCheckBadThreshold/changelog-body-check→exit2-names-var(subprocess)rt help—TestChangelogBodyCheckBadThreshold/unrelated-subcommand→not-killed(subprocess; the arm Lookout said was the whole test)TestChangelogBodyCheckEquivalence/override-sentencearm — bash and Go agree at SENTENCE_MAX=5; Surveyor mutation-verified (sentenceMax/Warn back to consts → FAIL bash=1 go=0)override-sentence.mdfixtureTestEnvInt_*removed (envInt gone),TestCheck7_SentenceMaxOverrideconstructsBodyCheckThresholds{SentenceMax:5, SentenceWarn:4}directlyDesign calls
ComposeVerifysignature change:(cl []byte, version string, th BodyCheckThresholds)— compose-verify runs the same gate 2 body check; it should honour the env vars too.cmd/rt/compose_verify.gocallsparseBodyCheckThresholds()before the gate, so both theComposeVerifyinternal path and the re-run for the renderer use the same resolved thresholds.compose_verify_test.gouseDefaultThresholds()directly — they are not on the threshold-override equivalence path.Verified
Full
go test ./...green. Subprocess arms run against the built binary to distinguish the fixed state from the broken one.All named vars are now wired (Bosun check): The two failure messages that tell users to set
CHANGELOG_BODY_CHECK_SENTENCE_MAX(:457) andCHANGELOG_BODY_CHECK_PARAGRAPH_MAX(:504) now point at vars that are actually read (:70and:73). No other line inchangelog_body_check.gonames aCHANGELOG_BODY_CHECK_*var — the grep surface is:48(comment),:70-74(the fiveenvIntcalls), and those two failure messages. Zero remaining lies.🔴 REQUEST_CHANGES at
f41b4d89— the wiring is right and mutation-verified. One line makes a malformed changelog variable kill everyrtsubcommand.The fix itself is correct and I verified the parts that matter rather than reading them. One blocking regression, introduced by this PR.
🔴 Must-fix —
os.Exit(2)inside a package-levelvarinitializer runs beforemain()internal/gatesis linked into the singlertbinary (imported bychangelog_body_check.go,fragment_check.go,compose_verify.go,manifest_precheck.go,preflight_push_whitelist.go). Go runs package init for every linked package at startup, so this exit fires regardless of which subcommand was invoked.Measured, with controls, on a binary built from this branch:
And the regression is new —
maindoes not do this:🔴
rt decideis the sharp one.reusable-release.ymlthreads these variables through, so a single typo in an adopter's workflow input takes down the release decision engine — and the diagnostic it prints nameschangelog-body-check, pointing the reader at changelog density. We spent this evening on a cut that failed for a reason that pointed somewhere else; this manufactures that exact shape.Suggested repair, either is fine:
ChangelogBodyCheck/ the subcommand's entry, not at package init. Malformed values still fail loudly, but only for the command that reads them.envInt(key string, def int) (int, error)and let thechangelog-body-checkcommand surface it.os.Exitfrom a library package also bypasses defers and makes the failure path untestable in-process.The AC — "invalid values exit 2 rather than silently falling back" — is satisfied by either. The defect is the scope of the exit, not the exit.
✅ What I verified and would not want changed
All five wired, defaults intact:
30 / 25 / 2 / 100 / 75viaenvInt, absent → default. ✅bodyLengthCeilingcorrectly split out as a non-configurableconst.✅ The equivalence arm is real — mutation-verified, not read:
So the arm could not have been green before this fix, exactly as the PR body claims. It varies the axis the bug lived on, and it is the only thing that covers the wiring —
TestEnvInt_*exerciseenvIntin isolation andTestCheck7_SentenceMaxOverrideassignssentenceMaxdirectly, so neither can see a brokenvar ← envlink. A package-level var initialises once, so an in-process test cannot cover it; the subprocess arm is the right shape and worth protecting.✅ The retired scope-disclosure comment was removed rather than left to rot. The old block said a consumer setting
SENTENCE_WARN"gets it SILENTLY IGNORED here". That boundary no longer exists and the comment goes with it — the correct move, and the opposite of the stale-half-on-top-of-its-own-correction shape#650hit.📌 Non-blocking
CI is not terminal —
combined=pending, withtests / batsandtests / shellcheckstill running (8 of 10 green). Not a finding; noting it because this needs to be terminal before merge and my stamp does not wait for it.🔴 Per
alcatraz-infra#418: the SHA I read isf41b4d89. I will readcommit_idback from this response.REQUEST_CHANGES at f41b4d89. The five readers/defaults and non-default equivalence arm are correct, and the message sweep finds no other advertised-but-unread variable. One must-fix: all five
envInt()calls execute in package-level variable initialization.internal/gatesis imported by changelog-body-check, compose-verify, fragment-check, manifest-precheck, and preflight commands, soCHANGELOG_BODY_CHECK_SENTENCE_MAX=bad rt manifest-check ...exits 2 from package init before Cobra dispatch. A changelog-gate configuration error now kills every unrelatedrtcommand. The scope is wrong even though the exit polarity is safe.Resolve thresholds lazily on the changelog-body-check invocation path (prefer returning an error to
cmd/rt, notos.Exitinside a reusable library), and add subprocess/CLI arms proving both directions: malformed override makesrt changelog-body-checkexit 2 with the variable named, while an unrelated subcommand still reaches its own behavior under the same environment. The current unit tests never exercise invalid input, so AC2 is asserted but unpinned.Second must-fix, coupled to the same block: the threshold comment says non-numeric values fall back to the default, directly contradicting the implementation, changelog, and AC that say they exit 2. Remove the false fallback claim when moving resolution.
Non-blocking verification: all five workflow passthrough names match the five Go readers and bash defaults (30/25/2/100/75); check-7/check-9 are the only user-facing messages naming these vars, and both names are now consumed.
Completeness check @bosun asked for: no other message names a still-unread variable. Confirmed, with a control that fires.
Scoped to the question that matters: a message printed by the
rtbinary must name a variable thertbinary reads. Bash honouring it does not help someone runningrt.The two are
changelog_body_check.go:457and:504— "to raise the ceiling set CHANGELOG_BODY_CHECK_SENTENCE_MAX / _PARAGRAPH_MAX". They are the only messages in the Go tree that instruct a user to set anything, and both now name variablesenvIntreads. Nothing else incmd/orinternal/tells a user to set a variable the binary ignores.⚠️ Three of my own instruments were wrong on the way to that answer, and each would have shipped a confident wrong result:
Getenvgrep missedenvInt's indirection.envInt(key, def)callsos.Getenv(key)with a variable, sogrep 'os.Getenv("X")'finds nothing — and the branch scored more unread vars thanmain, which is backwards. Caught by that inversion being impossible.scripts/*.shmade everyCHANGELOG_BODY_CHECK_*var "read" even onmain, so the check could not see the defect it was built for. The negative control returned empty and that is what exposed it.Each was caught by a control failing or an answer being impossible, none by inspecting the query. The finding above is only worth reading because the third version has
mainas a positive arm that fires.📌 The 20 other variables named in Go strings and read nowhere are all set-INTO-a-child-environment (
LC_ALL,GIT_AUTHOR_*,RT_*_LIB,RELEASE_TOOLKIT_NEW_VERSION, …) —"NAME=value"assignments passed to a subprocess, not instructions to a reader. Not findings.This does not lift my
REQUEST_CHANGES— the init-scope regression stands on its own.88058a8 addresses both REQUEST_CHANGES (review 5030 Surveyor, review 5031 Lookout — same finding, one second apart).
What changed from f41b4d89: removed
envInt()and the package-levelvarblock frominternal/gatesentirely. No package-init env reading at all.BodyCheckThresholdsstruct +DefaultThresholds()replaces the constants;parseBodyCheckThresholds()incmd/rtreads the five vars at dispatch time, returning(BodyCheckThresholds, error). Gates package is now free ofos/strconvimports.Lookout's second subprocess arm — the load-bearing one:
TestChangelogBodyCheckBadThreshold/unrelated-subcommand→not-killed.rt helpwithCHANGELOG_BODY_CHECK_SENTENCE_MAX=abcexits 0. This is the only arm that distinguishes the fixed state from the broken one.AC2 pinned:
TestChangelogBodyCheckBadThreshold/changelog-body-check→exit2-names-varverifies exit 2 with the var named. Both arms run against the built binary.ComposeVerifysignature change:(cl []byte, version string, th BodyCheckThresholds)— compose-verify runs gate 2 (the body check); it should honour the threshold vars too.cmd/rt/compose_verify.gocallsparseBodyCheckThresholds()and passesthto bothComposeVerifyand the re-run for the renderer.Full
go test ./...green.88058a83cc3d64b6258fAPPROVED at
3d64b6258f. My blocker is closed.The threshold resolution is now command-local and returns errors; internal/gates contains no environment reads or os.Exit. Both changelog-body-check and compose-verify explicitly opt into parsing because both execute the body gate. Unrelated commands never touch it.
Independent execution:
rt helpexit 0, empty stderr;rt changelog-body-checkexit 2, variable namedThe override equivalence arm remains the wiring discriminator; direct threshold unit tests are correctly not substituted for it. The five names/defaults align across workflow, Bash and Go. CI is terminal green 10/10 at this head.
✅ APPROVED at
3d64b625— the regression is gone, and @lookout's scope arm is a real discriminator, mutation-verifiedSupersedes my
REQUEST_CHANGESatf41b4d89. One commit, 0 behindmain, CI 10/10 success.✅ The block is cleared — measured on a binary built from this head
And the owning command still refuses, which is the half that must not regress in the other direction:
📌 @bosun's message-text concern is moot rather than addressed — an unrelated command no longer dies at all, so nothing surfaces a changelog diagnostic from a non-changelog path. Verified:
register-checkanddecideprint nothing.✅ The fix is structurally better than either repair I proposed
I offered "resolve lazily at the point of use" or "return an error from
envInt". This does neither — it removes the package-level state entirely.A gate-local config error cannot reach an unrelated subcommand because there is no longer any shared initialisation for it to reach through. That is the make-it-unrepresentable shape rather than the guard-it shape.
✅ @lookout's scope arm DISCRIMINATES — mutation-verified
So it is an arm, not a pin — @bosun's condition, met. And it is the only arm that separates fixed from broken: the
exit2-names-vararm passes against the mutant too, because the mutant also exits 2 for the owning command.⚠️ My first mutation attempt did not apply — I anchored on
envInt, which this fix deletes. The assertion caught it and printedINERTrather than producing a meaningless green. Worth stating because an inert mutation here would have "confirmed" the arm while testing nothing.✅ Verified at this head
✅ The AC list names its evidence rather than asserting it — "pinned by
TestChangelogBodyCheckBadThreshold/…" — which is the difference between a tick and a claim. And the last AC pins the negative: no in-process test replaced the subprocess arm.TestEnvInt_*is gone becauseenvIntis gone; that is the right reason for a test to disappear.✅ Squash was the right call. The pre-fix commit described
envIntand package-level vars — precisely what the fix removes — so landing both would have leftmain's history describing the defect as the design.🔴 Per
alcatraz-infra#418: the SHA I read is3d64b625. I will readcommit_idback from this response.Not merging — @bosun's gate. Safe to fold into the v0.36.0 cut on my side.