CHANGELOG.md: what goes in it and how to keep it current
6 min read
A CHANGELOG.md is a Markdown file in the root of a repository that lists, for each released version, what changed in terms a user of the project can act on. The file is read by people deciding whether to upgrade, by people debugging after an upgrade, and by package registries and release tooling that render it on the project’s page. The format most projects follow is the one described at keepachangelog.com: newest version at the top, an Unreleased section above it, and entries grouped under Added, Changed, Deprecated, Removed, Fixed and Security.
| Section | What goes in it | Example line |
|---|---|---|
Unreleased | Changes merged but not yet in a version | - Export now streams, so files over 100 MB no longer time out. |
Added | New capability a user can reach | - \—dry-run` flag on `deploy` prints the plan without applying it.` |
Changed | Existing behaviour that now works differently | - Default timeout raised from 30 s to 60 s. |
Deprecated | Still works, scheduled for removal | - \legacyAuth()` will be removed in 3.0; use `auth()`.` |
Removed | Gone in this version | - Dropped Node 16 support. |
Fixed | A bug a user could have hit | - Search no longer fails on queries containing a hyphen. |
Security | Fixes a reader should upgrade for today | - Fixed a path traversal in the static file handler (CVE pending). |
What is a CHANGELOG.md file?
It is the human-readable history of a project, one version at a time, kept next to the code so it ships with it. It differs from git log in audience and selection: the git log records every commit for the people who wrote them, while the changelog records the changes a user would notice, written for the person who did not write them. A commit that renames an internal variable has no changelog line. A commit that changes a default timeout has one, even if the diff was a single character.
The file lives at the repository root under that exact name because tooling looks for it there. GitHub links it from the repository page, npm and PyPI render it when it is included in the package, and release tools such as release-please and semantic-release read and write it in place. Lowercase changelog.md and CHANGES.md also work for people, but some tools only look for the canonical name.
What does a complete CHANGELOG.md look like?
A short, correct example is worth more than a long description. This one follows the Keep a Changelog 1.1.0 layout and semantic versioning for the numbers.
# Changelog
All notable changes to this project are listed here.
Format: Keep a Changelog. Versions: Semantic Versioning.
## [Unreleased]
### Added
- `--dry-run` flag on `deploy` prints the plan without applying it.
## [2.3.1] - 2026-10-01
### Fixed
- Search no longer fails on queries containing a hyphen. Broken
since 2.3.0.
### Security
- Fixed a path traversal in the static file handler. Upgrade today
if you serve user-uploaded files.
## [2.3.0] - 2026-09-18
### Added
- Streaming export. Files over 100 MB no longer time out.
### Deprecated
- `legacyAuth()` still works but will be removed in 3.0.
Use `auth()`.
[Unreleased]: https://github.com/acme/tool/compare/v2.3.1...HEAD
[2.3.1]: https://github.com/acme/tool/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/acme/tool/compare/v2.2.0...v2.3.0
Four details carry most of the value. Every version has a date in ISO format, so a reader can match it against when their problem started. Each version heading is a link to the compare view, so the full diff is one click away. A fix says which version broke it. And the Security section sits apart, so someone scanning for “do I need to upgrade now” finds it in seconds.
How do you keep a CHANGELOG.md up to date?
Write the line in the same pull request as the change, under Unreleased, and move the section into a version heading when you release. That is the whole method. The file goes stale when the line is written later by someone else, at release time, from a list of commit messages they did not author.
Two things make the habit stick. The first is a check in CI that fails a pull request touching src/ without touching the changelog, with a label to skip it for changes that have no user-visible effect; a changelog check as a GitHub Action shows the forty-line version. The second is a release step that renames Unreleased to the version and date in one command, so nobody has to remember to do it by hand. Who writes the changelog covers how teams settle the question of whose job the line is.
Should the changelog be generated from commits?
Generation gives you a complete draft; it does not give you a changelog. Tools built on conventional commits, and release automation such as release-please, read feat:, fix: and BREAKING CHANGE: prefixes and produce a grouped list with every pull request linked. The list is only as good as the commit subjects, and commit subjects describe the code.
The workable middle is to generate, then edit. Let the tool create the Unreleased section from commits, and have the release pull request rewrite the lines that a user would read: the symptom instead of the function name, the setting that moved instead of the file that changed. From conventional commits to a changelog walks through the pipeline, and a browser-side changelog generator is enough to get a draft from a pasted git log without installing anything.
What should not go in CHANGELOG.md?
Internal refactors, dependency bumps with no behaviour change, test-only commits, CI configuration and formatting changes. They belong in the git history, where anyone who needs them can find them. Including them buries the lines that matter and trains readers to skip the file.
The other thing to leave out is the marketing version of a change. “Blazing fast exports” tells a reader nothing they can check. “Export now streams; files over 100 MB no longer time out” does. The file is a reference document, and its tone should match: short lines, concrete nouns, a version or date on everything. Keep a Changelog, implemented covers the parts of the spec that teams most often drop, and changelog file formats covers when a JSON or YAML source beside the Markdown file earns its keep.
Does a product with a changelog page still need the file?
A library does, because the file is what package registries render and what a developer reads offline. A SaaS product with no installable artifact can get by with the page alone, since its users never see the repository. Many teams run both: the file as the engineering record, and a public changelog page that is written from it for a wider audience, with the detail split the way changelog vs release notes describes. If the page is fed from the file, keep the file the source of truth and generate the page, so there is one place to edit.
FAQ
What is the difference between CHANGELOG.md and release notes? The changelog is the complete running record of every notable change, kept in the repository. Release notes are a message about one release, often shorter and rewritten for a wider audience. Many projects publish the changelog section for a version as its release notes.
Where should CHANGELOG.md live? In the repository root, with that exact name, so GitHub, package registries and release tools find it. A monorepo keeps one per package, next to each package’s own manifest.
Should I generate CHANGELOG.md from git commits? Generate the draft, then edit the lines a user will read. Commit messages describe the code change; the changelog describes what a user notices. Tools such as conventional-changelog and release-please produce a good starting point.
Does every commit need a changelog entry? No. Only changes a user of the project could notice: new capability, changed behaviour, deprecations, removals, fixes and security patches. Refactors, test changes and CI edits stay in the git history.
The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.