The guide
A Changelog Is a Promise to the People Who Upgrade, Not a Diary of Commits
Who reads a changelog, why a raw commit log fails them, what the six Keep a Changelog sections and a Semantic Versioning number promise, and how commit conventions can carry a release.
- 6 sections the types of change Keep a Changelog groups entries under
- beta.2 < beta.11 numeric pre-release identifiers compare as numbers, not text
- 12 commits since the last tag in the worked example, one of them breaking
- 0.4.0 the worked example's next version while the major version is zero
Read the full guide Fold the guide
Somebody is about to upgrade your software. They have a working system, a deadline, and a question: what will this new version do to me? The changelog is where they go for the answer. It is not written for the people who made the changes. They already know. It is written for the people who have to live with them.
That one fact explains almost every rule worth following. Keep a Changelog, a guide to the file maintained by Olivier Lacan, opens its principles with a blunt sentence: changelogs are for humans, not machines. It defines the file as a curated, chronologically ordered list of notable changes for each version of a project, and it says the point is to let users and contributors see precisely what changed between one release and the next.
Why the commit log is the wrong answer
Every repository already has a list of changes. Run git log and git prints every commit, by default in reverse chronological order, newest first. It is tempting to paste that list into a file and call it done. Keep a Changelog calls this a bad idea in plain words: commit log diffs are full of noise, such as merge commits, commits with obscure titles and documentation changes.
The deeper problem is that a commit and a changelog entry answer different questions. A commit records one step in the evolution of the source code, and its message is written for the next developer who reads the diff. A changelog entry records the noteworthy difference, often spread across several commits, and it is written for someone who will never read the diff at all. Ten commits that fix, refactor, test and document one new option become one line: the option exists now.
So a changelog is an act of selection. Something has to decide which commits matter to the person upgrading, which ones belong together, and which ones can be left out. That judgment is the expensive part, and it is why so many projects either skip the file or let it rot. The Wikipedia article on changelogs notes the same split: a version control system can generate a list of code changes, but some view this as unhelpful and prefer a curated list of notable changes.
Six kinds of change, and one section that waits
Keep a Changelog proposes a small, fixed vocabulary. Every entry goes under one of 6 sections: Added for new features, Changed for changes in existing functionality, Deprecated for features that will soon be removed, Removed for features that are now gone, Fixed for bug fixes, and Security for vulnerabilities. Each version gets a heading with its number and its release date, the latest version comes first, and versions and sections should be linkable.
The sections are not decoration. They sort entries by what the reader has to do about them. Added is an invitation. Fixed is reassurance. Deprecated is a warning with a deadline, and the guide is firm that deprecations, removals and breaking changes are the entries that must never be left out, because a reader should be able to upgrade to a version that lists deprecations, remove what is deprecated, and then upgrade again safely.
The date format is chosen for the same reader. The guide recommends dates written like 2017-07-17, ordered from the largest unit to the smallest, because that order cannot be confused with regional formats that swap month and day. This is the calendar date of ISO 8601, a standard first published in 1988, and the same full-date form that RFC 3339 uses for internet timestamps. RFC 3339 even prints the leap year rule as a line of C: divisible by four, except that a century year must also be divisible by four hundred. That is why 2024-02-29 is a date and 1900-02-29 is not.
Then there is the section with no version at all. Keep a Changelog suggests an Unreleased section at the top of the file, collecting changes as they land. It does two jobs. People can see what is coming, and at release time the work of writing the notes is already half done: the Unreleased items move under a new version heading, and the Unreleased section starts again empty. A project that keeps it never faces the blank page the night before a release.
Links matter more than they look. In Markdown, a heading such as ## [1.1.0] - 2026-02-01 becomes a link only if the file defines that label somewhere, usually in a block at the end. The CommonMark specification calls this a link reference definition: the label in brackets, a colon, then the destination. Labels match after case folding and whitespace collapsing, and when the same label is defined twice the first definition wins, a case the specification itself says deserves a warning. A heading whose label is never defined simply shows its brackets as text, which is the most common quiet fault in real changelogs.
What a version number promises
A changelog says what changed. A version number says what kind of change it was, in three digits. Semantic Versioning, originally written by Tom Preston-Werner, turns that idea into rules. Given a version MAJOR.MINOR.PATCH, you increment the patch for backward compatible bug fixes, the minor for backward compatible new functionality, and the major for incompatible changes to the public API. The specification adds one rule people often forget: the minor version must also be incremented when any public functionality is marked as deprecated.
The order of versions is spelled out just as precisely, and this is where intuition fails. Major, minor and patch are compared as numbers, so 1.10.0 comes after 1.9.0. A pre-release, written after a hyphen, sorts below the normal version it leads to, so 1.0.0-rc.1 comes before 1.0.0. Pre-release identifiers are compared one at a time from the left: identifiers made only of digits are compared as numbers, others in ASCII order, a numeric identifier always sorts below a non-numeric one, and a longer list wins when everything before it is equal. Build metadata, written after a plus sign, is ignored entirely.
Those rules produce results that look wrong at a glance. The specification’s own example chain runs 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-alpha.beta, 1.0.0-beta, 1.0.0-beta.2, 1.0.0-beta.11, 1.0.0-rc.1 and finally 1.0.0. Compare the identifiers as plain strings and beta.11 lands before beta.2, because the character 1 sorts before 2. The rule that rescues it is the numeric one: beta.2 < beta.11, since two is less than eleven. Any tool that sorts versions as text gets this wrong, and so does anyone working from memory.
Major version zero is the other trap. The specification says 0.y.z is for initial development, that anything may change at any time, and that the public API should not be considered stable. The strict bump rules are written for versions above zero. Its FAQ suggests starting at 0.1.0 and incrementing the minor for each later release, and it adds that software already used in production should probably already be 1.0.0. So a breaking change at 0.3.0 does not force 1.0.0. Moving to 1.0.0 is a declaration that the API is now stable, and only the maintainer can make it.
Letting the commits carry the release
If the commit messages follow a convention, much of the sorting can be read straight from the history. Conventional Commits 1.0.0 asks for a header of the form type, optional scope in parentheses, colon, description. It ties two types to Semantic Versioning: fix correlates with a patch, feat with a minor. A breaking change is marked either by an exclamation mark before the colon or by a footer that starts with BREAKING CHANGE, in capitals, and that correlates with a major release whatever the type. Other types such as docs, test, ci and chore are allowed, and the specification gives them no implicit effect on the version unless they carry a breaking change.
To read that structure, a tool needs more than the one-line log. Bodies and footers carry the breaking notes, and merge commits are only recognisable by their parents.
The git documentation for pretty formats lists the codes for each piece: the commit hash, the parent hashes, the author date in short YYYY-MM-DD form, the ref names with any tags, the subject and the body. It also allows any byte written in hex, so a format can put a record separator before each commit and a unit separator between fields. Messages can then hold blank lines, colons and anything else without breaking the parse. The last version usually sits in those ref names as a tag, and the most recent tag reachable from a commit is exactly what git describe looks for.
Here is a worked example you can reproduce in the tool, which loads it on first visit. A small project is tagged v0.3.0. Since that tag there are 12 commits.
Three are new features, one of them marked with an exclamation mark and a BREAKING CHANGE footer that renames an option. Two are fixes, one is a performance change, one deprecates a switch through a Deprecated footer, and one fixes a security problem in the HTML report. Three are noise the reader does not need: a documentation commit, a test commit and a merge commit. The last, a commit titled Update README badges, follows no convention at all, so the tool lists it for a person to place instead of guessing.
The highest change is breaking, which would mean a major release. But the project is still at 0.3.0, so under the reading of major version zero described above the breaking change raises the minor instead, and the next version is 0.4.0. Choose the option that treats zero like any other major and the same history gives 1.0.0. Both answers are correct under the specification. They encode different decisions about stability, which is why the choice is stated and left to the maintainer.
What a tool cannot know
Automation only reads what the commits say. A message can call a breaking change a fix, a refactor can quietly remove a public function, and a feature can be squashed into a commit titled with a single word. Conventional Commits concedes that a commit which does not meet the specification will simply be missed by tools built on it. The log also shows one line of history. It cannot see unpushed tags, other branches, or what a commit really changed in the code.
So the useful split is clear. Let the tooling do what it does exactly: parse every record, apply the version precedence rules without error, check that headings descend, dates are real and every version has its link. Then let a person do the part that needs judgment: merge related entries, rewrite a line for a reader who never saw the code, and decide when 0.y.z becomes 1.0.0. A changelog built that way is still curated by a human. It is just no longer built from scratch the night before a release.
Sources
Every source consulted for this guide. Links checked October 24, 2026.
- Keep a Changelog Olivier Lacan, keepachangelog.com, version 1.1.0 (MIT)
- Semantic Versioning 2.0.0 semver.org (CC BY 3.0)
- Conventional Commits conventionalcommits.org, version 1.0.0 (CC BY 3.0)
- Git - pretty-formats Documentation Git reference manual, git-scm.com (GPL-2.0)
- Git - git-log Documentation Git reference manual, git-scm.com (GPL-2.0)
- Git - git-describe Documentation Git reference manual, git-scm.com (GPL-2.0)
- Git - git-tag Documentation Git reference manual, git-scm.com (GPL-2.0)
- CommonMark Spec John MacFarlane, spec.commonmark.org, version 0.31.2 (CC BY-SA 4.0)
- RFC 3339: Date and Time on the Internet: Timestamps IETF, RFC Editor
- ISO 8601 - Wikipedia Wikipedia (CC BY-SA)
- Changelog - Wikipedia Wikipedia (CC BY-SA)