Conventional Commits: Structure Your Git History for Humans and Machines
Git history is documentation. A well-structured commit history lets you understand why code changed, generate accurate changelogs, and automate semantic version bumps. Conventional Commits is a lightweight specification that adds structure to commit messages — and unlocks a set of powerful tooling built around that structure.
The Format
A conventional commit message has three parts:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types describe the kind of change:
feat: A new feature (bumps minor version)fix: A bug fix (bumps patch version)docs: Documentation changes onlystyle: Formatting, whitespace (no logic change)refactor: Code change that's neither a feature nor a fixperf: Performance improvementtest: Adding or fixing testsbuild: Build system or dependency changesci: CI configurationchore: Other maintenance tasks
Breaking changes add ! after the type or BREAKING CHANGE: in the footer:
feat!: change authentication API to JWT
Or:
feat: change authentication API to JWT
BREAKING CHANGE: Existing session tokens are invalidated.
Users must log in again to receive a JWT.
Examples
feat: add OAuth2 login with Google
fix(auth): prevent session fixation on login
docs: update API reference for /users endpoint
refactor(database): extract connection pool to separate module
perf: cache user permissions to reduce database queries
ci: add semgrep security scanning to PR checks
feat!: require API versioning header
BREAKING CHANGE: All API requests must include X-API-Version header
Scopes are optional and can be any identifier meaningful to your project — a module name, layer name, or area of the codebase.
Why It Matters
1. Readable History
Compare:
# Bad
git log --oneline
abc1234 fix stuff
def5678 update
bcd9012 changes
# Conventional
git log --oneline
abc1234 fix(auth): prevent null pointer on expired sessions
def5678 feat(billing): add Stripe webhook endpoint for renewals
bcd9012 docs: add API authentication guide
The second history is scannable. You know what changed and why without opening each commit.
2. Automated Changelogs
Tools like semantic-release, release-please, and standard-version read your commit history to generate changelogs automatically:
## [1.4.0] - 2026-03-04
### Features
- add OAuth2 login with Google (#234)
- add Stripe webhook endpoint for renewals (#237)
### Bug Fixes
- fix null pointer on expired auth sessions (#235)
- fix race condition in background job processor (#238)
This is generated from commit messages — no manual changelog editing.
3. Automatic Version Bumps
Semantic versioning (MAJOR.MINOR.PATCH) becomes deterministic:
fix:commits → patch bump (1.0.0 → 1.0.1)feat:commits → minor bump (1.0.0 → 1.1.0)BREAKING CHANGE→ major bump (1.0.0 → 2.0.0)
CI can publish new versions automatically when commits land on main.
Tooling
Commitizen (Writing Commits)
Commitizen provides an interactive CLI for writing conventional commits:
# Install globally
npm install -g commitizen cz-conventional-changelog
# Or per-project
npm install --save-dev commitizen cz-conventional-changelog
# Configure in package.json
{
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
Replace git commit with git cz (or npx cz):
? Select the type of change: feat
? What is the scope of this change: (auth)
? Write a short description: add Google OAuth2 login
? Is this a breaking change? No
? Issues this commit closes: #234
This generates: feat(auth): add Google OAuth2 login (#234)
Commitlint (Validating Commits)
Commitlint enforces the format via a git hook:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
Create .commitlintrc.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
};
Add to Husky (or Lefthook):
# Lefthook
# .lefthook.yml
commit-msg:
commands:
commitlint:
run: npx commitlint --edit {1}
Now bad commits are rejected before they're created:
git commit -m "fix stuff"
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]
semantic-release (Publishing)
semantic-release automates the full release pipeline:
npm install --save-dev semantic-release
Configure .releaserc.json:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github"
]
}
Add to CI (GitHub Actions):
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
On every push to main, semantic-release:
- Analyzes commits since the last release
- Determines the version bump
- Updates CHANGELOG.md
- Creates a GitHub release
- Publishes to npm
No manual versioning or changelog editing.
release-please (Google's Alternative)
release-please is GitHub's approach — instead of releasing directly, it opens a PR with the version bump and changelog:
# .github/workflows/release-please.yml
on:
push:
branches:
- main
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node
Benefits: The release PR lets you review and edit the changelog before publishing. Useful when you want human oversight on releases.
Adopting in an Existing Project
- Start fresh: Don't rewrite history. Just apply the convention to new commits.
- Add commitlint: Enforce going forward so the history stays consistent.
- Set expectations in CONTRIBUTING.md: Document the format with examples.
- Squash-merge feature branches: PR titles become the merge commit — make them conventional. GitHub allows setting the default PR title from the first commit.
Monorepo Considerations
In a monorepo with multiple packages, scopes map to packages:
feat(api): add rate limiting
fix(frontend): correct button alignment on mobile
chore(deps): bump axios to 1.7.0
Tools like Changesets (.changeset/) integrate with monorepos and handle separate version bumps per package. Commitlint can be configured to require scopes from a specific list.
Non-Node.js Projects
Conventional Commits is language-agnostic. The specification is just a text format. For Python projects, use python-semantic-release. For Go, goreleaser supports Conventional Commits for changelog generation. For any language, git-cliff generates changelogs from conventional commits without language-specific tooling:
# Install git-cliff
cargo install git-cliff
# Or: brew install git-cliff
# Generate a changelog
git-cliff --output CHANGELOG.md
The Bottom Line
Conventional Commits adds maybe 10 seconds to each commit — the time it takes to prefix with feat: or fix:. The return is automated changelogs, predictable versioning, and a history that communicates intent instead of just change. For any project with multiple contributors or regular releases, the convention pays for itself quickly.