from commit to automated release pipeline
Teams that write commit messages however they feel like lose the ability to evaluate changes programmatically and plan releases automatically. Conventional Commits standardizes the format around type(scope): description, turning the commit history into a reliable foundation for automated changelogs, semantic version numbers, and fully automated releases, all without manual steps in the CI/CD pipeline.
Table of Contents
- 1. The specification at a glance: type(scope): description
- 2. Allowed commit types in detail
- 3. Marking breaking changes: ! and the BREAKING CHANGE footer
- 4. More than formatting: a machine-readable commit history
- 5. Automatic Semantic Versioning from commit history
- 6. Automated changelog generation
- 7. Commitlint as an enforcement tool
- 8. CI/CD integration for automated releases
- 9. Common mistakes, monorepos, and the comparison to freeform commits
- 10. Summary
- 11. FAQ
1. The specification at a glance: type(scope): description
Conventional Commits is a lightweight specification for structuring commit messages that emerged from the Angular commit convention in 2018 and has since become the de facto standard in open-source and enterprise projects. The basic format is type(scope): description, followed by an optional body for more detailed explanations and an optional footer for metadata such as breaking changes or issue references. The type classifies the kind of change, the scope in parentheses narrows down the affected area, such as a module or a component, and the description summarizes the change in a short sentence written in the imperative.
The decisive difference from freeform commit messages isn't style, it's structure: a parser can reliably extract type, scope, and description without having to interpret free text. Tools like semantic-release, standard-version, or Angular rely exactly on this format to automatically categorize commits. Anyone who applies the specification correctly gets a machine-readable history essentially for free, without having to maintain additional metadata in separate files.
# Conventional Commits: real-world examples
git commit -m "feat(checkout): add express payment button"
git commit -m "fix(cart): prevent duplicate items on quick add"
git commit -m "docs(readme): document environment variables"
git commit -m "chore(deps): bump magento/module-catalog to 104.0.5"
git commit -m "refactor(api): extract price calculation into service"
git commit -m "test(checkout): add coverage for tax calculation"
git commit -m "perf(catalog): cache category tree for 5 minutes"
git commit -m "build(webpack): enable tree shaking for vendor bundle"
git commit -m "ci(github-actions): run phpstan on every pull request"
# With scope and multi-line body
git commit -m "feat(auth): add two-factor authentication" -m "Adds TOTP-based 2FA for admin accounts. Users can enroll via account settings and recovery codes are generated on activation."
2. Allowed commit types in detail
The Angular convention that Conventional Commits builds on defines a fixed set of types: feat for new features, fix for bug fixes, docs for documentation-only changes, style for formatting without code changes, refactor for restructuring without behavior changes, perf for performance improvements, test for added tests, build for changes to the build system or dependencies, ci for CI/CD configuration, and chore for maintenance work unrelated to production code. An additional type, revert, marks the reversal of an earlier commit and references its hash in the body.
Not every type carries the same weight for later versioning: feat and fix are the only types semantic-release uses by default to determine the next version automatically; all others are considered version-neutral and usually appear in their own, less prominent sections of the changelog. The scope in parentheses should come from a limited, documented, project-wide list, such as module names like checkout, catalog, or api, rather than being reinvented with every commit. Without this discipline, the scope quickly dilutes into noise that adds no value either when reading the history or during automated analysis.
3. Marking breaking changes: ! and the BREAKING CHANGE footer
Breaking changes are the one case where Conventional Commits enforces a marking that goes beyond plain categorization: they signal that a change alters the public API incompatibly and that consumers of the package will need to adjust their code. There are two equivalent notations: an exclamation mark directly after type or scope, for example feat(api)!: remove legacy price endpoint, or a footer BREAKING CHANGE: followed by an explanation of what changes and how to migrate. Both notations can also be combined, with the footer offering extra room for a detailed migration guide.
For package consumers pointing at a package via Composer or npm with a caret constraint like ^2.4.0, this marking isn't a minor detail, it's the basis for whether an update may be applied automatically. semantic-release detects BREAKING CHANGE regardless of the underlying type, so a breaking fix triggers a major version bump just like a breaking feat. In practice, it pays off to always accompany breaking changes in the footer with a concrete migration guide, because the changelog later serves as the only source of information for consumers who never read a diff of the codebase.
4. More than formatting: a machine-readable commit history
The real value of Conventional Commits isn't prettier commit messages, it's that the Git history becomes a structured data source. git log --grep="^feat" isolates every new feature since any given tag without having to interpret free text. When debugging with git bisect, a type: fix immediately signals that a commit was intended to resolve a specific problem, which speeds up narrowing down regressions. Code reviews benefit too: a reviewer can tell from the subject line alone whether a pull request is feature-, bugfix-, or maintenance-driven before even opening the diff.
For audits and compliance evidence, for instance around security-relevant fix commits, the history can be filtered and exported by type without any extra tooling. Unstructured commit messages like "minor tweak" or "fixes" systematically prevent all of this: they're barely classifiable by humans after the fact and completely unusable for scripts. The investment in Conventional Commits therefore doesn't only pay off at the next release, it pays off every time a team searches the history throughout the life of a project.
5. Automatic Semantic Versioning from commit history
Semantic Versioning follows the MAJOR.MINOR.PATCH scheme, and Conventional Commits delivers exactly the signals needed to determine the next version number automatically: a fix commit bumps the PATCH version, a feat commit bumps the MINOR version and resets PATCH to zero, and any breaking change marking bumps the MAJOR version and resets MINOR and PATCH. Tools like semantic-release for the npm ecosystem or standard-version for locally driven releases evaluate the commits since the last tag and deterministically derive the next version from them, without a human ever having to set the version number manually.
The process is always the same: the tool collects every commit since the last published tag, classifies them by type, determines the highest required version bump, and generates the tag, release notes, and, where applicable, the package publish step, all in a single automated run. If a batch of commits contains both feat and fix commits, the higher bump always wins, so MINOR instead of PATCH. A single breaking-change commit is enough to trigger a MAJOR version regardless of all other commits, even if it's declared as a fix.
6. Automated changelog generation
Once commit messages are structured, a CHANGELOG.md can be generated without any manual upkeep: tools like conventional-changelog or the built-in changelog plugin of semantic-release group all commits since the last release by type, format them as a Markdown list, and link each entry directly to its commit hash or pull request. Features and fixes typically appear in their own sections near the top of the changelog, while chore and ci commits are usually left out entirely, since they add no value for end users of the package.
The advantage over manually maintained changelogs isn't just the saved effort, it's consistency: an automatically generated changelog can never forget an entry, because it's derived directly from the same data source that also determines the version number. The example below shows an excerpt from a generated CHANGELOG.md, the kind semantic-release would produce from a series of commits including feat, fix, and a breaking-change commit.
# Generated by semantic-release, excerpt from CHANGELOG.md
cat CHANGELOG.md
## [3.0.0](https://github.com/mironsoft/checkout-module/compare/v2.4.1...v3.0.0) (2026-07-10)
### BREAKING CHANGES
* api: the /v1/price endpoint has been removed, use /v2/price instead
### Features
* checkout: add express payment button (a1b2c3d)
* auth: add two-factor authentication (e4f5a6b)
### Bug Fixes
* cart: prevent duplicate items on quick add (c7d8e9f)
* checkout: correct tax calculation for digital goods (1a2b3c4)
### Performance Improvements
* catalog: cache category tree for 5 minutes (9f8e7d6)
7. Commitlint as an enforcement tool
A specification alone doesn't guarantee compliance: without technical enforcement, freeform commits creep back in within a few weeks. commitlint validates every commit message against a configurable rule set, usually based on the @commitlint/config-conventional preset, which checks type, casing, maximum description line length, and other details. commitlint is installed as a dev dependency together with the desired config preset, typically via npm install --save-dev @commitlint/cli @commitlint/config-conventional.
The actual enforcement happens through a Git hook, usually managed with Husky, which installs the commit-msg hook and runs commitlint against the written message on every commit attempt. If the message doesn't match the pattern, Husky aborts the commit before it ever lands in the local repository. The same check can additionally be repeated in the CI pipeline as a pull-request gate, to catch commits that bypassed the local hook, for example via --no-verify or a merge from outside.
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [2, "always", ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"]],
"scope-enum": [2, "always", ["checkout", "cart", "catalog", "auth", "api", "deps"]],
"subject-case": [2, "never", ["start-case", "pascal-case", "upper-case"]],
"header-max-length": [2, "always", 100]
}
}
#!/usr/bin/env sh
# .husky/commit-msg - enforce Conventional Commits before the commit is created
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"
8. CI/CD integration for automated releases
Once commit messages are reliably structured, the entire release process can move into the CI/CD pipeline: a push or merge to the main branch triggers a workflow that first runs tests and static analysis, then calls semantic-release. The tool determines the next version from the commits since the last tag, generates the changelog, creates a Git tag, and publishes the package, without a developer ever typing npm publish or composer config by hand.
For PHP and Magento projects, the mechanism is identical, only the target platform differs: instead of npm publish, the pipeline ends with a push to a private Packagist repository or a Satis rebuild, triggered by the same versioning scheme. It's important to restrict the release job to protected branches and to provide secrets like NPM_TOKEN or Packagist credentials exclusively through the CI secret store, never inside the repository itself. The example below shows a minimal GitHub Actions workflow that automates exactly this flow.
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
9. Common mistakes, monorepos, and the comparison to freeform commits
In practice, adopting Conventional Commits fails less often because of the specification itself than because of missing discipline within the team. The most common mistake is an uncontrolled, growing set of scope values, because every developer invents their own labels instead of sticking to a documented list, which erodes the searchability of the history all over again. Almost as common is using fix or chore as a catch-all type for practically every change, because the team never agreed on how to distinguish between types. The third classic mistake: the convention is only communicated as a recommendation instead of being technically enforced via commitlint and Husky, causing it to erode within a few weeks.
In monorepos with multiple independently versioned packages, for instance managed with Nx, Lerna, or Changesets, the scope additionally takes on the job of uniquely identifying a package, for example feat(ui-kit): or fix(api-client):. Tools like semantic-release-monorepo or Nx's native release mechanisms then only evaluate commits touching files within the respective package path, and determine an independent version number for each package. Without consistent scope names, this mapping can't be automated reliably, which is why a scope-enum rule enforced via commitlint is essential especially in monorepos.
| Criterion | Freeform commit messages | Conventional Commits | Impact |
|---|---|---|---|
| Changelog creation | Manual, often outdated | Automatically generated from the history | No more manual upkeep |
| Determining the version number | Manual judgment, error-prone | Automatic via semantic-release/standard-version | Consistent, deterministic releases |
| Searching the history | Freetext search with grep | git log --grep="^feat" structured and filterable | Faster debugging and audits |
| Detecting breaking changes | Only by reading every diff | Explicit via ! or BREAKING CHANGE | Consumers are reliably warned |
| CI/CD automation | Not possible without extra parsing | Direct trigger for release pipelines | Releases without manual intervention |
Taken together, the comparison shows that Conventional Commits isn't a stylistic preference, it's the prerequisite for any form of automation along the release process. The more packages and teams work on a codebase, the greater the leverage becomes, because the saved manual effort multiplies with every additional repository and every additional release.
Mironsoft
Git workflows, release automation, and CI/CD pipelines for Magento and PHP teams
Ready to roll out Conventional Commits across your team and ship releases automatically?
We set up commitlint and Husky in your repository, connect semantic-release to your CI/CD pipeline, and make sure the changelog, version number, and package release are generated automatically on every merge, whether the target is npm, Composer, or a private Packagist repository.
Commitlint setup
Configure the rule set, scope-enum, and Husky hooks to match your project
Release automation
Wire up semantic-release or standard-version with changelog generation and versioning
Monorepo versioning
Package-scoped scopes and independent version numbers for Nx, Lerna, or Changesets
10. Summary
Conventional Commits solves a fundamental problem that many teams underestimate: without structured commit messages, every form of release automation remains manual work. The format type(scope): description makes every commit machine-readable, the types feat and fix drive automatic Semantic Versioning, and breaking changes marked via ! or the BREAKING CHANGE footer reliably warn consumers about incompatible changes. From the very same data source come automatically generated changelogs that never forget an entry, because they're derived directly from the commit history.
The key to long-term success lies in technical enforcement: commitlint with a project-specific configuration and a Husky commit-msg hook prevent the convention from eroding after a few weeks. Combined with semantic-release or standard-version in the CI/CD pipeline, the entire release process, from version determination through changelog generation to package publication, becomes fully automated, whether for a single npm package, a Composer module, or a monorepo with multiple independently versioned packages.
Conventional Commits: Standardized Commit Messages - The Essentials at a Glance
Specification
type(scope): description with fixed types like feat, fix, docs, chore, refactor, test, perf, build, and ci.
Semantic Versioning
fix bumps PATCH, feat bumps MINOR, BREAKING CHANGE bumps MAJOR, automatically via semantic-release.
Changelog automation
CHANGELOG.md is generated directly from the commit history, grouped by type, with no manual upkeep.
Enforcement
commitlint plus a Husky commit-msg hook blocks freeform commits before they even reach the local commit.