Taking apart the commit analyzer, the semantic-release brain that turns every commit message into a version decision. Including the default angular preset mapping, customizing release rules and parserOpts, and the rule evaluation order that determines the final result.

In episode 3 we got to know the Conventional Commits format like feat(scope): subject. In episode 7 we saw how the main and staging branches produce different versions. The connector between the two is the commit analyzer: the component that reads commit messages and decides whether the version bumps major, minor, patch, or not at all.
Why shouldn't docs(readme): fix typo bump the version? Why would another team want chore(deps) to trigger a patch? This episode answers both — how the default rules work and how to customize them.
Semantic-release runs plugins in pipeline order: verifyConditions, analyzeCommits, verifyRelease, generateNotes, prepare, publish, and fail. The commit analyzer works at the analyzeCommits stage: it takes all commits since the last tag, tests them one by one, then returns a single release type.
The final result is the highest level of change found. One feat among many fixes means a minor release, not a patch.
Without any configuration, semantic-release uses the angular preset with the following mapping:
| Commit type | Release type | Example result |
|---|---|---|
feat | minor | 1.2.0 -> 1.3.0 |
fix | patch | 1.2.0 -> 1.2.1 |
perf | patch | 1.2.0 -> 1.2.1 |
revert | patch | 1.2.0 -> 1.2.1 |
| breaking change | major | 1.2.0 -> 2.0.0 |
docs, style, test, chore, build, ci | none | not released |
A breaking change is detected from the BREAKING CHANGE: footer or an exclamation mark on the subject like feat!: drop Node 16 support. Without special rules, types like docs and chore are considered not worth releasing.
The default rules are conservative: only feat and fix trigger a release. But many teams want small fixes to be released too:
build(deps): update esbuild — updating build tooling often means fixing a bug or a security hole.chore(deps): bump lodash — a dependency version bump can also be patch-like.Conversely, some teams want docs to keep not triggering a release so the changelog doesn't fill with noise. That's where releaseRules comes in: your custom rules are evaluated first and override the preset.
The rules in releaseRules are evaluated sequentially from top to bottom, and the first matching rule immediately determines the result. Put the most specific rules first — for example, breaking: true must be at the top so a breaking-marked commit always becomes major, whatever its type.
Before rules are applied, commits are first split by a parser. With parserOpts you can customize the note keywords and the header pattern:
parserOpts: {
noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"],
headerPattern: /^(\w*)(?:\(([\w$.\-* ]*)\))?: (.*)$/,
breakingHeaderPattern: /^(\w*)(?:\(([\w$.\-* ]*)\))?!: (.*)$/,
}noteKeywords determines which footers count as breaking. headerPattern and breakingHeaderPattern define the type(scope): subject structure, including support for the ! marker. If your team uses a custom commit format, this is where you adapt it.
const config = {
branches: ["main", { name: "staging", prerelease: "rc" }],
plugins: [
[
"@semantic-release/commit-analyzer",
{
preset: "angular",
parserOpts: {
noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"],
headerPattern: /^(\w*)(?:\(([\w$.\-* ]*)\))?: (.*)$/,
breakingHeaderPattern: /^(\w*)(?:\(([\w$.\-* ]*)\))?!: (.*)$/,
},
releaseRules: [
{ breaking: true, release: "major" },
{ type: "feat", release: "minor" },
{ type: "fix", release: "patch" },
{ type: "perf", release: "patch" },
{ type: "build", release: "patch" },
{ type: "chore", release: "patch" },
{ type: "revert", release: "patch" },
{ type: "docs", release: false },
{ type: "style", release: false },
{ type: "test", release: false },
],
},
],
"@semantic-release/release-notes-generator",
"@semantic-release/github",
],
};
module.exports = config;The main changes compared to the default: build and chore now trigger a patch, while docs, style, and test still don't trigger a release. revert stays patch. The angular preset is still used as the foundation, and the custom rules override its behavior.
Warning
Mapping chore to patch can trap you in a release loop.
The release commit created by @semantic-release/git has the type
chore(release): and would trigger another release. The solution:
include [skip ci] in the release commit message, because
semantic-release ignores trigger commits that contain that keyword.
After changing rules, verify before actually releasing:
git log --oneline -5
npx semantic-release --dry-run --no-ciRead the line mentioning "next release version". If the latest commit is docs and there's no feat or fix, the result is no release. If there's a chore(deps), the version will bump patch — matching the rules you wrote.
| Mistake | Symptom | Solution |
|---|---|---|
breaking placed at the bottom | Breaking becomes minor/patch | Put the breaking: true rule at the top |
| Unknown commit type | No release | Choose a preset matching your convention or add a rule |
chore triggers a release loop | Release keeps repeating | Add [skip ci] to the release commit |
| Misspelled preset name | Error while analyzing commits | Make sure the preset used is installed |
In episode 8 you:
analyzeCommits stage of the pipeline.angular preset mapping: feat minor, fix/perf/revert patch, breaking major, others not released.releaseRules with first-match-wins ordering and parserOpts for custom commit formats.npx semantic-release --dry-run --no-ci.In episode 9 we'll put together Linting & Build Automation — the quality gate before release, so only tested code reaches users' hands. See you in episode 9!