← All articles
monitor showing C++

Conventional Commits: Structure Your Git History for Humans and Machines

Git 2026-03-04 · 4 min read conventional-commits git semantic-versioning changelog commitizen changesets
By DevTools Guide Editorial TeamSoftware engineers and developer advocates covering tools, workflows, and productivity for modern development teams.

Git history is documentation. A well-structured commit history lets you understand why code changed, generate accurate changelogs, and automate semantic version bumps. Conventional Commits is a lightweight specification that adds structure to commit messages — and unlocks a set of powerful tooling built around that structure.

Photo by RoonZ nl on Unsplash

The Format

A conventional commit message has three parts:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Types describe the kind of change:

Breaking changes add ! after the type or BREAKING CHANGE: in the footer:

feat!: change authentication API to JWT

Or:

feat: change authentication API to JWT

BREAKING CHANGE: Existing session tokens are invalidated.
Users must log in again to receive a JWT.

Examples

feat: add OAuth2 login with Google

fix(auth): prevent session fixation on login

docs: update API reference for /users endpoint

refactor(database): extract connection pool to separate module

perf: cache user permissions to reduce database queries

ci: add semgrep security scanning to PR checks

feat!: require API versioning header
BREAKING CHANGE: All API requests must include X-API-Version header

Scopes are optional and can be any identifier meaningful to your project — a module name, layer name, or area of the codebase.

Why It Matters

1. Readable History

Compare:

# Bad
git log --oneline
abc1234 fix stuff
def5678 update
bcd9012 changes

# Conventional
git log --oneline
abc1234 fix(auth): prevent null pointer on expired sessions
def5678 feat(billing): add Stripe webhook endpoint for renewals
bcd9012 docs: add API authentication guide

The second history is scannable. You know what changed and why without opening each commit.

2. Automated Changelogs

Tools like semantic-release, release-please, and standard-version read your commit history to generate changelogs automatically:

## [1.4.0] - 2026-03-04

### Features
- add OAuth2 login with Google (#234)
- add Stripe webhook endpoint for renewals (#237)

### Bug Fixes
- fix null pointer on expired auth sessions (#235)
- fix race condition in background job processor (#238)

This is generated from commit messages — no manual changelog editing.

3. Automatic Version Bumps

Semantic versioning (MAJOR.MINOR.PATCH) becomes deterministic:

CI can publish new versions automatically when commits land on main.

Like what you're reading? Subscribe to DevTools Guide — free weekly guides in your inbox.

Tooling

Commitizen (Writing Commits)

Commitizen provides an interactive CLI for writing conventional commits:

# Install globally
npm install -g commitizen cz-conventional-changelog

# Or per-project
npm install --save-dev commitizen cz-conventional-changelog

# Configure in package.json
{
  "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }
}

Replace git commit with git cz (or npx cz):

? Select the type of change: feat
? What is the scope of this change: (auth)
? Write a short description: add Google OAuth2 login
? Is this a breaking change? No
? Issues this commit closes: #234

This generates: feat(auth): add Google OAuth2 login (#234)

Commitlint (Validating Commits)

Commitlint enforces the format via a git hook:

npm install --save-dev @commitlint/cli @commitlint/config-conventional

Create .commitlintrc.js:

module.exports = {
  extends: ['@commitlint/config-conventional'],
};

Add to Husky (or Lefthook):

# Lefthook
# .lefthook.yml
commit-msg:
  commands:
    commitlint:
      run: npx commitlint --edit {1}

Now bad commits are rejected before they're created:

git commit -m "fix stuff"
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]

semantic-release (Publishing)

semantic-release automates the full release pipeline:

npm install --save-dev semantic-release

Configure .releaserc.json:

{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    "@semantic-release/npm",
    "@semantic-release/github"
  ]
}

Add to CI (GitHub Actions):

- name: Release
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
  run: npx semantic-release

On every push to main, semantic-release:

  1. Analyzes commits since the last release
  2. Determines the version bump
  3. Updates CHANGELOG.md
  4. Creates a GitHub release
  5. Publishes to npm

No manual versioning or changelog editing.

release-please (Google's Alternative)

release-please is GitHub's approach — instead of releasing directly, it opens a PR with the version bump and changelog:

# .github/workflows/release-please.yml
on:
  push:
    branches:
      - main
jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        with:
          release-type: node

Benefits: The release PR lets you review and edit the changelog before publishing. Useful when you want human oversight on releases.

Adopting in an Existing Project

  1. Start fresh: Don't rewrite history. Just apply the convention to new commits.
  2. Add commitlint: Enforce going forward so the history stays consistent.
  3. Set expectations in CONTRIBUTING.md: Document the format with examples.
  4. Squash-merge feature branches: PR titles become the merge commit — make them conventional. GitHub allows setting the default PR title from the first commit.

Monorepo Considerations

In a monorepo with multiple packages, scopes map to packages:

feat(api): add rate limiting
fix(frontend): correct button alignment on mobile
chore(deps): bump axios to 1.7.0

Tools like Changesets (.changeset/) integrate with monorepos and handle separate version bumps per package. Commitlint can be configured to require scopes from a specific list.

Non-Node.js Projects

Conventional Commits is language-agnostic. The specification is just a text format. For Python projects, use python-semantic-release. For Go, goreleaser supports Conventional Commits for changelog generation. For any language, git-cliff generates changelogs from conventional commits without language-specific tooling:

# Install git-cliff
cargo install git-cliff
# Or: brew install git-cliff

# Generate a changelog
git-cliff --output CHANGELOG.md

The Bottom Line

Conventional Commits adds maybe 10 seconds to each commit — the time it takes to prefix with feat: or fix:. The return is automated changelogs, predictable versioning, and a history that communicates intent instead of just change. For any project with multiple contributors or regular releases, the convention pays for itself quickly.

Get free weekly tips in your inbox. Subscribe to DevTools Guide