Release Notes & Changelogs
A changelog is a contract. Anyone depending on your software needs to know what changed before they upgrade — what broke, what's new, and what they have to do about it. Get this wrong and users either upgrade blind and get burned, or stop upgrading at all, which is worse for both of you because they're now stuck on a version you don't want to support.
The distinction worth drawing early is between a changelog and release notes. A changelog is a chronological, complete, developer-facing record: every version, every notable change, in a consistent format. Release notes are narrative and audience-facing: here's what's new in 3.0 and why you'd care. They serve different readers and shouldn't be the same document, though many projects need both.
The failure mode almost everyone starts with is the auto-generated commit dump — two hundred lines of fix: update deps and chore: lint with the one breaking change buried in the middle. It's technically complete and practically useless, because a changelog's job is to tell a reader what changed for them, not what changed in the repository.
TL;DR
- Keep a Changelog format: reverse chronological, grouped by Added / Changed / Deprecated / Removed / Fixed / Security.
- Write for humans. A commit log is not a changelog.
- Semantic versioning: MAJOR breaks, MINOR adds, PATCH fixes.
- Breaking changes need migration steps, not just an announcement. Before/after code.
- Automate with Changesets or conventional commits — the author writes the entry at PR time.
- Keep an
## [Unreleased]section so entries accumulate as work lands. - Changelog ≠ release notes. Complete and chronological vs. narrative and curated.
- Every entry answers: what changed, does it affect me, what do I do?
Quick Example
Run npx @acme/codemod v3-client-options to migrate automatically.
- Node 18 is no longer supported. Minimum is now Node 20.
Node 18 reached end of life in April 2025.
client.query()returnsResult<T>instead of throwing. Errors that
were exceptions are now returned values.
See the v3 migration guide.
Added
- Streaming responses via
client.stream()for result sets too large to
buffer (#412).
AbortSignalsupport on every request method.- TypeScript 5.6
usingdeclarations for automatic connection cleanup.
Changed
- Default request timeout raised from 5s to 30s. Long-running queries were
the single most common support issue.
- Connection pooling is now enabled by default (previously opt-in).
Deprecated
client.rawQuery()— useclient.query()with{ raw: true }.
Removal planned for 4.0.
Removed
client.legacyAuth(), deprecated since 2.4.0.- The bundled
polyfillsexport, unnecessary on Node 20+.
Fixed
- Connection leak when a request was aborted mid-flight (#398).
- Timestamps in
us-west-2were off by one hour during DST transitions (#405).
Security
- Updated
undicito 6.21.1, addressing CVE-2025-22150 (request smuggling
via malformed headers). Users on 2.x should upgrade to 2.9.4 or later.
[2.9.4] — 2026-06-30
...
[Unreleased]: https://github.com/acme/lib/compare/v3.0.0...HEAD
[3.0.0]: https://github.com/acme/lib/compare/v2.9.4...v3.0.0
Conventions that make it usable: reverse chronological order (newest first), an [Unreleased] section at the top, ISO dates, and link definitions at the bottom pointing at version comparison URLs. Consistency matters more than the specific format — a reader who knows where to look for breaking changes finds them in two seconds.
Semantic versioning
Pre-1.0 (0.x.y), anything may break at any time — which is what 0.x signals and why publishing a 1.0.0 is a commitment rather than a milestone.
The discipline that makes semver useful is being honest about what constitutes a break. A behavior change that doesn't alter the type signature is still breaking if code depending on the old behavior stops working. Changing a default value, tightening validation, and altering error messages that people parse are all breaking in practice regardless of what the types say. See API Versioning.
Changelog vs. release notes
A library needs a changelog and may not need release notes. A SaaS product needs release notes and probably doesn't publish a changelog. A developer platform needs both, and the release notes should link to the changelog for detail.
Automation
Two approaches, and the difference matters:
Conventional commits derive the changelog from commit messages:
Changesets ask the author to write the entry in the PR:
Conventional commits are lower friction and produce a changelog that reads like a commit log, because it is one. Changesets require a deliberate step and produce entries written for the reader — the author is explaining the change to a user, not describing it to a reviewer. For anything with external consumers, changesets are worth the friction.
Either way, generate the version bump and the release from the accumulated entries in CI:
Migration guides
For any major release, the changelog entry states what broke and the migration guide states how to fix it:
Time estimates and a codemod are what turn an upgrade from a project into an afternoon. Projects that ship codemods with breaking changes see dramatically higher adoption of new majors, and the reason is simply that the cost is lower.
Best Practices
Write entries for the user, not the repository
The reader doesn't care what you changed internally. They care whether it affects them.
Put breaking changes first and mark them clearly
A ### ⚠ Breaking changes section at the top of a major release, before Added. Anything a reader must act on should be impossible to miss while skimming.
Ship a migration path with every breaking change
A diff at minimum, a codemod where the transformation is mechanical. "This API changed" without showing the new form makes every consumer independently derive the same answer.
Keep an [Unreleased] section
Entries accumulate as work merges, so the release is a matter of renaming a heading and adding a date rather than reconstructing three months of history from git log — a process that reliably misses things.
Link to issues and PRs
Every entry that has one. Readers debugging a specific problem want the discussion, and the link costs nothing when it's added at PR time.
Date releases in ISO format
2026-07-14, unambiguously. 07/14/26 means different things to different readers, and a changelog has an international audience by default.
Announce security fixes clearly
A ### Security section, the CVE if there is one, the affected versions, and the minimum version to upgrade to — including backported patches for older majors. Burying a security fix among dependency bumps means the people who most need to act won't.
Deprecate before removing
Announce the deprecation in one minor release, emit a runtime warning, document the replacement, and remove in the next major. A feature that disappears without warning is a breaking change the reader had no chance to prepare for.
Common Mistakes
Auto-generated commit dumps
Breaking changes without migration steps
Run npx @acme/codemod v3-client-options.
This is the fastest way to make people stop trusting your version numbers, after which they pin exact versions and never upgrade.
No changelog at all
Writing it at release time
Mixing internal noise with user-facing changes
Internal changes with no user-visible effect don't belong in a changelog.
FAQ
Do we need a changelog for an internal service?
If anyone else depends on it, yes — other teams consuming your API need to know what changed, and "ask us in Slack" doesn't scale. For a leaf service with no consumers, deployment notes and git history are usually sufficient. The test is whether anyone outside your team would be surprised by a change.
Should the changelog be generated or hand-written?
Hybrid, and this is the practical answer for most projects. Generate the scaffolding — version numbers, dates, links, categorization — from changesets or conventional commits, and require a human-written description per change. Fully automated changelogs read like commit logs; fully manual ones don't get written.
How do we handle a monorepo?
A changelog per published package, since consumers depend on packages rather than on the repository. Changesets handles this natively — a single changeset can bump several packages, and each gets its own entry. A single repository-wide changelog forces every consumer to read about changes to packages they don't use. See Monorepos.
What counts as a breaking change?
Anything that could break code that worked before. Beyond the obvious signature changes: removing or renaming anything public, changing a default value, tightening input validation, altering error types or messages that might be parsed, changing output format, and raising the minimum runtime version. Behavior changes count even when types don't change — the honest test is "could a reasonable consumer's code stop working?"
How often should we release?
Frequently, in small increments. Small releases are easier to review, easier to roll back, and easier to write changelog entries for. Large batched releases accumulate breaking changes, make the changelog overwhelming, and concentrate risk. Continuous release on merge is workable for libraries with good automation.
Should release notes be marketing?
Some framing is legitimate — release notes are read by people deciding whether to upgrade, and explaining why a change matters is genuinely useful. What isn't legitimate is omitting breaking changes or downplaying regressions to keep the announcement positive. Keep the changelog strictly factual and complete, and let the release notes be the narrative layer on top of it.
Related Topics
- Technical Writing — The broader practice
- API Versioning — Versioning strategy beyond libraries
- READMEs — The companion front-door document
- Docs-as-Code — Changelog in the repo, generated in CI
- CI/CD — Automated version bumps and publishing
- Monorepos — Per-package changelogs
- Open Source Licensing — The other file consumers check
- Deployment Strategies — Releasing safely alongside announcing