changelog-monotonic: the uniqueness half is gated behind base-side conditions it does not need #143
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#143
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
changelog-monotonic.shhas two halves. Containment (no shipped heading wasdeleted) is a property of a diff and genuinely needs the merge base.
Uniqueness (no version heading appears twice) is a property of HEAD
alone — it needs no base ref, no merge base, and no base blob.
But uniqueness sits downstream of all three. At
.github/scripts/changelog-monotonic.sh#L95-L99:
dupes=is at L118, after that. So a tree carrying a duplicate exits 0 ona message that is true about deletion and silent about the duplicate sitting in
front of it.
Reproduced
A branch that introduces
CHANGELOG.md(absent at the merge base) carrying two## 0.8.0headings, underSTRICT=1:The duplicate is right there —
grep -c '^## 0.8.0'returns 2.STRICTdoes not cover this path: it guards the twoskip()calls, and this isa plain
exit 0. Theskip()paths have the same shape for the same reason —locally (STRICT unset) a shallow clone or a non-git tree returns 0 without ever
looking at a duplicate the author is about to push.
Credit: found by
claude-bot-andresmgslreviewing the ports inheavy-duty/rig#99 and heavy-duty/cast#134, which inherited the ordering from
here.
Why it matters more than the exit code suggests
The duplicate half is not the redundant one.
release-notes.shre-arms itsgrab on every matching
##line, so a duplicated heading makes the publishedbody absorb whatever sits between the copies — that is the box#118 shape, and
the reason the uniqueness assert was added alongside containment rather than
instead of it. It is also the half with the most ways to silently not run.
Fix
A move, not a rewrite: hoist
headings_rawand thedupesblock to directlyafter the
[ -f "$changelog" ]check, abovegit rev-parse --is-inside-work-tree. The skip messages then become accurate — they would beskipping only containment, the half that actually needed the history.
Tests should pin the ordering, not just the outcome: the existing base-absent
case asserts
exit 0without asserting that uniqueness still ran. Add a casewhere the PR introduces
CHANGELOG.mdcarrying a duplicate — green today,red once the block moves.
The trigger, which is the same gap one level up
ci.ymlruns the guardif: github.event_name == 'pull_request'. That is wellargued for containment — on a push to main the merge base is HEAD, so the
assert is vacuous — but it is wrong for uniqueness, which is vacuous on no tree.
If a duplicate ever reaches main by a route other than a PR, nothing asserts it.
Dropping the
ifnaively does not work, and it fails closed in the noisiestway: on a
pusheventgithub.base_refis empty, so the argument becomesorigin/, which does not resolve, andSTRICT=1promotes that skip to a hardfailure. Verified:
So the trigger change needs a base-ref fallback:
On a PR that is
origin/<base>as before. On a push to main it isorigin/main, whose merge base with HEAD is HEAD or its parent — containmentpasses vacuously, exactly as the
ifintended, while uniqueness now runs onevery push.
Scope
.github/scripts/changelog-monotonic.sh— hoist the uniqueness block.github/workflows/ci.yml— drop theif, add the base-ref fallbacktest/release.sh— a case that pins uniqueness running when the base blob isabsent
Should land in all three repos together — rig and cast are porting this script
now (heavy-duty/rig#99, heavy-duty/cast#134) and will carry the fix in those
PRs, so box is the one that needs its own.