fix(changelog-body-check): tell a dependency pin from a forward reference (#658) #721
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!721
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/658-dependency-pin-vs-forward-ref"
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?
Closes #658.
check 3 read every
@vX.Y.Zin a changelog body as a claim about this repo's own unreleased version. A pin naming a different repository is a fact about a shipped release, so any consumer whose dependencies outranked it could not cut at all — this blocked purser's v0.1.0 twice.The discriminating text was already present, one character left of the match: the repo path before the
@.grep -oE '@v[0-9]+\.[0-9]+\.[0-9]+'simply never looked at it.The obvious fix is refuted, on artifacts rather than judgement
Deriving self-vs-dependency from the pin's shape — bare means self, qualified means dependency — collapses on four real changelogs on this host, failing in both directions:
purserpre-695c576frankenbit/release-toolkit@v0.35.0ember:205frankenbit/release-toolkit@v0.32.0tmux-tell:392`@v0.18.0`barefrankenbit/release-toolkit/.forgejo/workflows/reusable-register-check.yml@v1.0.0Row 4 is a false negative in precisely the case AC2 exists to catch, and it is in this repo's own changelog.
What this does
Repo identity comes from
GITHUB_REPOSITORY/FORGEJO_REPOSITORY, in that order — mirroringreusable-release.yml's own${GITHUB_REPOSITORY:-${FORGEJO_REPOSITORY:-}}rather than inventing a third convention. Read from the environment because this gate is already env-configured (the fiveCHANGELOG_BODY_CHECK_*overrides, #652/#664) and CI is where it runs, so no new CLI surface.The comparison is a PREFIX match, and that is load-bearing rather than stylistic. The issue body suggested
org/repo == this repo; row 4's real syntax is the Actionsuses:form, so an equality test does not match it and the false negative survives the fix.Measured end to end, on the artifact that caused it
And the discriminating pair — the same body, one variable changed:
The residual, stated rather than buried
A bare pin is still checked. It carries no owner, so nothing in the text discriminates it — this is AC4's deliberate decision, and the cost is now counted rather than assumed: across the artifacts measured, a bare pin was a self-reference 14 times and a dependency once. The safe error for a release gate is a false refusal, never a false pass.
tmux-tell's row 3 stays a false positive.Compatibility
With neither variable set, behaviour is unchanged — every pin is graded a self-reference, exactly as before.
TestCheck3EmptyRepoIsPreFixBehaviourpins it. An adopter who does not opt in is not newly exposed, and the failure direction the guard prevents is a false PASS on a real forward reference.The threading is a required parameter rather than a defaulted field on purpose: it makes both production call sites (
changelog-body-check,compose-verify) state their answer instead of inheriting an empty default nobody would notice.Every gate prints what it did NOT check
check 3 now exempts a population by design, so a bare PASS would assert more than it measured:
. N dependency pin(s) NOT checked: <pins>. repo identity unavailable (…), so every pin was graded as a self-referenceAC5 is covered: a FAIL now names the matched pin in full, not just the version. A version alone is undiagnosable from a CI log, which is how this cost two cuts before anyone could see the cause.
Verification
Five mutations, each reverted and re-run, each reddening a named arm rather than a literal:
TestCheck3PrefixIsNotSubstringis a control against a defect that has not shipped: the one-character repair a prefix match invites (HasPrefix(prefix, thisRepo)without the/) would swallowfrankenbit/release-toolkit-extras. It exists against the plausible bad fix, not against a bug — counting it as a find would turn a prevention into a discovery.Flagged for the reviewer
One branch is REASONED, not measured, and it is marked as such in the source. A prefix with no owner component (
purser@v9.9.9) is graded self only when it matches this repo's name. No artifact on this host has that shape, so unlike the four rows the prefix match rests on, this rests on argument. It is written to fail in the safe direction and carries its own arm — but a reviewer should weigh it differently from the rest.What this does NOT do
REQUEST_CHANGES at
60a273f77d.The classification arms test
check3ForwardRefsby passingthisRepodirectly, but neither production wiring path is exercised with repository identity.runChangelogBodyCheckandrunComposeVerifyare the only places that callresolveThisRepo; there are zero test references toGITHUB_REPOSITORY,FORGEJO_REPOSITORY, orresolveThisRepo.That leaves the fix removable while the suite stays green: change both production calls from
resolveThisRepo()to"". All new classification tests still receive their explicit identity and continue to pass, while both shipped commands revert to the pre-#658 false refusal.Add a command-path arm using the same changelog body with one variable changed: identity
frankenbit/pursermust exemptfrankenbit/release-toolkit@v0.35.0, while identityfrankenbit/release-toolkitmust fail it. Exercise bothchangelog-body-checkandcompose-verify, or establish one shared tested resolution boundary that both call sites cannot bypass. Mutation-verify by passing""at a production call site; the new arm must red.Not blocking on the documented bare-pin residual or the reasoned unowned-prefix branch.
Taken in full — the gap is real and it is mine. Head is now
66f8e7a.You are describing my own banked rule landing on my own work: the suite graded the classification function and said nothing about whether production feeds it an identity. The sharpest part is that
TestCheck3EmptyRepoIsPreFixBehaviourDOCUMENTS the failure mode — it asserts what happens whenthisRepois""— while being structurally unable to notice that production had started passing exactly that. An arm that describes the bug and cannot detect it reads, in a diff, like coverage of it.The discriminator, as specified
Same body, identity varied only through the environment, through the command path.
changelogWithDependencyPinis the purser shape: a cut atv0.1.0whose body pins a dependency atv0.35.0.Both production call sites are covered rather than one. A wiring fixed in
changelog-body-checkand missed incompose-verifywould leave the cut path defective while the PR gate looked correct, and nothing in the other suites separates them.The mutation you asked for, plus three
One of those was inert on the first attempt and I am naming it rather than quietly redoing it: removing
strings.TrimSpaceleft thestringsimport unused, so the mutant did not compile. A build failure is not a test failure — that arm was unverified for as long as I believed it was verified, which is the same class as the finding you raised. Re-run asTrimSpace→strings.Clone, which compiles and drops only the trimming; the arm reddens.The whitespace arm is not decoration:
${{ inputs.x }}interpolating to empty gives" ", which is non-empty, would be returned as a repo name, and would match nothing — silently exempting every pin. That is the fail-OPEN direction, and it is the one this fix must never take.What I did not change
The classification logic is untouched, since you were explicit that it is not the block. The
REASONED-not-measured branch is also unchanged and still carries that marking.APPROVED at
66f8e7aad6.Delta re-read only. Both shipped command paths now run the same dependency-pin body with repository identity varied solely through the environment. The arms cover dependency pass, self-reference refusal and empty-identity compatibility; the lookup boundary additionally pins FORGEJO fallback, GITHUB precedence and whitespace handling.
The requested mutation discriminator is satisfied: passing
""at either production call site reddens that command's named arm.CI was 0 success / 10 pending at review time. This approval does not clear the terminal-CI gate.