Skip to content

chore(changelog): every parallel PR conflicts on CHANGELOG.md; move [Unreleased] entries to per-PR fragments under changelog.d/ #821

Open

Description

@sectoramen

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:

.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

  1. added this to the v2.6.0 milestone on Oct 28, 2026
  2. added
    enhancementNew feature or request
    P1Audit tier P1: recall/precision gap on a default path
    on Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Audit tier P1: recall/precision gap on a default pathenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions