Skip to content

chore: replace changelog merge=union with changelog.d fragments - #501

Draft
alex-clickhouse wants to merge 3 commits into
mainfrom
chore/changelog-fragments
Draft

chore: replace changelog merge=union with changelog.d fragments#501
alex-clickhouse wants to merge 3 commits into
mainfrom
chore/changelog-fragments

Conversation

@alex-clickhouse

Copy link
Copy Markdown
Collaborator

Problem

The merge=union attributes on CHANGELOG.md / RELEASENOTES.md added in #489 don't work: GitHub ignores .gitattributes merge drivers when merging pull requests (github/community#9288), which is exactly where the conflicts happen.

It isn't the right answer locally either — union merge resolves by interleaving both sides' lines, so entries land under the wrong heading and can duplicate. It removed conflicts without producing correct output.

Approach

The fragment pattern, as used by towncrier (Python) and changesets (JS). Each change drops its own file in changelog.d/, so two PRs add two distinct paths and git has nothing to reconcile. No merge driver required.

dotnet run scripts/changelog.cs -- --new fixes 512-variant-null

scripts/changelog.cs is a .NET 10 file-based app — no dependencies, ~280 lines, 0.33s warm. It lives outside every project directory, so the solution build ignores it.

Command
--new <category> <name> scaffold a fragment
--check CI gate
--render preview the pending Unreleased section
--release <version> fold fragments in, regenerate notes, delete them
--sync-notes regenerate RELEASENOTES.md

RELEASENOTES.md is now generated

The two files were byte-identical over their overlap apart from three drifted lines — RELEASENOTES.md was just CHANGELOG.md truncated at v1.0.0. So every PR was writing the same prose twice, doubling the conflict surface for no extra information.

CHANGELOG.md is now the single source of truth; RELEASENOTES.md is generated from it (it ships in the package via PackageReleaseNotes). CI fails if it drifts.

The two behave oppositely at release time: CHANGELOG is prepended to and never rewritten; RELEASENOTES is fully replaced, so sections fall off the bottom when ReleaseNotesFloor is bumped at a major.

Verification

Fragment bodies were extracted by line number, not retyped. The round-trip is proven: --release reproduces main's Unreleased section with zero prose lines changed — a sorted-line diff shows only the two added header lines.

That mattered for one non-obvious reason: the 158-line v1.0.0 section uses bare --- lines as horizontal rules, which a naive "line followed by ---" parser reads as phantom sections. The header rule also requires a version-shaped title; those rules survive --release intact.

Also covered: all four fragment-validation failures, unknown category, the double-apply guard, and the version floor (0 pre-1.0 sections in RELEASENOTES, 3 retained in CHANGELOG).

Reviewer notes

  • Entries reorder. Sorting by PR number moves 7 improvement bullets within their section. Identical as a set; ordering is now deterministic instead of merge-order.
  • The empty Unreleased stub is dropped from RELEASENOTES. Since --check keeps that section empty, the generated notes would otherwise ship a stub header inside the .nupkg. It now opens on the newest released version.
  • One content change reaches the shipped notes. RELEASENOTES.md regains (see ClickHouse.Driver.Common/Vendor/K4os) on the LZ4 entry — the single place the two files intentionally diverged. Flagging it explicitly since it's a repo-internal path; say the word and I'll reword the CHANGELOG entry instead.
  • Known gap: CI can't detect "behavioral change with no changelog entry" — a PR touching only source doesn't trip the path filter. Still a reviewer responsibility, which the updated review skill now asks about explicitly.

Docs updated in CONTRIBUTING.md, AGENTS.md, changelog.d/README.md, and both review-skill mirrors.

🤖 Generated with Claude Code

GitHub ignores .gitattributes merge drivers when merging pull requests
(github/community discussion #9288), so the CHANGELOG.md / RELEASENOTES.md
merge=union added in #489 never took effect where the conflicts actually
happen. Union merge is not the right answer locally either: it resolves by
interleaving both sides' lines, which lands entries under the wrong heading.

Replace it with the fragment pattern used by towncrier and changesets. Each
change drops its own file in changelog.d/, so two pull requests add two
distinct paths and git has nothing to reconcile. scripts/changelog.cs folds
them into CHANGELOG.md at release time.

CHANGELOG.md is now the single source of truth. RELEASENOTES.md is generated
from it by dropping every section below v1.0.0 and the empty Unreleased stub,
which removes the duplicated prose both files carried (they were byte-identical
over their overlap apart from three drifted lines) and the need to write every
entry twice.

The 23 pending Unreleased entries are migrated to fragments named after the
pull request that introduced them. Their bodies were extracted by line number
rather than retyped: --release reproduces the section with no prose line
changed, only reordered by PR number within each section.

scripts/changelog.cs is a .NET 10 file-based app with no dependencies, and
lives outside every project directory so the solution build ignores it.

Co-Authored-By: Claude <noreply@anthropic.com>
@alex-clickhouse
alex-clickhouse force-pushed the chore/changelog-fragments branch from 7fac529 to 198560c Compare August 3, 2026 12:42
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the previously attempted merge=union approach for changelog conflict avoidance with a fragment-based workflow (changelog.d/), backed by a new scripts/changelog.cs tool and updated docs/review guidance. This fits the repo by making changelog updates conflict-free across concurrent PRs and by generating RELEASENOTES.md from CHANGELOG.md as a single source of truth.

Changes:

  • Add scripts/changelog.cs to scaffold, validate, render, and release changelog.d/ fragments, and to generate/sync RELEASENOTES.md.
  • Move existing “Unreleased” prose out of CHANGELOG.md / RELEASENOTES.md into per-change fragment files under changelog.d/.
  • Update contributor/agent docs and review-skill checklists to require fragments and treat RELEASENOTES.md as generated.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/changelog.cs New tool to manage changelog fragments and generate release notes.
CHANGELOG.md Removes “Unreleased” body so entries come only from fragments.
RELEASENOTES.md Drops “Unreleased” stub; keeps release notes content generated from changelog.
changelog.d/README.md Documents the fragment workflow and naming/categories.
changelog.d/496-getschema-columns-where-clause.fixes.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/495-getschema-columns-dispose-command.fixes.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/482-typed-array-reads.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/481-small-tuple-construction.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/480-presize-map-read-dictionary.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/477-midstream-exception-detection.fixes.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/474-insertoptions-copy-drops-options.fixes.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/457-request-uri-allocations.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/453-dynamic-type-singletons.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/451-pooled-response-read-buffer.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/448-fixed-size-read-allocations.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/446-schema-table-numeric-scale.fixes.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/444-insert-serializer-buffer.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/442-stream-insert-payload.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/442-memorystreammanager-obsolete.deprecations.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/441-empty-http-error-body.fixes.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/437-pooled-compressor-write-buffer.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/434-poco-insert-boxing.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/432-decimal-write-allocations.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/431-builtin-lz4-codec.features.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/427-pluggable-insert-compression.features.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/420-variant-write-type-lookup.improvements.md Changelog fragment extracted from prior Unreleased entry.
changelog.d/392-useformdataparameters-connection-string.improvements.md Changelog fragment extracted from prior Unreleased entry.
CONTRIBUTING.md Adds contributor guidance for fragments and release workflow usage.
AGENTS.md Updates agent guidance to use fragments and the new CI gate.
.gitattributes Removes reliance on changelog merge drivers; documents fragment approach.
.github/workflows/changelog.yml Adds CI validation/rendering for fragments and generated release notes.
.claude/skills/review/SKILL.md Updates review checklist to require fragments and treat notes as generated.
.agents/skills/review/SKILL.md Same review-checklist update (mirrored skill).

Comment thread scripts/changelog.cs
Comment thread CONTRIBUTING.md Outdated
Comment thread RELEASENOTES.md Outdated
alex-clickhouse and others added 2 commits August 3, 2026 15:22
Both outputs of the release workflow are permanent: a NuGet package can only
be delisted, never unpublished, and RELEASENOTES.md is baked into the package
via PackageReleaseNotes. So dispatching a release before the changelog.d/
fragments have been folded in ships a package whose notes describe the previous
version, silently, with nothing to roll back.

Add `--verify-release <version>`, which asserts no fragments are still pending,
that CHANGELOG.md's newest released section is the version being released, and
that RELEASENOTES.md is in sync. Run it as the first job in release.yml, gating
build (and therefore sign, push and the GitHub release).

It compares base versions rather than section titles, because release
candidates are cut against the section for the version they lead up to --
1.3.0-rc1 shipped against the v1.3.0 section, so an exact title match would
have blocked it. Release inputs are also bare (1.3.0) while changelog sections
carry a v prefix, which the existing version normalization already bridges.

The version reaches the script through the environment rather than being
interpolated into the run script, so the workflow_dispatch input cannot inject
shell.

Co-Authored-By: Claude <noreply@anthropic.com>
Addresses the Copilot review on #501.

The fragment-body validation accepts both '* ' and '- ' as the leading
bullet, but the failure message named only '* ', understating what passes.

The stray double space before "Note:" was fixed in CHANGELOG.md rather
than in RELEASENOTES.md, since the latter is now generated and would be
overwritten by --sync-notes; the notes are regenerated here instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants