Conventional Commits (a.k.a. semantic commit messages)
A commit convention that makes history machine-readable: a parser reads each
message, decides the next semver version, and builds the changelog from it. "Semantic
commits" is the common nickname for the same thing. It's the foundation the release
automation stands on — semantic-release-automation
covers the tooling that consumes these messages.
The format
<type>(<optional scope>)<optional !>: <description>
<optional body — the "why", wrapped>
<optional footer(s) — BREAKING CHANGE:, Closes #123, Refs: ABC-12>
type / scope: lowercase by convention — the spec is case-insensitive,
except BREAKING CHANGE, which MUST be uppercase. scope is an optional noun in
parens for the area touched.
! before the colon or a BREAKING CHANGE: footer marks a breaking change
(BREAKING-CHANGE: with a hyphen is an accepted synonym).
- description: imperative mood, concise, lowercase, no trailing period.
- Blank line before any body/footer.
feat(auth): add passkey sign-in
fix(parser): handle empty input without throwing
feat(api)!: drop the v1 token endpoint
docs: fix typo in install steps
Types → meaning → version bump
These are the semantic-release defaults (the angular preset plus
commit-analyzer's built-in release rules). They're configurable via releaseRules,
but treat them as the contract.
| Type |
Use for |
Default release |
feat |
a new feature |
minor (x.Y.0) |
fix |
a bug fix |
patch (x.y.Z) |
perf |
performance improvement |
patch |
docs, style, refactor, test, build, ci, chore |
maintenance, no user-facing behaviour change |
no release on their own |
revert |
revert a previous commit |
patch (the change is being undone) |
any type with ! / BREAKING CHANGE: |
incompatible change |
major (X.0.0) |
The "no release" types still belong in history — the tooling just won't cut a
version for them alone. A release containing only chore/docs commits produces
no new version, which is usually correct.
Breaking changes
Either form triggers a major bump and a highlighted changelog section:
feat(api)!: require auth on all routes
…or via a footer (works on any type, and lets you explain the migration):
refactor(db): rename users.email column
BREAKING CHANGE: `users.email` is now `users.email_address`; update queries.
Scopes
Optional, but meaningful: a noun naming the area (fix(toggle): …). In a
monorepo the scope is how releases/changelogs are routed per package — repos
often require it (e.g. feat(piaf-web): …, feat(react-typed-form-kit): …).
Keep a scope vocabulary documented so it stays consistent.
Squash merges: the PR title is the commit
With squash merging (the common modern setup), the PR title becomes the single
commit message on main — so the PR title must be a valid Conventional
Commit, or the release/changelog step sees a non-conventional message and skips
it. Put the individual semantic commits in the squash body for detail.
git-trunk-branch-and-pr-automation
covers the CI that enforces this.
- Single-package repo: the title's scope is optional.
- Monorepo: scope the title to the package, and keep a PR to one package
where practical — a squash collapses everything to one type+scope, so a
multi-package PR applies the same bump to all and muddies per-package changelogs.
Writing good ones
- Imperative mood: "add", not "added"/"adds" (it completes "this commit will…").
- Keep the subject short: the spec sets no limit; commitlint defaults to 100 chars
and Git display favours ~72 — match the project's commitlint config. The body
explains why, not what.
- One logical change per commit. If a change is genuinely both a feature and a
fix, split it; if you can't, the type reflects the highest-impact change
(a
feat that also fixes something is feat).
- Reference issues in the footer:
Closes #123, Refs: ABC-12.
Gotchas
- Non-conventional → silently no release. A typo'd type or a prose subject is
ignored by the analyzer: no bump, no changelog entry. This is the #1 "why
didn't it release / why is the changelog blank?" cause — check the merge commit /
PR title first.
- Don't hand-write
chore(release): … commits. Those are produced by the
release bot; writing one yourself can confuse tooling (and such commits are often
CI-skipped deliberately).
- Issue IDs in the subject can misbehave. A Linear/Jira key like
ABC-123 in
the subject may auto-transition the ticket or render oddly in changelog links;
some setups keep IDs in the footer or de-link them (ABC-123 → ABC - 123) in
the generated changelog.
- Revert format: a header
revert: <subject of the reverted commit> plus a
body line This reverts commit <sha>. — that body line is what the parser
keys on. git revert generates Revert "<subject>" + that line; lowercase it to
revert: so it's recognised. Reverts default to a patch release.
Verify
- Lint locally or in CI with commitlint (
@commitlint/config-conventional), or
validate PR titles with a PR-title action (see
git-trunk-branch-and-pr-automation).
- Confirm the intended bump with a semantic-release dry run
(
npx semantic-release --dry-run) — it prints the next version and release notes
without publishing.
See also
Companion skills in this set:
Sources
- Conventional Commits 1.0.0 — grammar, the
! / BREAKING CHANGE rules, the
BREAKING-CHANGE hyphen synonym, and case-insensitivity (except BREAKING CHANGE,
which must be uppercase): https://www.conventionalcommits.org/en/v1.0.0/
- semantic-release
commit-analyzer default release rules (angular preset) —
feat→minor, fix/perf→patch, breaking→major, revert→patch, every other
type → no release:
https://github.com/semantic-release/commit-analyzer/blob/master/lib/default-release-rules.js
- The type list and per-package scope rules also match conventions enforced in
production repos (
cphk's PR-title validator allows
feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert; piaf-monorepo
requires per-package scopes).
1---2name: conventional-commits3description: The Conventional Commits format — also called "semantic commits" / semantic commit messages — a `type(scope): description` header (`feat`, `fix`, …) plus an optional `BREAKING CHANGE` footer, and how it makes history machine-readable to drive automated version bumps and changelogs. Use when writing a commit message or PR title, deciding the type/scope/bump for a change, setting up or fixing a repo's commit convention, making commits parseable by semantic-release / changelog tooling, validating PR titles, or when a release didn't bump or the changelog came out blank because a commit wasn't conventional. Covers the type→semver mapping, breaking changes, scopes, monorepo scopes, and squash-merge PR titles.4---56# Conventional Commits (a.k.a. semantic commit messages)78A commit convention that makes history **machine-readable**: a parser reads each9message, decides the next semver version, and builds the changelog from it. "Semantic10commits" is the common nickname for the same thing. It's the foundation the release11automation stands on — [`semantic-release-automation`](../semantic-release-automation/SKILL.md)12covers the tooling that consumes these messages.1314## The format1516```text17<type>(<optional scope>)<optional !>: <description>1819<optional body — the "why", wrapped>2021<optional footer(s) — BREAKING CHANGE:, Closes #123, Refs: ABC-12>22```2324- `type` / `scope`: lowercase **by convention** — the spec is case-*insensitive*,25 except `BREAKING CHANGE`, which MUST be uppercase. `scope` is an optional noun in26 parens for the area touched.27- `!` before the colon **or** a `BREAKING CHANGE:` footer marks a breaking change28 (`BREAKING-CHANGE:` with a hyphen is an accepted synonym).29- **description**: imperative mood, concise, lowercase, **no trailing period**.30- Blank line before any body/footer.3132```text33feat(auth): add passkey sign-in34fix(parser): handle empty input without throwing35feat(api)!: drop the v1 token endpoint36docs: fix typo in install steps37```3839## Types → meaning → version bump4041These are the **semantic-release defaults** (the `angular` preset plus42commit-analyzer's built-in release rules). They're configurable via `releaseRules`,43but treat them as the contract.4445| Type | Use for | Default release |46| --- | --- | --- |47| `feat` | a new feature | **minor** (`x.Y.0`) |48| `fix` | a bug fix | **patch** (`x.y.Z`) |49| `perf` | performance improvement | patch |50| `docs`, `style`, `refactor`, `test`, `build`, `ci`, `chore` | maintenance, no user-facing behaviour change | **no release** on their own |51| `revert` | revert a previous commit | **patch** (the change is being undone) |52| any type with `!` / `BREAKING CHANGE:` | incompatible change | **major** (`X.0.0`) |5354The "no release" types still belong in history — the tooling just won't cut a55version for them alone. A release containing only `chore`/`docs` commits produces56**no new version**, which is usually correct.5758## Breaking changes5960Either form triggers a **major** bump and a highlighted changelog section:6162```text63feat(api)!: require auth on all routes64```6566…or via a footer (works on any type, and lets you explain the migration):6768```text69refactor(db): rename users.email column7071BREAKING CHANGE: `users.email` is now `users.email_address`; update queries.72```7374## Scopes7576Optional, but meaningful: a noun naming the area (`fix(toggle): …`). **In a77monorepo the scope is how releases/changelogs are routed per package** — repos78often *require* it (e.g. `feat(piaf-web): …`, `feat(react-typed-form-kit): …`).79Keep a scope vocabulary documented so it stays consistent.8081## Squash merges: the PR title *is* the commit8283With squash merging (the common modern setup), **the PR title becomes the single84commit message on `main`** — so the **PR title must be a valid Conventional85Commit**, or the release/changelog step sees a non-conventional message and skips86it. Put the individual semantic commits in the squash **body** for detail.87[`git-trunk-branch-and-pr-automation`](../git-trunk-branch-and-pr-automation/SKILL.md)88covers the CI that enforces this.8990- **Single-package repo:** the title's scope is optional.91- **Monorepo:** scope the title to the package, and keep a PR to **one package**92 where practical — a squash collapses everything to one type+scope, so a93 multi-package PR applies the same bump to all and muddies per-package changelogs.9495## Writing good ones9697- **Imperative mood:** "add", not "added"/"adds" (it completes "this commit will…").98- Keep the subject short: the spec sets no limit; commitlint defaults to 100 chars99 and Git display favours ~72 — match the project's commitlint config. The body100 explains **why**, not what.101- **One logical change per commit.** If a change is genuinely both a feature and a102 fix, split it; if you can't, the type reflects the **highest-impact** change103 (a `feat` that also fixes something is `feat`).104- Reference issues in the **footer**: `Closes #123`, `Refs: ABC-12`.105106## Gotchas107108- **Non-conventional → silently no release.** A typo'd type or a prose subject is109 *ignored* by the analyzer: no bump, no changelog entry. This is the #1 "why110 didn't it release / why is the changelog blank?" cause — check the merge commit /111 PR title first.112- **Don't hand-write `chore(release): …` commits.** Those are produced *by* the113 release bot; writing one yourself can confuse tooling (and such commits are often114 CI-skipped deliberately).115- **Issue IDs in the subject can misbehave.** A Linear/Jira key like `ABC-123` in116 the subject may auto-transition the ticket or render oddly in changelog links;117 some setups keep IDs in the footer or de-link them (`ABC-123` → `ABC - 123`) in118 the generated changelog.119- **Revert format:** a header `revert: <subject of the reverted commit>` plus a120 **body line** `This reverts commit <sha>.` — that body line is what the parser121 keys on. `git revert` generates `Revert "<subject>"` + that line; lowercase it to122 `revert:` so it's recognised. Reverts default to a **patch** release.123124## Verify125126- Lint locally or in CI with **commitlint** (`@commitlint/config-conventional`), or127 validate PR titles with a PR-title action (see128 [`git-trunk-branch-and-pr-automation`](../git-trunk-branch-and-pr-automation/SKILL.md)).129- Confirm the intended bump with a semantic-release **dry run**130 (`npx semantic-release --dry-run`) — it prints the next version and release notes131 without publishing.132133## See also134135Companion skills in this set:136137- [`semantic-release-automation`](../semantic-release-automation/SKILL.md) — turns138 these commits into versions, a changelog, and GitHub releases.139- [`git-trunk-branch-and-pr-automation`](../git-trunk-branch-and-pr-automation/SKILL.md)140 — branch naming + squash + the PR-title141 validation that enforces this format.142143## Sources144145- **Conventional Commits 1.0.0** — grammar, the `!` / `BREAKING CHANGE` rules, the146 `BREAKING-CHANGE` hyphen synonym, and case-insensitivity (except `BREAKING CHANGE`,147 which must be uppercase): <https://www.conventionalcommits.org/en/v1.0.0/>148- **semantic-release `commit-analyzer` default release rules** (`angular` preset) —149 `feat`→minor, `fix`/`perf`→patch, breaking→major, **`revert`→patch**, every other150 type → no release:151 <https://github.com/semantic-release/commit-analyzer/blob/master/lib/default-release-rules.js>152- The type list and per-package scope rules also match conventions enforced in153 production repos (`cphk`'s PR-title validator allows154 `feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert`; `piaf-monorepo`155 requires per-package scopes).