Introduction
Releasaurus 🦕 automates releases across multiple languages and Git
forges. Point it at a repository and it analyzes your commit history,
generates a changelog, and publishes a tagged release — no
configuration required. Add a releasaurus.toml when you want version
file updates, monorepo support, or custom changelog formatting.
# 1. Open a release PR (analyzes commits, writes the changelog)
releasaurus release-pr --repo "https://github.com/your-org/your-repo"
# 2. After merging the PR, tag and publish the release
releasaurus release --repo "https://github.com/your-org/your-repo"
That two-command loop — release-pr to prepare, release to
publish — is the whole workflow. The pull request gives you a review
step; Releasaurus handles the tedious version and changelog work.
Key Features
- Zero config by default — changelog generation and tagging work immediately. Configure only when you need more.
- Multi-forge — GitHub, GitLab, Gitea, Forgejo, and Azure DevOps (experimental), whether cloud-hosted or self-hosted.
- Multi-language version updates — Rust, Node.js, Python, Java, PHP, Ruby, Go, and a generic regex-based updater for anything else.
- Monorepo ready — multiple independently-versioned packages, with combined or separate release PRs.
- Conventional-commit aware — version bumps follow conventional commits and semver.
- Forge API native — runs entirely through forge APIs with no local clone required, ideal for CI/CD. An optional hybrid mode uses a local clone for git operations.
- Command-line overrides — change branch, tag prefix, and prerelease settings per run without editing your config.
Optional Commands
releasaurus start-next— bump patch versions right after a release to start the next development cycle.releasaurus get— query projected and published release data as JSON for automation, notifications, and debugging.
Where to Go Next
- Getting Started — install and cut your first release.
- Commands — every command, flag, and mode.
- Configuration — version file updates, monorepos, and prereleases.
Credit and Inspiration
Releasaurus builds on the proven ideas of git-cliff, release-please, and release-plz, extending them to a broader set of languages, frameworks, and platforms.
Getting Started
Install Releasaurus and cut your first release in a few minutes.
Install
Pre-built binary (recommended)
The fastest option, via cargo-binstall:
cargo binstall releasaurus
From crates.io
Compiles from source:
cargo install releasaurus
Docker
docker pull rgonnella/releasaurus:latest
docker run --rm rgonnella/releasaurus:latest --help
You can also download a binary directly from the
releases page, or
build from source — see Contributing. Confirm the
install with releasaurus --version.
Preview Without Any Risk
Run Releasaurus against a local checkout to see what it would do — no token, no config, no changes:
cd /path/to/your/repo
releasaurus release-pr --forge local --repo "."
The output shows the next version and the generated changelog without touching your repository. See Local & Dry-Run Modes for more.
Cut Your First Release
1. Set an access token
Releasaurus picks the right variable from the --forge you use:
export GITHUB_TOKEN="ghp_your_token_here" # GitHub
export GITLAB_TOKEN="glpat_your_token_here" # GitLab
export GITEA_TOKEN="your_token_here" # Gitea
Every token variable and its required scopes are listed in the Configuration Reference.
2. Open a release PR
releasaurus release-pr --repo "https://github.com/your-org/your-repo"
This analyzes your commits, picks the next version, generates a
changelog, and opens a pull request. (--forge is inferred for known
hosts like github.com; pass it explicitly for self-hosted instances.)
3. Merge, then publish
After reviewing and merging the PR:
releasaurus release --repo "https://github.com/your-org/your-repo"
This tags the release commit and publishes the release on your forge.
Add Version File Updates (Optional)
By default Releasaurus only writes changelogs and tags. To also bump
versions in your manifests (package.json, Cargo.toml, etc.), add a
releasaurus.toml at the repository root:
[[package]]
path = "."
release_type = "node" # or rust, python, java, php, ruby, go, generic
See Configuration for monorepos, prereleases, changelog customization, and the full option list.
Next Steps
- Commands — all commands, overrides, and the
start-nextandgetcommands. - CI/CD Integration — automate with GitHub Actions, GitLab CI, and more.
- Troubleshooting — common issues and fixes.
Migration Guide
Upgrading from v0.22.x to v1.0.0.
This release restructures releasaurus.toml completely, changes two
--set-package override paths, drops support for two legacy release-PR
state formats, and reshapes the releasaurus-core public API. Nothing
here is optional: a 0.22 config will not load on 1.0.0.
At a glance
- Before you upgrade — drain release PRs created by versions older than v0.17.0.
- TOML config restructure — every key now
lives under
[repository],[defaults], or[[package]]. - CLI override paths — prerelease
--set-packagepaths gained aversioning.segment. - Monorepo commit message and PR title — the default now includes the repository name.
custom_major_increment_regex— matching commits are now grouped as breaking, not just bumped.- Library API — for Rust consumers of
releasaurus-core.
Unknown keys are now rejected at config load. A key you forget to move is a hard error naming the offending field, not a silent no-op, so the upgrade fails loudly rather than quietly computing the wrong version. Work through section 2 and let the error messages guide you.
Before you upgrade: drain open release PRs
Releasaurus keeps release state in the PR itself — a releasaurus
label and a JSON metadata block in the PR body. v1.0.0 removes the
compatibility shims for the pre-v0.17.0 forms of both:
- The legacy pending label
releasaurus:pending(single colon) is no longer recognized. Only the scopedreleasaurus::pendingis, which has been the format written since v0.17.0-rc.1. - The legacy PR-body metadata format is no longer parsed. Only the current JSON HTML-comment block is, written since v0.17.0-rc.2.
Who this affects: only repositories with an open — or merged but
not yet released — release PR created by a version older than
v0.17.0. After upgrading, releasaurus release will not find that
PR, so its tag and release are never published.
What to do: while still on 0.22.x, either merge the PR and run
releasaurus release to finish it, or close it and let v1.0.0 open a
fresh one. If your last release ran on v0.17.0 or newer, there is
nothing to do.
TOML config restructure
Configuration is now grouped under three top-level tables:
[repository]— settings that apply to the repository as a whole.[defaults]— release defaults for every package, with[defaults.versioning]and[defaults.changelog]subtables.[[package]]— one entry per package, unchanged in spirit; each may override[defaults]with its ownversioningandchangelog.
The full reference lives in the Configuration Reference. The tables below cover only what moved.
Root-level keys
| v0.22.x | v1.0.0 |
|---|---|
base_branch | [repository].base_branch |
first_release_search_depth | [repository].first_release_search_depth |
tag_search_depth | [repository].tag_search_depth |
separate_pull_requests | [repository].separate_pull_requests |
auto_start_next | [defaults.versioning].auto_start_next |
breaking_always_increment_major | [defaults.versioning].breaking_always_increment_major |
features_always_increment_minor | [defaults.versioning].features_always_increment_minor |
custom_major_increment_regex | [defaults.versioning].custom_major_increment_regex |
custom_minor_increment_regex | [defaults.versioning].custom_minor_increment_regex |
[prerelease] | [defaults.versioning.prerelease] |
The [changelog] table
The old [changelog] table mixed two concerns, so its keys split three
ways. Options that only affect how commits are rendered stay in
[defaults.changelog]. Options that determine which commits are
counted, and therefore change the computed version, moved to
[defaults.versioning]. skip_shas and reword act on the
repository’s shared commit history and cannot be scoped to a package,
so they moved to [repository].
v0.22.x [changelog] | v1.0.0 |
|---|---|
body | [defaults.changelog].body |
include_author | [defaults.changelog].include_author |
aggregate_prereleases | [defaults.changelog].aggregate_prereleases |
skip_merge_commits | [defaults.versioning].skip_merge_commits |
skip_shas | [repository].skip_shas |
[[changelog.reword]] | [[repository.reword]] |
the nine skip_* flags | [defaults.versioning.named_parsers] — see below |
skip_* flags become named_parsers
The nine per-type skip flags are replaced by the named_parsers table,
where each commit group carries a pattern, title, order, and
skip. Setting skip = true on a group is the direct equivalent of
the old flag:
| v0.22.x | v1.0.0 |
|---|---|
skip_ci | ci.skip |
skip_chore | chore.skip |
skip_doc | documentation.skip |
skip_test | test.skip |
skip_style | style.skip |
skip_refactor | refactor.skip |
skip_perf | performance.skip |
skip_revert | revert.skip |
skip_miscellaneous | miscellaneous.skip |
Three group names are spelled out rather than abbreviated — watch
documentation (not doc), performance (not perf), and feature
(not feat) if you go on to retitle or reorder groups. The full set is
breaking, feature, fix, revert, refactor, performance,
documentation, style, test, chore, ci, miscellaneous.
So this:
[changelog]
skip_ci = true
skip_chore = true
becomes:
[defaults.versioning.named_parsers]
ci.skip = true
chore.skip = true
named_parsers merges field-by-field over the built-in defaults, so
naming one group leaves the other eleven untouched — you never have to
restate the whole set.
Filtering behavior is unchanged. In 0.22.x a skipped commit was
dropped before it reached version calculation, so skipping a group
already suppressed both its changelog entries and its version bump.
Only the config location moved. What is new is that groups the old
flags could not reach — feature, fix, breaking — can now be
skipped too, and that you can define additional groups with
[[defaults.versioning.custom_parser]].
Package-level keys
Six keys are no longer direct [[package]] keys. They now live under
the package’s versioning table, mirroring [defaults.versioning]:
prereleaseauto_start_nextbreaking_always_increment_majorfeatures_always_increment_minorcustom_major_increment_regexcustom_minor_increment_regex
Unchanged: name, path, workspace_root, release_type,
tag_prefix, sub_packages, additional_paths, and
additional_manifest_files.
Because packages are an array of tables, set versioning as an
inline table on the package entry. A separate [package.versioning]
header would bind to whichever [[package]] was declared last, which
is almost never what you want:
Before:
[[package]]
name = "backend"
path = "./services/api"
prerelease = { suffix = "alpha", strategy = "versioned" }
After:
[[package]]
name = "backend"
path = "./services/api"
versioning = { prerelease = { suffix = "alpha", strategy = "versioned" } }
One asymmetry to be aware of when splitting config across [defaults]
and packages: a package’s versioning and changelog tables merge
field-by-field with their [defaults] counterpart, but prerelease
replaces the [defaults] one outright. suffix and strategy
describe a single prerelease identity, so restate strategy alongside
a package-level suffix whenever your default strategy is not the
built-in versioned.
Worked example
This repository’s own config, before and after. Before:
#:schema ./schema/schema.json
tag_search_depth = 25
[changelog]
skip_ci = true
skip_chore = true
skip_miscellaneous = false
include_author = true
aggregate_prereleases = true
[prerelease]
strategy = "versioned"
suffix = "rc"
[[package]]
name = "workspace"
tag_prefix = "v"
release_type = "rust"
After:
#:schema ./schema/schema.json
[repository]
tag_search_depth = 25
[defaults.changelog]
include_author = true
aggregate_prereleases = true
[defaults.versioning.named_parsers]
ci.skip = true
chore.skip = true
miscellaneous.skip = false
[defaults.versioning.prerelease]
strategy = "versioned"
suffix = "rc"
[[package]]
name = "workspace"
tag_prefix = "v"
release_type = "rust"
CLI override paths
Two --set-package paths gained a versioning. segment so that they
mirror the TOML layout they override:
| v0.22.x | v1.0.0 |
|---|---|
<pkg>.prerelease.suffix=<value> | <pkg>.versioning.prerelease.suffix=<value> |
<pkg>.prerelease.strategy=<value> | <pkg>.versioning.prerelease.strategy=<value> |
# v0.22.x
releasaurus release-pr --set-package frontend.prerelease.suffix=beta ...
# v1.0.0
releasaurus release-pr \
--set-package frontend.versioning.prerelease.suffix=beta ...
<pkg>.tag_prefix is unchanged, and so are all the global flags:
--base-branch, --tag-prefix, --prerelease-suffix,
--prerelease-strategy, --skip-sha, and --reword. Emptying a
suffix to disable prereleases still works the same way
(--set-package <pkg>.versioning.prerelease.suffix=).
A stale path now fails with a config error naming the offending override rather than being silently ignored, so a missed occurrence in a CI workflow surfaces on the next run instead of producing an unexpected version.
Monorepo commit message and PR title
Release commit messages and PR titles are now rendered from Tera
templates, and the default for combined monorepo PRs includes the
repository name. This is one of two changes that alter output without
any config edit on your part (the other is
custom_major_increment_regex),
so check anything that matches on release PR titles — branch protection
rules, required status checks, CI if: conditions, merge automation.
0.22.x picked the format from how many packages happened to be in the PR on that run:
- one package in the PR —
chore(<branch>): release <pkg> <tag> - more than one — a bare
chore(<branch>): release
1.0.0 picks from config instead. A repo with separate_pull_requests = true, or with a single [[package]], uses the per-package template
(default chore({{ branch }}): release {{ package_name }} {{ tag }},
identical to before). Any other repo produces a combined PR and uses
the monorepo template, whose default is:
chore({{ branch }}): release {{ repo_name }}
For a multi-package repo with separate_pull_requests = false, that
means the title now always carries the repository name and no longer
collapses to release <pkg> <tag> on runs where only one package
changed. Deciding from config keeps the format stable from one release
to the next.
To keep the old bare title, set the templates explicitly:
[defaults]
monorepo_commit_message_template = "chore({{ branch }}): release"
monorepo_pr_title_template = "chore({{ branch }}): release"
The monorepo templates have branch and repo_name in scope. The
per-package templates additionally have package_name, tag, and
semver. Referencing a variable that is not in scope is rejected at
config load.
custom_major_increment_regex now groups commits as breaking
The key moved to [defaults.versioning] like the rest, but its effect
also widened. In 0.22.x it only influenced the version bump; a matching
commit still appeared under whatever group its type prefix selected. In
1.0.0 a commit it matches is treated as breaking outright: grouped under
❌ Breaking, marked [**breaking**] in the default body template, and
bumping major as before.
Who this affects: anyone who already sets
custom_major_increment_regex. No config edit is required and the
computed version does not change — only which changelog heading those
commits appear under.
If you were relying on the old split — bump major, but keep the commit
filed under its own type — there is no longer a setting for that; the
two concepts are deliberately unified. The reverse case is now possible
though: [defaults.versioning.named_parsers] breaking.pattern is the
same mechanism under a different name, so pick whichever key reads
better and know they are combined if you set both.
custom_minor_increment_regex is unchanged — it affects the version
bump only, with no grouping effect.
Library API
For Rust consumers of the releasaurus-core crate. The CLI binary
needs none of this.
| v0.22.x | v1.0.0 |
|---|---|
config::resolved::{GlobalOverrides, PackageOverrides, CommitModifiers, PackageName} | config::overrides::* — same names |
config::resolved::ResolvedConfig | resolver::ResolvedConfig — reshaped |
config::changelog::RewordedCommit | config::repository::RewordedCommit |
config::{DEFAULT_COMMIT_SEARCH_DEPTH, DEFAULT_TAG_SEARCH_DEPTH} | config::repository::* |
Config { base_branch, changelog, prerelease, … } | Config { repository, defaults, packages } |
Beyond the moves:
Resolver::resolve()returns one value. It now yieldsRc<ResolvedConfig>rather than a(ResolvedConfig, ResolvedPackageHash)tuple; the resolved packages hang offResolvedConfig::package_configs. Drop the.package_configs(...)call fromOrchestrator::builder(). The crate-level quick start incrates/core/src/lib.rsshows the full builder chain, and Library API has the narrative version.PrereleaseConfig::suffixis aString, not anOption<String>. Thesuffix()accessor that unwrapped it is gone — read the field directly, and treat""as “no prerelease”.- New
config::versioningmodule holdingVersionType,Group,Parser,ParserList,VersioningConfig, andNAMED_PARSERS.Group’s variants were renamed to spell outFeature,Documentation,Performance, andCI— which is why the TOML keys readdocumentationandperformancerather thandocandperf. Forgegained a required method,get_merged_pull_request_for_commit, which resolves the merged PR that introduced a commit. If you implementForgeoutside this crate, add it; returningOk(None)is a valid no-op and simply means theinclude_pr_linkchangelog option renders nothing for that forge.
Not affected
- Existing tags and changelogs. Git tags,
CHANGELOG.mdfiles, and published releases are read and appended to exactly as before. No retagging or history rewrite is needed. - Custom
bodytemplates. Group headings still carry the<!-- NN -->ordering tag, so the standard{{ group | striptags | trim }}idiom keeps working unchanged. The template context only gained fields:short_sha,include_pr_link, andcommit.pr. - Environment variables. Every
RELEASAURUS_*variable and bare*_TOKENfallback behaves as it did, with the same precedence. release_typevalues and version-file updaters. All languages, manifest files, lock files,sub_packages, andadditional_manifest_filesbehavior are unchanged.- The JSON schema location.
#:schema ./schema/schema.jsonstill points at the right file; the schema itself was regenerated for the new layout, so editor completion reflects it.
Commands
Releasaurus operates entirely through forge platform APIs — no local clone required — so every command can run from any machine with network access to your forge. An optional hybrid mode uses a local clone for git operations.
The core workflow is two commands:
# 1. Prepare: analyze commits, bump versions, write changelog, open a PR
releasaurus release-pr --repo "https://github.com/owner/repo"
# 2. Review and merge the PR in your forge's UI, then publish:
releasaurus release --repo "https://github.com/owner/repo"
release-direct replaces both steps for repos that don’t want a review PR.
start-next and get are optional helpers covered below.
release-pr
Analyzes commits since the last release, determines the version bump
(patch/minor/major) from conventional commits, updates version files (if
a release_type is configured), generates the changelog, and creates or
updates a release pull request.
# All packages
releasaurus release-pr --repo "https://github.com/owner/repo"
# A single package in a monorepo
releasaurus release-pr --package my-pkg \
--repo "https://github.com/owner/repo"
Supports prereleases, dry-run, and the overrides below.
release
Run after the release PR is merged. Validates the release commit, creates and pushes the git tag, and publishes the release on your forge. Reads the release notes directly from the merged PR body (see Editing Release Notes).
# All packages with merged release PRs
releasaurus release --repo "https://github.com/owner/repo"
# A single package
releasaurus release --package my-pkg \
--repo "https://github.com/owner/repo"
release-direct
Does everything release-pr and release do together, in a single pass
and with no pull request: analyzes commits, bumps versions, writes the
changelog, commits directly to the base branch, tags that commit, and
publishes the release.
# All packages
releasaurus release-direct --repo "https://github.com/owner/repo"
# A single package in a monorepo
releasaurus release-direct --package my-pkg \
--repo "https://github.com/owner/repo"
Use it for trunk-based or fully automated releases where a review step
adds nothing — internal tools, nightly builds, or CI that already gates
on the merge into the base branch. Prefer release-pr + release
whenever you want the version bump and changelog reviewed before they
land.
In a monorepo, every package released in a run shares a single release
commit, and each package’s tag points at that commit. Setting
separate_pull_requests gives
you one commit per package instead, matching how that setting splits
release PRs.
Confirmation
Because there is no PR to review and no way to finish a partially failed
run by repeating it, release-direct stops and asks you to type yes
before it changes anything:
release-direct will make changes that re-running it cannot undo.
repository: https://github.com/owner/repo
branch: main
It commits the version bumps and changelog to that branch, creates and
pushes the release tag(s), and publishes the release(s) on your forge.
No pull request is created and there is no review step.
Type 'yes' to continue:
Pass --auto-approve to skip it. CI must pass --auto-approve — when
stdin is not a terminal the command refuses to run rather than hang or
guess, and tells you to add the flag.
--dry-run never prompts, since it writes nothing.
Warning: This is a separate, out-of-band flow — it never creates a release PR. Do not mix it with the
release-pr/releaseworkflow on the same packages: the tagrelease-directcreates will collide with the onereleaselater tries to create for the merged PR. As a safety net,release-directrefuses to run if a package it is about to release still has a merged release PR waiting to be tagged, but it cannot detect every ordering.
Note: Like
start-next, this commits directly to your base branch, so your branch protection rules must permit it. Unlikerelease, it is not driven byauto_start_nextand will not trigger a follow-up patch bump.
Supports dry-run and the overrides below. Run
it with --dry-run first to see exactly what it would commit, tag, and
publish.
start-next
Bumps the patch version for each previously-tagged package and commits
the manifest changes directly to the base branch as a chore commit.
It does not open PRs or create tags, and skips packages that have never
been tagged. Use it right after a release to keep manifest versions ahead
of the last release.
# All previously-tagged packages
releasaurus start-next --repo "https://github.com/owner/repo"
# Specific packages only
releasaurus start-next --repo "https://github.com/owner/repo" \
--packages pkg-a,pkg-b
Note: This commits directly to your base branch. Ensure your branch protection rules permit it. It can also run automatically after
release— seeauto_start_next.
get
Queries release information as JSON without making any changes — useful
for debugging version detection and for building custom notifications.
(show is kept as an alias.)
get next-release
Projects the next release for each package as JSON.
releasaurus get next-release --repo "https://github.com/owner/repo"
# Single package, or write to a file
releasaurus get next-release --package my-pkg --out-file releases.json \
--repo "https://github.com/owner/repo"
get current-release
Returns the most recent release for each package (packages without a release are omitted).
releasaurus get current-release --repo "https://github.com/owner/repo"
get release
Returns the data for an existing tag — tag, sha, and notes.
releasaurus get release --tag v1.0.0 \
--repo "https://github.com/owner/repo"
get notes
Re-renders release notes from a get next-release JSON file using your
configured Tera template. This lets you transform the data (for example,
replacing author names with Slack IDs) before producing final notes.
(recompiled-notes is kept as an alias.)
# 1. Capture release data
releasaurus get next-release --out-file releases.json \
--repo "https://github.com/owner/repo"
# 2. Transform it however you like (custom script), then re-render:
releasaurus get notes --file releases.json \
--repo "https://github.com/owner/repo"
Output is a JSON array of { name, notes } objects.
Global Options & Forge Selection
These apply to every command:
| Flag | Env fallback | Description |
|---|---|---|
--repo <url> | RELEASAURUS_REPO | Repository URL |
--forge <forge> | RELEASAURUS_FORGE | Forge type (see below) |
--token <token> | RELEASAURUS_<FORGE>_TOKEN, <FORGE>_TOKEN | Auth token |
--local-path <path> | RELEASAURUS_LOCAL_PATH | Local clone for hybrid mode |
--base-branch <branch> | — | Override the base branch |
--debug | RELEASAURUS_DEBUG | Verbose logging |
--config | RELEASAURUS_CONFIG | Custom file path location |
Available forge types: github, gitlab, gitea, forgejo,
azure-devops (experimental), and local (testing). For the full list
of token variables and required scopes, see the
Configuration Reference.
Automatic forge inference
When --repo points at a recognized cloud host, --forge can be
omitted:
| Host | Inferred forge |
|---|---|
github.com | github |
gitlab.com | gitlab |
gitea.com | gitea |
codeberg.org | forgejo |
dev.azure.com | azure-devops |
Self-hosted instances (e.g. https://gitlab.company.com/...) and
--forge local always require the flag, since the host alone can’t
identify the forge software.
Testing Modes
Three ways to run safely or against a local checkout.
Dry-Run Mode
Performs all analysis and validation and logs exactly what would
happen, but makes no changes — no branches, PRs, tags, or releases.
Dry-run automatically enables debug logging (output is prefixed
dry_run:).
releasaurus release-pr --dry-run --repo "https://github.com/owner/repo"
# Or via environment variable
export RELEASAURUS_DRY_RUN=true
Local Repository Mode
--forge local reads commits, tags, and files from your working
directory and never contacts a remote forge — ideal for validating a
releasaurus.toml change before pushing. No token required.
releasaurus release-pr --forge local --repo "."
# Or from a specific path
releasaurus release-pr --forge local --repo "/path/to/repo"
Hybrid Mode (Local Git + Remote Forge)
--local-path performs git operations (reading commits/tags/files,
creating branches, committing, pushing) against a local clone, while
still creating real PRs and releases via the forge API. Use it when you
already have a checkout and want to avoid repeated API calls for data
gathering. A forge token is still required.
releasaurus release-pr \
--repo "https://github.com/owner/repo" \
--token "$GITHUB_TOKEN" \
--local-path /path/to/checkout
CI fetch depth: in hybrid mode the local checkout must include full history and all tags back to the previous release. Most CI systems shallow-clone by default — set
fetch-depth: 0(GitHub/Gitea Actions) orGIT_DEPTH: 0(GitLab CI), or rungit fetch --unshallow. See CI/CD Integration for per-platform setup.
Configuration Overrides
Override config from the command line without editing
releasaurus.toml — handy for testing, one-off releases, and per-branch
CI settings.
| Flag | Effect |
|---|---|
--base-branch <branch> | Override the base branch |
--tag-prefix <prefix> | Global tag prefix for all packages |
--version-type <value> | Global version type for all packages |
--prerelease-suffix <suffix> | Global prerelease suffix (empty "" disables) |
--prerelease-strategy <versioned|static> | Global prerelease strategy |
--skip-sha <sha> | Skip a commit by SHA prefix (repeatable) |
--reword <sha>=<message> | Rewrite a commit message (repeatable) |
--set-package <pkg>.<property>=<value> | Per-package override (repeatable) |
--set-package takes precedence over all other overrides and config.
Supported properties: tag_prefix, versioning.version_type,
versioning.prerelease.suffix, versioning.prerelease.strategy. Setting an
unsupported property prints an error listing valid values.
Precedence (highest to lowest): --set-package → global CLI
overrides → [[package]] config → [defaults] config → built-in
defaults.
# Override base branch and global prerelease suffix
releasaurus release-pr --base-branch develop --prerelease-suffix beta \
--repo "https://github.com/owner/repo"
# Per-package override (e.g. only the frontend gets a beta suffix)
releasaurus release-pr \
--set-package frontend.versioning.prerelease.suffix=beta \
--repo "https://github.com/owner/repo"
# Date-based versioning for just the nightly package
releasaurus release-pr \
--set-package nightly.versioning.version_type=year.month.day \
--repo "https://github.com/owner/repo"
# Skip one commit and reword another
releasaurus release-pr --skip-sha abc123d \
--reword "def456e=feat: improved authentication" \
--repo "https://github.com/owner/repo"
See Configuration for what these settings mean.
Known Limitations
Gitea < v1.26 / Forgejo < v16: Force Push Not Supported
Releasaurus force-pushes the release branch on each run so repeated
runs update the existing release PR in place rather than piling up new
ones. On Gitea and Forgejo this relies on a force-overwrite option on
the /contents API route that was only added in Gitea v1.26 and
Forgejo v16.
Fix (recommended): upgrade your Gitea/Forgejo instance to v1.26 /
v16 or later. Alternative: use
hybrid mode (--local-path),
which pushes the release branch over git and avoids the API limitation
entirely.
Gitea and Forgejo Actions: Injected Token Shadows Your PAT
Gitea and Forgejo Actions runners (including Codeberg) automatically
inject an ephemeral, limited per-job token into the job environment
under the names GITHUB_TOKEN, GITEA_TOKEN, and FORGEJO_TOKEN. If
you supply your own token through one of those environment variables
— for example env: FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }} — the
runner’s injected value can take precedence inside the action, and
Releasaurus authenticates with the limited token instead of your PAT.
That injected token can usually read the repository, so startup
succeeds, but it cannot create a pull request on a private repo.
Gitea/Forgejo return 404 Not Found for the unauthorized write
against the .../pulls endpoint, which is easy to misread as a
missing repository. Public repos hide the problem because reads are
anonymous.
Fix (recommended): supply your token through the
RELEASAURUS_-prefixed environment variable — e.g.
RELEASAURUS_FORGEJO_TOKEN for Forgejo, RELEASAURUS_GITEA_TOKEN for
Gitea. Releasaurus reads it before the bare *_TOKEN name, and the
runner does not inject the prefixed name, so it can’t be shadowed:
env:
RELEASAURUS_FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
Alternative: pass the token on the command line with --token,
which takes precedence over every environment variable. Note that
command arguments are more likely to appear in CI logs than env vars:
command_args: >-
--forge forgejo
--repo ${{ github.server_url }}/${{ github.repository }}
--token ${{ secrets.RELEASE_TOKEN }}
Azure DevOps: Release Branch Requires “Allow rewriting history”
When updating a release PR, Releasaurus resets the release branch to the
tip of the base branch and replays the changelog commit. If the existing
release branch has diverged, this is a non-fast-forward update that Azure
DevOps rejects unless Allow rewriting history is granted on the
release branch (typically releasaurus-release-*).
Grant it under Project Settings → Repositories → {repo} → Security →
Branches → {release branch}, setting Allow rewriting history to
Allow for the identity holding the PAT. Azure DevOps release also only
pushes the git tag — there is no native release object, so no release
notes page is published.
Azure DevOps: Merge Commits Are Not Detected
Azure DevOps omits the parents field from its commit list API — only
the single-commit endpoint returns it. Releasaurus reads history in bulk, so
recovering parents would cost one extra API request per commit. Every Azure
commit is therefore reported as a non-merge commit.
Two settings quietly have no effect as a result:
skip_merge_commits(defaulttrue) filters nothing out.- The default
bodytemplate’sfilter(attribute="merge_commit", value=false)clause excludes nothing.
This only matters for completion strategies that create a merge commit. If
you complete PRs with Merge (no fast forward) or Semi-linear merge,
Azure adds a commit titled Merged PR <n>: <PR title> on top of the source
branch’s own commits. That commit becomes its own changelog entry, so the
PR’s change is represented twice — once by the merge commit, once by the
commits it brought in.
Fix (recommended): complete PRs with Squash commit. Azure then adds a single commit per PR, so there is nothing to filter and no duplication. Rebase and fast-forward is likewise unaffected, since it creates no merge commit.
Alternative: drop individual merge commits with skip_shas (see
Skipping or Rewording Commits). That setting applies to the
next release only, so it has to be repeated each cycle.
Getting Help
releasaurus --help # general help
releasaurus <cmd> --help # command-specific help
releasaurus --version # version information
Configuration
Releasaurus works with zero configuration for changelog generation and
tagging. Add an optional releasaurus.toml at your repository root when
you need more. This page covers the common cases; for the exhaustive
option list see the Configuration Reference.
Do You Need a Config File?
You don’t need one if you only want changelog generation and tagging
with the default format and the default v tag prefix.
You do need one to:
- update version files (set a
release_type) - manage multiple packages (monorepo)
- create prereleases (alpha/beta/rc/snapshot)
- customize the changelog or use custom tag prefixes
Place the file at the repository root:
my-project/
├── releasaurus.toml
├── src/
└── README.md
Config is organized under three top-level tables:
[repository]— repo-wide settings (base branch, search depths, combined vs. separate PRs, and commit modifiers —skip_shas/reword).[defaults]— release defaults for every package, split into[defaults.versioning](what affects the computed version, including commit filtering and grouping) and[defaults.changelog](how the changelog is rendered). Most keys can be overridden per package.[[package]]— one entry per independently-versioned package.
See the Configuration Reference for every key.
Single Package
The most common setup — bump versions in one package’s manifests:
[[package]]
path = "."
release_type = "node" # or rust, python, java, php, ruby, go, generic
release_type selects which manifest and lock files are updated. See
Supported Languages
for the file list per language.
Monorepos
Define one [[package]] per independently-versioned package. Each gets
its own version, tag prefix, and manifest updates.
[[package]]
path = "./frontend"
release_type = "node"
tag_prefix = "frontend-v"
[[package]]
path = "./backend"
release_type = "rust"
tag_prefix = "backend-v"
Tag prefix defaults to v for a root package (path = ".") and
<name>-v for nested packages.
Combined vs. Separate PRs
By default all packages with changes are released in a single PR. Set
separate_pull_requests = true under [repository] to give each package
its own PR (branches like releasaurus-release-main-frontend):
[repository]
separate_pull_requests = true
[[package]]
path = "./frontend"
release_type = "node"
tag_prefix = "frontend-v"
[[package]]
path = "./backend"
release_type = "rust"
tag_prefix = "backend-v"
- Combined (default) — best for tightly-coupled packages that release together and a single, atomic review.
- Separate — best for large monorepos and independently-versioned packages with different release cadences or owners.
In either mode, target one package with --package <name> on release-pr
and release.
Commit Message & PR Title Templates
The release commit message and the PR title are Tera templates. By default they match:
chore(main): release my-package v1.2.3 # one package, or separate PRs
chore(main): release my-repo # combined PR, multiple packages
A PR that covers a single package can name that package and its version; one that covers several can’t. So there are two sets of templates, and which set applies is fixed by your config rather than by what changed:
| Your config | Commit message | PR title |
|---|---|---|
separate_pull_requests = true | commit_message_template | pr_title_template |
One [[package]] | commit_message_template | pr_title_template |
Multiple [[package]] and separate_pull_requests = false | monorepo_commit_message_template | monorepo_pr_title_template |
Each package gets its own PR under separate_pull_requests = true, and a
single-package repo only ever has one, so both cases can use the
per-package pair. A combined PR across multiple packages uses the
monorepo_* pair.
Only top-level [[package]] entries count. A single package with
sub-packages is still one package, since
sub-packages share their parent’s PR and tag.
Because the choice comes from your config, a multi-package repo with
separate_pull_requests = falseuses themonorepo_*pair on every run — including runs where only one package changed, and including--package <name>. That keeps the format predictable, at the cost of those PRs not naming the package. Useseparate_pull_requests = trueif you want every PR titled after its package.
Set them under [defaults], and override the per-package pair on
individual packages:
[defaults]
commit_message_template = "chore({{ branch }}): release {{ package_name }} {{ semver }}"
pr_title_template = "🚀 Release {{ package_name }} {{ tag }}"
monorepo_commit_message_template = "chore({{ branch }}): release {{ repo_name }}"
monorepo_pr_title_template = "🚀 Release {{ repo_name }}"
[[package]]
name = "frontend"
path = "./apps/web"
release_type = "node"
# This package alone gets a ticket-scoped commit and a plainer title.
commit_message_template = "chore(web): release {{ tag }} [skip ci]"
pr_title_template = "Web release {{ tag }}"
Variables
Available in all four templates:
| Variable | Description | Example |
|---|---|---|
branch | The base branch being released to | main |
repo_name | Repository name | my-repo |
Available in commit_message_template and pr_title_template only,
because a PR spanning several packages has no single one of these:
| Variable | Description | Example |
|---|---|---|
package_name | Name of the package being released | frontend |
tag | Full tag, including the tag prefix | web-v1.2.3 |
semver | Version alone, without the prefix | 1.2.3 |
Referencing a variable that isn’t in scope is an error, and it’s caught
when the config loads rather than partway through a release — so
monorepo_pr_title_template = "{{ package_name }}" fails before anything
is pushed.
Filters work as they do in the changelog body template, so you can
reshape a value rather than needing a variable for every form of it:
[defaults]
pr_title_template = "Release {{ package_name | upper }} {{ tag }}"
Tracking Shared Code
Use additional_paths so a package also releases when shared directories
change:
[[package]]
path = "./apps/web"
release_type = "node"
tag_prefix = "web-v"
additional_paths = ["shared/types", "shared/utils"]
Workspaces in a Subdirectory
When a workspace isn’t at the repo root, set workspace_root so lock
files resolve correctly:
[[package]]
name = "api-server"
workspace_root = "backend"
path = "services/api"
release_type = "rust"
tag_prefix = "api-v"
This updates backend/services/api/Cargo.toml and the workspace
backend/Cargo.lock.
Naming & Path Rules
- Names must be unique across all packages. If omitted, the name is
derived from the last path component. Match the manifest’s
namefield where one exists (package.json,Cargo.toml, etc.). - The full path (
workspace_root+path) must be unique. Two packages may share apathonly if theirworkspace_rootdiffers. - Sub-packages count for both rules. A sub-package name or full path
may not repeat one used by any other package or sub-package anywhere in
the config. Names key dependency entries in lock files, and two entries
on one path would both write the same
CHANGELOG.md.
Grouped Releases (Sub-Packages)
Use sub_packages to release several packages under one shared tag,
changelog, and release, while each sub-package still gets its own manifest
updates based on its release_type. A sub-package does not produce its
own tag.
[[package]]
name = "platform"
workspace_root = "."
path = "."
tag_prefix = "v"
sub_packages = [
{ name = "web", path = "packages/web", release_type = "node" },
{ name = "cli", path = "packages/cli", release_type = "rust" },
]
Result: one tag (v1.0.0), one changelog covering everything, one
release — with package.json (web) and Cargo.toml (cli) updated
independently. Reach for this when a group of packages must always ship
together with the same version.
Sub-packages vs. separate packages: separate
[[package]]entries are versioned and tagged independently;sub_packagesshare the parent’s single tag and changelog.
Version Types
By default Releasaurus produces semantic versions (major.minor.patch). Set
version_type to change the version format — globally, or per package
to override the global value.
version_type | Example output |
|---|---|
major.minor.patch (default) | 1.4.0 |
major.minor.patch+timestamp.sha | 1.4.0+1700000000.abc1234 |
year.month.day | 2026.6.14 |
year.month.day+hour.minute.second | 2026.6.14+15.30.45 |
year.month.day+hour.minute.second.micro | 2026.6.14+15.30.45.123456 |
[defaults.versioning]
version_type = "major.minor.patch"
[[package]]
path = "./nightly"
release_type = "node"
versioning = { version_type = "year.month.day+hour.minute.second" }
major.minor.patch— standard semver driven by conventional commits.major.minor.patch+timestamp.sha— semver with build metadata of the form{commit-timestamp}.{short-sha}, for sortable, traceable builds.year.month.dayand the+hour.minute.second[.micro]variants — calendar-based versions derived from the current UTC time; commits and the previous tag are ignored. Plainyear.month.dayallows one release per day by design; a same-day re-run reports nothing to release. Use a time-based variant when you need multiple releases per day.
major.minor.patch and major.minor.patch+timestamp.sha both honor
[prerelease] (below) and the semver increment controls
(breaking_always_increment_major, features_always_increment_minor,
custom_major_increment_regex, custom_minor_increment_regex). For
date-based types those settings are ignored — if you set any of them
explicitly alongside a date-based version_type, Releasaurus logs a warning
naming the package and setting so the no-op config does not pass silently.
Prereleases
Publish alpha/beta/rc/snapshot versions before a stable release. Configure
for every package with [defaults.versioning.prerelease], or per-package
with a versioning.prerelease table. Prereleases apply to the
major.minor.patch and major.minor.patch+timestamp.sha
version types only.
[defaults.versioning.prerelease]
suffix = "alpha"
strategy = "versioned" # or "static"
[[package]]
path = "."
release_type = "node"
Strategies
versioned(default) — appends an incrementing counter:1.1.0-alpha.1,1.1.0-alpha.2, …static— appends the suffix as-is, with no counter:1.0.1-SNAPSHOT(common in Java).
Lifecycle
Change behavior by editing the config and opening a new release PR:
| From | Config change | Result |
|---|---|---|
v1.0.0 | suffix = "alpha" (+ feature commit) | v1.1.0-alpha.1 |
v1.1.0-alpha.1 | unchanged (+ fix commit) | v1.1.0-alpha.2 |
v1.0.0-alpha.3 | suffix = "beta" (+ feature) | v1.1.0-beta.1 |
v1.0.0-alpha.5 | remove the prerelease table (or suffix = "") | v1.0.0 |
Switching the suffix recalculates the base version and resets the counter. Removing the prerelease config graduates to a stable release.
Per-Package Overrides
[defaults.versioning.prerelease]
suffix = "beta"
strategy = "versioned"
[[package]]
path = "./stable"
release_type = "rust"
# inherits the default beta prerelease
[[package]]
path = "./experimental"
release_type = "rust"
versioning = { prerelease = { suffix = "alpha", strategy = "versioned" } }
A package’s prerelease table replaces the [defaults] one rather than
merging with it, so experimental restates strategy even though it
matches the default value. Omit it and that package falls back to the
versioned built-in — not to your [defaults] setting.
Aggregating Prerelease Notes
When graduating to stable, include the changelog entries from every prior prerelease:
[defaults.changelog]
aggregate_prereleases = true
You can also override prerelease settings per run without editing the
config — see Configuration Overrides
(--prerelease-suffix, --prerelease-strategy, --set-package).
Per-Package Changelog
Changelog settings normally live under [defaults.changelog] (rendering) and
[defaults.versioning] (commit filtering and grouping) and apply to every
package. A single package can override either on its own changelog or
versioning key. Since packages are an array of tables ([[package]]),
set them as inline tables so they stay scoped to that entry:
[defaults.changelog]
include_author = true
include_pr_link = true
[defaults.versioning.named_parsers]
ci.skip = true
[[package]]
name = "frontend"
path = "./apps/web"
release_type = "node"
changelog = { include_author = false }
versioning = { named_parsers = { ci = { skip = false } } }
Both merge field-by-field with their
[defaults]counterpart. Any field you set on the package wins; any field you omit is inherited from[defaults](and then the built-in defaults).custom_parserentries from[defaults]and the package are combined, with the package’s checked first, andnamed_parsersoverrides apply per group and per field. See Changelog Customization.
Skipping or Rewording Commits
skip_shas and reword live under [repository]. They operate on the
repository’s shared commit history and affect version calculation as well
as the changelog, so they are repo-wide and cannot be overridden per
package.
skip_shas removes specific commits by SHA prefix (use 7+ characters) —
handy for commits that shouldn’t affect versioning or appear in the
changelog:
[repository]
skip_shas = ["abc123d", "def456e"]
reword rewrites a commit’s message. The new message affects both the
changelog text and the version bump — changing fix: to feat:, for
example, bumps minor instead of patch:
[[repository.reword]]
sha = "abc123d"
message = "feat: added user authentication"
Both have CLI equivalents for one-off runs: --skip-sha <sha> and
--reword <sha>=<message>; a --reword for a SHA already in config wins.
See the Configuration Reference
for the terse lookup form.
skip_shas and reword only affect each package’s next release.
Releasaurus processes a package’s commits from its most recent tag forward,
so once a release is tagged those commits are never reprocessed. (In a
monorepo a single entry can therefore apply to more than one package — but
only that package’s next release in each case.)
If you just want to change how a single release’s notes read — without affecting the version bump — edit them directly in the release PR instead; see Editing Release Notes.
Testing Your Configuration
Validate any config change locally before pushing — no token, no remote changes:
releasaurus release-pr --forge local --repo "."
Check that packages are detected, tag prefixes match, and the combined/separate PR strategy behaves as expected. See Local Repository Mode.
Next Steps
- Changelog Customization — filter commits and customize the template.
- Configuration Reference — every option, default, and the full example config.
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:
| Field | Type | Effect |
|---|---|---|
pattern | regex | Matched against the raw commit message to decide if the parser applies |
title | string | The changelog heading commits in this group appear under |
order | int | Position of the heading in the changelog, 0-99, lowest first |
skip | bool | When 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) | Pattern | Default title | Order |
|---|---|---|---|
breaking | (none) | ❌ Breaking | 0 |
feature | ^feat | 🚀 Features | 1 |
fix | ^fix | 🐛 Bug Fixes | 2 |
revert | ^revert | ◀️ Revert | 3 |
refactor | ^refactor | 🚜 Refactor | 4 |
performance | ^perf | ⚡ Performance | 5 |
documentation | ^doc | 📚 Documentation | 6 |
style | ^style | 🎨 Styling | 7 |
test | ^test | 🧪 Testing | 8 |
chore | ^chore | 🧹 Chore | 9 |
ci | ^ci | ⏩ CI/CD | 10 |
miscellaneous | .* | ⚙️ Miscellaneous Tasks | 11 |
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]:
| Option | Default | Effect |
|---|---|---|
skip_merge_commits | true | Excludes merge commits |
In [defaults.changelog]:
| Option | Default | Effect |
|---|---|---|
include_author | false | Adds the commit author’s name to each entry |
include_pr_link | false | Adds a link to the pull request that introduced each commit |
aggregate_prereleases | false | When 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.
Pull request links
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
| Variable | Description |
|---|---|
version | Semantic version (e.g. 1.2.3) |
tag_name | Full tag including prefix/suffix |
link | URL to the release |
tag_compare_link | Diff vs. previous tag (empty for first release) |
sha_compare_link | Diff vs. previous tag, by commit SHA (empty for first release) |
sha | Release commit SHA |
short_sha | Abbreviated release commit SHA |
timestamp | Unix timestamp |
include_author | Whether author display is enabled |
include_pr_link | Whether PR-link display is enabled |
Commit (each item in commits)
| Variable | Description |
|---|---|
id / short_id | Full / abbreviated SHA |
group | Category (Features, Bug Fixes, …) |
scope | Optional conventional-commit scope |
title | Message without type/scope |
body | Optional extended description |
link | URL to the commit |
pr | Introducing PR, or unset (see below) |
breaking / breaking_description | Breaking-change flag and details |
merge_commit | Whether it’s a merge commit |
timestamp | Commit timestamp |
author_name / author_email | Commit author |
raw_title / raw_message | Original 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:
| Variable | Description |
|---|---|
pr.id | User-visible PR number (e.g. 42) |
pr.link | URL 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.
Editing Release Notes
Releasaurus lets you customize the release notes for a specific release
directly in the pull request body — without touching releasaurus.toml
or the CHANGELOG.md.
How It Works
When release-pr creates or updates a release PR, it renders the PR
body with a structured layout per package:
<details open>
<summary>v1.2.3</summary>
<div id="my-package-header"></div>
<div id="my-package" data-tag="v1.2.3">
<!--{"metadata":{"sha_compare_link":"...","tag_compare_link":"..."}}-->
## [v1.2.3](...) - 2026-04-10 ### Features - feat: some new feature
(abc1234)
</div>
<div id="my-package-footer"></div>
</details>
At release time, releasaurus release reads the notes directly from
the PR body rather than regenerating them from commit history. This
means any edits you make before merging are reflected in the published
forge release.
Note: Edits to the PR body affect only the forge release notes.
CHANGELOG.mdis generated from commit history and is not affected.
Editing the Release Notes
Open the PR body and edit the text inside the notes <div>. The
metadata comment (<!--{...}-->) must be left intact — it carries
the tag and link information needed at publish time. For example, adding
a summary paragraph above the generated entries:
<div id="my-package" data-tag="v1.2.3">
<!--{"metadata":{"sha_compare_link":"...","tag_compare_link":"..."}}-->
## [v1.2.3](...) - 2026-04-10 This release improves startup performance and
fixes a crash on empty input. See the [migration guide](https://example.com)
for details. ### Features - feat: some new feature (abc1234)
</div>
Persistent Header and Footer
For content that should survive re-runs of release-pr (for example,
if you run the command again after new commits land), place it in the
dedicated header and footer <div>s.
<div id="my-package-header">
## Highlights This is a major stability release. All users on v1.x are
encouraged to upgrade.
</div>
<div id="my-package-footer">
Full migration guide: https://example.com/migrate
</div>
When release-pr regenerates the PR body, it reads back the content
of these <div>s and re-embeds it. The header is prepended and the
footer is appended to the final release notes at publish time.
Tip: Leave the header and footer
<div>s empty (the default) if you have nothing to add. They will not appear in the published release notes.
Monorepo: Multiple Packages
In a monorepo, each package gets its own set of sections. The id
attributes are derived from the package name with any characters
outside [a-zA-Z0-9-_] replaced by -.
For example, a package named @scope/my-pkg gets:
<div id="-scope-my-pkg">— notes<div id="-scope-my-pkg-header">— header<div id="-scope-my-pkg-footer">— footer
Edit each package’s sections independently.
Backward Compatibility
PRs created by an older version of Releasaurus use a different body
format. The release command detects the format automatically and
falls back to reading release notes from the hidden metadata for those
PRs — no manual migration required.
Limits
- The metadata comment (
<!--{...}-->) inside the notes<div>must not be removed or modified. - Do not write a literal
</div>anywhere inside the notes, header, or footer sections. Releasaurus parses the PR body as HTML and a bare</div>will close the section early, truncating any content that follows. - Header and footer content is preserved verbatim. Markdown is supported by all major forge platforms.
- Re-running
release-prregenerates the notes from commit history and overwrites any direct edits to the notes<div>. Use the header/footer sections for content you want to survive re-runs.
CI/CD Integration
Releasaurus provides official integrations for GitHub Actions, Gitea Actions, and Forgejo Actions. For GitLab CI and Azure Pipelines, use the Docker image directly.
Note on fetch depth: When using
--local-path(hybrid mode), Releasaurus reads commit history and tags directly from the local clone. Most CI systems shallow-clone by default, which will cause missing commits or tags. Configure your CI checkout for full depth when using--local-path. Platform-specific instructions are in each section below.
Required token scopes
| Forge | Scopes / permissions |
|---|---|
| GitHub (classic) | repo |
| GitHub (fine-grained) | Contents, Issues, Pull requests — all read & write. Add Actions/Workflows read & write only if using the Action to modify workflow files. |
| GitLab | api, write_repository |
| Gitea | repository (read/write), issue (read/write), misc (read/write) management |
| Forgejo | repository (read/write), issue (read/write), misc (read/write) management |
| Azure DevOps | Code: Read & Write, Pull Request Threads: Read & Write |
GitHub Actions, Gitea Actions & Forgejo Actions
A single action works for GitHub Actions, Gitea Actions, and Forgejo
Actions workflows. See the
action README
for inputs, usage examples, and fetch depth configuration for
--local-path.
Gitea / Forgejo runners (including Codeberg): supply your token through
env: RELEASAURUS_FORGEJO_TOKEN(orRELEASAURUS_GITEA_TOKEN) rather than the bareFORGEJO_TOKEN/GITEA_TOKEN. These runners auto-inject their own limited per-job token under the bare names, which shadows your PAT and makes private-repo PR creation fail with an opaque404 Not Found. Releasaurus reads theRELEASAURUS_-prefixed name first, and the runner doesn’t inject it, so it can’t be shadowed. Passing--tokenworks too (it beats every env var). See Gitea and Forgejo Actions: Injected Token Shadows Your PAT.
GitLab CI
Use the Releasaurus Docker image directly in your .gitlab-ci.yml.
You may provide an authentication token either by specifying a CI/CD
variable named GITLAB_TOKEN, or by directly passing the --token
option with a reference to your defined variable, e.g.
--token $RELEASE_TOKEN.
Required Scopes:
api(full API access)write_repository(repository write access)
Run both commands in a single job so they execute sequentially:
release first (it tags any merged release PR), then release-pr (it
opens or updates the next one). This matches the order used by the
GitHub, Gitea, and Forgejo action. Defining them as two separate jobs
with the same rules: lets GitLab schedule them in the same stage
concurrently, which races: release-pr may observe a merged but
not-yet-tagged release PR and abort with
must finish previous release first.
Example
releasaurus:
image:
name: rgonnella/releasaurus:vX.X.X
entrypoint: [""]
script:
# Assumes a CI/CD variable named $GITLAB_TOKEN for authentication.
# Alternatively, pass `--token $RELEASE_TOKEN` to each command.
#
# Run `release` BEFORE `release-pr`: `release` tags any merged
# release PR, then `release-pr` opens/updates the next one. The
# reverse order (or two parallel jobs) lets `release-pr` see a
# merged-but-untagged release PR and abort with
# "must finish previous release first".
- releasaurus release --forge gitlab --repo $CI_PROJECT_URL
- releasaurus release-pr --forge gitlab --repo $CI_PROJECT_URL
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Using --local-path
When using --local-path, Releasaurus reads commit history and tags
from the local clone and requires a full checkout. Configure
GIT_DEPTH: 0 to ensure a full clone when the runner starts fresh:
variables:
GIT_DEPTH: 0
If the runner reuses an existing workspace from a prior job
(i.e. GIT_STRATEGY: fetch), GIT_DEPTH has no effect on the
already-shallow repository. Unshallow explicitly in before_script:
before_script:
- git fetch --unshallow || true # no-op if already full-depth
Using both together is safe and covers all runner states.
Azure Pipelines (EXPERIMENTAL)
Azure DevOps support is experimental. No first-party Azure Pipelines
task is provided — use the Releasaurus Docker image directly in your
pipeline. Note that the release command only pushes the git tag
(the changelog commit lands when the release PR is merged); Azure
DevOps Git has no native release object, so no release notes page is
created.
Provide a PAT via the AZURE_DEVOPS_TOKEN pipeline secret variable.
The PAT needs Code: Read & Write and Pull Request Threads: Read & Write scopes.
The release branch (typically releasaurus-release-*) must have
Allow rewriting history
enabled for the build service identity — releasaurus performs
a non-fast-forward reset to the base branch when updating an existing
release PR. See the Azure DevOps known limitation for
the exact setting.
Run release first (it tags any merged release PR), then release-pr (it
opens or updates the next one). This matches the order used by the
GitHub, Gitea, and Forgejo action. Running release-pr first may observe a
merged but not-yet-tagged release PR and abort with
must finish previous release first.
trigger:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
container: rgonnella/releasaurus:vX.X.X
steps:
- checkout: self
fetchDepth: 0 # required if you also pass --local-path
- script: |
releasaurus release \
--forge azure-devops \
--repo "$(Build.Repository.Uri)"
env:
AZURE_DEVOPS_TOKEN: $(AZURE_DEVOPS_TOKEN)
- script: |
releasaurus release-pr \
--forge azure-devops \
--repo "$(Build.Repository.Uri)"
env:
AZURE_DEVOPS_TOKEN: $(AZURE_DEVOPS_TOKEN)
Troubleshooting
Common issues and how to diagnose them. If your problem isn’t covered here, check the GitHub issues.
Inspect Before You Run
get next-release shows exactly what Releasaurus would do — version,
included commits, and release notes — without making any changes. It’s
the fastest way to debug version detection, tag matching, and config:
releasaurus get next-release --repo "https://github.com/owner/repo"
# Or fully offline against a local checkout
releasaurus get next-release --forge local --repo "."
For deeper diagnostics, add --debug (or --dry-run, which also enables
debug). See Testing Modes.
Releasaurus doesn’t find existing tags
Usually a tag prefix mismatch — the package’s tag_prefix must match
your existing tags:
[[package]]
path = "."
tag_prefix = "v" # for v1.0.0; use "api-v" for api-v1.0.0; "" for 1.0.0
If no matching tag exists, Releasaurus treats it as a first release and
analyzes up to first_release_search_depth commits (default 400). Raise
it for a fuller first changelog, or lower it for speed. This affects
only the first release — once a matching tag exists, all commits back
to that tag are analyzed. To control how many tags are fetched while
searching, use tag_search_depth.
“Authentication failed” / 401 Unauthorized
- Confirm the token is set for the forge you’re targeting
(
echo $GITHUB_TOKEN), or pass--tokenexplicitly. - Check the scopes — see required token scopes.
- Check expiration — regenerate if expired.
“Repository not found” with a valid repo
The token lacks access, or the URL is wrong.
-
Verify the URL format, e.g.
--repo "https://github.com/owner/repository". -
Confirm the token’s account has access to the repository.
-
Reproduce offline to rule out config issues:
git clone https://github.com/owner/repo && cd repo releasaurus get next-release --forge local --repo "."
“must finish previous release first”
release-pr found a merged-but-not-yet-tagged release PR. Run release
first to tag it, then release-pr. In CI, always order the two commands
release → release-pr in a single job — see
CI/CD Integration.
Getting Help
When opening an issue, include: debug output (with secrets removed), your
repository structure, the exact command, expected vs. actual behavior,
your OS, releasaurus --version, and your forge platform and hosting
type.
Configuration Reference
Complete reference for releasaurus.toml, environment variables, and
supported languages. For guidance and examples, see
Configuration.
Configuration is grouped under three top-level tables: [repository]
(repo-wide settings), [defaults] (release defaults for every package,
with [defaults.versioning] and [defaults.changelog] subtables), and
one or more [[package]] entries. All keys are optional.
Unknown keys are rejected, so a misspelled or misplaced option fails at config load rather than being silently ignored.
[repository]
Repository-wide settings:
| Key | Type | Default | Description |
|---|---|---|---|
base_branch | string | repo default | Branch targeted for PRs, tagging, and releases. Override: --base-branch. |
first_release_search_depth | integer | 400 | Commits to analyze for the first release (when no matching tag exists). |
tag_search_depth | integer | 100 | Max tags fetched when searching for a previous release. 0 = all tags. |
separate_pull_requests | bool | false | One PR per package (true) vs. a single combined PR (false). |
skip_shas | string[] | none | Skip commits by SHA prefix (7+ chars); affects changelog and version bump. Repo-wide. CLI: --skip-sha. |
reword | object[] | none | Rewrite commit messages (affects changelog and version bump). Repo-wide. CLI: --reword. |
skip_shas and reword operate on the repository’s shared commit history,
so they are repo-wide and cannot be overridden per package. A --reword for
a SHA already listed in config takes precedence over the config entry.
[repository]
base_branch = "main"
tag_search_depth = 100
separate_pull_requests = false
skip_shas = ["abc123d", "def456e"]
[[repository.reword]]
sha = "abc123d"
message = "fix: corrected description"
[defaults]
Keys set directly on [defaults], rather than in one of its subtables.
All four are Tera templates for the release commit message and PR title;
see Commit Message & PR Title Templates for which one
applies when.
| Key | Type | Default | Applies when |
|---|---|---|---|
commit_message_template | string | chore({{ branch }}): release {{ package_name }} {{ tag }} | separate_pull_requests = true, or one [[package]]. Overridable per package. |
pr_title_template | string | chore({{ branch }}): release {{ package_name }} {{ tag }} | separate_pull_requests = true, or one [[package]]. Overridable per package. |
monorepo_commit_message_template | string | chore({{ branch }}): release {{ repo_name }} | Multiple [[package]] with separate_pull_requests = false. No per-package override. |
monorepo_pr_title_template | string | chore({{ branch }}): release {{ repo_name }} | Multiple [[package]] with separate_pull_requests = false. No per-package override. |
The two contexts differ: branch and repo_name are always available,
while package_name, tag, and semver exist only in the per-package
templates. Referencing a variable that isn’t in scope is rejected at
config load.
[defaults]
pr_title_template = "🚀 Release {{ package_name }} {{ tag }}"
monorepo_pr_title_template = "🚀 Release {{ repo_name }} ({{ branch }})"
[defaults.versioning]
Everything that affects the computed version, plus the commit filtering
and grouping rules (which affect the version as well as the changelog).
Each package may override these individually via its own versioning
table (see [[package]]).
| Key | Type | Default | Description |
|---|---|---|---|
version_type | string | major.minor.patch | Version format to produce. See Version Types for the five accepted values. |
auto_start_next | bool | false | Bump patch versions automatically after a release (see start-next). |
breaking_always_increment_major | bool | true | Breaking changes (feat!:, BREAKING CHANGE:) bump major. |
features_always_increment_minor | bool | true | feat: commits bump minor. |
custom_major_increment_regex | string | none | Additional regex marking a commit breaking: bumps major and groups it under ❌ Breaking. |
custom_minor_increment_regex | string | none | Additional regex that triggers a minor bump. No grouping effect. |
skip_merge_commits | bool | true | Exclude merge commits. |
named_parsers | table | built-in groups | Override built-in commit groups (pattern/title/order/skip per group). See Changelog Customization. |
custom_parser | array | none | Define additional commit groups, checked before the defaults. Note the singular key. pattern, title and order are all required. |
prerelease | table | none (stable) | Prerelease settings; see [defaults.versioning.prerelease]. |
Custom increment regexes
custom_major_increment_regex and custom_minor_increment_regex are
additive — breaking changes always bump major and feat: always
bumps minor regardless. The pattern is matched against the full commit
message. In TOML double-quoted strings, escape backslashes (\\):
[defaults.versioning]
custom_major_increment_regex = "\\[MAJOR\\]" # matches "[MAJOR]"
custom_minor_increment_regex = "FEATURE" # no escaping needed
The two are not symmetric. custom_major_increment_regex marks a
matching commit breaking, which groups it under ❌ Breaking and
marks it [**breaking**] in the default body template as well as bumping
major. custom_minor_increment_regex only affects the version.
[defaults.versioning.named_parsers] breaking.pattern is the same
mechanism as custom_major_increment_regex under a different name; set
either, or both, in which case a commit matching either is breaking. See
Commit Groups & Filtering.
An invalid pattern is rejected when the config loads, not part-way through a release.
[defaults.versioning.prerelease]
Default prerelease config; can be overridden per package via that
package’s versioning.prerelease. See
Prereleases. Applies only when
version_type is major.minor.patch or
major.minor.patch+timestamp.sha.
| Key | Type | Default | Description |
|---|---|---|---|
suffix | string | none (stable) | Identifier such as alpha, beta, rc, SNAPSHOT. Override: --prerelease-suffix. |
strategy | string | versioned | versioned (adds .1, .2, …) or static (suffix as-is). Override: --prerelease-strategy. |
[defaults.versioning.prerelease]
suffix = "beta"
strategy = "versioned"
[defaults.versioning.named_parsers]
[defaults.versioning.named_parsers]
ci.skip = true
chore.skip = true
feature.title = "✨ New Stuff"
feature.order = 1
order (0-99, lowest first) sets the heading’s position in the
changelog and is independent of the title text. See
Ordering groups.
[[defaults.versioning.custom_parser]]
[[defaults.versioning.custom_parser]]
pattern = "^deps"
title = "📦 Dependencies"
order = 3
skip = false
pattern, title and order are all required — a custom group has no
built-in position to fall back on.
[defaults.changelog]
Controls how the included commits are rendered. See Changelog Customization for the template and variables.
| Key | Type | Default | Description |
|---|---|---|---|
include_author | bool | false | Include commit author names. |
include_pr_link | bool | false | Link the pull request that introduced each commit. Costs extra API requests — see below. |
aggregate_prereleases | bool | false | On graduation, fold prior prerelease notes into the stable release. |
body | string | standard template | Tera template for the changelog body. |
[defaults.changelog]
include_author = true
include_pr_link = true
include_pr_link is paid for per package: one that leaves it off costs no
requests even when a sibling turns it on. Note that a custom body must
carry the commit.pr clause itself; see
Pull request links.
[[package]]
One entry per package; repeatable.
| Key | Type | Default | Description |
|---|---|---|---|
path | string | . | Package directory, relative to workspace_root. |
workspace_root | string | . | Workspace root, relative to repo root. |
name | string | derived from path | Explicit package name; must be unique. |
release_type | string | none | Language for version updates (see Supported Languages). Omit for changelog/tagging only. |
tag_prefix | string | v (root) / <name>-v (nested) | Git tag prefix. Override: --tag-prefix or --set-package <name>.tag_prefix=. |
sub_packages | object[] | none | Group packages under one shared tag/changelog (see Grouped Releases). |
additional_paths | string[] | none | Extra directories whose changes trigger a release for this package. |
additional_manifest_files | string[] / object[] | none | Extra files to version-bump (see below). |
versioning | table | inherits [defaults.versioning] | Per-package versioning override (see Per-package overrides). |
changelog | table | inherits [defaults.changelog] | Per-package changelog override (see Per-package overrides). |
commit_message_template | string | inherits [defaults] | Release commit message for this package’s PR (see [defaults]). |
pr_title_template | string | inherits [defaults] | Release PR title for this package’s PR (see [defaults]). |
sub_packages entries take name, path, and release_type.
Versioning options are not direct package keys — they live under the
package’s versioning table, mirroring [defaults.versioning]. For example,
a per-package prerelease is
versioning = { prerelease = { suffix = "alpha" } }, and the matching CLI
override is --set-package <name>.versioning.prerelease.suffix=.
additional_manifest_files
Extra files whose version strings should be kept in sync — custom
VERSION files, docs, config, etc. Accepts plain string paths (using a
default regex) or objects with a custom version_regex. All paths are
relative to the package path.
[[package]]
path = "."
release_type = "rust"
additional_manifest_files = [
"VERSION", # default regex
"README.md", # default regex
{ path = "helm/Chart.yaml", version_regex = "appVersion:\\s*\"?(?<version>\\d+\\.\\d+\\.\\d+)\"?" },
]
The default regex matches common forms like version = "1.0.0",
version: "1.0.0", VERSION='1.0.0', and "version": "1.0.0". A custom
version_regex must include a named capture group (?<version>...);
only that group is replaced. Files without a match are skipped; an invalid
regex errors during config resolution.
Per-package overrides
A package can carry its own versioning and changelog config, using
exactly the same fields as
[defaults.versioning] and
[defaults.changelog]. Because packages are an array of
tables ([[package]]), set them as inline tables on the package itself
so they are unambiguously scoped to that entry — a separate
[package.changelog] header would only ever bind to the
most-recently-declared package:
[[package]]
name = "frontend"
path = "./apps/web"
release_type = "node"
changelog = { include_author = true }
versioning = { named_parsers = { ci = { skip = true } } }
Both merge field-by-field with their [defaults] counterpart. Any
field you set on the package wins; any field you omit is inherited from
[defaults], falling back to the built-in default only when [defaults]
doesn’t set it either. So if [defaults.changelog] enables
include_author and a package sets its own changelog without it, that
package keeps include_author = true. include_pr_link merges the same way.
Two fields compose rather than replace:
custom_parser— entries from[defaults]and the package are combined, with the package’s checked first.named_parsers— overrides apply per group and per field. Each group you list is merged onto the[defaults]value for that group, then onto the built-in default, so you only specify the fields you want to change. A package that only retitlescistill inheritsci.skip = truefrom[defaults].
One table replaces rather than merges:
prerelease— a package’sprereleasetable replaces the[defaults]one outright.suffixandstrategydescribe a single prerelease identity (rc+versioned,SNAPSHOT+static), so inheriting one field across a change to the other would produce combinations you didn’t ask for. Restatestrategyalongside a package-levelsuffixwhenever your default strategy isn’t theversionedbuilt-in.
commit_message_template and pr_title_template are plain strings rather
than tables, so there is nothing to merge: the package’s value wins, else
the [defaults] value, else the built-in. Set them directly on the
package, not inside a nested table. The monorepo_* templates have no
package-level form at all — they describe a PR spanning several packages,
so no one package owns them, and setting one on a [[package]] is a
config error.
Complete Example
[repository]
base_branch = "main"
first_release_search_depth = 400
separate_pull_requests = false
[defaults]
pr_title_template = "🚀 Release {{ package_name }} {{ tag }}"
monorepo_pr_title_template = "🚀 Release {{ repo_name }}"
[defaults.versioning]
auto_start_next = false
version_type = "major.minor.patch"
breaking_always_increment_major = true
features_always_increment_minor = true
[defaults.versioning.prerelease]
suffix = "beta"
strategy = "versioned"
[defaults.versioning.named_parsers]
ci.skip = true
chore.skip = true
[[defaults.versioning.custom_parser]]
pattern = "^deps"
title = "📦 Dependencies"
order = 3
skip = false
[defaults.changelog]
include_author = false
[[package]]
name = "frontend"
path = "./apps/web"
release_type = "node"
tag_prefix = "web-v"
# Per-package overrides (merge over the [defaults.*] equivalents).
changelog = { include_author = true }
[[package]]
name = "backend"
path = "./services/api"
release_type = "rust"
tag_prefix = "api-v"
versioning = { prerelease = { suffix = "alpha", strategy = "versioned" } }
pr_title_template = "api: {{ tag }}"
Environment Variables
Releasaurus selects the auth token automatically from the --forge type;
--token overrides it. The RELEASAURUS_* variables are fallbacks for
their matching CLI flags, and flags always win.
For the auth token, each forge accepts two env vars: a
RELEASAURUS_-prefixed name and the bare name. The prefixed name takes
precedence. Prefer it on Gitea/Forgejo CI runners (including Codeberg),
which auto-inject their own limited token into the bare *_TOKEN name
and would otherwise shadow your PAT — see
the CI/CD integration notes and
this known limitation.
| Variable | Purpose |
|---|---|
RELEASAURUS_GITHUB_TOKEN / GITHUB_TOKEN | GitHub auth token |
RELEASAURUS_GITLAB_TOKEN / GITLAB_TOKEN | GitLab auth token |
RELEASAURUS_GITEA_TOKEN / GITEA_TOKEN | Gitea auth token |
RELEASAURUS_FORGEJO_TOKEN / FORGEJO_TOKEN | Forgejo auth token |
RELEASAURUS_AZURE_DEVOPS_TOKEN / AZURE_DEVOPS_TOKEN | Azure DevOps PAT (experimental) |
RELEASAURUS_FORGE | Default --forge |
RELEASAURUS_REPO | Default --repo |
RELEASAURUS_LOCAL_PATH | Default --local-path (hybrid mode) |
RELEASAURUS_CONFIG | Default --config |
RELEASAURUS_DEBUG | Enable debug logging when set to any non-empty value |
RELEASAURUS_DRY_RUN | Enable dry-run (auto-enables debug) when set to any non-empty value |
Required token scopes
| Forge | Scopes / permissions |
|---|---|
| GitHub (classic) | repo |
| GitHub (fine-grained) | Contents, Issues, Pull requests — all read & write. Add Actions/Workflows read & write only if using the Action to modify workflow files. |
| GitLab | api, write_repository |
| Gitea | repository (read/write), issue (read/write), misc (read/write) management |
| Forgejo | repository (read/write), issue (read/write), misc (read/write) management |
| Azure DevOps | Code: Read & Write, Pull Request Threads: Read & Write |
RELEASAURUS_DEBUG and RELEASAURUS_DRY_RUN are enabled by any
non-empty value (including false or 0); unset or empty to disable.
The --debug / --dry-run flags always enable regardless of the
variable.
Supported Languages
Set release_type on a package and Releasaurus updates the matching
manifest and lock files. Lock files are updated when present, and all
languages support workspace/monorepo layouts.
release_type | Files updated |
|---|---|
generic | Custom files via additional_manifest_files |
go | version.go, version/version.go, internal/version.go, internal/version/version.go |
java | pom.xml, build.gradle, build.gradle.kts, gradle.properties, gradle/libs.versions.toml |
node | package.json, package-lock.json, yarn.lock |
php | composer.json, composer.lock |
python | pyproject.toml, setup.py, setup.cfg |
ruby | *.gemspec, Gemfile, Gemfile.lock |
rust | Cargo.toml, Cargo.lock |
Library API
The releasaurus-core crate exposes the full release pipeline as a
public Rust API — use it to embed release automation in your own tooling
instead of shelling out to the CLI. (For CI/CD and simple automation, the
CLI is the better choice.)
Adding the Dependency
[dependencies]
releasaurus-core = "x.x.x"
tokio = { version = "1", features = ["full"] } # async-first, built on Tokio
Architecture
Orchestrator (pipeline entry point)
└─ ResolvedConfig (merged settings)
└─ ResolvedPackageHash (resolved package configs)
└─ ForgeManager (caching + dry-run wrapper)
└─ Forge (GitHub / GitLab / Gitea / Local)
All operations go through Orchestrator, which needs two pieces:
- A
ForgeManagerwrapping a concreteForge. - A
ResolvedConfig— built byResolver::builder()from the loaded TOML plus any runtime overrides, then produced byResolver::resolve(). It carries the base branch, the PR-splitting flag, and aResolvedPackageHashof the resolved packages.
See the crate-level quick start on docs.rs for the full builder chain with per-step comments.
Internally each call drives packages through typed stages —
ResolvedPackage → PreparedPackage → AnalyzedPackage → ReleasablePackage → ReleasePRPackage. The stage name appears in most error contexts, which
helps when reading errors.
Constructing a RepoUrl
Forge constructors (Github::new, Gitlab::new, Gitea::new) take a
RepoUrl defined in this crate rather than a third-party URL type, so
your dependency tree stays stable. Build it from your parsed URL’s
components:
#![allow(unused)]
fn main() {
use releasaurus_core::forge::{RepoUrl, config::Scheme};
let url = RepoUrl {
scheme: Scheme::Https,
host: "github.com".into(),
owner: "my-org".into(),
name: "my-repo".into(),
// Full project path — nested GitLab groups may be "group/subgroup/repo"
path: "my-org/my-repo".into(),
port: None,
token: None,
};
}
Set token only when the credential is embedded in the URL
(https://TOKEN@host/...); otherwise leave it None and pass the token
as Option<secrecy::SecretString> to the forge constructor (add
secrecy to construct one).
The Forge Trait
Forge is the extension point for platform support. The crate ships four
implementations:
| Type | When to use |
|---|---|
forge::github::Github | GitHub (cloud or Enterprise) |
forge::gitlab::Gitlab | GitLab (cloud or self-hosted) |
forge::gitea::Gitea | Gitea self-hosted |
forge::local::LocalRepo | Local git2 operations (testing) |
To target a custom platform, implement Forge from
releasaurus_core::forge::traits and pass it to
ForgeManager::new(Box::new(my_forge), ...):
#![allow(unused)]
fn main() {
use async_trait::async_trait;
use releasaurus_core::{
config::Config,
forge::{
request::Tag,
request::{
Commit, CreateCommitRequest, CreatePrRequest,
CreateReleaseBranchRequest, ForgeCommit,
GetFileContentRequest, GetPrRequest, PrLabelsRequest,
PullRequest, ReleaseByTagResponse, UpdatePrRequest,
},
traits::Forge,
},
result::Result,
};
use std::any::Any;
use url::Url;
pub struct MyForge { /* ... */ }
#[async_trait]
impl Forge for MyForge {
fn repo_name(&self) -> String { todo!() }
fn release_link_base_url(&self) -> Url { todo!() }
fn compare_link_base_url(&self) -> Url { todo!() }
fn default_branch(&self) -> String { todo!() }
async fn load_config(
&self,
branch: Option<String>,
) -> Result<Config> { todo!() }
async fn get_file_content(
&self,
req: GetFileContentRequest,
) -> Result<Option<String>> { todo!() }
// ... remaining trait methods (see docs.rs for the full list)
async fn get_release_by_tag(&self, _: &str)
-> Result<ReleaseByTagResponse> { todo!() }
async fn create_release_branch(&self, _: CreateReleaseBranchRequest)
-> Result<Commit> { todo!() }
async fn create_commit(&self, _: CreateCommitRequest)
-> Result<Commit> { todo!() }
async fn tag_commit(&self, _: &str, _: &str)
-> Result<()> { todo!() }
async fn get_latest_tags_for_prefix(&self, _: &str, _: &str)
-> Result<Vec<Tag>> { todo!() }
async fn get_commits(&self, _: Option<String>, _: Option<String>)
-> Result<Vec<ForgeCommit>> { todo!() }
async fn get_open_release_pr(&self, _: GetPrRequest)
-> Result<Option<PullRequest>> { todo!() }
async fn get_merged_release_pr(&self, _: GetPrRequest)
-> Result<Option<PullRequest>> { todo!() }
async fn create_pr(&self, _: CreatePrRequest)
-> Result<PullRequest> { todo!() }
async fn update_pr(&self, _: UpdatePrRequest)
-> Result<()> { todo!() }
async fn replace_pr_labels(&self, _: PrLabelsRequest)
-> Result<()> { todo!() }
async fn create_release(&self, _: &str, _: &str, _: &str)
-> Result<()> { todo!() }
}
}
Dry-Run & Testing
Pass ForgeOptions { dry_run: true } to ForgeManager::new to skip all
write operations (logged at WARN) while read operations proceed
normally. For tests, LocalRepo runs everything against a local git2
repository; the Forge trait is also #[cfg_attr(test, automock)], so
mockall’s MockForge is available under #[cfg(test)].
Contributing
Thanks for your interest in contributing to Releasaurus! Bug reports, feature requests, code, docs, tests, and community support are all welcome. Bugs and feature requests go through GitHub Issues; general questions through GitHub Discussions.
Development Setup
Prerequisites: Rust 1.92+ (rustup), Git, and a GitHub/GitLab/Gitea account for testing.
This project uses Mise to manage the Rust version and dev tools (see mise.toml). After installing and activating mise:
git clone https://github.com/your-username/releasaurus.git
cd releasaurus
mise trust && mise install
This installs the correct Rust toolchain and tools (including just),
switches to them whenever you cd into the repo, and auto-loads any
variables from a local .env.
A Justfile provides common recipes:
just build # build (add --release for a release build)
just run --help # = cargo run -p releasaurus -- --help
just help # list all recipes
To build and install from source directly:
cargo install --path crates/cli
Running Tests
There are two kinds of tests:
Unit tests use mocks and never touch a real forge:
just test # run unit tests
just test-cov # with coverage
Integration tests run against real forges and require per-forge
environment variables (*_TEST_REPO, *_TEST_TOKEN, *_RESET_SHA for
GITHUB, GITLAB, GITEA, FORGEJO, and AZURE_DEVOPS). You can put
them in .env for mise to load automatically.
⚠️ The configured test repositories WILL be overwritten. All PRs, tags, releases, and branches are deleted and the repo is hard-reset to the configured reset SHA at the start of the suite. Use dedicated, disposable repositories with minimal-permission tokens.
just test-all # all tests, including integration
just test-github-integration # a single forge's integration tests
# (also: gitlab, gitea, forgejo, azure-devops)
Azure DevOps test setup: the test repo’s default branch must have no branch policies (the reset routine force-resets history via a temporary branch swap). The PAT needs
Code: Read & WriteandPull Request Threads: Read & Write.
Coding Standards
- Format with
cargo fmtand lint withcargo clippy. - Write documentation comments for public APIs.
- Test outcomes, not implementation; keep tests minimal and use the
existing
test_helpers.rspatterns; name tests descriptively (returns_all_manifest_targets, nottest_1).
Adding a New Language Updater
Each language updater lives under crates/core/src/updater/<lang>/ and
consists of a ReleaseType variant, a manifests module, an updater
module, file parsers, tests, and docs. To add one:
- Add the
ReleaseTypevariant incrates/core/src/config/release_type.rs. - Create the manifests module
(
crates/core/src/updater/<lang>/manifests.rs, implementingManifestTargets) and register it incrates/core/src/updater/manager.rsunderrelease_type_manifest_targets(). - Create the updater (
crates/core/src/updater/<lang>/updater.rsimplementingPackageUpdater, plus per-format file parsers), declare the module incrates/core/src/updater.rs, and register it in theupdater()function inmanager.rs. - Add tests for manifest generation, updater integration, and each file parser.
- Update the docs — add the language to the Supported Languages
table in
book/src/configuration-reference.md.
Reference implementations: PHP and Python are good simple starting points; Node, Rust, and Java show workspace support, lock files, and multiple build tools. Verify your work end-to-end with the local and hybrid modes:
just run release-pr --forge local --repo "/path/to/test/project" --debug
Code of Conduct
This project follows the Rust Code of Conduct. Report unacceptable behavior to the project maintainers.
Thank you for contributing to Releasaurus!