changelog-armed.sh does not notice a deleted release heading — a PR can erase a shipped section and CI stays green #122

Closed
opened 2026-07-19 23:43:27 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-19 23:43:27 +00:00 (Migrated from github.com)

Found in review of #118 (both claude-bot-andresmgsl and grok-bot-andresmgsl caught it; CI did not).

What happened

#118 added a ### Fixed entry under ## Unreleased. The edit replaced the line ## 0.8.0 — 2026-07-19 instead of inserting above it. Net effect on that branch: the 0.8.0 heading was gone, and its entire ### Added body — the whole shipped 0.8.0 record — was silently absorbed into ## Unreleased.

Caught in review and fixed. Nothing shipped. But nothing in CI objected.

The gap

.github/scripts/changelog-armed.sh was run against that exact tree and exited 0:

changelog-armed: VERSION '0.8.1-dev' agrees with the top section (Unreleased)

It is correct on its own terms — it asks only whether the top section agrees with VERSION. Deleting ## 0.8.0 leaves ## Unreleased on top, so the invariant it checks still holds. The guard is not wrong; it is narrow.

release-notes.sh would then have failed to extract 0.8.0 — but only at the next release that asked for it, long after the merge, with no obvious link back to the PR that caused it.

Why it matters more than one near-miss

This is the same failure shape the changelog work has been chasing (#108, and heavy-duty/rig#66): a changelog edit that git merges cleanly, that no human diff-reads carefully, and that no check objects to — where the damage only surfaces at release time. #108 was about a missing ## Unreleased; this is about a vanished release section. Same class, opposite end.

Consequences if it had merged:

  • 0.8.0 loses its extractable notes anchor — release-notes.sh extracts by heading.
  • The next release cut from that state republishes all of 0.8.0's notes as if they were new.

Suggested fix

A monotonicity assert: the set of ^## X.Y.Z headings on a PR head must be a superset of the set on the merge base. Release headings are append-only — one is added by the ceremony (#96) and none is ever legitimately removed, so the rule has no false positives to tune. It also naturally covers the rename case (a stamp rewriting ## Unreleased## X.Y.Z adds a heading, removes none).

This is a class check in the same shape as eof_guard_sweep (#112) and #118's own sweep-totality assert: guard the state, not the instance.

Worth checking whether the siblings want the same guard — heavy-duty/rig and heavy-duty/cast both extract release bodies by heading the same way.

Refs

  • #118 (where it was caught), #108/#110 (the arming guard), #96 (release ceremony), heavy-duty/rig#66 (the sibling incident)
Found in review of #118 (both `claude-bot-andresmgsl` and `grok-bot-andresmgsl` caught it; CI did not). ## What happened #118 added a `### Fixed` entry under `## Unreleased`. The edit **replaced** the line `## 0.8.0 — 2026-07-19` instead of inserting above it. Net effect on that branch: the 0.8.0 heading was gone, and its entire `### Added` body — the whole shipped 0.8.0 record — was silently absorbed into `## Unreleased`. Caught in review and fixed. Nothing shipped. But nothing in CI objected. ## The gap `.github/scripts/changelog-armed.sh` was run against that exact tree and exited **0**: ``` changelog-armed: VERSION '0.8.1-dev' agrees with the top section (Unreleased) ``` It is correct on its own terms — it asks only whether the **top** section agrees with `VERSION`. Deleting `## 0.8.0` leaves `## Unreleased` on top, so the invariant it checks still holds. The guard is not wrong; it is narrow. `release-notes.sh` would then have failed to extract 0.8.0 — but only at the next release that asked for it, long after the merge, with no obvious link back to the PR that caused it. ## Why it matters more than one near-miss This is the same failure *shape* the changelog work has been chasing (#108, and heavy-duty/rig#66): **a changelog edit that git merges cleanly, that no human diff-reads carefully, and that no check objects to** — where the damage only surfaces at release time. #108 was about a missing `## Unreleased`; this is about a *vanished release section*. Same class, opposite end. Consequences if it had merged: - 0.8.0 loses its extractable notes anchor — `release-notes.sh` extracts by heading. - The next release cut from that state republishes all of 0.8.0's notes as if they were new. ## Suggested fix A monotonicity assert: the set of `^## X.Y.Z` headings on a PR head must be a **superset** of the set on the merge base. Release headings are append-only — one is added by the ceremony (#96) and none is ever legitimately removed, so the rule has no false positives to tune. It also naturally covers the rename case (a stamp rewriting `## Unreleased` → `## X.Y.Z` adds a heading, removes none). This is a class check in the same shape as `eof_guard_sweep` (#112) and #118's own sweep-totality assert: guard the state, not the instance. Worth checking whether the siblings want the same guard — heavy-duty/rig and heavy-duty/cast both extract release bodies by heading the same way. ## Refs - #118 (where it was caught), #108/#110 (the arming guard), #96 (release ceremony), heavy-duty/rig#66 (the sibling incident)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#122
No description provided.