| name | changelog-gen |
| description | Generate changelogs from git history: conventional commits, git-cliff, keep-a-changelog format, release notes generation. Trigger: when generating a changelog, conventional commits, git-cliff, release notes, keep-a-changelog, CHANGELOG.md generation, semantic release |
| version | 1 |
| argument-hint | [generate|preview|release <version>|since <tag>] |
| allowed-tools | ["bash","read","write","grep","glob"] |
Changelog Generation
You are now operating in changelog generation mode.
Conventional Commits Format
Changelogs are generated from commit messages that follow the Conventional Commits specification:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types and Their Changelog Sections
| Type | Changelog Section | Example |
|---|
feat | Added | feat(auth): add OAuth2 login support |
fix | Fixed | fix(api): correct pagination offset calculation |
perf | Changed | perf(db): add index on user_id column |
refactor | Changed | refactor(core): extract runner into separate package |
docs | - (omitted) | docs: update README installation steps |
test | - (omitted) | test(auth): add JWT expiry tests |
build | - (omitted) | build: upgrade Go to 1.22 |
ci | - (omitted) | ci: add race detector to test workflow |
BREAKING CHANGE | Breaking Changes | Footer: BREAKING CHANGE: removed /v1/users endpoint |
Examples of Good Conventional Commits
git commit -m "feat(billing): add Stripe subscription management"
git commit -m "fix(auth): prevent session fixation on password change"
git commit -m "feat(api)!: remove deprecated /v1/search endpoint
BREAKING CHANGE: /v1/search has been removed. Use /v2/search with the new filter syntax.
Migration guide: https://docs.example.com/migration/v2-search"
git commit -m "fix(scheduler): handle concurrent job cancellation
Closes #142"
git-cliff — Automated Changelog Generation
Installation
brew install git-cliff
cargo install git-cliff
curl -s https://api.github.com/repos/orhun/git-cliff/releases/latest \
| jq -r '.assets[] | select(.name | contains("x86_64-unknown-linux-musl.tar.gz")) | .browser_download_url' \
| xargs curl -L -o git-cliff.tar.gz
tar -xzf git-cliff.tar.gz && mv git-cliff /usr/local/bin/
git-cliff --version
Configuration (cliff.toml)
[changelog]
header = """
# Changelog
All notable changes to this project are documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
"""
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
"""
footer = """
<!-- generated by git-cliff -->
"""
trim = true
[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
commit_preprocessors = []
commit_parsers = [
{ message = "^feat", group = "Added" },
{ message = "^fix", group = "Fixed" },
{ message = "^refactor|^perf", group = "Changed" },
{ message = "^docs", skip = true },
{ message = "^test", skip = true },
{ message = "^build|^ci|^chore", skip = true },
{ body = ".*security", group = "Security" },
]
protect_breaking_commits = false
filter_commits = false
tag_pattern = "v[0-9].*"
skip_tags = "v0.1.0-beta.1"
ignore_tags = ""
sort_commits = "newest"
git-cliff Usage
git-cliff
git-cliff -o CHANGELOG.md
git-cliff --unreleased
git-cliff --tag v1.2.3 -o CHANGELOG.md
git-cliff v1.1.0..v1.2.0
git-cliff --unreleased --tag v1.2.0
git-cliff --strip header
git-cliff --output-format json
git-cliff --init
Keep a Changelog Format (Manual)
# Changelog
All notable changes to this project are documented here.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
This project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
- New dashboard analytics page with real-time metrics
### Fixed
- Session expiry now correctly respects custom TTL settings
---
## [1.2.0] - 2024-03-15
### Added
- OAuth2 login with GitHub and Google
- Export to CSV from all list views
- Webhook support for issue status changes
### Changed
- Improved query performance for large datasets (10x faster)
- Updated error messages to be more actionable
### Fixed
- Fixed pagination on the search results page
- Fixed incorrect timestamps in export files (was UTC, now user timezone)
### Security
- Upgraded dependencies to address CVE-2024-1234 in lodash
---
## [1.1.0] - 2024-02-01
### Added
- Rate limiting on all public API endpoints
### Deprecated
- `/v1/search` endpoint is deprecated; use `/v2/search` instead
### Removed
- Removed legacy XML response format support
---
## [1.0.0] - 2024-01-01
Initial stable release.
[Unreleased]: https://github.com/your-org/your-repo/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/your-org/your-repo/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/your-org/your-repo/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/your-org/your-repo/releases/tag/v1.0.0
Script-Based Changelog Generation (No External Tool)
#!/bin/bash
set -euo pipefail
REPO_URL="${REPO_URL:-https://github.com/your-org/your-repo}"
OUTPUT="${1:-CHANGELOG.md}"
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
DATE=$(date +%Y-%m-%d)
generate_section() {
local type="$1"
local heading="$2"
local commits
if [ -n "$LAST_TAG" ]; then
commits=$(git log "${LAST_TAG}..HEAD" --pretty=format:"%s" 2>/dev/null | grep "^${type}" || true)
else
commits=$(git log --pretty=format:"%s" | grep "^${type}" || true)
fi
if [ -n "$commits" ]; then
echo "### ${heading}"
echo ""
echo "$commits" | sed "s/^${type}[^:]*: /- /" | sed 's/^- (/- **/' | sed 's/): /**: /'
echo ""
fi
}
{
echo "## [Unreleased] - ${DATE}"
echo ""
generate_section "feat" "Added"
generate_section "fix" "Fixed"
generate_section "perf\|refactor" "Changed"
generate_section "security" "Security"
if [ -n "$LAST_TAG" ]; then
echo "[Unreleased]: ${REPO_URL}/compare/${LAST_TAG}...HEAD"
fi
} > "/tmp/new-changes.md"
if [ -f "$OUTPUT" ]; then
awk '/^## \[/{if (!done) {system("cat /tmp/new-changes.md"); done=1} print; next} 1' "$OUTPUT" > "${OUTPUT}.tmp"
mv "${OUTPUT}.tmp" "$OUTPUT"
else
cat /tmp/new-changes.md > "$OUTPUT"
fi
echo "Changelog updated: $OUTPUT"
Release Notes from Git Log
from_tag="v1.1.0"
to_tag="v1.2.0"
git log "${from_tag}..${to_tag}" \
--pretty=format:"%s" \
--no-merges | \
grep -E "^(feat|fix|perf|refactor|security)" | \
sort | \
awk '
/^feat/ { print "- " $0 > "/tmp/added.txt" }
/^fix/ { print "- " $0 > "/tmp/fixed.txt" }
/^perf|refactor/ { print "- " $0 > "/tmp/changed.txt" }
/^security/ { print "- " $0 > "/tmp/security.txt" }
'
echo "## Release Notes for ${to_tag}"
echo ""
for section in added fixed changed security; do
if [ -s "/tmp/${section}.txt" ]; then
echo "### ${section^}"
cat "/tmp/${section}.txt"
echo ""
fi
done
CI/CD Integration
git-cliff --tag "${GITHUB_REF_NAME}" -o CHANGELOG.md
gh release create "${GITHUB_REF_NAME}" \
--title "${GITHUB_REF_NAME}" \
--notes-from-tag \
--notes "$(git-cliff --unreleased --strip all)"
Best Practices
- Follow Conventional Commits in every commit message — automation depends on consistent types.
- Use scopes (
feat(auth):, fix(api):) for multi-module projects to make changelogs navigable.
- Never manually edit the auto-generated sections of CHANGELOG.md — only edit the
[Unreleased] section.
- Run
git-cliff --unreleased before creating a release tag to preview the generated notes.
- Add
CHANGELOG.md to version control and update it as part of the release process.
- Include the compare URL at the bottom of each version section for easy diffing.
- Mark breaking changes with
! in the commit type (e.g., feat!:) and include a BREAKING CHANGE: footer.
- Generate release notes automatically in CI to reduce manual overhead and ensure consistency.