Conventional commits

Also known as: conventional commit, conventional commit message, conventional-commits, semantic commits, commit convention

Updated 2026-08-314 questions

Conventional commits is a lightweight convention for commit messages that machines can parse. Each message starts with a type (feat, fix, chore), an optional scope in parentheses, and a short imperative subject. Tools use those types to automate version bumps, generate changelogs, and decide when a release pipeline should run.

How do conventional commits work?

Conventional commits is a written contract for the first line of every commit. The subject follows a machine-readable shape (<type>[optional scope]: <subject>), an optional body explains the reasoning, and an optional footer carries metadata like breaking-change notices or issue references. That is the whole spec.

Every message follows this skeleton:

<type>(<optional scope>): <short imperative subject>

<optional body: why the change, in plain prose>

<optional footer, e.g. "BREAKING CHANGE: ..." or "Refs: #123">

The common types (from the official spec) are:

  • feat: a user-visible new capability. Triggers a MINOR version bump.
  • fix: a bug fix. Triggers a PATCH bump.
  • chore, build, ci, docs, refactor, test, perf, style, revert: the housekeeping types. They land on the trunk without cutting a release by themselves.
  • BREAKING CHANGE: footer, or a ! after the type (feat!:): an intentional backward-incompatible change. Triggers a MAJOR bump.

Two rules glue the convention to a pipeline. First, commit messages are linted before they land, usually with commitlint in a Husky pre-commit hook and again in CI so the rule holds even when the local hook is skipped. Second, a release job walks the commit range since the last tag, groups messages by type, and computes the highest bump present: MAJOR beats MINOR beats PATCH. That single traversal decides both the next version number and the shape of the changelog.

Why does the convention matter?

The value shows up once you stop hand-writing changelogs and hand-picking version numbers.

  • Version numbers become a function of the diff, not a debate. No more "is this a minor or a major?" arguments in the release meeting. The commit types already answered the question, and the answer is auditable in git log.
  • Changelogs write themselves. Group by type, keep the subjects, expose the footer for breaking changes. The output beats what a tired human writes on release day.
  • git log becomes searchable. git log --grep="^fix" v1.4.0..HEAD returns exactly the bug fixes since the last release. No tagging ceremony needed.
  • CI can gate merges on message quality. A malformed message fails commitlint the same way a failing test fails the pipeline. The rule is enforced by the machine, not by the reviewer's memory.
  • Trunk-based teams get a natural release cadence. Because the convention encodes intent per commit, a trunk-based team can ship on any merge without a release manager first classifying what changed.

The honest cost: developers write commit messages with slightly more care than "wip" or "fix stuff", and a squash-merge workflow needs a rule for what the squashed subject looks like (most teams point commitlint at the PR title instead). If your team ships once a quarter and hand-writes the release notes anyway, the payoff is small.

Conventional commits vs semantic versioning

The two are complements, not competitors.

  • Semantic versioning is the output format (MAJOR.MINOR.PATCH) and the meaning of each digit.
  • Conventional commits is the input signal that decides which digit to increment.

Without conventional commits, SemVer still works: someone reads the diff and picks a number. Without SemVer, conventional commits still parse; you just do not cut releases from them. Together they turn the release into a script: read commits since the last tag, compute the next SemVer, write a changelog, tag, publish.

How do popular tools handle conventional commits?

Adoption is broad enough that most CI stacks have a native path.

  • commitlint with Husky is the industry-standard local check. A Git hook rejects a message that fails the @commitlint/config-conventional rules before it ever leaves the developer's laptop. Cheap, universal, works with any CI.
  • semantic-release (Node) is the workhorse for JavaScript packages: it reads the commit range, computes the next version, generates the changelog, tags, publishes to npm, and opens the GitHub release. If you ship an npm package and want zero-touch releases on every merge, this is the standard.
  • release-please (Google) takes a different shape. Instead of cutting a release on every merge, it maintains an open "Release PR" that grows with each conventional commit and cuts the release when you merge that PR. If your source of truth is GitHub and you want a human-in-the-loop review of each release before it goes out, release-please is the better fit here: the PR shows the exact changelog and version bump before you approve it.
  • GitLab ships a first-party Semantic Release CI/CD component plus a built-in changelog API that consumes conventional commits directly. If GitLab is already your platform, the pieces are in the box.
  • GitHub Actions does not own the convention itself but has excellent building blocks: wagoid/commitlint-github-action, googleapis/release-please-action, cycjimmy/semantic-release-action. You assemble the pieces yourself, which is either a feature or a chore depending on how much shell you like writing.
  • Jenkins has no first-class conventional-commits support, but its shared-library pattern makes rolling your own analyser straightforward for teams that already run a Jenkins controller.
  • Buddy is one of the options we recommend when the same pipeline that ships the code should also decide the version. A single .buddy/buddy.yml can lint the commit range on push, run semantic-release in a BUILD action, and once the tag lands build the artifact, publish it, and route the distribution at the new version. Concrete reason: because the lint, release and deploy stages live in one file, you avoid three separate tools disagreeing on what "released" means, and the artifact that shipped is bound to the commit range whose types justified the version.

