Problem
When 5+ PRs land in a single day, every one touches CHANGELOG.md (added an entry to the [Unreleased] block), AGENTS.md ("Last verified against main"), and cmd/sin-code/main.go (subcommand registration). Every rebase produces a 3-way merge conflict in those files. The current mitigation (manual marker-stripping) works but is per-PR hand-work.
Why
The Linux kernel, Python, Rust, and Postgres all use a fragments/ or CHANGELOG.d/ directory with one file per PR, then an aggregator script builds the final CHANGELOG.md. The result:
- No merge conflicts in CHANGELOG.md (every PR adds a new file, not edits)
- The final output is deterministic (sort order = PR number or file name)
- Reviewers can see one PR's changelog entry in isolation
Proposal
Layout
CHANGELOG.d/
├── _template.md # the format spec
├── 218-lifecycle.md
├── 219-triage.md
├── 220-grill.md
└── ...
CHANGELOG.md # generated, do not edit directly
scripts/build_changelog.sh
Per-PR fragment format
<!-- one file per PR, named <PR>-<slug>.md -->
### Added — <feature name> (PR #<n>)
- <bullet 1>
- <bullet 2>
- <bullet 3>
Aggregator
$ scripts/build_changelog.sh
# Concatenates CHANGELOG.d/*.md (sorted by PR number),
# prepends the static header from CHANGELOG.md, writes the result.
Pre-commit hook
$ cat .git/hooks/pre-commit
#!/bin/sh
exec scripts/build_changelog.sh --check
# Exit 1 if CHANGELOG.md is out of sync with CHANGELOG.d/.
Scope
- v0: implement
scripts/build_changelog.sh + pre-commit hook + migrate the existing [Unreleased] block to fragments
- v1: extend to
AGENTS.md (the "Last verified against main" line)
- v1: extend to
cmd/sin-code/main.go subcommand list (a static list + a generated test)
Acceptance criteria
scripts/build_changelog.sh produces byte-identical CHANGELOG.md from the existing [Unreleased] block + a few new fragments
- Pre-commit hook refuses a PR that adds a
CHANGELOG.d/ file but does not run the aggregator
- Two simultaneous PRs each adding one fragment can be rebased without conflicts
- The operator workflow is:
echo '### foo' > CHANGELOG.d/222-foo.md && git add CHANGELOG.d/ (one file, no conflict possible)
Estimate
~2 hours for the spike + script + hook. Then a migration PR. Then a follow-up for AGENTS.md / main.go.
Backwards compatibility
The aggregator preserves the existing CHANGELOG.md format. Old PRs that edited CHANGELOG.md directly are grandfathered; new PRs use fragments. The migration PR converts the existing [Unreleased] block into one CHANGELOG.d/_unreleased.md so the initial state is clean.
Problem
When 5+ PRs land in a single day, every one touches
CHANGELOG.md(added an entry to the[Unreleased]block),AGENTS.md("Last verified against main"), andcmd/sin-code/main.go(subcommand registration). Every rebase produces a 3-way merge conflict in those files. The current mitigation (manual marker-stripping) works but is per-PR hand-work.Why
The Linux kernel, Python, Rust, and Postgres all use a fragments/ or CHANGELOG.d/ directory with one file per PR, then an aggregator script builds the final
CHANGELOG.md. The result:Proposal
Layout
Per-PR fragment format
Aggregator
Pre-commit hook
Scope
scripts/build_changelog.sh+ pre-commit hook + migrate the existing[Unreleased]block to fragmentsAGENTS.md(the "Last verified against main" line)cmd/sin-code/main.gosubcommand list (a static list + a generated test)Acceptance criteria
scripts/build_changelog.shproduces byte-identicalCHANGELOG.mdfrom the existing[Unreleased]block + a few new fragmentsCHANGELOG.d/file but does not run the aggregatorecho '### foo' > CHANGELOG.d/222-foo.md && git add CHANGELOG.d/(one file, no conflict possible)Estimate
~2 hours for the spike + script + hook. Then a migration PR. Then a follow-up for AGENTS.md / main.go.
Backwards compatibility
The aggregator preserves the existing
CHANGELOG.mdformat. Old PRs that editedCHANGELOG.mddirectly are grandfathered; new PRs use fragments. The migration PR converts the existing[Unreleased]block into oneCHANGELOG.d/_unreleased.mdso the initial state is clean.