Changelog
A changelog is a curated, human-readable record of what changed in each release. Its value comes entirely from being written for a reader deciding whether to upgrade — which is why a generated dump of commit messages, however complete, is not a changelog. This page covers the six headings, what a version number promises, deprecation, and how far automation can honestly take you.
- Written for
- Humans, not tools
- Version scheme
- MAJOR.MINOR.PATCH
- Convention
- Keep a Changelog
A git log is not a changelog
A commit history records what the team did, in the order it happened, in the team's own vocabulary. A changelog records what changed for the person using the software, grouped by significance. These are different documents with different audiences, and generating one from the other without editing produces something technically accurate and practically unreadable.
Changelog entry
- Session cookies now expire after 30 days instead of 7
- Fixed export failing for files over 50 MB
- Removed the v1 search endpoint — use /v2/search
Commit message
- fix: bump TTL constant
- chore: refactor stream handler
- feat: drop legacy route
- Merge branch develop into main
Read the right-hand column as a user of the software. Not one entry tells you whether you need to do anything — and the last one tells you nothing at all.
Three documents people conflate
Much of what makes changelogs bad is one file trying to be three. A changelog is complete and terse: every change, one line each, for a reader deciding whether to upgrade. Release notes are selective and promotional: the three things worth announcing, written for someone who has not been following. A migration guide is instructional: for each breaking change, the old code and the new code, written for someone in the middle of an upgrade that has just failed.
They have different lengths, different tones and different lifetimes, and trying to satisfy all three at once produces a document that is too long to scan and too shallow to act on. Link between them instead: the changelog line names the breaking change, and points at the migration guide section that explains it.
The version number is a promise
Semantic Versioning turns three numbers into a compatibility contract. The point is not tidiness — it is that a reader can decide whether an upgrade is safe without reading the diff, and that a dependency manager can decide it on their behalf.
The rule that carries all the weight: if existing users must change anything to keep working, that is a MAJOR bump — no matter how small the change looks from the inside. Renaming a field, tightening a validation rule, making an optional parameter required and removing a default are all breaking changes, and shipping them as a patch is how a dependency earns a reputation.
Zero, pre-release and build metadata
Anything starting 0. is explicitly outside the promise — during 0.y.z the specification says anything may change at any time. That is honest while a design is still moving, and dishonest once real users depend on it. A project that has been on 0.x for four years with production consumers is using the zero as a way of avoiding the commitment, not as a signal of instability.
A pre-release is marked with a hyphen — 2.0.0-rc.1 — and sorts before the release it precedes, so a dependency manager asking for 2.0.0 will not pick it up by accident. Build metadata is marked with a plus — 2.0.0+build.482 — and is ignored entirely when comparing versions, which makes it the right place for a commit hash and the wrong place for anything a user needs to know.
What a good entry contains
The Keep a Changelog convention groups every release under six headings. Using the same six everywhere means a reader can scan for the one they care about — most often Removed and Changed, because those are the ones that create work.
| Heading | For | Reader asks |
|---|---|---|
| Added | New capabilities | Is there something I now can do? |
| Changed | Existing behaviour that differs | Will my current usage behave differently? |
| Deprecated | Still works, will be removed | What must I migrate off, and by when? |
| Removed | Gone in this version | What will break the moment I upgrade? |
| Fixed | Defects resolved | Is my workaround still needed? |
| Security | Vulnerabilities addressed | Do I need to upgrade urgently? |
The rest of the convention is mechanical and worth following exactly, because it is what lets a reader find things. Newest version at the top. One section per released version, with an ISO date — 2026-08-19, not 19/08/26, which means two different things depending on where the reader lives. An Unreleased section above them all. And a note saying which versioning scheme the project follows, so nobody has to guess what a minor bump implies.
Two habits do most of the remaining work: keep the Unreleased section populated as changes land, so entries are written while the change is fresh, and write each line as the effect on the user rather than the edit in the code. A changelog assembled the day before release is always the thin one, because the person assembling it is reconstructing a month from commit subjects.
Deprecation is the part that buys goodwill
Removed is the heading that causes work; Deprecated is the heading that gives people time to do it. A deprecation is a promise with three parts, and a deprecation notice missing any of them is just a complaint about your own code.
- What is going away, named precisely enough that a reader can search their own code for it.
- What to use instead, with the replacement available now rather than promised for later.
- When it will be removed, stated as a version rather than a season.
A workable policy is one line: anything deprecated in a minor release is removed no earlier than the next major, and never in between. Publish that once and every deprecation notice inherits it. The cost is carrying dead code for a cycle; the return is that upgrading your software stops being a thing people postpone, which is the difference between a dependency people keep current and one that quietly forks in six organisations.
How far automation honestly goes
Conventional Commits standardises the commit subject line so a tool can read it: a type, an optional scope, an optional exclamation mark for a breaking change, then the description — "feat(auth): accept refresh tokens", "fix!: reject unsigned webhooks". A breaking change can also be stated in a footer beginning "BREAKING CHANGE:", which is the better place when it needs a sentence of explanation.
| Commit | Implies | Lands under |
|---|---|---|
| fix: | PATCH bump | Fixed |
| feat: | MINOR bump | Added |
| ! or BREAKING CHANGE: | MAJOR bump | Changed or Removed |
| chore:, docs:, refactor: | No bump | Usually nothing — no user-visible effect |
This genuinely helps: it can derive the next version number and assemble a draft grouped under the right headings, which removes the two most tedious steps. What it cannot do is decide whether a change is breaking, because that depends on how people actually use the thing, and that knowledge lives outside the repository. A commit marked fix: that changes a default value is breaking; the tool will call it a patch and be confidently wrong.
The workable split is therefore: machine-assembled draft, human-edited before release. Fifteen minutes of editing turns a correct list into a readable one — merging three commits into one entry, deleting the six the user cannot observe, and rewriting the two that are worded from inside the code.
How this shows up in real delivery
The test of a changelog is whether anyone opens it under pressure. Something broke after an upgrade at 16:40; a person who has never read your code opens the file and needs to find, in under a minute, whether the version they moved to changed the thing they are looking at. Everything in the convention — newest first, six fixed headings, one line per change, effects rather than edits — exists to make that minute possible.
Where it degrades
- A generated commit dump published unedited, which nobody reads twice.
- Breaking changes shipped as a minor or patch bump, which destroys the version contract.
- No stated public API, so every refactor is arguably a breaking change and nobody can tell.
- Sitting on 0.x for years with production consumers, which uses the zero to dodge the commitment.
- Deprecation notices with no removal version, which nobody acts on until the removal lands.
- "Bug fixes and improvements" as the entire entry — a non-answer to every question a reader has.
- Written at release time from memory, so the small breaking change is the one that gets forgotten.
When to use it
Use it when
- Other people or teams depend on your software and decide when to upgrade.
- You publish a library, an API or anything versioned.
- Support needs to know which version a reported problem belongs to.
- Users install updates themselves rather than receiving them silently.
Avoid it when
- An internal service with one consumer sitting next to you — a message is cheaper.
- A continuously deployed web app where users never choose a version.
- It would be generated and never read, which is maintenance without a reader.
- Release notes for end users already cover it — do not maintain two overlapping documents.
Found this useful?
Share it with someone who is working on the same problem.