Refuse a pull request that files a new entry inside a shipped release - #528
Merged
Conversation
Reviewer's GuideThis PR adds a fail-closed pull-request CI check that compares versioned CHANGELOG.md sections with the base commit, preventing clean rebases or merges from silently adding entries to shipped releases while preserving legitimate release cuts and Unreleased edits. Sequence diagram for the frozen changelog pull request checksequenceDiagram
participant GitHub
participant CI
participant Git
participant Guard as changelog_frozen.py
participant Changelog as CHANGELOG.md
GitHub->>CI: pull_request
CI->>Git: git fetch --depth=1 origin BASE_SHA
CI->>Git: git show BASE_SHA:CHANGELOG.md
Git-->>CI: base CHANGELOG.md
CI->>Guard: compare base and head paths
Guard->>Changelog: sections(base, head)
alt released section differs
Guard-->>CI: exit 1 with section diff
else all shared released sections identical
Guard-->>CI: exit 0
end
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
`## [Unreleased]` is a stable heading that a release cut renames. What was
`[Unreleased]` becomes `## [0.25.0] - 2026-08-28`, and a fresh empty
`[Unreleased]` is inserted above it. A branch cut before that release carries its
entry anchored by context under the old heading, which is now the release. Git
sees lines added below a heading that still exists, resolves the merge with no
conflict, and the pull request reports MERGEABLE with every check green while a
shipped version quietly grows bullets describing fixes it never contained.
That is not a lapse of attention, which is why it gets a job rather than a note.
Over one afternoon it happened four times independently to three agents who did
not know of each other, on `wayfinder/devlaunch-{305,308,349,354}`, and twice
inside a single build on #346. Every instance was caught by somebody reading the
diff, because until now there was nothing else that could catch it. The signal a
reviewer reaches for is precisely the one that says nothing about this failure.
The rule is that a version section already present on the base branch must be
byte identical on the branch. Phrased that way it permits the one commit that
legitimately rewrites this file: a release cut adds a heading that was not there
and modifies none that was. The tempting phrasing -- that the released portion of
the file is unchanged -- is wrong on its own terms rather than merely
inconvenient, since it fails every release cut, and a guard whose first firing is
a false positive on the project's own release ritual is switched off before it
ever catches anything.
The fixture performs the actual merge rather than writing a bad changelog by
hand. A hand-written one would prove only that the parser rejects input somebody
already knew was wrong; what has to be proved is that the default outcome of the
ordinary operation is wrong, so the test asserts git reports no conflict and the
entry still lands in the wrong section.
The guard fails rather than passes when it cannot read or parse either side. A
check that has not checked anything must not report success, which is the same
species of defect as #517.
Closes #527.
blooop
force-pushed
the
fix/527-changelog-frozen-sections
branch
from
August 29, 2026 20:21
03ff5d5 to
a18f48c
Compare
This was referenced Aug 29, 2026
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.
Closes #527.
What this catches
## [Unreleased]is a stable heading that a release cut renames. What was[Unreleased]becomes## [0.25.0] - 2026-08-28, and a fresh empty[Unreleased]is inserted above it. A branch cut before that release carries its entry anchored, by context, under the old heading, which is now the release.Git sees lines added below a heading that still exists. It resolves that with no conflict, the pull request reports
MERGEABLE, every check is green, and a shipped version quietly grows bullets describing fixes it never contained.Why a CI job and not a note in AGENTS.md
Over one afternoon it happened four times independently, to three agents who did not know of each other (
wayfinder/devlaunch-{305,308,349,354}), plus twice inside a single build on #346, plus on #519 and #515 in the parallel session. Every instance was caught by a human or an agent reading the diff. Nothing automated caught any, because there was nothing that could.The trap is that the signal a reviewer reaches for,
MERGEABLE, is exactly the one that says nothing about this failure.The rule, and why it is phrased this way
A version section already present on the base branch must be byte identical on the branch.
Phrased that way it permits the one commit that legitimately rewrites this file: a release cut adds a heading that was not there and modifies none that was, so it passes by construction rather than by exemption.
The tempting phrasing, "the released portion of the file is unchanged", is wrong on its own terms rather than merely inconvenient. A release cut necessarily rewrites that region, so the first thing that guard would ever fail is the one commit that is definitionally correct, and a guard whose opening act is a false positive on the project's own release ritual gets switched off before it catches anything.
test_the_release_cut_that_creates_the_hazard_passes_the_guardpins that.The fixture is a real merge
test_a_clean_merge_across_a_release_cut_files_the_entry_inside_the_releasebuilds a repository, branches, cuts a release onmain, and performs the merge. It asserts git returns 0 (the premise is that git has no signal here, so a conflict would mean the fixture was not reproducing the bug), asserts the entry really did land under## [0.2.0], and only then asserts the guard rejects it.A hand-written bad changelog would prove the parser rejects input somebody already knew was wrong. What needed proving is that the default outcome of the ordinary operation is wrong.
It fails closed
Unreadable file, no
## [version]headings at all, or a version heading appearing twice: all exit 1 with a reason, never 0. A check that has not checked anything must not report success, which is the same species of defect as #517.Scope and cost
cijob onpull_requestonly, so the required check set is unchanged andgatealready covers it. On a push there is no base to be frozen against.--depth=1rather than switching the job's checkout tofetch-depth: 0, which would pull the whole history into every run to serve onegit show.CHANGELOG.mdalone. Deleting a section is not policed: that is loud in a diff and has never been the silent failure. What is silent is a section growing.Verification
there is no dl to test(no release binary built here) and none are in files this touches.prekclean.🤖 Generated with Claude Code
Summary by Sourcery
Protect shipped changelog releases from accidental entries introduced by clean merges across release cuts.
New Features:
Bug Fixes:
Enhancements:
CI:
Documentation:
Tests: