Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog Customization

Releasaurus generates changelogs from conventional commits. Two sections of releasaurus.toml control the result:

  • [defaults.versioning] — which commits are included and how they’re grouped. These settings affect the version bump as well as the changelog, which is why they live alongside the versioning options.
  • [defaults.changelog] — how the included commits are rendered (the Tera template and display flags).

Both can also be set per package.

Commit Groups & Filtering

Each commit is matched against a set of parsers. A parser decides which group (changelog heading) a commit belongs to, and whether the commit is skipped entirely. Configure them in [defaults.versioning].

A parser has four fields:

FieldTypeEffect
patternregexMatched against the raw commit message to decide if the parser applies
titlestringThe changelog heading commits in this group appear under
orderintPosition of the heading in the changelog, 0-99, lowest first
skipboolWhen true, matching commits are dropped from both the changelog and version calculation

Built-in groups (named_parsers)

Releasaurus ships with these default parsers:

Group (toml key)PatternDefault titleOrder
breaking(none)❌ Breaking0
feature^feat🚀 Features1
fix^fix🐛 Bug Fixes2
revert^revert◀️ Revert3
refactor^refactor🚜 Refactor4
performance^perf⚡ Performance5
documentation^doc📚 Documentation6
style^style🎨 Styling7
test^test🧪 Testing8
chore^chore🧹 Chore9
ci^ci⏩ CI/CD10
miscellaneous.*⚙️ Miscellaneous Tasks11

breaking is the one group not selected by its pattern. A commit is breaking when conventional-commit syntax says so — a ! before the colon, or a BREAKING CHANGE: footer — and breaking always wins over the commit’s type, so feat!: … lands under ❌ Breaking rather than 🚀 Features.

Setting breaking.pattern adds to that detection rather than replacing it. Commits matching your pattern are treated as breaking on top of the ones conventional syntax already catches, so you cannot lose a feat!: by writing a pattern that doesn’t happen to match it:

[defaults.versioning.named_parsers]
breaking.pattern = "^breaking"

With that config, both breaking: drop the v1 endpoint and feat!: drop the v1 endpoint are grouped under ❌ Breaking and bump the major version.

breaking.pattern and custom_major_increment_regex are two spellings of the same thing — a pattern that marks a commit breaking. Either one groups the commit under ❌ Breaking, marks it [**breaking**] in the default template, and bumps major. Setting both is fine; the two are combined, and a commit matching either is breaking. Reach for breaking.pattern when you are already customizing named_parsers, and custom_major_increment_regex when versioning is all you care about.

Because breaking is decided before the type patterns are consulted, skip on another group cannot swallow a breaking commit — a feat!: reaches ❌ Breaking even with feature.skip = true. The two ways a breaking change can still be dropped are both explicit: breaking.skip = true, or a custom parser with skip = true that matches it (see below).

Override only the fields you want to change under [defaults.versioning.named_parsers]; everything you omit falls back to the built-in default. For example, to drop CI and chore commits — the only change needed is skip:

[defaults.versioning.named_parsers]
ci.skip = true
chore.skip = true

To skip a group, set its skip = true. You can also retitle a group, move it, or change its matching pattern the same way. A retitle does not move the group — position comes from order alone:

[defaults.versioning.named_parsers]
feature.title = "✨ New Stuff"
fix.order = 1                   # bug fixes above features
feature.order = 2

Custom groups (custom_parser)

Define entirely new groups with [[defaults.versioning.custom_parser]]. Note the key is singular, matching the [[package]] convention. Each custom parser is checked before the built-in parsers, so it takes precedence over the defaults:

[[defaults.versioning.custom_parser]]
pattern = "^deps"
title = "📦 Dependencies"
order = 3
skip = false

Unlike named parsers, custom parsers have no defaults to fall back on: pattern, title and order are all required. Omitting any of them is a configuration error.

Because custom parsers are checked first, they also win over breaking — so a custom parser with skip = true drops matching commits even when they are breaking changes, removing them from the changelog and from the version bump. Keep custom patterns narrow, or leave skip = false if you only want to regroup commits rather than discard them.

Ordering groups

Each group’s order places its heading in the changelog, lowest first (see the table above for the built-in values). Groups sharing an order fall back to title order.

Order is independent of the heading text, so retitling a group never moves it. Mechanically, order is rendered into the group attribute as an <!-- NN --> prefix, which the default template sorts on and then strips:

{% ... | sort(attribute="group") | group_by(attribute="group") %}
### {{ group | striptags | trim }}

A custom template that sorts on group gets ordering for free; one that prints {{ group }} without striptags will show the prefix. See The body Template below for the full template.

Other options

In [defaults.versioning]:

OptionDefaultEffect
skip_merge_commitstrueExcludes merge commits

In [defaults.changelog]:

OptionDefaultEffect
include_authorfalseAdds the commit author’s name to each entry
include_pr_linkfalseAdds a link to the pull request that introduced each commit
aggregate_prereleasesfalseWhen graduating a prerelease to stable, folds in the changelog entries from all prior prereleases (see Prereleases)

To drop specific commits entirely or rewrite their messages — which also affects the version bump — see “Skipping or Rewording Commits” in the configuration guide.

include_pr_link appends the PR that introduced each commit, so an entry reads:

- add retry handling [_(a1b2c3d)_](…/commit/a1b2c3d) ([PR 42](…/pull/42))

Only merged pull requests targeting the release branch are linked; commits pushed directly render without the segment.

Two things are worth knowing before turning it on:

  • It costs extra API requests — roughly one per commit in the release. Expect a slower run, and on a large first release, watch for forge rate limits. A request that fails is logged as a warning and that entry renders without a link; it never fails the release.
  • Only the packages that enable it pay for it. A package that leaves it off costs nothing, even when a sibling turns it on. Where two enabled packages share a commit, that commit is looked up once.

