Conventional Commits are a lightweight convention for structuring Git commit messages. This format uses a simple pattern ([optional scope]: ) to make commit history both human and machine friendly. By consistently tagging commits as features
, fixes
, docs
updates
, etc., teams can automatically generate changelogs, bump versions, and clearly communicate changes. In this post, we’ll break down the Conventional Commits spec, show examples, and explore tools like commit lint and semantic-release that leverage this convention.
Commit Message Format
Each Conventional Commit has three main parts: a type, an optional scope, and a brief description. The basic syntax is:
<type>[optional scope]: <description> [optional body] [optional footer(s)]
For example:
feat(auth): add OAuth2 login flow fix(ui): correct button alignment on dashboard docs(readme): update installation instructions
Here, feat
, fix
, docs
, etc. are the types. A scope like (auth)
, (ui)
, or (readme)
gives context (e.g. which part of the codebase is affected).
The description after the colon is a short imperative summary of the change (e.g. “add” not “added”), ideally no longer than 50 characters.
Optional bodies or footers can provide more detail. For breaking changes, you can either append !
after the type/scope or add a footer BREAKING CHANGE: ...
.
Tip: Keep the header line (type/scope/description) concise, and use the body to explain what and why, not how. Always write in present tense and imperative mood (e.g., “Add feature” instead of “Added feature”). This makes the log read like a list of commands or tasks.
Key Types
The official spec defines a few key types and their meanings:
-
feat
: A new feature. Bumps the minor version in Semantic Versioning. -
fix
: A bug fix. Bumps the patch version. -
BREAKING CHANGE
: Marks a change that breaks backward compatibility. This can be indicated by!
in the header or aBREAKING CHANGE: ...
footer. Breaking changes bump the major version. - Additional types: Other common categories include
chore
(maintenance tasks),docs
(documentation),style
(formatting),refactor
,perf
(performance),ci
(CI config),build
,test
, etc.
These have no direct SemVer impact unless paired with a breaking change.
For example:
feat(ui): implement dark mode toggle fix(api): validate input parameters docs(config): clarify environment variable names chore: update dependencies to latest versions perf(db): improve query speed BREAKING CHANGE: remove support for Node 8 in runtime
Using Scopes and Descriptions
The scope (in parentheses) adds context about what the commit affects. Scopes are optional but useful in larger projects. For instance, feat(parser)
vs. feat(renderer)
immediately tells developers which subsystem changed. Multiple scopes are possible (e.g., (core/server)
), but one clear scope is usually enough.
The description after the colon should be a succinct, imperative summary. Make it clear what changed. Avoid vague terms. For example, prefer:
fix(auth): handle expired tokens correctly
over
fix: auth stuff
If more detail is needed, include a longer body separated by a blank line, or use footers for references (e.g. Refs: #123
, Reviewed-by: Name
). The conventional spec even shows multi-line examples:
fix(cache): prevent stale data Use a request ID to track cache validity. Remove old timeout logic. Reviewed-by: @alice Fixes: #42
Clear Commit Messages
Good commit messages are as important as clean code. A helpful guideline: write the header like a commit to the current state, e.g. “Add user sign-out button” (not “Added ...”).
The first word should be capitalized only if it’s a proper noun (types are usually lowercase). Keep line lengths to ~50-72 characters. If the commit requires explanation, use the body and footers. Over time, this makes version control logs a clear story of changes.
Benefits of Conventional Commits
Using Conventional Commits brings many advantages:
- Automated Changelogs: Tools can parse commit types and auto-generate CHANGELOG.md entries.
- Semantic Versioning: Commit types (feat/fix/major) map to SemVer rules, so release tooling can bump versions automatically.
- Clear Communication: Teammates and stakeholders instantly understand the nature of each commit.
- CI/CD Triggers: Automated pipelines (like deploying to staging on new
feat
commits) become easier to configure. - Easier Collaboration: A consistent format lowers the barrier for new contributors and reviewers. PRs with tidy commit history are simpler to audit.
In short, Conventional Commits make your git history systematic. The changelog and version reflect what changed, not who wrote it. As the spec notes, this “dovetails with SemVer” and lets you “write automated tools on top of” your commit history.
Tooling: commitlint and semantic-release
Several tools work hand-in-hand with Conventional Commits:
- commitlint: A CLI tool that checks commit messages against your rules. It often uses the
@commitlint/config-conventional
preset which enforces Conventional Commits. You can set it up with Husky to lint commits on the fly. For example:
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky npx husky install echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js npx husky add .husky/commit-msg "npx --no-install commitlint --edit \"$1\""
Now, if a commit message doesn’t match (e.g. missing type or too long), commitlint will block it. It enforces patterns like type(scope?): subject
and common types (build, chore, ci, docs, feat, fix, perf, refactor, style, test
). This ensures everyone follows the spec.
- semantic-release: A fully automated release manager. It reads your Conventional Commits to decide version bumps, generate release notes, and publish packages. For example, commits tagged
fix:
produce a patch release,feat:
a minor release, and anyBREAKING CHANGE
a major bump. A typical setup adds semantic-release to CI:
npm install --save-dev semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/changelog @semantic-release/github
Configure it (e.g. in .releaserc.json
):
{ "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/github" ] }
Then in CI (GitHub Actions example):
on: push: branches: [main] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: npm ci - run: npx semantic-release
With this, every merge to main
triggers semantic-release
. It inspects commits, bumps the package version (e.g. in package.json
), updates CHANGELOG.md
, and creates a GitHub release automatically.
Conclusion
Conventional Commits are a simple, powerful way to standardize your commit messages and unlock automation. By using types like feat:
, fix:
, docs:
, and a clear description, teams gain immediate benefits: automated changelogs, consistent versioning, and smoother collaboration. Paired with tools like commitlint and semantic-release, you can enforce this convention and automate your release workflow. Start tagging your commits today, and make your project’s history a clear, machine-readable story.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.