Conventional Commits is a lightweight convention for structuring commit messages so your history is readable by humans and parsable by tools. With it, you can auto‑generate changelogs, version numbers, and even publish releases without manual bookkeeping.
TL;DR
- Format:
type(scope)?: subject+ optional body + optional footer. - Types:
feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert. - Breaking changes: add
!after type/scope or include aBREAKING CHANGE:footer. - Lint with
@commitlint/config-conventionaland Husky to enforce the spec. - Automate changelogs and versioning with
semantic-release(or conventional-changelog).
The Spec (Quick Overview)
Anatomy:
type(scope)?: subject
body (optional, wrapped to ~72 chars)
footer (optional)
BREAKING CHANGE: description (if any)
Refs: #123Rules that matter:
- type: one of the conventional types (see cheat sheet below).
- scope: optional, lower‑case identifier like
auth,billing,ui/button. - subject: imperative, lower case, no trailing period.
- breaking: mark with
!(e.g.,feat(auth)!:) or aBREAKING CHANGE:footer.
Examples:
feat(search): add debounced SearchInput and result highlighting
fix(blog): correct sitemap lastModified for MDX posts
refactor(ui/card): remove variant prop in favor of intent
perf(image): inline critical hero image to improve LCP
revert: revert "feat(search): add debounced SearchInput and result highlighting"Breaking Changes
Two ways to signal a breaking change:
- Punctuation:
feat(api)!: drop legacy v1 endpoints - Footer: add a section in the message body:
feat(api): drop legacy v1 endpoints
BREAKING CHANGE: The /v1 routes are removed. Use /v2 equivalents.Why it matters: release tools bump major versions automatically when they see breaking changes.
Enforce the Convention Locally (commitlint + Husky)
Install dev dependencies:
pnpm add -D @commitlint/cli @commitlint/config-conventional huskyAdd commitlint config (TS or JS). Minimal JS config:
// commitlint.config.cjs
module.exports = { extends: ["@commitlint/config-conventional"] };Initialize Husky and add a commit-msg hook:
pnpm dlx husky init
echo "pnpm commitlint --edit \"$1\"" > .husky/commit-msg
chmod +x .husky/commit-msgOptional: add a prepare script so hooks install after pnpm install:
{
"scripts": {
"prepare": "husky"
}
}Now any commit failing the convention will be rejected with a clear error.
Auto‑Generate Changelogs and Releases (semantic‑release)
semantic-release reads your Conventional Commits and decides the next version:
feat→ minor bumpfix→ patch bumpBREAKING CHANGE/!→ major bump
Install:
pnpm add -D semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/npm @semantic-release/githubAdd a basic config:
// release.config.cjs
module.exports = {
branches: ["main"],
plugins: [
["@semantic-release/commit-analyzer", { preset: "conventionalcommits" }],
["@semantic-release/release-notes-generator", { preset: "conventionalcommits" }],
["@semantic-release/changelog", { changelogFile: "CHANGELOG.md" }],
["@semantic-release/npm", {}],
["@semantic-release/github", {}],
["@semantic-release/git", { assets: ["CHANGELOG.md", "package.json", "pnpm-lock.yaml"], message: "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" }],
],
};Run locally (dry run) or in CI:
pnpm semantic-release --dry-runIn CI, add a job on main that runs semantic-release with a token that can create GitHub releases and push tags.
Conventional Commits in PRs and Squash Merges
- Prefer squash merges and ensure the final squash message follows the convention.
- Keep the PR title conventional; many bots validate PR titles (
conventional-prchecks). - If you must merge multiple commits, ensure each respects the spec for a clean history.
Monorepos and Scopes
- Use scope to denote package or app:
feat(web),fix(api),build(repo). - Some release tools can map scopes to independent packages; check
semantic-releasemonorepo setups orchangesetsif you need per‑package versioning.
Cheat Sheet of Types
feat: user‑facing featurefix: bug fixdocs: documentation onlystyle: formatting (no code meaning changes)refactor: code change that neither fixes a bug nor adds a featureperf: performance improvementstest: add/modify testsbuild: build system or external dependencies changesci: CI configuration changeschore: maintenance tasks (no src/test changes)revert: revert a previous commit
Pitfalls and How to Avoid Them
- Too‑broad scopes: prefer
ui/cardoveruifor clarity. - Stuffing multiple concerns into one commit: split refactor vs feature.
- Forgetting BREAKING markers: if you remove/rename public APIs, mark it.
- Overusing chore: if it’s user‑facing, it’s probably
featorfix.
Minimal Team Checklist
- Add commitlint + Husky and block non‑conforming messages.
- Use scopes meaningful to your architecture (app/package/feature).
- Enable semantic‑release in CI for automatic versioning and changelogs.
- Prefer squash merges; ensure PR titles follow the convention.
- Document your team’s examples in
docs/commits.mdand link from the README.
Adopting Conventional Commits takes an hour and pays off forever. Your history becomes searchable, your changelogs write themselves, and releases are predictable - all while keeping the team aligned on what shipped and why.
