Changelog best practices are well-established, yet most changelogs are terrible. They are either auto-generated commit dumps that no one reads, or hand-written paragraphs that omit critical details. A changelog that says "various bug fixes and improvements" tells your users nothing. A changelog that pastes 200 raw commit messages tells them everything except what matters.
The gap between "we shipped something" and "our users understand what changed" is where most engineering teams lose trust. Good release notes turn updates into credibility. Bad ones turn them into noise.
This guide covers the standards, automation tools, and publishing workflows that make changelogs worth maintaining.
Why changelog best practices matter more than you think
A changelog is a contract between your team and your users. It says: here is what changed, here is why, and here is what you should know about it. Without that contract, users are left guessing. Did the update fix the bug they reported? Did it introduce a breaking change that will affect their integration? Is the feature they requested finally live?
The business case is straightforward. Support tickets decrease when users can self-serve answers about what changed. Adoption increases when new features are communicated clearly. Trust compounds when users see a pattern of consistent, well-documented improvement. And for developer tools, the changelog is often the first thing a prospective customer reads after the README.
According to a 2024 survey by Tidelift, 65% of open source maintainers consider a changelog essential for project health. GitHub's Octoverse report found that repositories with structured release notes receive 2.4x more contributions than those without.
Yet the majority of changelogs fail at their basic purpose. They are either too technical (commit hashes, internal refactoring details) or too vague ("improved performance"). The best changelogs strike a balance: specific enough to be useful, human enough to be readable.
The Keep a Changelog standard
The most widely adopted changelog format is Keep a Changelog, currently at version 1.1.0. It defines a simple, consistent structure that solves the most common problems.
The core rules:
- The file is named
CHANGELOG.mdand lives at the repository root - Entries are grouped by version, with the latest version first
- Each version uses an ISO 8601 date format (YYYY-MM-DD)
- An
[Unreleased]section sits at the top for in-progress changes - Changes are categorized into six types: Added, Changed, Deprecated, Removed, Fixed, Security
Here is what a well-structured changelog looks like:
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Document search via Cmd+K with full-text search and snippet highlighting
## [1.5.0] - 2026-02-20
### Added
- Internal linking with [[wikilinks]] syntax
- Document graph visualization (2D and 3D)
- Backlinks panel showing all documents that reference the current document
### Changed
- Sidebar redesigned with folder tree navigation
- Template picker now shows favorites at the top
### Fixed
- Amber indicator no longer triggers on layout mode changes
- Published pages now use a frozen template snapshot
## [1.4.0] - 2026-02-15
### Added
- Nested folders with drag-and-drop organization
- Soft-delete trash with 30-day retention
- Multi-select for bulk document operations
### Deprecated
- The flat document list view will be removed in v2.0
### Security
- Rate limiting added to all public API endpoints
The six categories serve distinct purposes:
- Added signals new capabilities. Users scan this section to see if their feature request shipped.
- Changed communicates modifications to existing behavior. This is where breaking workflow changes surface.
- Deprecated gives advance warning. Users who depend on a feature get time to migrate.
- Removed confirms what was deprecated is now gone.
- Fixed addresses known issues. Users check this to see if their reported bug was resolved.
- Security highlights vulnerability patches. This category should prompt immediate updates.
The [Unreleased] section is particularly valuable. It gives your team a running list of what will go into the next release, and it gives users visibility into what is coming.
Common Changelog: a stricter variant
Common Changelog builds on Keep a Changelog with additional requirements. The most notable is that every entry must reference a pull request, commit, or issue. This creates an audit trail from the user-facing description back to the code change.
## [2.1.0] - 2026-02-20
### Added
- Full-text document search with PostgreSQL tsvector ([#142](https://github.com/example/repo/pull/142))
- Keyboard shortcut Cmd+K to open search modal ([#145](https://github.com/example/repo/pull/145))
### Fixed
- Template CSS no longer bleeds into published page headers ([#139](https://github.com/example/repo/issues/139))
Common Changelog also enforces that the Changed category explicitly calls out breaking changes, and that entries are written in imperative mood ("Add search" not "Added search"). For teams building developer tools or libraries where breaking changes carry real cost, this stricter format reduces ambiguity.
Conventional Commits: the foundation for automation
Before you can automate changelog generation, you need structured commit messages. Conventional Commits provides the standard format:
type(scope): description
The required types:
| Type | Purpose | Changelog Category |
|---|---|---|
feat | New feature | Added |
fix | Bug fix | Fixed |
docs | Documentation only | (often excluded) |
style | Formatting, no logic change | (often excluded) |
refactor | Code restructuring | Changed |
perf | Performance improvement | Changed |
test | Adding or updating tests | (often excluded) |
build | Build system changes | (often excluded) |
ci | CI configuration | (often excluded) |
chore | Maintenance tasks | (often excluded) |
Breaking changes are marked with a ! after the type or scope:
feat(api)!: change authentication from API keys to OAuth 2.1
Or with a BREAKING CHANGE: footer in the commit body:
feat(api): migrate to OAuth 2.1
BREAKING CHANGE: API key authentication is no longer supported.
Existing integrations must migrate to OAuth by March 1.
Real examples from a typical week of development:
feat(search): add full-text document search with Cmd+K
fix(sidebar): prevent DnD reorder failure for expanded folders
feat(graph): add 2D/3D document graph visualization
fix(amber): exclude layoutMode from auto-save dependency array
refactor(templates): extract CSS generator into shared module
perf(pipeline): cache rehype processor between conversions
docs(api): add rate limiting documentation to developers page
The discipline of writing structured commits pays dividends beyond changelog automation. It makes git log readable, makes cherry-picking safer, and makes code review faster because reviewers understand the intent before reading the diff.
Automation tools compared
Once your commits follow Conventional Commits, several tools can generate changelogs automatically.
| Tool | Language | Approach | Key Feature |
|---|---|---|---|
| conventional-changelog | Node.js | CLI, generates from git history | Angular, ESLint, and custom presets |
| semantic-release | Node.js | Fully automated releases | Determines version, generates changelog, publishes |
| git-cliff | Rust | Highly customizable templates | Tera templating, regex-based grouping |
| Release Drafter | GitHub Action | Label-based drafting | Auto-categorize PRs by label |
| commit-and-tag-version | Node.js | npm version replacement | Bumps version, updates changelog, tags |
| release-it | Node.js | Interactive releases | Monorepo support, plugin ecosystem |
| Towncrier | Python | Fragment files per change | Each PR adds a news fragment, merged at release |
The right choice depends on your workflow. For most teams, git-cliff or semantic-release covers the common case.
Setting up automated changelog generation
Here is a practical setup using git-cliff, which works with any language and offers the most customization.
Installation
# macOS
brew install git-cliff
# Cargo (any platform)
cargo install git-cliff
# npm (via npx)
npx git-cliff@latest
Configuration
Create cliff.toml at your repository root:
[changelog]
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [Unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\
{{ commit.message | upper_first }}\
{% if commit.breaking %} (**BREAKING**){% endif %}\
{% endfor %}
{% endfor %}\n
"""
trim = true
[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
commit_parsers = [
{ message = "^feat", group = "Added" },
{ message = "^fix", group = "Fixed" },
{ message = "^perf", group = "Changed" },
{ message = "^refactor", group = "Changed" },
{ message = "^doc", group = "Documentation" },
{ message = "^style", skip = true },
{ message = "^test", skip = true },
{ message = "^chore", skip = true },
{ message = "^ci", skip = true },
]
filter_commits = false
tag_pattern = "v[0-9].*"
Generating the changelog
# Generate full changelog
git-cliff -o CHANGELOG.md
# Generate changelog for a specific version range
git-cliff v1.4.0..v1.5.0 -o CHANGELOG.md
# Prepend unreleased changes to existing changelog
git-cliff --unreleased --prepend CHANGELOG.md
# Preview without writing
git-cliff --unreleased
Integrating with CI
Add a GitHub Action that generates the changelog on each release:
name: Release
on:
push:
tags:
- 'v*'
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: CHANGES.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body_path: CHANGES.md
This generates release notes automatically when you push a version tag. No manual writing required.
The last mile problem: publishing your changelog
Here is where most teams stop. The changelog exists in the repository. It is accurate, well-structured, and generated automatically. But the people who need to read it do not read repositories.
Your product manager reads Slack. Your customers read email. Your stakeholders check a web page. Your support team scans a published document. The raw CHANGELOG.md file in your GitHub repo serves developers. It does not serve everyone else.
This is the distribution problem. The changelog content is correct, but the format and location are wrong for most audiences.
Some teams maintain separate release notes in a blog, a Notion page, a Confluence wiki, and Slack announcements. That means rewriting the same information four times in four different formats, and inevitably one of them falls out of date.
The better approach is to write the changelog once, in markdown, and publish it to every destination from a single source.
Using Unmarkdown™ to distribute release notes
Unmarkdown™ solves the last mile by converting your markdown changelog into whatever format each audience needs.
Publish as a web page
Paste your changelog into Unmarkdown™, select a template that matches your brand, and publish. You get a clean, styled URL you can share with customers, link from your docs, and embed in your product.
If you use Claude with Unmarkdown™'s MCP tools, you can automate this entirely. Tell Claude: "Update the changelog document with the v1.5.0 release notes and re-publish it." Claude reads the existing document, appends the new version, and publishes. You get the updated URL without opening a browser.
Send via Slack
Your team needs to know about the release immediately. Copy the latest version section from your changelog, paste it into Unmarkdown™, and click "Copy for Slack." The markdown converts to Slack's mrkdwn format automatically: **bold** becomes *bold*, headings convert to bold section labels, links use <url|text> syntax, and tables convert to a readable monospace layout that Slack can display. Paste into your #releases channel and it renders correctly.
Email to stakeholders
For customers or external stakeholders, click "Copy for Email." The changelog section converts to HTML with all CSS inlined (because email clients strip <style> tags). Headings, tables, lists, and code blocks all render properly in Gmail, Outlook, and Apple Mail. The email looks like you spent time formatting it. You did not.
Format for documentation
If your changelog also needs to appear in Google Docs or Word, those destinations are available too. The same markdown content, converted to the native format of each destination. One source, multiple outputs, zero reformatting. Whether you are preparing a document for a compliance review or sharing formatted notes with non-technical stakeholders, the conversion handles the formatting details.
This is the core idea behind markdown publishing: write once, publish everywhere. Your changelog is already structured markdown. The only missing step is converting it to the right format for each audience.
Template for a great changelog entry
Here is a reusable template for writing individual changelog entries that are both complete and readable:
## [X.Y.Z] - YYYY-MM-DD
### Added
- **Feature name:** One-sentence description of what it does and why it matters.
Users who do X can now do Y without Z.
### Changed
- **Behavior change:** Describe what changed, what the old behavior was, and what
the new behavior is. If this is a breaking change, say so explicitly.
### Fixed
- **Bug description:** What was broken, what caused it, and confirmation that it
is now resolved. Reference the issue number if applicable.
### Security
- **Vulnerability:** What was patched. Link to CVE if applicable. What users
should do (update immediately, rotate keys, etc).
The pattern is: category header, bold name, plain-language description. Skip internal implementation details. Focus on what the user experiences, not what the developer changed.
A changelog that follows this structure, generated automatically from conventional commits, and published to every destination where stakeholders actually read, is a changelog that earns trust. That is the standard worth aiming for.
