Release Notes and Changelogs: How to Write Ones People Actually Read

A changelog records what changed; release notes explain what it means for the reader. Most teams write one document trying to be both, and it ends up serving neither. Here is how to split them, what belongs in each, and how to write an entry someone can act on.

A weathered ledger lying open at a page of handwritten entries, a quill resting across it.

A changelog and a set of release notes are different documents with different readers. The changelog is a complete, dated, machine-friendly record of what changed, written for someone deciding whether to upgrade and what will break. Release notes are a curated narrative for someone deciding whether to care. Write both, keep them in the same repository, and never let one pretend to be the other.

The failure mode is familiar: a page called “Changelog” containing eleven bullet points per release, nine of which are internal refactors, and none of which say whether the upgrade is safe. Readers stop opening it within two releases.

Changelog or release notes: which are you writing?

ChangelogRelease notes
ReaderSomeone about to upgradeSomeone deciding whether to
Question it answers“What changed, and will it break me?”“Is there anything here for me?”
CoverageComplete. Every user-visible change.Selective. Three to five things that matter.
ToneTerse and factual. One line per change.Explanatory. Screenshots, examples, links.
LivesIn the repository, next to the code.On the blog or the docs site.
PublishedEvery release, without exception.When there is something worth saying.

If you only have capacity for one, write the changelog. It is the one with the legal-adjacent function: it is what somebody reads at eleven at night when a deployment has gone sideways.

Use a format, and use the same one every time

Keep a Changelog is the de facto standard, and its value is entirely in being conventional. Readers know where to look, tooling knows how to parse it, and nobody has to invent a category scheme.

markdownCHANGELOG.md
## [4.2.0] — 2026-08-19

### Added

- Bulk export of a collection as a single Markdown archive.

### Changed

- `GET /v1/documents` now returns `next_cursor` instead of `page`.
  The `page` parameter keeps working until 5.0.

### Fixed

- Search skipped documents whose title contained an em dash.

Six section headings cover everything: Added, Changed, Deprecated, Removed, Fixed, Security. Resist adding more. A seventh category is almost always a sign that an entry is being written for the team rather than for the reader.

What makes an entry useful

The test for every line is whether a reader can act on it without opening anything else. Four rules get you there.

  1. Describe the change from outside the code. “Search now matches on the document title”, not “refactored the indexer to weight titles.” Nobody outside your team knows what the indexer is.
  2. Name the thing that changed, exactly. The endpoint, the flag, the setting, the CLI argument. The reader is going to search this page for a string, and it should be the string in their own code.
  3. Say what happens to existing usage. Does the old form keep working? Until when? This one sentence is the reason the document exists.
  4. Link the detail, do not include it. One line in the changelog, a link to the guide or the issue for anyone who wants more.
Write the entry with the pull request

A changelog assembled at release time from commit messages reads like commit messages. Requiring an entry in the pull request that makes the change costs the author two minutes while the change is still fresh, and it means somebody reviews the wording alongside the code.

Breaking changes deserve their own treatment

A breaking change is not just another entry under Changed. It is the reason someone is reading, and it needs four things a normal entry does not.

Every breaking change needs all four

What breaks, named precisely. What to do instead, with a before-and-after snippet. How to find every affected call site: a grep pattern, a lint rule, a deprecation warning already shipped in a previous release. And when the old behaviour stops working, as a version number rather than a vague “in a future release”.

Announce breaking changes twice: once as a deprecation, in a release where the old behaviour still works, and once as a removal. A breaking change that appears for the first time in the release that ships it has cost your readers the chance to prepare, whatever the semantic version number says.

How often should you publish?

The changelog gets an entry every release, including the boring ones: a release with no entry is indistinguishable from a release nobody documented. Release notes are published when there is something to say, which for most products is monthly or quarterly rather than per release.

A changelog entry for a release with nothing in it still tells the reader something: that you looked.

Batching release notes has a second benefit. Three months of small improvements described together read as momentum; the same improvements announced individually read as noise, and readers unsubscribe.

A workflow that survives contact with a release

  1. Keep an ## [Unreleased] section at the top of the changelog. Every pull request that changes user-visible behaviour adds its line there.

  2. At release, rename the heading to the version and the ISO date. This takes ten seconds and is the entire release-time changelog work.

  3. Read the new section as a stranger. Anything you cannot act on gets rewritten or deleted. Internal refactors that changed nothing observable get deleted.

  4. Pull the three most interesting entries into release notes, and give each one a paragraph, an example and a link.

  5. Check the deprecation ledger. Anything scheduled for removal in this version either gets removed and documented, or gets its date moved and that gets documented too.

Common questions

Should the changelog live in the repository or on the website?

Both, from one source. The file in the repository is what contributors edit and what tooling reads; the site renders it so that readers who do not use the repository can find it, and so it can be linked from a release banner. Generating the page from the file keeps them honest, because maintaining two copies guarantees they diverge.

Do we include entries for changes nobody can see?

No. Internal refactors, dependency bumps with no behavioural effect and test changes belong in the git history, which is where somebody looking for them will look. Every line in the changelog costs the reader attention, and lines that cannot be acted on spend it for nothing.

Can we generate the changelog from commit messages?

Partially. Conventional commits plus a generator gives you a complete list and a consistent structure, which is most of the mechanical work. It cannot decide what matters, rewrite an entry for a reader who does not know your codebase, or write the migration note for a breaking change, so treat the generated output as a first draft that a human edits before release.

How far back should the changelog go?

All the way. It is append-only and it costs nothing to keep. Somebody upgrading across four years of releases needs to read every entry between where they are and where they are going, and truncating the file is exactly the thing that makes that upgrade unquotable.

Rendering the changelog

Doku renders a Markdown changelog as an ordinary doc page, with anchors on every version heading so a support reply can link straight to the release in question. For how versioned documentation and the changelog fit together, see How to Version Your Documentation.