TL;DR:
- git-cliff generates a
CHANGELOG.mdautomatically from your git commit history — no manual writing required - It works best with Conventional Commits (a standard format for commit messages:
feat:,fix:,chore:etc.) - You can customise output completely via Tera templates, making it suitable for everything from open-source libraries to internal services
Most projects have a CHANGELOG.md that’s either missing, perpetually out of date, or a copy-paste of git log output with minimal editing. Writing changelogs manually is time-consuming and gets deprioritised under deadline pressure. The result is that users and reviewers can’t tell what changed between versions without reading the full git log.
git-cliff — written in Rust, released in 2021, now widely adopted across the Rust and Go ecosystems — solves this by generating changelogs programmatically from your commit history. With a few minutes of configuration and a CI step, you get a well-structured changelog that updates automatically on every release.
Installation
# macOS (Homebrew)
brew install git-cliff
# Cargo (if you have Rust)
cargo install git-cliff
# npm (works in any JS project)
npm install --save-dev git-cliff
# Binary from GitHub releases (works anywhere)
# Download from https://github.com/orhun/git-cliff/releases
The Conventional Commits Foundation
git-cliff can parse any commit format, but it shines brightest with Conventional Commits — a standard that structures commit messages as:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Common types:
feat:— new feature (maps toMINORversion bump in semver)fix:— bug fix (maps toPATCH)docs:— documentation changeschore:— maintenance tasks, dependency updatesrefactor:— code restructuring without behaviour changeperf:— performance improvementstest:— adding or fixing testsfeat!:orBREAKING CHANGE:footer — breaking change (maps toMAJOR)
With Conventional Commits in place, git-cliff can categorise every commit, group them by type, and determine the version bump needed — without any manual input.
Basic Setup
Initialise a config file in your project root:
git cliff --init
This creates a cliff.toml with sensible defaults. The default configuration groups commits into categories (Features, Bug Fixes, etc.), formats them as markdown, and excludes chore: commits from the output.
Generate your first changelog:
# Full changelog from the beginning of history
git cliff --output CHANGELOG.md
# Just the unreleased changes since the last tag
git cliff --unreleased --output CHANGELOG.md
# For a specific version range
git cliff v1.0.0..v2.0.0
Customising the Output
The real power is in the configuration. cliff.toml lets you control everything:
[changelog]
header = """
# Changelog
All notable changes to this project will be documented here.
"""
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 %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}\
([{{ commit.id | truncate(length=7, end="") }}]({{ remote.url }}/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}\n
"""
trim = true
[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
{ message = "^docs", group = "Documentation" },
{ message = "^test", group = "Tests" },
{ message = "^chore\\(release\\)", skip = true },
{ message = "^chore", skip = true },
{ body = ".*security", group = "Security" },
]
protect_breaking_commits = false
filter_commits = true
tag_pattern = "v[0-9].*"
skip_tags = "v0.1.0-beta.1"
ignore_tags = ""
topo_order = false
sort_commits = "oldest"
The body field uses Tera templating (similar to Jinja2). You can produce any format — GitHub-flavoured markdown, JSON, RST, or a custom format your documentation system expects.
Including Commit Links
If you set your remote URL, git-cliff auto-generates links to each commit on GitHub/GitLab/Bitbucket:
[remote.github]
owner = "your-org"
repo = "your-repo"
This generates [abc1234](https://github.com/your-org/your-repo/commit/abc1234) links in the changelog, making it easy to click through to the full commit from the release notes.
CI Integration
The typical CI workflow generates the changelog as part of the release process. Here’s a GitHub Actions step that generates the changelog for the unreleased commits and uses it as the GitHub Release body:
- name: Generate changelog
id: changelog
uses: orhun/git-cliff-action@v3
with:
config: cliff.toml
args: --verbose --latest --strip header
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
body: ${{ steps.changelog.outputs.content }}
tag_name: ${{ github.ref_name }}
For projects using release-please, semantic-release, or similar release automation, git-cliff integrates as the changelog generation step, replacing the built-in changelog writers with more customisable output.
Enforcing Conventional Commits
git-cliff generates the best output when commits are consistently formatted. To enforce this across your team, use a commit-msg hook via commitlint:
# Install commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# Configure
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
# Add as pre-commit hook (if using lefthook)
# lefthook.yml
commit-msg:
commands:
lint:
run: npx commitlint --edit {1}
This rejects commits that don’t follow the Conventional Commits format before they’re saved, which means git-cliff’s input is always well-structured.
When It’s Most Useful
git-cliff is highest-value for:
- Libraries and packages with external consumers who care what changed between versions
- Internal services where the changelog serves as a lightweight audit trail for deployments
- Monorepos (git-cliff supports per-package changelogs via the
--include-pathflag)
For small personal projects with infrequent releases and no external consumers, the setup overhead may not be worth it. For anything with active users or a team maintaining it, the one-time setup pays dividends on every release.
The full documentation and example configurations are at git-cliff.org, including examples for GitHub Actions, GitLab CI, and manual release workflows.