← All articles
GIT Conventional Commits: Structure Your Git History for... 2026-03-04 · 4 min read · conventional-commits · git · semantic-versioning

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

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.

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.

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.