fix(ci): block pull requests that delete released CHANGELOG history - #3941
Merged
Yeachan-Heo merged 1 commit intoAug 6, 2026
Merged
Conversation
Removing `packages/*/CHANGELOG.md merge=union` in Yeachan-Heo#3932 was correct -- union never conflicts, it concatenates both sides of an overlapping hunk, which silently filed entries into versions that had already shipped (35 such entries audited on dev, Yeachan-Heo#3929). What it did not account for is the transition: these files now conflict on rebase for the first time, and a bad resolution drops the whole history with no marker. That is not hypothetical. Yeachan-Heo#3932 merged at 11:25:32Z. Between 11:29:29Z and 11:35:02Z, ten open pull requests across six authors force-pushed heads whose CHANGELOG was a single newline -- every released section gone. Yeachan-Heo#3920 Yeachan-Heo#3697 Yeachan-Heo#3870 Yeachan-Heo#3908 Yeachan-Heo#3887 Yeachan-Heo#3864 Yeachan-Heo#3729 Yeachan-Heo#3869 Yeachan-Heo#3866 Yeachan-Heo#3873. Nothing caught it: the files still parse, no test reads them, and the loss looks like a large deletion inside an otherwise legitimate diff. The guard asserts the one property that matters and nothing more: every `## [X.Y.Z]` heading present at the merge base must still be present at the head. Additions pass, rewording passes, and a release commit that consumes `## [Unreleased]` into a new version passes. Only losing a released section fails, and the message names the recovery command. Runs in `affected-plan`, which already checks out full history and carries the immutable event base sha, so it costs one bun invocation and needs no new job. Constraint: a release bump must still be able to add a version heading Constraint: must not depend on byte-size heuristics -- a legitimately small changelog is not a violation Rejected: threshold on deleted line count | fires on large legitimate edits and misses a small changelog emptied completely Rejected: restore merge=union | reinstates the silent misfiling this replaced, and GitHub ignores the driver anyway Confidence: high Scope-risk: narrow Reversibility: trivial Tested: bun test scripts/changelog-history-guard.test.ts (11 pass); guard run against the three real broken heads (Yeachan-Heo#3873 Yeachan-Heo#3920 Yeachan-Heo#3869) exits 1 and names the lost sections; clean range exits 0; bun run check:tools exit 0 Not-tested: a real release-bump PR end to end
This was referenced Aug 6, 2026
Contributor
Author
|
head 지금 이 가드가 잡아낼 열린 PR이 12건이다(#3942 갱신본): #3920 #3908 #3887 #3873 #3870 #3869 #3868 #3866 #3864 #3846 #3729 #3697. 그중 #3920과 #3864는 가드가 들어가면 이 상태가 CI에서 자동으로 막힌다. 그 전까지는 수동 주의가 유일한 방어다. |
Closed
5 tasks
Owner
|
This item is being moved to the Tier 3 review lane. Further review is deferred until after the current emergency stabilization period. — |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
scripts/changelog-history-guard.tsand runs it on every pull request: a PR fails if it removes any## [X.Y.Z]heading from apackages/*/CHANGELOG.md.Why — this is live damage right now
#3932 removed
packages/*/CHANGELOG.md merge=union. That removal was correct: union never conflicts, it concatenates both sides of an overlapping hunk, which silently filed entries into already-shipped versions (35 audited instances ondev, #3929).What it did not account for is the transition. These files now produce a real conflict on rebase for the first time, and a bad resolution empties them.
Ten open PRs. Six authors. Six minutes. Every one of those heads now has a one-byte changelog:
#3920 had already been approved by @Yeachan-Heo at the previous head. Nothing in CI noticed — the files still parse, no test reads them, and the loss looks like a large deletion inside an otherwise legitimate diff. I have commented the recovery command on all ten.
This is fallout from a change I authored and did not think through, so the guard is mine to add.
The check
Every released heading at the merge base must still be present at the head. That is the whole rule.
## [Unreleased]— passes.## [Unreleased]into## [0.12.13]— passes (headings are only ever added).Deliberately not a byte-size or deleted-line threshold: a threshold fires on large legitimate edits and still misses a small changelog emptied completely. The heading set is the actual invariant.
Runs in
affected-plan, which already checks out full history and carries the immutable event base sha (GITHUB_BASE_SHA), so this adds onebuninvocation and no new job. Gated ongithub.event_name == 'pull_request'.Testing
Verified against the real broken heads rather than only synthetic fixtures:
CONTRIBUTING.mdgains the pre-push self-check and the recovery command in the existing "Rebasing ontodev" section.GJC verdict
Self-approval is BLOCK.
devbun checkpasses (check:tools)