Automating changelogs in Flutter projects
By Ann Tech · 16 February 2026
Automating changelogs ensures every release has accurate release notes without relying on anyone to remember what changed. With conventional commits and a generation script, changelogs write themselves.
Conventional commits
The foundation is consistent commit messages. Conventional Commits is a specification:
type(scope): description
feat(orders): add order cancellation
fix(auth): fix token refresh loop
docs(readme): update installation guide
chore(deps): bump firebase_core to 3.0.0
refactor(cart): extract CartItemWidget
test(orders): add cancellation unit tests
perf(images): lazy-load product images
Types that appear in changelogs: feat, fix, perf, BREAKING CHANGE.
Types that don't: docs, chore, refactor, test, style, ci.
enforce with git hooks
# .git/hooks/commit-msg (or use lefthook/husky)
#!/bin/sh
COMMIT_MSG=$(cat $1)
PATTERN='^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\(.+\))?: .+'
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "ERROR: Commit message must follow Conventional Commits:"
echo " feat(scope): description"
echo " fix(scope): description"
exit 1
fi
With Lefthook (better cross-platform support):
# lefthook.yml
commit-msg:
commands:
validate:
run: npx --no -- commitlint --edit {1}
Generating changelogs with git-cliff
git-cliff reads conventional commits and generates a structured changelog:
# Install
brew install git-cliff # macOS
# Generate changelog since last tag
git cliff --output CHANGELOG.md
# Generate for a specific range
git cliff v1.0.0..HEAD --output CHANGELOG.md
Configuration in cliff.toml:
[changelog]
header = "# Changelog\n\n"
body = """
{% if version %}## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}{% else %}## Unreleased{% endif %}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message }}
{% endfor %}
{% endfor %}
"""
[git]
conventional_commits = true
filter_unconventional = true
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^docs", skip = true },
{ message = "^chore", skip = true },
{ message = "^ci", skip = true },
]
Melos changelog (for monorepos)
In a Melos monorepo, changelogs are per-package:
# melos.yaml
command:
version:
message: "chore(release): publish packages\n\n{new_package_versions}"
includeScopes: true
linkToCommits: true
branch: main
changelogs:
- path: CHANGELOG.md
description: All notable changes
# Bump versions and generate changelogs
melos version
# Preview what would be generated
melos version --no-git-commit-version
GitHub Actions workflow
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for git-cliff
- name: Generate changelog
uses: orhun/git-cliff-action@v3
id: cliff
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: CHANGELOG.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body: ${{ steps.cliff.outputs.content }}
token: ${{ secrets.GITHUB_TOKEN }}
Tagging releases
# Tag after bumping version
git tag v2.5.1
git push origin v2.5.1
# Or with Fastlane:
lane :tag_release do
version = get_version_number
git_tag(tag: "v#{version}")
push_git_tags
end
Play Store and App Store release notes
Extract the latest changelog entry for store submission:
# Get the first section from CHANGELOG.md
awk '/^## /{if(found) exit; found=1} found' CHANGELOG.md | tail -n +2 > release_notes.txt
In Fastlane:
lane :deploy_android do
changelog = File.read('../release_notes.txt').strip
supply(
track: 'production',
rollout: '0.1',
release_status: 'draft',
changelog: changelog,
)
end
Common pitfalls
Inconsistent commit message style. git-cliff only works with conventional commits. Without enforcement (commit-msg hook), developers default to free-form messages and the generated changelog is empty.
Not including fetch-depth: 0 in CI. actions/checkout defaults to a shallow clone. git-cliff needs the full commit history to see all commits since the last tag. Always use fetch-depth: 0.
Generating changelog after tagging. Tag after the changelog is committed, not before. Otherwise the release commit (chore: update changelog) appears in the next version's changelog.
Sign in to like, dislike, or report.