Release Notes as Code — Turn Changelogs Into a Reliable Product Pipeline
Every team writes release notes. Almost every team writes them poorly. The reason is structural: release notes are usually written after the fact, by the wrong people, from incomplete memory, hours before a deadline. The output is what you’d expect.
The fix is treating release notes the same way you treat any other piece of versioned content: they belong in the repo, they belong to the PR that introduces them, and they should be assembled by a machine.
What we do
Every PR that ships user-visible behavior includes a release note fragment as part of the change. The fragment is a small, structured file checked in alongside the code:
---
type: feature # feature, fix, change, deprecation, security
audience: customer # customer, internal, developer
summary: Reports now export as Parquet in addition to CSV.
ticket: PROD-1487
breaking: false
---
We’ve added Parquet export to the reports API. Existing CSV
exports continue to work unchanged. To switch, set
format=parquet on the export call.
CI rejects PRs that touch user-facing code without a fragment, or
labels them internal-only if the author asserts that.
At release time, a build step concatenates fragments by version,
groups them by type, and produces:
- The public CHANGELOG.md.
- The customer email release notes (audience=customer).
- The internal release tracker entry (everything).
- The in-app changelog modal content.
One source, four outputs. All deterministic. All reproducible from git.
Why this works better than a Friday writeup
The information is fresh. The author writes the note while they remember why they wrote the code. Not a week later, not from someone else’s commit messages.
Categorization is correct. Authors mark their own type and audience. They know if a thing is a fix or a feature; the release manager doesn’t.
It’s reviewable. The release note fragment goes through code review with the code. Reviewers catch marketing-speak, ambiguity, and missed breaking-change flags before merge, not after.
It’s reproducible. Need to rebuild the changelog for v1.4.2 because someone deleted the public page? Check out the v1.4.2 tag and re-run the build. Done.
The snags we hit
Internal-only PRs. A lot of PRs aren’t user-visible. Don’t force fragments on those — let authors mark them as internal in the PR description and skip the requirement. A bot-enforced “fragment OR internal label” rule is enough.
Multi-audience entries. Some changes matter to both customers and internal teams. Allow multiple audiences per fragment; the build step deduplicates at output time.
Breaking changes. The breaking: true
flag should fail CI unless a separate migration doc is also
present. Breaking changes that ship without migration notes are
how customer trust gets eroded.
Empty release windows. If a release has no user-visible fragments, don’t ship a vague “various improvements and bug fixes” — ship nothing. Silence is more credible.
Tooling reality
Several open-source tools do this well already (Towncrier, Reno, Changie). You don’t need to build the assembler yourself. You do need to build the discipline: enforce the fragment in CI, review fragments like code, run the build step on every release.
The deeper point
Changelogs are a customer-facing surface. They’re how users learn what changed and decide whether to upgrade. Treating them as a last-minute writeup signals that you don’t take that surface seriously. Treating them as a build artifact signals that you do.