Example

The pipeline below wires conventional commits into a release. On every push to main it lints the commit range that arrived, uses semantic-release to compute the next SemVer tag and changelog, then publishes a versioned artifact and pings a webhook so the release feed catches up.

# .buddy/buddy.yml - conventional commits -> SemVer -> release
- pipeline: "release-from-conventional-commits"
  events:
    - type: "PUSH"
      refs:
        - "refs/heads/main"
  actions:
    - action: "Lint incoming commit messages"
      type: "BUILD"
      docker_image_name: "node"
      docker_image_tag: "20"
      commands: |-
        npm ci --no-audit --no-fund
        npx commitlint --from "$BUDDY_EXECUTION_PREVIOUS_REVISION" --to "$BUDDY_EXECUTION_REVISION" --verbose

    - action: "Cut the next release from commit types"
      type: "BUILD"
      docker_image_name: "node"
      docker_image_tag: "20"
      commands: |-
        npm ci --no-audit --no-fund
        npm run build
        npx semantic-release
        cat .release-version

    - action: "Publish the versioned artifact"
      type: "BUILD"
      docker_image_name: "ubuntu"
      docker_image_tag: "22.04"
      commands: |-
        VERSION="$(cat .release-version)"
        bdy artifact publish web:"$VERSION" ./dist --create

    - action: "Ping the release feed"
      type: "HTTP"
      method: "GET"
      notification_url: "https://hooks.example.com/release-published"
      retry_count: 3
      retry_interval: 10

Two things to notice. The first action fails the pipeline if any message in the pushed range breaks the convention, so a bad message can never quietly land on main. The second action reads only the commits that landed since the last tag; the resulting version number is a deterministic function of what the humans wrote, not what the release engineer felt like on Friday.

Frequently asked questions

What are the standard conventional commit types?

The core spec defines `feat` (a user-visible new capability, triggers a MINOR bump) and `fix` (a bug fix, triggers a PATCH bump). Most teams also use `chore`, `build`, `ci`, `docs`, `refactor`, `test`, `perf`, `style`, and `revert` for housekeeping work that lands on the trunk without cutting a release by itself. A trailing `!` on the type, or a `BREAKING CHANGE:` footer in the body, forces a MAJOR bump.

What does the BREAKING CHANGE footer do?

It marks a commit as backward-incompatible so downstream tooling bumps the MAJOR version and highlights the change at the top of the changelog. You can write it as a footer line (`BREAKING CHANGE: renamed the /users endpoint to /accounts`) or shorthand it in the subject with an exclamation mark (`refactor!: rename /users to /accounts`). Both are equivalent to the parser; the footer gives you room to explain the migration path.

Do conventional commits require squash merging?

No, but a squash-merge workflow makes the convention easier to enforce. If every pull request lands as one commit, you only lint one message: the squashed subject, which most teams source from the PR title. If you preserve individual commits, run commitlint over the whole PR range (`--from origin/main --to HEAD`) so every message on the branch counts.

Can I retrofit conventional commits onto an existing project?

Yes, and the cheap path is start-forward, not rewrite-history. Add commitlint plus a Husky pre-commit hook, add a CI job that lints new messages, and pick a starting tag for your release tool ("only look at commits after v2.0.0"). Old commits keep their freeform messages; new ones follow the shape. Within a release cycle the changelog is mostly conventional; within two, everyone is onboarded.

Missing a term? Spotted a mistake?

Suggest a new word or an edit to an existing one. Every submission is reviewed before it goes live.