A git log is either a liability or an asset. On most teams it’s a liability — a stream of “fix stuff,” “wip,” and “final FINAL v3” that tells you nothing about what changed or why. The Conventional Commits specification turns your commit history into something you can actually use: automated changelogs, reliable semantic versioning, and a record that makes debugging regressions tractable. Here’s how to implement it end to end.
What Conventional Commits Actually Is
The Conventional Commits specification is a lightweight standard for structuring commit messages. The format is:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
A commit message looks like:
feat(auth): add OAuth2 login with Google provider
Implements the full OAuth2 flow using the passport-google-oauth20 library.
Adds session persistence and user profile mapping.
Closes #142
That’s it. The value is in what you can do with that structure once it’s consistently applied.
The Commit Types That Matter
The spec defines a small set of types. These are the ones your team will actually use:
feat — A new feature. Maps to a MINOR version bump in semantic versioning. Use this when you’re adding capability.
fix — A bug fix. Maps to a PATCH version bump. Use this for any corrective change that doesn’t add new behaviour.
chore — Maintenance work that doesn’t affect production code: dependency updates, build scripts, tooling config. No version bump.
docs — Documentation only changes. No version bump.
refactor — Code changes that neither fix a bug nor add a feature. No version bump.
test — Adding or updating tests. No version bump.
perf — Performance improvements. PATCH bump.
ci — Changes to CI configuration files and scripts. No version bump.
Breaking changes — Any commit can include BREAKING CHANGE: in the footer, or append ! to the type (e.g., feat!:), which maps to a MAJOR version bump.
The scope in parentheses is optional but useful in larger codebases: feat(payments):, fix(api):, chore(deps):. It gives readers immediate context about which part of the system changed.
Automated Changelog Generation
The real payoff of conventional commits is automation. Two tools dominate here.
release-please (Google’s tool, works well with GitHub Actions) monitors your main branch, groups commits by type, determines the correct semantic version bump, and opens a PR that updates CHANGELOG.md and bumps package.json. When you merge that PR, it tags the release. The entire release process — versioning, changelog, tagging — becomes a PR merge. Teams using GitHub as their primary platform will find this the cleanest option.
standard-version (the original, CLI-based) works similarly but runs locally. A single command — npx standard-version — reads your commits since the last tag, determines the version bump, updates the changelog, bumps the version file, creates a commit, and tags it. Fast, reliable, and works outside of any specific CI platform.
For teams on GitHub, release-please is the cleaner long-term choice. For teams on GitLab or Bitbucket, standard-version or its actively maintained fork commit-and-tag-version is the pragmatic pick.
Enforcing the Standard with commitlint and husky
Adoption only holds if the standard is enforced. You don’t want to discover that three months of commits are non-conformant when you try to generate your first changelog.
commitlint validates commit messages against the Conventional Commits specification. Set it up in about five minutes:
npm install --save-dev @commitlint/config-conventional @commitlint/cli
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.mjs
husky runs commitlint as a git hook on every commit attempt. A non-conformant message is rejected immediately, at the developer’s machine, before it ever hits the remote:
npm install --save-dev husky
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
With both in place, every commit on every developer’s machine is validated. No CI step required, no process discipline required — the tooling enforces it.
For teams using other package managers, simple-git-hooks is a lightweight alternative to husky with near-zero configuration.
CI-Level Enforcement
Hooks on developer machines can be bypassed with --no-verify. For completeness, add commitlint to your CI pipeline to validate all commits in a pull request:
- name: Lint commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
This is a safety net, not a primary control, but it catches the rare bypass and makes the expectation visible in PR checks.
Team Adoption: What Actually Works
The specification is simple, but changing commit habits is behavioural, not technical. A few things that genuinely help:
Start with tooling, not documentation. Install commitlint and husky before you announce the change. People learn faster from immediate feedback than from a wiki page.
Provide a cheatsheet. A one-page reference with the seven commit types and three examples, living in the repo root or the team wiki. Most developers need to look it up exactly twice before it’s memorised.
Review commits in PRs. During the first month, code reviewers mention commit message quality alongside code quality. Normalising it in review makes it part of the definition of “done.”
Show the output. The first time you run release-please and a clean changelog appears from three weeks of commits, developers see the value concretely. Run it in a team meeting or share the generated PR — it’s a convincing demo.
Don’t retrofit. Start the standard from a specific date and don’t require rewriting old commits. The old history is what it is; new commits follow the standard.
The investment is low and the return compounds. A year into adoption, you’ll have a structured, searchable history, reliable versioning automation, and a changelog that actually communicates changes to stakeholders — all without ongoing manual effort.