A PR can delete a shipped release heading and CI stays green — port box#122's monotonicity guard #123
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:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#123
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?
Port of heavy-duty/box#122, filed per that issue's closing note ("Worth
checking whether the siblings want the same guard") and #126's port verdict.
cast is the yes-with-a-caveat of the two siblings — read the caveat
section before treating this as a copy job.
The class
A changelog edit that deletes a shipped
## X.Y.Zheading — absorbing itsbody into
## Unreleased— merges cleanly, produces no conflict, and nocheck objects. The damage surfaces only at the next release that asks for
that section, long after the merge, with no obvious link back to the PR that
caused it.
box caught a live near-miss (box#118 replaced
## 0.8.0 — 2026-07-19instead of inserting above it — the entire shipped 0.8.0 record silently
absorbed into
## Unreleased). Caught in review; nothing shipped;nothing in CI objected.
Why cast is exposed
.github/scripts/release-notes.shextracts the release body by heading, and.github/workflows/release.yml:164makes it the published release notes. Adeleted heading fails exactly the way it would in box: the extraction finds
nothing at release time, and a release cut from that state republishes the
wrong section.
This is not a straight copy, for the same structural reason as rig: box
extends a family that already includes
.github/scripts/changelog-armed.sh.cast has no CI-side changelog guard — arming is enforced from the test
suite (
test/release.test.ts, keyed to the version, per CONTRIBUTING). Sothe port decides where the guard lives rather than which script it joins.
The caveat — settle this before importing the premise
The rule box uses is: the set of
^## X.Y.Zheadings on a PR head must be asuperset of the set at the merge base. Its "no false positives to tune"
claim rests entirely on release headings being append-only.
box#126 flags that cast is where that premise most needs checking: cast#108
was one of the two reverted unconditional
## Unreleasedguards. Thathistory is the reason to confirm cast's ceremony shape matches box's before
porting the append-only rule. Concretely — if cast ever legitimately rewrites
or removes a released heading, the premise does not transfer, and the port
needs a different design, not a copy. The failure mode of getting this
wrong is well documented in this family: a guard that fires on the ceremony's
own tree makes the release unshippable, which is precisely why cast#108 and
rig#44 were reverted.
So step one of this issue is a finding, not an implementation: does cast's
release ceremony only ever add
## X.Y.Zheadings?What the port costs, if the premise holds
.github/workflows/ci.yml:12is a bareactions/checkout@v4— depth 1, no base history, so there is no merge baseto compare against today. box chose
fetch-depth: 0over a narrowgit fetch origin <base>because getting that subtly wrong on fork PRsdegrades to a silent skip — the exact failure mode the guard prevents.
in CI (box uses
CHANGELOG_MONOTONIC_STRICT=1).test/release.shrather than folding the check intochangelog-armed.sh,because its existing cases run against constructed trees that are not git
repos — worth checking whether cast's
release.test.tshas the sameproperty before choosing where the new tests live.
pull_request-only: on a push to main the merge base is HEAD and theassert is vacuous.
Blocked
On heavy-duty/box#126 landing — so this ports the reviewed design rather
than the proposed one.
Refs
heavy-duty/box#122 (the defect), heavy-duty/box#126 (implementation + port
verdict), heavy-duty/box#118 (the near-miss), #108 (cast's reverted
unconditional guard — the caveat), heavy-duty/rig#66 (the sibling incident)
Closing — satisfied by the ceremony conversion (#143).
The guard this issue asked to port now runs on every PR:
.github/workflows/ci.ymlinvokesheavy-duty/ceremony/actions/changelog-monotonic@0.1.0, and states the covered class in as many words — "no shipped heading deleted or duplicated", compared against the merge base. That is box#122's rule, arrived by a different door: the named blocker (heavy-duty/box#126) landed the reviewed design, ceremony absorbed it into the shared machinery, and cast now consumes it by pin instead of carrying a local port.The caveat this issue insisted on — whether cast's release ceremony only ever adds
## X.Y.Zheadings — is settled the same way: the append-only premise is ceremony's now, exercised on its own releases, rather than a local re-derivation in the cast#108 shape whose revert prompted the caveat.No cast-side work survives this, so nothing is re-minted. If the guard ever misfires on a legitimate release tree here, that is a bug against heavy-duty/ceremony, filed there.