Automated Changelogs with Git-Cliff
You've meticulously crafted your commit messages using the Conventional Commits standard. Now what? Do you manually copy-paste them into a CHANGELOG.md file like some kind of digital peasant?
Absolutely not. That's what interns are for. And since we don't have any, we'll automate it.
Automated changelog generation is one of the biggest payoffs of a disciplined commit history. It turns your git log into a structured, human-readable document that tells the story of your project's evolution.
Why Automate Your Changelog?
"But I can just write it myself! It only takes a few minutes."
Sure, and you could also churn your own butter. The point isn't just about saving time; it's about consistency, accuracy, and professionalism.
- It's Always Up-to-Date: An automated changelog is never forgotten. It's generated from the single source of truth: your Git history.
- It's Professional: A well-formatted changelog shows users and stakeholders that you run a tight ship. It communicates progress clearly and effectively.
- It's Effortless: Once set up, it requires zero extra work. Your commit messages are your release notes.
Introducing git-cliff
There are many tools for this, but I'm a big fan of git-cliff. It's fast, highly customizable, and written in Rust, because of course it is...
It parses your Git history, filters commits based on our conventional commit types (feat, fix, etc.), and generates a beautiful Markdown file.
Installation
You can install git-cliff in several ways. Pick your favorite.
apk add git-cliffpacman -S git-cliff# Install via an npm wrapper
npm install -g git-cliff-npm# For Windows users
winget install orhun.git-cliff# On macOS or Linux
brew install git-cliff# If you have the Rust toolchain installed
cargo install git-cliffUsage in CI/CD with Docker
The easiest and most portable way to run git-cliff in a CI/CD pipeline is by using its official Docker image. This avoids having to install it on your runners and ensures you're always using a specific, tested version.
Here’s an example of a GitLab CI job that generates the changelog and attaches it as a build artifact. The same principle applies to GitHub Actions, Jenkins, or any other CI system.
# .gitlab-ci.yml
stages:
- release
create_changelog:
stage: release
image: orhunp/git-cliff:latest
script:
# We need to fetch the full history to parse all commits
- git fetch --all --tags
# Generate the changelog
- git-cliff --output CHANGELOG.md --tag ${CI_COMMIT_TAG}
artifacts:
paths:
- CHANGELOG.md
rules:
# Only run this job when a new tag is pushed
- if: $CI_COMMIT_TAGThis job will automatically:
- Run only when you push a new Git tag.
- Use the
git-cliffDocker image. - Fetch all Git history to ensure the log is complete.
- Generate a
CHANGELOG.mdfile for the specific tag being released. - Save the
CHANGELOG.mdas a downloadable artifact for the release.
Now your release process is not only automated but also documented. That's how you run a professional operation.
Basic Usage
To generate a changelog for all versions, you can run:
git-cliff --output CHANGELOG.mdTo generate the notes for a specific upcoming version (e.g., v1.1.0):
git-cliff --tag v1.1.0 > new_release_notes.mdConfiguration
The real power of git-cliff comes from its configuration file, cliff.toml. You can generate a default one to get started:
git-cliff --initThis gives you a decent starting point. git-cliff is highly customizable. Tailor it to your needs. Here's a this cliff.toml I use with the conventional commits format described in our workflow, emphasing on scope being ticket IDs. It's minimal but effective.
[changelog]
# template for the changelog footer
header = """
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%d-%b-%Y") }}
{% 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 }}\
{% endfor %}
{% endfor %}\n
Full changelog available at [{{ previous.version }}...{{ version }}]({{ get_env(name="CI_PROJECT_URL") }}/-/compare/{{ previous.version }}...{{ version }})
"""
# template for the changelog footer
footer = """
"""
# remove the leading and trailing s
trim = true
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = []
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"This configuration does exactly what we need:
- Groups commits into logical sections (
Features,Bug Fixes, etc.). - Includes the scope (like a ticket ID) for context.
- Filters out noise, focusing only on what matters for a release.
- Provides a link to the full diff between releases. The template uses a GitLab-specific URL; adjust it in the
bodysection for other platforms.
It's a strong foundation. You can always explore the official docs later if you feel the need to tweak it further.