Auto-Generate a Changelog From Git Commits Step by Step
As engineers, we live in Git. Our daily work revolves around commits, branches, pull requests, and merges. But while Git is fantastic for tracking code changes, it doesn't automatically translate those highly technical revisions into a user-friendly changelog that explains what's new, what's fixed, and what's improved in a way your users, product managers, or even other teams can understand.
Manually crafting changelogs is a tedious, error-prone, and often last-minute task that few enjoy. It pulls you away from actual development and can quickly become a bottleneck. The good news? You can largely automate this process by leveraging your existing Git history. This article will walk you through the principles and practical steps to auto-generate a changelog from your Git commits, exploring both DIY methods and dedicated tools like Shipnote.
The Foundation: Structured Git Commits
The secret to effective changelog automation lies in your commit messages. If your commit history is a jumble of "fix bug," "update code," or "stuff," no tool can magically divine user-friendly release notes. You need structure.
The most widely adopted standard for structured commit messages is Conventional Commits. This specification provides a lightweight convention on top of commit messages, defining a set of rules for creating an explicit commit history. A Conventional Commit typically looks like this:
<type>([scope]): <description>
[body]
[footer(s)]
Here's a breakdown:
<type>: This is crucial. It tells you the nature of the change. Common types include:feat: A new feature.fix: A bug fix.docs: Documentation only changes.style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.).refactor: A code change that neither fixes a bug nor adds a feature.perf: A code change that improves performance.test: Adding missing tests or correcting existing tests.build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm).ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs).chore: Other changes that don't modify src or test files.revert: Reverts a previous commit.
([scope]): An optional scope that provides additional contextual information. For example,feat(auth)orfix(api-gateway).<description>: A concise, imperative summary of the change.[body]: An optional longer description providing more context.[footer(s)]: Optional footers for metadata, such asBREAKING CHANGE:orCloses #123. TheBREAKING CHANGE:footer is particularly important for automated tools, as it signals a major version bump and a critical note for users.
Example 1: Conventional Commits in action
feat(checkout): Add gift card redemption option
This feature allows users to apply gift card codes directly
on the checkout page. It integrates with the existing payment
gateway service.
Closes #456
fix(dashboard): Correct pagination display for empty results
Previously, the dashboard would show incorrect page numbers
when a search returned no results. This commit ensures
the pagination component correctly handles zero-result sets.
perf(image-upload): Optimize thumbnail generation
Reduced CPU usage by 20% during thumbnail generation
by switching to a more efficient image processing library.
BREAKING CHANGE: Requires Node.js 16 or higher due to new library dependencies.
By consistently using Conventional Commits, you create a machine-readable history that can be parsed and transformed into a structured changelog.
The Manual Approach: Scripting Your Own Changelog
If you're a DIY enthusiast, you might consider scripting your own changelog generation. This typically involves using git log and some command-line parsing tools.
Here's a simplified step-by-step outline:
- Identify the relevant commits: You usually want changes since the last release or tag.
bash git log --pretty=format:"%s%n%b" v1.0.0..HEADThis command fetches the subject (%s) and body (%b) of all commits between thev1.0.0tag and the currentHEAD. - Filter by commit type: Use
grepor similar tools to filter forfeat,fix,perf, etc.bash git log --pretty=format:"%s%n%b" v1.0.0..HEAD | grep -E "^(feat|fix|perf):" - Parse and format: Extract the relevant description and format it into a user-friendly list. This is where it gets tricky. You'll likely need
sed,awk, or a scripting language like Python or Node.js to strip the type and scope, and then format it into Markdown.
Example 2: A basic attempt at parsing and formatting with git log and sed
Let's say you want a list of new features and bug fixes since the last tag.
echo "## New Features"
git log --pretty=format:"- %s" v1.0.0..HEAD | grep "^- feat:" | sed 's/^- feat(\([^)]*\)): /- /g' | sed 's/^- feat: /- /g'
echo ""
echo "## Bug Fixes"
git log --pretty=format:"- %s" v1.0.0..HEAD | grep "^- fix:" | sed 's/^- fix(\([^)]*\)): /- /g' | sed 's/^- fix: /- /g'
This rudimentary script attempts to:
* Print a header for "New Features".
* git log fetches commit subjects.
* grep filters for lines starting with ^- feat:.
* sed then tries to strip the feat: or feat(scope): part, leaving just the description.
* It repeats for "Bug Fixes".
Pitfalls of the Manual Approach:
- Complexity: Building a robust parser for all Conventional Commit nuances (especially multi-line bodies, breaking changes, and different footers) is complex and time-consuming.
- Maintenance: Your script needs to be maintained. What if you introduce a new commit type? What if the Conventional Commit spec evolves?
- User-friendliness:
git logoutput is inherently technical. Translating "feat(auth): Refactor JWT token validation" into "Improved security and reliability of user authentication" requires more than simple string manipulation. - PR Merges vs. Commits: If you squash and merge PRs, you might want the changelog entry to come from the PR title/description, not individual commits. Handling this requires additional logic.
- Edge Cases: How do you handle reverts? What about commits that are part of a larger feature but aren't meant for external release notes?
- Versioning: Automatically determining the next semantic version (major, minor, patch) based on commit types (especially
BREAKING CHANGE:) is another layer of complexity.
While a manual script can work for very simple cases, it quickly becomes a significant engineering effort that distracts from core product development.
The Automated Approach: Leveraging Dedicated Tools
Recognizing the challenges of manual changelog generation, a variety of tools have emerged to automate this process. These tools typically:
- Parse Git history: They understand Conventional Commits, PR titles, and merge messages.
- Categorize changes: Grouping features, fixes, performance improvements, etc.
- Generate release notes: Outputting a structured changelog in Markdown or other formats.
- Handle versioning: Often integrating with semantic versioning