Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

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

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

  1. Before you upgrade — drain release PRs created by versions older than v0.17.0.
  2. TOML config restructure — every key now lives under [repository], [defaults], or [[package]].
  3. CLI override paths — prerelease --set-package paths gained a versioning. segment.
  4. Monorepo commit message and PR title — the default now includes the repository name.
  5. custom_major_increment_regex — matching commits are now grouped as breaking, not just bumped.
  6. 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 scoped releasaurus::pending is, 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 own versioning and changelog.

The full reference lives in the Configuration Reference. The tables below cover only what moved.

Root-level keys

v0.22.xv1.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.xv1.0.0
skip_cici.skip
skip_chorechore.skip
skip_docdocumentation.skip
skip_testtest.skip
skip_stylestyle.skip
skip_refactorrefactor.skip
skip_perfperformance.skip
skip_revertrevert.skip
skip_miscellaneousmiscellaneous.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]:

  • prerelease
  • auto_start_next
  • breaking_always_increment_major
  • features_always_increment_minor
  • custom_major_increment_regex
  • custom_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.xv1.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.xv1.0.0
config::resolved::{GlobalOverrides, PackageOverrides, CommitModifiers, PackageName}config::overrides::* — same names
config::resolved::ResolvedConfigresolver::ResolvedConfig — reshaped
config::changelog::RewordedCommitconfig::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 yields Rc<ResolvedConfig> rather than a (ResolvedConfig, ResolvedPackageHash) tuple; the resolved packages hang off ResolvedConfig::package_configs. Drop the .package_configs(...) call from Orchestrator::builder(). The crate-level quick start in crates/core/src/lib.rs shows the full builder chain, and Library API has the narrative version.
  • PrereleaseConfig::suffix is a String, not an Option<String>. The suffix() accessor that unwrapped it is gone — read the field directly, and treat "" as “no prerelease”.
  • New config::versioning module holding VersionType, Group, Parser, ParserList, VersioningConfig, and NAMED_PARSERS. Group’s variants were renamed to spell out Feature, Documentation, Performance, and CI — which is why the TOML keys read documentation and performance rather than doc and perf.
  • Forge gained a required method, get_merged_pull_request_for_commit, which resolves the merged PR that introduced a commit. If you implement Forge outside this crate, add it; returning Ok(None) is a valid no-op and simply means the include_pr_link changelog option renders nothing for that forge.

Not affected

  • Existing tags and changelogs. Git tags, CHANGELOG.md files, and published releases are read and appended to exactly as before. No retagging or history rewrite is needed.
  • Custom body templates. 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, and commit.pr.
  • Environment variables. Every RELEASAURUS_* variable and bare *_TOKEN fallback behaves as it did, with the same precedence.
  • release_type values and version-file updaters. All languages, manifest files, lock files, sub_packages, and additional_manifest_files behavior are unchanged.
  • The JSON schema location. #:schema ./schema/schema.json still 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 / release workflow on the same packages: the tag release-direct creates will collide with the one release later tries to create for the merged PR. As a safety net, release-direct refuses 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. Unlike release, it is not driven by auto_start_next and 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 — see auto_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:

FlagEnv fallbackDescription
--repo <url>RELEASAURUS_REPORepository URL
--forge <forge>RELEASAURUS_FORGEForge type (see below)
--token <token>RELEASAURUS_<FORGE>_TOKEN, <FORGE>_TOKENAuth token
--local-path <path>RELEASAURUS_LOCAL_PATHLocal clone for hybrid mode
--base-branch <branch>Override the base branch
--debugRELEASAURUS_DEBUGVerbose logging
--configRELEASAURUS_CONFIGCustom 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:

HostInferred forge
github.comgithub
gitlab.comgitlab
gitea.comgitea
codeberg.orgforgejo
dev.azure.comazure-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) or GIT_DEPTH: 0 (GitLab CI), or run git 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.

