Home DevOps Conventional Commits: Stop Writing Useless Git Messages Forever
Beginner 3 min · July 07, 2026

Conventional Commits: Stop Writing Useless Git Messages Forever

Conventional Commits standardize git messages for automated changelogs and semantic versioning.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 07, 2026
last updated
214
articles · all by Naren
Before you start⏱ 20 min
  • Basic Git usage (init, add, commit, push)
  • Familiarity with command line
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Conventional Commits is a standard for writing commit messages like feat: add user login or fix(api): handle null pointer. It makes your git history machine-readable and human-friendly, enabling automatic version bumps and changelog generation.

✦ Definition~90s read
What is Conventional Commits?

Conventional Commits is a lightweight specification for structuring commit messages. It defines a format that includes a type, an optional scope, a description, and optional body/footer. This enables automated tooling for changelogs, semantic versioning, and release notes.

Think of commit messages as labels on jars in a pantry.
Plain-English First

Think of commit messages as labels on jars in a pantry. Without a system, you get jars labeled 'stuff' or 'things'. Conventional Commits is like labeling each jar with its contents (type: feat, fix) and the specific shelf (scope: auth, api). Now you can instantly find what you need and know if opening a jar will break your recipe (breaking change).

Your commit history is a mess. I've seen teams waste hours digging through 'fixed stuff' and 'updated code' trying to find when a bug was introduced. That's not just annoying — it's dangerous when you need to revert a change in production at 2 AM. Conventional Commits fixes this by giving every commit a structured, meaningful label.

The problem it solves is simple: unstructured commit messages are useless for automation. Without a standard, you can't auto-generate changelogs, you can't automatically determine the next semantic version, and you can't easily filter commits by type. Teams end up manually curating release notes — a tedious, error-prone process.

By the end of this, you'll be able to write Conventional Commits from memory, set up a commitlint hook to enforce them, and integrate with semantic-release to automate versioning and changelogs. You'll never look at a 'fixed bug' commit the same way again.

The Anatomy of a Conventional Commit

Before we dive into tooling, you need to understand the format. It's dead simple: <type>(<scope>): <description>. The type is mandatory. Scope is optional but recommended. Description is a short summary in imperative mood. Then you can add a body and footer after a blank line.

Here's the full structure: ``` <type>(<scope>): <description>

[optional body]