Per-package changelog

Everything on this page applies to every package by default. To customize a single package, set the same fields on that package’s changelog and versioning keys — matching the [defaults] table each option belongs to. Packages are an array of tables ([[package]]), so use an inline table to keep it scoped to the right entry:

[[package]]
name = "frontend"
path = "./apps/web"
release_type = "node"
changelog = { include_author = true }
versioning = { named_parsers = { ci = { skip = true } } }

Both keys merge field-by-field with their [defaults] counterpart: any field you set on the package wins, and any field you omit is inherited from [defaults] (falling back to the built-in default). custom_parser entries from [defaults] and the package are combined, with the package’s checked first, and named_parsers overrides apply per group and per field — so the example above turns on include_author and skips ci for frontend while still inheriting every other default. The one exception is versioning.prerelease, which is replaced as a whole table rather than merged. See Per-package overrides in the reference for the exact precedence rules.

The body Template

body is a Tera template rendered once per release. The default groups commits by type, links each commit, and highlights breaking changes:

[defaults.changelog]
body = '''# [{{ version  }}]{% if tag_compare_link %}({{ tag_compare_link }}){% else %}({{ link }}){% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
{% for group, commits in commits | filter(attribute="merge_commit", value=false) | sort(attribute="group") | group_by(attribute="group") %}
### {{ group | striptags | trim }}
{% for commit in commits %}
{% if commit.breaking -%}
{% if commit.scope %}_({{ commit.scope }})_ {% endif -%}[**breaking**]: {{ commit.title }} [_({{ commit.short_id }})_]({{ commit.link }}){% if include_author %} ({{ commit.author_name }}){% endif %}{% if include_pr_link and commit.pr %} ([PR {{ commit.pr.id }}]({{ commit.pr.link }})){% endif %}
{% if commit.body -%}
{%- set body_lines = commit.body | split(pat="\n") -%}
{%- for body_line in body_lines %}
> {{ body_line }}
{%- endfor %}
{% endif -%}
{% if commit.breaking_description -%}
{%- set breaking_lines = commit.breaking_description | split(pat="\n") -%}
{%- for breaking_line in breaking_lines %}
> {{ breaking_line }}
{%- endfor %}
{% endif -%}
{% else -%}
- {% if commit.scope %}_({{ commit.scope }})_ {% endif %}{{ commit.title }} [_({{ commit.short_id }})_]({{ commit.link }}){% if include_author %} ({{ commit.author_name }}){% endif %}{% if include_pr_link and commit.pr %} ([PR {{ commit.pr.id }}]({{ commit.pr.link }})){% endif %}
{% endif -%}
{% endfor %}
{% endfor %}'''

Commit bodies and breaking descriptions are often several lines long, so the template splits them and gives every line its own > . Interpolating the field whole quotes only its first line and leaks the rest out as body text.

The ''' delimiters are deliberate. A TOML literal string passes the template through verbatim, so what you write is what Tera sees. A """ string processes escapes first, and it recognizes only a fixed set of them: the \n above survives as a real newline and Tera splits on that just the same, but any other backslash in a custom template — a \d in a regex, say — is a TOML parse error before Tera is ever reached. Prefer ''' for templates.

Note that include_author and include_pr_link only do anything where the template checks them. A custom body gets nothing for free — setting include_pr_link = true against a template with no commit.pr clause renders no links (while still paying for the lookups). Copy the guard above into your own template:

{% if include_pr_link and commit.pr %} ([PR {{ commit.pr.id }}]({{ commit.pr.link }})){% endif %}

Guard on commit.pr as well as the flag: commits pushed straight to the branch have no PR, and dereferencing commit.pr.id unguarded renders an empty link.

A simpler custom template:

[defaults.changelog]
body = """## Release v{{ version }} — {{ timestamp | date(format="%Y-%m-%d") }}

{% for group, commits in commits | group_by(attribute="group") %}
### {{ group }}
{% for commit in commits %}
- {{ commit.title }} ({{ commit.short_id }}){% if include_author %} by {{ commit.author_name }}{% endif %}
{% endfor %}
{% endfor %}"""

Template Variables

Release

VariableDescription
versionSemantic version (e.g. 1.2.3)
tag_nameFull tag including prefix/suffix
linkURL to the release
tag_compare_linkDiff vs. previous tag (empty for first release)
sha_compare_linkDiff vs. previous tag, by commit SHA (empty for first release)
shaRelease commit SHA
short_shaAbbreviated release commit SHA
timestampUnix timestamp
include_authorWhether author display is enabled
include_pr_linkWhether PR-link display is enabled

Commit (each item in commits)

VariableDescription
id / short_idFull / abbreviated SHA
groupCategory (Features, Bug Fixes, …)
scopeOptional conventional-commit scope
titleMessage without type/scope
bodyOptional extended description
linkURL to the commit
prIntroducing PR, or unset (see below)
breaking / breaking_descriptionBreaking-change flag and details
merge_commitWhether it’s a merge commit
timestampCommit timestamp
author_name / author_emailCommit author
raw_title / raw_messageOriginal unprocessed title / message

commit.pr is only populated when include_pr_link is enabled and the commit arrived via a merged pull request. When present it carries:

VariableDescription
pr.idUser-visible PR number (e.g. 42)
pr.linkURL to the pull request

Tips

Filter merge commits and conditionally show authors:

{% for commit in commits | filter(attribute="merge_commit", value=false) %}
- {{ commit.title }}{% if include_author %} <{{ commit.author_name }}>{% endif %}
{% endfor %}

Test any template change locally before committing it:

releasaurus release-pr --forge local --repo "."

See the Tera documentation for advanced filtering and formatting.