FlagEffect
--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 (default true) filters nothing out.
  • The default body template’s filter(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 configCommit messagePR title
separate_pull_requests = truecommit_message_templatepr_title_template
One [[package]]commit_message_templatepr_title_template
Multiple [[package]] and separate_pull_requests = falsemonorepo_commit_message_templatemonorepo_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 = false uses the monorepo_* 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. Use separate_pull_requests = true if 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:

VariableDescriptionExample
branchThe base branch being released tomain
repo_nameRepository namemy-repo

Available in commit_message_template and pr_title_template only, because a PR spanning several packages has no single one of these:

VariableDescriptionExample
package_nameName of the package being releasedfrontend
tagFull tag, including the tag prefixweb-v1.2.3
semverVersion alone, without the prefix1.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 name field where one exists (package.json, Cargo.toml, etc.).
  • The full path (workspace_root + path) must be unique. Two packages may share a path only if their workspace_root differs.
  • 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_packages share 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_typeExample output
major.minor.patch (default)1.4.0
major.minor.patch+timestamp.sha1.4.0+1700000000.abc1234
year.month.day2026.6.14
year.month.day+hour.minute.second2026.6.14+15.30.45
year.month.day+hour.minute.second.micro2026.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.day and the +hour.minute.second[.micro] variants — calendar-based versions derived from the current UTC time; commits and the previous tag are ignored. Plain year.month.day allows 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:

FromConfig changeResult
v1.0.0suffix = "alpha" (+ feature commit)v1.1.0-alpha.1
v1.1.0-alpha.1unchanged (+ fix commit)v1.1.0-alpha.2
v1.0.0-alpha.3suffix = "beta" (+ feature)v1.1.0-beta.1
v1.0.0-alpha.5remove 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_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. 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

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

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

Both can also be set per package.

Commit Groups & Filtering

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

A parser has four fields:

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

Built-in groups (named_parsers)

Releasaurus ships with these default parsers:

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

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

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

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

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

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

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

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

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

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

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

Custom groups (custom_parser)

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

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

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

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

Ordering groups

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

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

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

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

Other options

In [defaults.versioning]:

OptionDefaultEffect
skip_merge_commitstrueExcludes merge commits

In [defaults.changelog]:

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

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

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

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

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

Two things are worth knowing before turning it on:

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

Per-package changelog

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

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

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

The body Template

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

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

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

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

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

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

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

A simpler custom template:

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

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

Template Variables

Release

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

Commit (each item in commits)

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

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

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

Tips

Filter merge commits and conditionally show authors:

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

Test any template change locally before committing it:

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

See the Tera documentation for advanced filtering and formatting.

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.md is 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>

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-pr regenerates 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

ForgeScopes / 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.
GitLabapi, write_repository
Gitearepository (read/write), issue (read/write), misc (read/write) management
Forgejorepository (read/write), issue (read/write), misc (read/write) management
Azure DevOpsCode: 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 (or RELEASAURUS_GITEA_TOKEN) rather than the bare FORGEJO_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 opaque 404 Not Found. Releasaurus reads the RELEASAURUS_-prefixed name first, and the runner doesn’t inject it, so it can’t be shadowed. Passing --token works 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

  1. Confirm the token is set for the forge you’re targeting (echo $GITHUB_TOKEN), or pass --token explicitly.
  2. Check the scopes — see required token scopes.
  3. Check expiration — regenerate if expired.

“Repository not found” with a valid repo

The token lacks access, or the URL is wrong.

  1. Verify the URL format, e.g. --repo "https://github.com/owner/repository".

  2. Confirm the token’s account has access to the repository.

  3. 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 releaserelease-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:

KeyTypeDefaultDescription
base_branchstringrepo defaultBranch targeted for PRs, tagging, and releases. Override: --base-branch.
first_release_search_depthinteger400Commits to analyze for the first release (when no matching tag exists).
tag_search_depthinteger100Max tags fetched when searching for a previous release. 0 = all tags.
separate_pull_requestsboolfalseOne PR per package (true) vs. a single combined PR (false).
skip_shasstring[]noneSkip commits by SHA prefix (7+ chars); affects changelog and version bump. Repo-wide. CLI: --skip-sha.
rewordobject[]noneRewrite 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.

KeyTypeDefaultApplies when
commit_message_templatestringchore({{ branch }}): release {{ package_name }} {{ tag }}separate_pull_requests = true, or one [[package]]. Overridable per package.
pr_title_templatestringchore({{ branch }}): release {{ package_name }} {{ tag }}separate_pull_requests = true, or one [[package]]. Overridable per package.
monorepo_commit_message_templatestringchore({{ branch }}): release {{ repo_name }}Multiple [[package]] with separate_pull_requests = false. No per-package override.
monorepo_pr_title_templatestringchore({{ 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]]).

KeyTypeDefaultDescription
version_typestringmajor.minor.patchVersion format to produce. See Version Types for the five accepted values.
auto_start_nextboolfalseBump patch versions automatically after a release (see start-next).
breaking_always_increment_majorbooltrueBreaking changes (feat!:, BREAKING CHANGE:) bump major.
features_always_increment_minorbooltruefeat: commits bump minor.
custom_major_increment_regexstringnoneAdditional regex marking a commit breaking: bumps major and groups it under ❌ Breaking.
custom_minor_increment_regexstringnoneAdditional regex that triggers a minor bump. No grouping effect.
skip_merge_commitsbooltrueExclude merge commits.
named_parserstablebuilt-in groupsOverride built-in commit groups (pattern/title/order/skip per group). See Changelog Customization.
custom_parserarraynoneDefine additional commit groups, checked before the defaults. Note the singular key. pattern, title and order are all required.
prereleasetablenone (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.

KeyTypeDefaultDescription
suffixstringnone (stable)Identifier such as alpha, beta, rc, SNAPSHOT. Override: --prerelease-suffix.
strategystringversionedversioned (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.

KeyTypeDefaultDescription
include_authorboolfalseInclude commit author names.
include_pr_linkboolfalseLink the pull request that introduced each commit. Costs extra API requests — see below.
aggregate_prereleasesboolfalseOn graduation, fold prior prerelease notes into the stable release.
bodystringstandard templateTera 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.

KeyTypeDefaultDescription
pathstring.Package directory, relative to workspace_root.
workspace_rootstring.Workspace root, relative to repo root.
namestringderived from pathExplicit package name; must be unique.
release_typestringnoneLanguage for version updates (see Supported Languages). Omit for changelog/tagging only.
tag_prefixstringv (root) / <name>-v (nested)Git tag prefix. Override: --tag-prefix or --set-package <name>.tag_prefix=.
sub_packagesobject[]noneGroup packages under one shared tag/changelog (see Grouped Releases).
additional_pathsstring[]noneExtra directories whose changes trigger a release for this package.
additional_manifest_filesstring[] / object[]noneExtra files to version-bump (see below).
versioningtableinherits [defaults.versioning]Per-package versioning override (see Per-package overrides).
changelogtableinherits [defaults.changelog]Per-package changelog override (see Per-package overrides).
commit_message_templatestringinherits [defaults]Release commit message for this package’s PR (see [defaults]).
pr_title_templatestringinherits [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 retitles ci still inherits ci.skip = true from [defaults].

One table replaces rather than merges:

  • prerelease — a package’s prerelease table replaces the [defaults] one outright. suffix and strategy describe 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. Restate strategy alongside a package-level suffix whenever your default strategy isn’t the versioned built-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.

VariablePurpose
RELEASAURUS_GITHUB_TOKEN / GITHUB_TOKENGitHub auth token
RELEASAURUS_GITLAB_TOKEN / GITLAB_TOKENGitLab auth token
RELEASAURUS_GITEA_TOKEN / GITEA_TOKENGitea auth token
RELEASAURUS_FORGEJO_TOKEN / FORGEJO_TOKENForgejo auth token
RELEASAURUS_AZURE_DEVOPS_TOKEN / AZURE_DEVOPS_TOKENAzure DevOps PAT (experimental)
RELEASAURUS_FORGEDefault --forge
RELEASAURUS_REPODefault --repo
RELEASAURUS_LOCAL_PATHDefault --local-path (hybrid mode)
RELEASAURUS_CONFIGDefault --config
RELEASAURUS_DEBUGEnable debug logging when set to any non-empty value
RELEASAURUS_DRY_RUNEnable dry-run (auto-enables debug) when set to any non-empty value

Required token scopes

ForgeScopes / 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.
GitLabapi, write_repository
Gitearepository (read/write), issue (read/write), misc (read/write) management
Forgejorepository (read/write), issue (read/write), misc (read/write) management
Azure DevOpsCode: 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_typeFiles updated
genericCustom files via additional_manifest_files
goversion.go, version/version.go, internal/version.go, internal/version/version.go
javapom.xml, build.gradle, build.gradle.kts, gradle.properties, gradle/libs.versions.toml
nodepackage.json, package-lock.json, yarn.lock
phpcomposer.json, composer.lock
pythonpyproject.toml, setup.py, setup.cfg
ruby*.gemspec, Gemfile, Gemfile.lock
rustCargo.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:

  1. A ForgeManager wrapping a concrete Forge.
  2. A ResolvedConfig — built by Resolver::builder() from the loaded TOML plus any runtime overrides, then produced by Resolver::resolve(). It carries the base branch, the PR-splitting flag, and a ResolvedPackageHash of 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:

TypeWhen to use
forge::github::GithubGitHub (cloud or Enterprise)
forge::gitlab::GitlabGitLab (cloud or self-hosted)
forge::gitea::GiteaGitea self-hosted
forge::local::LocalRepoLocal 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 & Write and Pull Request Threads: Read & Write.

Coding Standards

  • Format with cargo fmt and lint with cargo clippy.
  • Write documentation comments for public APIs.
  • Test outcomes, not implementation; keep tests minimal and use the existing test_helpers.rs patterns; name tests descriptively (returns_all_manifest_targets, not test_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:

  1. Add the ReleaseType variant in crates/core/src/config/release_type.rs.
  2. Create the manifests module (crates/core/src/updater/<lang>/manifests.rs, implementing ManifestTargets) and register it in crates/core/src/updater/manager.rs under release_type_manifest_targets().
  3. Create the updater (crates/core/src/updater/<lang>/updater.rs implementing PackageUpdater, plus per-format file parsers), declare the module in crates/core/src/updater.rs, and register it in the updater() function in manager.rs.
  4. Add tests for manifest generation, updater integration, and each file parser.
  5. 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!