[optional footer(s)] ```

The key types are: feat (new feature), fix (bug fix), chore (maintenance), docs (documentation), style (formatting), refactor (code change that neither fixes nor adds), test (adding tests), perf (performance improvement).

Breaking changes are indicated by appending ! after the type/scope, or by adding BREAKING CHANGE: in the footer. This triggers a major version bump in semantic versioning.

example-commits.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — DevOps tutorial

# Good examples
git commit -m "feat: add user authentication"
git commit -m "fix(api): handle null pointer in user fetch"
git commit -m "feat!: drop support for Node 10"
git commit -m "fix: correct typo in email template

Closes #42"

# Bad examples (avoid these)
git commit -m "fixed bug"
git commit -m "update"
git commit -m "wip"
Output
No output — these are commit commands.
Production Trap:
Never use git commit -m with a multi-line message. It's easy to forget the blank line before the body. Use git commit without -m to open an editor, or use -m for the subject and -m again for the body: git commit -m "feat: add login" -m "Implements OAuth2 flow."

Why Bother? The Automation Payoff

The real value of Conventional Commits isn't just readability — it's automation. Once your commits are structured, tools can parse them to:

  1. Auto-generate changelogs: Tools like standard-version or semantic-release scan your git log, group commits by type, and produce a beautiful CHANGELOG.md.
  2. Automate semantic versioning: Based on the commits since the last release, the tool determines if it's a patch (fix), minor (feat), or major (breaking change) and bumps the version accordingly.
  3. Filter commits for release notes: You can easily see all features, fixes, or breaking changes in a release.

Without this, you're manually curating release notes — a task that gets skipped or done poorly. I've seen teams forget to mention breaking changes, causing production outages when consumers don't update their code.

auto-changelog.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — DevOps tutorial

# Install standard-version
npm install --save-dev standard-version

# Add to package.json scripts
# "release": "standard-version"

# Run release
npm run release

# This will:
# 1. Bump version in package.json based on commits
# 2. Generate/update CHANGELOG.md
# 3. Create a git tag
# 4. Commit the changes
Output
✔ bumping version in package.json from 1.0.0 to 1.1.0
✔ creating CHANGELOG.md
✔ committing (package.json, CHANGELOG.md)
✔ tagging (v1.1.0)
Senior Shortcut:
Use semantic-release for full CI/CD integration. It runs in your pipeline, analyzes commits since the last release, and publishes to npm (or any registry) automatically. No more manual version bumps.

Enforcing the Standard with Commitlint

You can't rely on everyone remembering the format. That's where commitlint comes in. It's a linter for commit messages. You hook it into your workflow via husky (Git hooks) so that every commit is validated before it's accepted.

Here's the setup: 1. Install commitlint and the conventional config. 2. Install husky and configure the commit-msg hook. 3. Write a commitlint.config.js file.

Now, if someone writes git commit -m "fixed bug", commitlint rejects it with a clear error message. The developer must rewrite it properly.

setup-commitlint.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — DevOps tutorial

# Install commitlint CLI and config
npm install --save-dev @commitlint/cli @commitlint/config-conventional

# Create config file
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

# Install husky
npm install --save-dev husky

# Enable husky hooks
npx husky install

# Add commit-msg hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

# Now test it
git commit -m "fixed bug"
# Output: ⧗   input: fixed bug
# ✖   subject may not be empty [subject-empty]
# ✖   type may not be empty [type-empty]
# ✖   found 2 problems, 0 warnings
Output
⧗ input: fixed bug
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warnings
The Classic Bug:

Semantic Versioning and Breaking Changes

Conventional Commits maps directly to semver
  • fix type → PATCH release (1.0.0 → 1.0.1)
  • feat type → MINOR release (1.0.0 → 1.1.0)
  • Breaking change (! or BREAKING CHANGE footer) → MAJOR release (1.0.0 → 2.0.0)

This is powerful because it removes human judgment from versioning. The tool decides based on the commits. But you must be disciplined: if you mark a commit as feat but it's actually a breaking change, you'll accidentally release a minor version instead of major. I've seen this cause downstream breakage when consumers auto-update.

Always use ! for breaking changes. Example: feat!: remove deprecated login endpoint. This ensures the next version is major.

breaking-change.shBASH
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge — DevOps tutorial

# Correct breaking change commit
git commit -m "feat!: drop support for Node 10"

# Alternative with footer
git commit -m "feat: drop support for Node 10

BREAKING CHANGE: Node 10 is no longer supported."

# Both will trigger a major version bump
Output
No output — commit commands.
Never Do This:
Don't use BREAKING CHANGE in the subject line. It must be in the footer. The subject line uses ! after the type/scope. Mixing them up confuses parsers.

Scopes: When and How to Use Them

Scopes are optional but incredibly useful for large projects. They indicate the area of the codebase affected. Examples: feat(auth): add OAuth2 login, fix(api): handle timeout, chore(deps): update lodash.

Scopes should be nouns that map to modules, components, or layers. Keep them consistent. I recommend defining a list of allowed scopes in commitlint config to prevent typos.

When not to use scopes: if your project is small (single module), scopes add noise. Also, avoid overly granular scopes like fix(src/utils/helpers/string): fix typo — that's too specific. Stick to high-level areas.

commitlint.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
// io.thecodeforge — DevOps tutorial

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'scope-enum': [2, 'always', ['auth', 'api', 'ui', 'deps', 'docs']]
  }
};
Output
No output — config file.
Try it live
Senior Shortcut:
Use commitlint with scope-enum rule to enforce consistent scopes. This prevents 'fix(auth)' vs 'fix(Auth)' vs 'fix(authentication)' — all referring to the same thing.

Integrating with CI/CD Pipelines

Enforcing commit messages locally is good, but you should also validate them in CI. This catches commits that bypass hooks (e.g., someone commits with --no-verify).

In GitHub Actions, you can use the commitlint action. In GitLab CI, run npx commitlint --from $CI_COMMIT_BEFORE_SHA --to $CI_COMMIT_SHA. This validates all commits in the push.

.github/workflows/commitlint.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — DevOps tutorial

name: Commitlint
on: [pull_request]

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v3
      - run: npm install
      - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}
Output
No output — workflow file.
Production Trap:
If you use squash merges, the individual commit messages are lost. Only the squash commit message matters. Ensure your squash commit message follows Conventional Commits. Configure your repo to require the PR title to be a valid conventional commit.

When NOT to Use Conventional Commits

Conventional Commits is overkill for personal projects or small teams that don't release often. If you're the only developer and you never publish a package, the overhead of enforcing a standard isn't worth it.

Also, if your team is not bought in, forcing it will lead to resentment and bypasses. Start with a trial period on a new project, or introduce it alongside tooling that provides immediate value (like auto-changelog).

For monorepos, consider using tools like lerna or changesets that work with Conventional Commits but add scope management.

Senior Shortcut:
Don't enforce on existing projects retroactively. It's a pain to rewrite history. Instead, start fresh with a new major version or a new repository.
● Production incidentPOST-MORTEMseverity: high

The 2 AM Revert That Took an Hour

Symptom
A production deployment broke the checkout flow. The team needed to revert the last 5 commits but couldn't tell which one introduced the bug.
Assumption
The team assumed the latest commit was the culprit because it was a 'big update'.
Root cause
Commit messages were vague: 'updated payment module', 'fixed stuff', 'wip'. No one could identify the actual change that broke checkout without reading every diff.
Fix
After that incident, the team adopted Conventional Commits. Now they run git log --oneline --grep='^fix(checkout)' to instantly find the bug fix commit. Reverts take seconds.
Key lesson
  • A vague commit message is a time bomb.
  • When production is down, clarity is speed.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Commitlint rejects a commit with 'subject may not be empty'
Fix
1. Check that the commit message has a type and description. 2. Ensure there's a space after the colon. 3. Run echo "feat: add login" | npx commitlint to test locally.
Symptom · 02
semantic-release bumps wrong version
Fix
1. Check commit history with git log --oneline. 2. Look for missing ! on breaking changes. 3. Verify that fix and feat types are used correctly. 4. Run npx semantic-release --dry-run to preview the next version.
Symptom · 03
Husky hooks not running after clone
Fix
1. Run npx husky install in the repo. 2. Ensure prepare script is in package.json: "prepare": "husky install". 3. Check .husky/ directory exists and contains the hook scripts.
★ Conventional Commits Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Commit rejected by commitlint with `type may not be empty`
Immediate action
Check the commit message format
Commands
echo "feat: add login" | npx commitlint
git log -1 --format=%s | npx commitlint
Fix now
Amend the commit: git commit --amend -m "feat: add login"
semantic-release fails with 'no commits since last release'+
Immediate action
Check if there are new commits
Commands
git log --oneline $(git describe --tags --abbrev=0)..HEAD
npx semantic-release --dry-run
Fix now
If commits exist but not parsed, ensure they follow Conventional Commits. If not, rebase to fix messages.
Changelog missing a commit+
Immediate action
Check if the commit type is included in changelog config
Commands
git log --oneline --grep='^feat'
cat CHANGELOG.md | grep 'feat'
Fix now
Update changelog config to include all types, or manually add the entry.
Husky hook not running on Windows+
Immediate action
Check if husky is installed correctly
Commands
npx husky install
ls .husky/
Fix now
Reinstall husky: npm rebuild husky && npx husky install. Ensure Git hooks path is set: git config core.hooksPath .husky.
FeatureConventional CommitsNo Standard
Changelog generationAutomatic via toolsManual or non-existent
Semantic versioningAutomated based on commitsManual guesswork
Filtering commitsEasy by type/scopeRequires reading each message
Learning curveLow (5 min to learn)None, but no benefits
EnforcementEasy with commitlintNot applicable
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
example-commits.shgit commit -m "feat: add user authentication"The Anatomy of a Conventional Commit
auto-changelog.shnpm install --save-dev standard-versionWhy Bother? The Automation Payoff
setup-commitlint.shnpm install --save-dev @commitlint/cli @commitlint/config-conventionalEnforcing the Standard with Commitlint
breaking-change.shgit commit -m "feat!: drop support for Node 10"Semantic Versioning and Breaking Changes
commitlint.config.jsmodule.exports = {Scopes
.githubworkflowscommitlint.ymlname: CommitlintIntegrating with CI/CD Pipelines

Key takeaways

1
Conventional Commits is a structured format
<type>(<scope>): <description> with optional body and footer.
2
Automation is the killer feature
auto-changelog, semantic versioning, and release notes without manual work.
3
Enforce with commitlint and husky locally, and validate in CI to catch bypasses.
4
Breaking changes must be marked with ! or BREAKING CHANGE footer to trigger major version bumps.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Conventional Commits handle breaking changes when there are mul...
Q02SENIOR
When would you choose Conventional Commits over a tool like Gitmoji?
Q03SENIOR
What happens when a commit has both `feat` and `fix` types? How does sem...
Q04JUNIOR
What is the purpose of the `scope` field in Conventional Commits?
Q05SENIOR
You have a monorepo with multiple packages. How do you manage Convention...
Q06SENIOR
How would you design a CI pipeline that enforces Conventional Commits on...
Q01 of 06SENIOR

How does Conventional Commits handle breaking changes when there are multiple breaking changes in one commit?

ANSWER
You can list multiple breaking changes in the footer, each on a new line prefixed with BREAKING CHANGE:. The parser will detect all of them. However, it's better practice to have one breaking change per commit to keep history clean.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the format of a Conventional Commit?
02
What's the difference between Conventional Commits and semantic versioning?
03
How do I set up commitlint for my project?
04
Can I use Conventional Commits with Gitmoji?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 07, 2026
last updated
214
articles · all by Naren
🔥

That's Git. Mark it forged?

3 min read · try the examples if you haven't

Previous
Git Internals: Objects, Trees, Blobs and the DAG
27 / 47 · Git
Next
Git Blame: Find Who Changed What and When