Guide
The Conventional Commits Guide
A structured commit format that machines can parse - so versioning, changelogs, and releases run themselves. Here's the spec, why it helps automation, and how to adopt it.
Conventional Commits is a lightweight convention for the text of your commit messages. It defines a small, structured grammar - a type, an optional scope, and a short description - that both humans and tooling can parse reliably. The spec (currently version 1.0.0) grew out of the Angular commit guidelines and is now used across thousands of open-source projects.
The payoff is automation. Because every message follows the same shape, tools can derive the next semantic version, generate changelogs, trigger releases, and route notifications without anyone hand-curating a list of what changed. A fix: becomes a patch bump, a feat: a minor bump, and a breaking change a major bump - mechanically, every time.
None of that requires a heavyweight process. Conventional Commits is a discipline you can adopt in an afternoon, enforce with a git hook, and layer measurement on top of. It pairs naturally with a build-and-measure workflow: structured history is the raw signal behind measurement, changelog generation, and release analytics.
Learn the grammar
A Conventional Commit message has a structured first line - the header - and optional body and footers:
- type: a noun describing the kind of change (required).
- scope: an optional area of the codebase in parentheses, e.g.
(api)or(auth). - description: a short, imperative summary after the colon and space.
The canonical shape is type(scope): description. Examples: feat(auth): add passwordless login, fix: prevent race in cache warmup, docs(readme): clarify install steps. Keep the description in the imperative mood ("add", not "added") and under about 50-72 characters.
Pick the right type
The spec mandates only two types, but a widely-used superset (from the Angular convention) covers most needs:
- feat - a new feature (triggers a minor version bump).
- fix - a bug fix (triggers a patch version bump).
- docs - documentation-only changes.
- refactor - a code change that neither fixes a bug nor adds a feature.
- perf - a change that improves performance.
- test - adding or correcting tests.
- build / ci - changes to the build system or CI configuration.
- chore - maintenance that doesn't touch src or tests.
Only feat and fix affect the released version by default; the rest are informational and keep your history legible.
Signal breaking changes explicitly
Breaking changes are the highest-signal event in your history because they force a major version bump. The spec gives you two ways to flag them:
- Append a
!after the type or scope, e.g.feat(api)!: drop support for v1 tokens. - Add a footer beginning with
BREAKING CHANGE:followed by a description of the migration.
You can use both together. The footer is preferred when you need room to explain the migration path, since that text can flow directly into your changelog and release notes.
Connect commits to SemVer and changelogs
The mapping to Semantic Versioning is the reason the convention exists:
fix:→ PATCH (x.x.1)feat:→ MINOR (x.1.x)BREAKING CHANGE/!→ MAJOR (1.x.x)
Tools like semantic-release, release-please, and changesets read the log, compute the next version, and generate a categorized changelog automatically. See our changelog generator guide for turning this structured history into human-readable release notes.
Enforce the format with tooling
Conventions decay without enforcement. Wire it into the toolchain so bad messages never land:
- commitlint validates messages against a ruleset (
@commitlint/config-conventionalships the standard rules). - husky or a native git hook runs commitlint on the
commit-msghook, rejecting non-conforming messages locally. - commitizen gives an interactive prompt so contributors don't have to memorize the grammar.
- A CI check on the PR title or squash-merge message catches anything that slips through.
If you squash-merge, enforce the convention on the PR title - that's the message that ends up in the default branch.
Roll it out to a team
Adoption is a change-management problem more than a technical one:
- Agree on the type list and any custom scopes, and document them in
CONTRIBUTING.md. - Start with a warning-level lint before flipping it to a hard error, so people learn without friction.
- Pick a merge strategy - squash-merge with an enforced PR title is the lowest-effort path to clean history.
- Add the commitizen prompt to lower the barrier for occasional contributors.
Once history is structured, you can measure it. Clean commit metadata feeds outcome measurement, review analytics, and release tracking - the point of the discipline is what it unlocks downstream, not the ceremony itself.