Repository navigation
chore(changelog): every parallel PR conflicts on CHANGELOG.md; move [Unreleased] entries to per-PR fragments under changelog.d/ #821
Description
Problem
Every pull request that changes what a release ships must add a hand-written entry under
## [Unreleased] in CHANGELOG.md (.github/workflows/changelog-check.yml →
internal/tools/changelogcheck). At release, .github/workflows/changelog-promote.yml →
scripts/promote-changelog.sh renames [Unreleased] to the tag. The entries are prose and stay
hand-written: git-chglog was removed in #646/#647 for regenerating the file from commit subjects,
and nothing here brings generation back.
The cost is where the entries go. Every PR edits the same few lines of the same [Unreleased]
subsection, so any two PRs open at the same time conflict when the second one merges:
- 2026-10-26: after fix(redaction): write overlapping findings once, over their union #811 merged, fix(secrets): scope the line pre-check veto to benign manifest lines #812 and fix(redaction): fail a cut-short redaction on every sink, keep partials #813 each needed a fix-up commit for
CHANGELOG.md
(a25de4c"rebase secrets: a "url", "checksum" or version/boolean member anywhere on a line skips token and keyword detection on that line #765 onto main after fix(redaction): write overlapping findings once, over their union #811", which also redid the goldencorpus census snapshot;
d5a395c"place the redaction, pkg/scan: when coverage is cut short, redaction paths return partial output without failing #770 bullet under Bug Fixes after the rebase"). - 2026-10-27: fix(redaction): fail a cut-short redaction on every sink, keep partials #813 against fix(secrets): scope the line pre-check veto to benign manifest lines #812 again. A plain three-way merge of fix(redaction): fail a cut-short redaction on every sink, keep partials #813's branch with main after
fix(secrets): scope the line pre-check veto to benign manifest lines #812 leaves one conflict hunk inCHANGELOG.md; the merge commit was resolved by the local union
driver.
.gitattributes already sets CHANGELOG.md merge=union, and that does not help on GitHub, whose
merge and conflict check ignore custom merge drivers
(keep-a-changelog#478;
rubocop f689210:
"GitHub doesn't support the feature"). rubocop's current process is one file per entry under
changelog/, "merged into CHANGELOG.md at release time"
(CONTRIBUTING.md, "Creating changelog entries").
Proposal
One fragment file per PR under changelog.d/, rendered into CHANGELOG.md when the release tag is
pushed. Two PRs add two different files, so they cannot conflict. The prose and the headings are
unchanged, and promotion stays additive: every bullet line is copied byte for byte.
Fragment format (documented in changelog.d/README.md). File changelog.d/NNNN-slug.md: NNNN
is the issue number zero-padded to four digits, the slug is lowercase kebab-case. Content: one
canonical heading (### ⚠️ Breaking Changes, ### 🔒 Security, ### 🚀 Features,
### 🐛 Bug Fixes, ### ⚡ Performance, ### 📚 Documentation, ### 🛠 Build System,
### 📦 Code Refactoring, ### ✅ Tests, ### 🔨 Internal), one blank line, then - bullets with
continuation lines indented two spaces, no blank line between bullets, a single final newline, and
nothing else. A PR that changes nothing a release ships needs none; the no-changelog label stays
the explicit opt-out.
Migration. The four entries under [Unreleased] today (#765, #773, #770, #799) move into four
fragments, byte for byte. ## [Unreleased] keeps its heading, because the promotion anchors on it,
and gets one paragraph pointing at changelog.d/.
Rendering. A new Go tool, internal/tools/changelogrender, parses the fragments (rejecting
anything it does not recognise), sorts them by file name and emits a ## [vX.Y.Z] - date section
with the headings in the canonical order above. scripts/promote-changelog.sh keeps its CLI, its
exit codes, its idempotence and its hard failure on a missing anchor; it calls the renderer,
inserts the section below the [Unreleased] block, git rms the rendered fragments, and refuses to
write a result whose line count it cannot account for. Old-style bullets still under [Unreleased]
(a PR that merged with one) are merged in by heading, ahead of the fragments.
make changelog-preview prints the section the next release would add.
Check. changelogcheck requires an added, valid fragment instead of an [Unreleased] edit
(same exemptions), validates every fragment a PR adds or edits, and fails a PR that adds entry lines
under [Unreleased]. The after-merge job goes: it existed because a release could move an open
PR's entry into the section that release had just promoted, and a fragment is a file of its own
that no release moves.
Docs: CONTRIBUTING.md, docs/RELEASE_PROCESS.md, the PR template, both workflow headers.
Activity