Conventional Commits Cheat Sheet: Every Type, Rule and Version Bump
The full conventional commits reference: the message grammar, all eleven types with a real example and the release bump each one triggers, breaking-change syntax, the casing rules the spec actually sets, and a commitlint config that enforces it.
What this solves
Find a complete, correct reference for conventional commit types, syntax and version-bump behaviour that can be pasted into a repository's contributing guide.
How S2P helps
You can write any conventional commit correctly, including breaking changes and reverts, and enforce the convention with commitlint, a husky hook and squash-merge pull request titles.
Key takeaways
- The Conventional Commits 1.0.0 specification requires exactly two types, feat and fix. Every other type in the familiar list is convention, not spec.
- fix maps to a PATCH release and feat to MINOR, while any commit carrying an exclamation mark or a BREAKING CHANGE footer maps to MAJOR regardless of its type.
- The spec tells implementors not to treat commit messages as case-sensitive, but commitlint's default config rejects a sentence-case subject, so the tool is stricter than the standard.
- The 50/72 rule comes from Tim Pope's 2008 post on git commit messages, not from Conventional Commits, and commitlint's default header limit is 100 characters.
- A commit-msg hook only runs on machines that installed it, so squash-merge titles and a lint step in CI are what actually enforce the convention across a team.
Section 1
The anatomy of a conventional commit
One required header line, then two optional blocks separated by blank lines.
The Conventional Commits 1.0.0 specification defines a header, an optional body and optional footers. The header is a type, an optional scope in parentheses, an optional exclamation mark, then a colon, a single space and a description.
Rule 1 of the spec makes the type, the terminal colon and the space mandatory, and marks the scope and the exclamation mark as optional. Rule 5 requires the description to follow immediately after the colon and space. Rule 6 requires the body to begin one blank line after the description, and rule 8 requires footers to start one blank line after the body.
Footer tokens replace spaces with a dash under rule 9, so Reviewed-by is a valid token and Reviewed by is not. BREAKING CHANGE is the one exception, allowed as a token with its space intact.
Anatomy of a conventional commit message
feat(api)!: return 404 instead of 200 for a missing user
^ ^ ^^ ^
| | || +-- description: required, one space after the colon
| | |+---- colon: required
| | +----- exclamation mark: optional, marks a breaking change
| +--------- scope: optional, a noun in parentheses
+-------------- type: required (feat, fix, or any other noun)
# the same commit as a full message
# header, required
feat(api)!: return 404 instead of 200 for a missing user
# body, optional, one blank line after the header
The old response returned 200 with an empty object, which hid
integration bugs in client code.
# footers, optional, one blank line after the body
BREAKING CHANGE: GET /v1/users/:id now returns 404 when the user
does not exist.
Refs: #421Section 2
Every conventional commit type, with an example and its bump
Eleven types cover almost every commit a team writes. Two are required by the spec, nine are convention.
This is the list that @commitlint/config-conventional accepts out of the box. Its type-enum rule ships with exactly these eleven values: build, chore, ci, docs, feat, fix, perf, refactor, revert, style and test. A type outside that list fails the lint, even though the specification itself allows it.
The meanings in the second column are the definitions published in the commitizen project's conventional-commit-types package, which matches the Angular guidelines the specification is based on.
The bump column is the default behaviour of semantic-release's commit analyzer. Its default release rules file maps breaking changes to major, feat to minor, and fix, perf and revert to patch. Every other type releases nothing, which is the point: a docs commit should never ship a version.
Conventional commit types, real examples and release bumps
| Type | What it means | Example commit | Release bump |
|---|---|---|---|
| feat | A new feature. Required by the spec. | feat(billing): add a yearly plan toggle | Minor |
| fix | A bug fix. Required by the spec. | fix(parser): handle multiple spaces in a string | Patch |
| docs | Documentation only changes. | docs: document the RATE_LIMIT env var | None |
| style | Changes that do not affect the meaning of the code, such as white-space, formatting or missing semi-colons. | style: run the formatter over the components folder | None |
| refactor | A code change that neither fixes a bug nor adds a feature. | refactor(auth): extract token refresh into a helper | None |
| perf | A code change that improves performance. | perf(search): cache the tokenizer between calls | Patch |
| test | Adding missing tests or correcting existing tests. | test(api): cover the retry path on a failed publish | None |
| build | Changes that affect the build system or external dependencies. | build(deps): bump esbuild to 0.23.1 | None |
| ci | Changes to CI configuration files and scripts. | ci: run the test matrix on node 22 | None |
| chore | Other changes that do not modify src or test files. | chore: update the issue templates | None |
| revert | Reverts a previous commit. | revert: feat(billing): add a yearly plan toggle | Patch |
Section 3
What the spec requires and what is only convention
The single most misquoted fact about Conventional Commits is that the type list is part of the standard. It is not.
The specification mandates two types. Rule 2 says feat MUST be used when a commit adds a new feature. Rule 3 says fix MUST be used when a commit represents a bug fix. Rule 14 then states that types other than feat and fix MAY be used in your commit messages. That is the whole of the spec's position on the other nine.
The familiar list comes from the Angular convention. The spec's own FAQ answers the question by pointing at @commitlint/config-conventional, describes it as based on the Angular convention, and lists build, chore, ci, docs, style, refactor, perf and test as recommendations. conventionalcommits.org also says the specification was inspired by and based heavily on the Angular commit guidelines.
The two have drifted apart since. Angular's current commit message guidelines allow build, ci, docs, feat, fix, perf, refactor and test, and no longer list chore or style, while commitlint's default type-enum still ships all eleven. If your linter and a contributor's memory of Angular disagree, that gap is why.
The practical consequence: a commit reading deps: bump esbuild is spec-compliant and will still be rejected by commitlint's default config. The specification is a grammar. The type list is a house rule, and you choose it.
Section 4
Six commit messages that cover almost everything
A plain fix, a scoped feature, both breaking-change forms, a revert and a multi-paragraph body.
Descriptions name the change, not the file that changed. The specification's own worked example is fix: array parsing issue when multiple spaces were contained in string.
The revert case is deliberately loose. Conventional Commits does not define revert behaviour at all; its FAQ leaves that to tooling authors and recommends using the revert type with a footer that references the commit SHAs being reverted.
Real conventional commit messages
# a plain fix
fix: prevent a race when two webhooks arrive together
# a scoped feature
feat(queue): retry a failed publish with exponential backoff
# a breaking change marked in the header
feat(api)!: require an Authorization header on every /v1 route
# a breaking change marked in a footer
refactor(config): read settings from a single file
Settings used to be merged from three places, in an order that
nobody could reconstruct from the code.
BREAKING CHANGE: CONFIG_DIR is no longer read. Set CONFIG_FILE
instead.
# a revert, with the reverted commit referenced in a footer
revert: feat(queue): retry a failed publish with exponential backoff
Refs: 7d2b1c4
# a multi-paragraph body with trailers
fix(auth): refresh the token before it expires, not after
Tokens were refreshed on the first 401, which cost every user one
failed request per hour.
The refresh now runs at 90 percent of the token lifetime, and the
old retry-on-401 path is kept as a fallback.
Reviewed-by: A. Maintainer
Refs: #318Section 5
Breaking changes: the exclamation mark and the footer
Two syntaxes, one meaning, one version bump.
Rule 11 says a breaking change MUST be indicated in the type or scope prefix or as a footer entry. Rule 13 covers the prefix form: an exclamation mark immediately before the colon, as in feat!: or feat(api)!:. When you use it, the BREAKING CHANGE footer MAY be omitted and the description describes the break.
Rule 12 covers the footer form: the uppercase text BREAKING CHANGE, then a colon, a space and a description. Rule 16 makes BREAKING-CHANGE with a hyphen synonymous as a footer token, and it is the one part of a conventional commit that must be uppercase.
The SemVer mapping is what makes the extra character worth typing. conventionalcommits.org states that fix commits should be translated to PATCH releases, feat commits to MINOR releases, and commits with BREAKING CHANGE to MAJOR releases regardless of their type. semver.org sets the matching rule from the other side: the major version MUST be incremented if any backward incompatible changes are introduced to the public API.
One caveat below 1.0.0. semver.org says major version zero is for initial development and anything MAY change at any time, so a breaking change during 0.x does not force you to 1.0.0. Release tools differ on what they do with it, so check yours before relying on the behaviour.
Section 6
Casing, length and the 50/72 rule
The specification is looser than your linter, and the 50/72 rule belongs to neither.
Conventional Commits does not require capitalization. Rule 15 tells implementors not to treat the units of information that make up a conventional commit as case-sensitive, and the spec's FAQ answers the question directly: any casing may be used, but it is best to be consistent. BREAKING CHANGE is the single exception and must be uppercase.
Your linter is stricter than that. @commitlint/config-conventional sets type-case to lower-case and sets subject-case to never sentence-case, start-case, pascal-case or upper-case, both at error level. So fix: Handle an empty payload fails the default config even though the specification permits it.
The 50/72 rule is older and separate. Tim Pope published it on tbaggery.com on 19 April 2008: a capitalized, short summary of 50 characters or less, then a body wrapped to about 72 characters or so. His reasoning was mechanical. git log does not wrap text, so a long line runs off an 80-column terminal, and git format-patch turns commits into plain-text email where nested reply markers need the margin.
The two conventions collide on exactly one point. Pope capitalizes the summary; commitlint's default rejects a sentence-case subject. Pick one and let the hook settle the argument. commitlint's own limits are header-max-length 100, body-max-line-length 100 and footer-max-line-length 100, all errors, all overridable.
Section 7
Enforce it with commitlint and a husky hook
Two dev dependencies, one hook file, one config line.
commitlint's getting-started guide installs @commitlint/cli and @commitlint/config-conventional as dev dependencies and puts a single export in commitlint.config.js. Its local-setup guide covers husky v9: run npx husky init, then write the commit-msg hook. That guide is explicit that the commit-msg hook is the supported one and pre-commit is not.
The hook runs commitlint --edit against the message file git passes as the first argument, so a message that breaks a rule aborts the commit and names the rule that failed.
Rules are three-element arrays: level, applicable, value. Level 0 disables a rule, 1 warns and 2 errors, and applicable is always or never. That is how you add a type to the enum or raise the header limit without abandoning the preset.
Hooks are local, and that is their limit. They exist only on machines that ran the install, and git commit --no-verify skips them. Treat the hook as fast feedback for contributors, not as the gate.
commitlint.config.js and the husky commit-msg hook
# install (commitlint getting-started and local-setup guides)
npm install --save-dev @commitlint/cli @commitlint/config-conventional
npm install --save-dev husky
npx husky init
# wire the commit-msg hook (husky v9)
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
# commitlint.config.js, the whole file
export default { extends: ["@commitlint/config-conventional"] };
# commitlint.config.js, extended: one extra type, a longer header
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test",
"deps",
],
],
"header-max-length": [2, "always", 120],
},
};
# check a message without committing
echo "feat: add new feature" | npx commitlint --default-configSection 8
Conventional commits in pull requests, and what about branches
Squash-merge turns the pull request title into the commit message, which is how most teams actually apply this.
GitHub's documentation on configuring commit squashing says the default squash message uses the commit title and message when a pull request contains one commit, and the pull request title plus the list of commits when it contains two or more. A repository setting makes the pull request title the default squash commit title in every case.
So the workflow that costs contributors nothing is: let people commit however they like on the branch, require a conventional pull request title, and squash-merge. The spec's FAQ endorses this directly, saying that with a squash-based workflow lead maintainers can clean up commit messages as they are merged, adding no workload to casual committers.
Lint that title instead of trusting review to catch it. commitlint reads a message from standard input, so a CI step that pipes the pull request title into the CLI applies the same rules as the local hook, on the one string that becomes the commit.
Branch naming is not part of this. The Conventional Commits specification says nothing about branch names at all. If you want the same structure on branches, Conventional Branch is a separate specification with its own prefixes, including feature/, bugfix/, hotfix/, release/ and chore/, and its own FAQ states it is inspired by Conventional Commits rather than part of it.
Section 9
What it costs, and who should skip it
The convention pays for itself when a tool reads your history, not when a human does.
Conventional Commits buys automatic versioning and generated changelogs. The price is a convention every contributor has to learn and a hook that rejects work in progress, and that price is paid on every commit.
Skip it if you never cut versioned releases, or if the repository is a private application deployed continuously with no public API. A machine-readable history you never feed to a machine is pure overhead.
Adopt it if you publish a package, maintain a public API, or want release notes to exist without somebody writing them. Start with feat and fix only, which is all the specification requires. You can add types later; you cannot easily rewrite two years of history.
Section 10
Why a machine-readable history is worth the discipline
Once commits carry a type and a breaking-change marker, the version, the changelog and the announcement all stop being written by hand.
Three things become derivable. The next version number is a function of the commit types since the last tag. The changelog is those same commits grouped by type, which is what conventional-changelog emits. And the announcement finally has structured input instead of a raw diff.
semantic-release and release-please both read a conventional history to do the first two, and GitHub can generate release notes from merged pull requests with no convention at all. Our guide to automating release notes on GitHub compares them side by side.
Ship 2 Post picks up the third job. It watches your repository for releases, tags, merged pull requests and deployments through the Ship 2 Post GitHub App, qualifies each signal with rules on semver, branch, path, label and repository scope, and drafts channel-native posts you approve before anything publishes. A conventional history is what makes those rules precise: a feat commit and a chore commit are the same event to a webhook and completely different events to your audience.
That is the real argument for the convention. You are not writing commit messages for your future self. You are writing structured input for everything downstream of the merge.
FAQ
Questions this article answers
What are conventional commits in git?
Conventional Commits is a specification for the format of a git commit message: a type such as feat or fix, an optional scope in parentheses, an optional exclamation mark for a breaking change, then a colon, a space and a short description. Version 1.0.0 defines that grammar so tools can read a repository's history and derive version numbers and changelogs from it.
What is perf in conventional commits?
perf marks a code change that improves performance. It sits between fix and refactor: a refactor changes structure with no observable effect, while a perf commit makes something measurably faster without changing behaviour. It is not required by the specification, it comes from the Angular convention, and semantic-release's default release rules treat perf as a patch release while refactor triggers no release.
Do Conventional Commits require capitalization?
No. Rule 15 of the 1.0.0 specification tells implementors not to treat the units of a conventional commit as case-sensitive, and the spec's FAQ says any casing may be used as long as you stay consistent. The one exception is BREAKING CHANGE, which must be uppercase. commitlint's default config is stricter: it requires a lowercase type and rejects a sentence-case subject.
How do I use Conventional Commits in pull requests?
Most teams squash-merge and make the pull request title the conventional message. GitHub's docs say the default squash commit message uses the commit title when a pull request has one commit and the pull request title plus the commit list when it has more, and a repository setting can always use the pull request title. Lint that title in CI.
What is the difference between Conventional Commits (chore) and build commits in git guidelines?
build covers changes that affect the build system or external dependencies, such as a bundler config or a dependency bump. chore covers other changes that do not modify src or test files, such as editing an issue template. Neither produces a release under semantic-release's default rules, so the distinction is about changelog readability rather than versioning.
What is the 50/72 rule?
A commit message convention from Tim Pope's April 2008 post on tbaggery.com: a summary of 50 characters or less, then a body wrapped to about 72 characters. It exists because git log does not wrap text and git format-patch turns commits into plain-text email. It is not part of Conventional Commits, whose tooling defaults to a 100-character header limit.
Related guides and pages
Where to go next
Hand-picked pages that go deeper on the workflow, channels, and tooling covered above.
