Introduction
Releasaurus 🦕 automates releases across multiple languages and Git
forges. Point it at a repository and it analyzes your commit history,
generates a changelog, and publishes a tagged release — no
configuration required. Add a releasaurus.toml when you want version
file updates, monorepo support, or custom changelog formatting.
# 1. Open a release PR (analyzes commits, writes the changelog)
releasaurus release-pr --repo "https://github.com/your-org/your-repo"
# 2. After merging the PR, tag and publish the release
releasaurus release --repo "https://github.com/your-org/your-repo"
That two-command loop — release-pr to prepare, release to
publish — is the whole workflow. The pull request gives you a review
step; Releasaurus handles the tedious version and changelog work.
Key Features
- Zero config by default — changelog generation and tagging work immediately. Configure only when you need more.
- Multi-forge — GitHub, GitLab, Gitea, Forgejo, and Azure DevOps (experimental), whether cloud-hosted or self-hosted.
- Multi-language version updates — Rust, Node.js, Python, Java, PHP, Ruby, Go, and a generic regex-based updater for anything else.
- Monorepo ready — multiple independently-versioned packages, with combined or separate release PRs.
- Conventional-commit aware — version bumps follow conventional commits and semver.
- Forge API native — runs entirely through forge APIs with no local clone required, ideal for CI/CD. An optional hybrid mode uses a local clone for git operations.
- Command-line overrides — change branch, tag prefix, and prerelease settings per run without editing your config.
Optional Commands
releasaurus start-next— bump patch versions right after a release to start the next development cycle.releasaurus get— query projected and published release data as JSON for automation, notifications, and debugging.
Where to Go Next
- Getting Started — install and cut your first release.
- Commands — every command, flag, and mode.
- Configuration — version file updates, monorepos, and prereleases.
Credit and Inspiration
Releasaurus builds on the proven ideas of git-cliff, release-please, and release-plz, extending them to a broader set of languages, frameworks, and platforms.
Getting Started
Install Releasaurus and cut your first release in a few minutes.
Install
Pre-built binary (recommended)
The fastest option, via cargo-binstall:
cargo binstall releasaurus
From crates.io
Compiles from source:
cargo install releasaurus
Docker
docker pull rgonnella/releasaurus:latest
docker run --rm rgonnella/releasaurus:latest --help
You can also download a binary directly from the
releases page, or
build from source — see Contributing. Confirm the
install with releasaurus --version.
Preview Without Any Risk
Run Releasaurus against a local checkout to see what it would do — no token, no config, no changes:
cd /path/to/your/repo
releasaurus release-pr --forge local --repo "."
The output shows the next version and the generated changelog without touching your repository. See Local & Dry-Run Modes for more.
Cut Your First Release
1. Set an access token
Releasaurus picks the right variable from the --forge you use:
export GITHUB_TOKEN="ghp_your_token_here" # GitHub
export GITLAB_TOKEN="glpat_your_token_here" # GitLab
export GITEA_TOKEN="your_token_here" # Gitea
Every token variable and its required scopes are listed in the Configuration Reference.
2. Open a release PR
releasaurus release-pr --repo "https://github.com/your-org/your-repo"
This analyzes your commits, picks the next version, generates a
changelog, and opens a pull request. (--forge is inferred for known
hosts like github.com; pass it explicitly for self-hosted instances.)
3. Merge, then publish
After reviewing and merging the PR:
releasaurus release --repo "https://github.com/your-org/your-repo"
This tags the release commit and publishes the release on your forge.
Add Version File Updates (Optional)
By default Releasaurus only writes changelogs and tags. To also bump
versions in your manifests (package.json, Cargo.toml, etc.), add a
releasaurus.toml at the repository root:
[[package]]
path = "."
release_type = "node" # or rust, python, java, php, ruby, go, generic
See Configuration for monorepos, prereleases, changelog customization, and the full option list.
Next Steps
- Commands — all commands, overrides, and the
start-nextandgetcommands. - CI/CD Integration — automate with GitHub Actions, GitLab CI, and more.
- Troubleshooting — common issues and fixes.
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"
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"
start-next
Bumps the patch version for each previously-tagged package and commits
the manifest changes directly to the base branch as a chore commit.
It does not open PRs or create tags, and skips packages that have never
been tagged. Use it right after a release to keep manifest versions ahead
of the last release.
# All previously-tagged packages
releasaurus start-next --repo "https://github.com/owner/repo"
# Specific packages only
releasaurus start-next --repo "https://github.com/owner/repo" \
--packages pkg-a,pkg-b
Note: This commits directly to your base branch. Ensure your branch protection rules permit it. It can also run automatically after
release— seeauto_start_next.
get
Queries release information as JSON without making any changes — useful
for debugging version detection and for building custom notifications.
(show is kept as an alias.)
get next-release
Projects the next release for each package as JSON.
releasaurus get next-release --repo "https://github.com/owner/repo"
# Single package, or write to a file
releasaurus get next-release --package my-pkg --out-file releases.json \
--repo "https://github.com/owner/repo"
get current-release
Returns the most recent release for each package (packages without a release are omitted).
releasaurus get current-release --repo "https://github.com/owner/repo"
get release
Returns the data for an existing tag — tag, sha, and notes.
releasaurus get release --tag v1.0.0 \
--repo "https://github.com/owner/repo"
get notes
Re-renders release notes from a get next-release JSON file using your
configured Tera template. This lets you transform the data (for example,
replacing author names with Slack IDs) before producing final notes.
(recompiled-notes is kept as an alias.)
# 1. Capture release data
releasaurus get next-release --out-file releases.json \
--repo "https://github.com/owner/repo"
# 2. Transform it however you like (custom script), then re-render:
releasaurus get notes --file releases.json \
--repo "https://github.com/owner/repo"
Output is a JSON array of { name, notes } objects.
Global Options & Forge Selection
These apply to every command:
| Flag | Env fallback | Description |
|---|---|---|
--repo <url> | RELEASAURUS_REPO | Repository URL |
--forge <forge> | RELEASAURUS_FORGE | Forge type (see below) |
--token <token> | GITHUB_TOKEN, etc. | Auth token |
--local-path <path> | RELEASAURUS_LOCAL_PATH | Local clone for hybrid mode |
--base-branch <branch> | — | Override the base branch |
--debug | RELEASAURUS_DEBUG | Verbose logging |
Available forge types: github, gitlab, gitea, forgejo,
azure-devops (experimental), and local (testing). For the full list
of token variables and required scopes, see the
Configuration Reference.
Automatic forge inference
When --repo points at a recognized cloud host, --forge can be
omitted:
| Host | Inferred forge |
|---|---|
github.com | github |
gitlab.com | gitlab |
gitea.com | gitea |
codeberg.org | forgejo |
dev.azure.com | azure-devops |
Self-hosted instances (e.g. https://gitlab.company.com/...) and
--forge local always require the flag, since the host alone can’t
identify the forge software.
Testing Modes
Three ways to run safely or against a local checkout.
Dry-Run Mode
Performs all analysis and validation and logs exactly what would
happen, but makes no changes — no branches, PRs, tags, or releases.
Dry-run automatically enables debug logging (output is prefixed
dry_run:).
releasaurus release-pr --dry-run --repo "https://github.com/owner/repo"
# Or via environment variable
export RELEASAURUS_DRY_RUN=true
Local Repository Mode
--forge local reads commits, tags, and files from your working
directory and never contacts a remote forge — ideal for validating a
releasaurus.toml change before pushing. No token required.
releasaurus release-pr --forge local --repo "."
# Or from a specific path
releasaurus release-pr --forge local --repo "/path/to/repo"
Hybrid Mode (Local Git + Remote Forge)
--local-path performs git operations (reading commits/tags/files,
creating branches, committing, pushing) against a local clone, while
still creating real PRs and releases via the forge API. Use it when you
already have a checkout and want to avoid repeated API calls for data
gathering. A forge token is still required.
releasaurus release-pr \
--repo "https://github.com/owner/repo" \
--token "$GITHUB_TOKEN" \
--local-path /path/to/checkout
CI fetch depth: in hybrid mode the local checkout must include full history and all tags back to the previous release. Most CI systems shallow-clone by default — set
fetch-depth: 0(GitHub/Gitea Actions) orGIT_DEPTH: 0(GitLab CI), or rungit fetch --unshallow. See CI/CD Integration for per-platform setup.
Configuration Overrides
Override config from the command line without editing
releasaurus.toml — handy for testing, one-off releases, and per-branch
CI settings.
| Flag | Effect |
|---|---|
--base-branch <branch> | Override the base branch |
--tag-prefix <prefix> | Global tag prefix for all packages |
--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, prerelease.suffix,
prerelease.strategy. Setting an unsupported property prints an error
listing valid values.
Precedence (highest to lowest): --set-package → global CLI
overrides → package config → global config → 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.prerelease.suffix=beta \
--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
Forgejo: Closed Release PRs on Repeated Runs
Forgejo’s API does not support force-pushing a branch. As a workaround,
Releasaurus deletes and re-creates the release branch on each run;
Forgejo auto-closes the PR targeting the deleted branch, so each run
leaves a closed PR behind. Use
hybrid mode (--local-path) to
avoid this. A patch to Forgejo to allow force pushing the release branch has
been accepted and will be available in v16.
https://codeberg.org/forgejo/forgejo/pulls/12663
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.
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
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 to give each package its own PR
(branches like releasaurus-release-main-frontend):
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.
Tracking Shared Code
Use additional_paths so a package also releases when shared directories
change:
[[package]]
path = "./apps/web"
release_type = "node"
tag_prefix = "web-v"
additional_paths = ["shared/types", "shared/utils"]
Workspaces in a Subdirectory
When a workspace isn’t at the repo root, set workspace_root so lock
files resolve correctly:
[[package]]
name = "api-server"
workspace_root = "backend"
path = "services/api"
release_type = "rust"
tag_prefix = "api-v"
This updates backend/services/api/Cargo.toml and the workspace
backend/Cargo.lock.
Naming & Path Rules
- Names must be unique across all packages. If omitted, the name is
derived from the last path component. Match the manifest’s
namefield where one exists (package.json,Cargo.toml, etc.). - The full path (
workspace_root+path) must be unique. Two packages may share apathonly if theirworkspace_rootdiffers.
Grouped Releases (Sub-Packages)
Use sub_packages to release several packages under one shared tag,
changelog, and release, while each sub-package still gets its own manifest
updates based on its release_type. A sub-package does not produce its
own tag.
[[package]]
name = "platform"
workspace_root = "."
path = "."
tag_prefix = "v"
sub_packages = [
{ name = "web", path = "packages/web", release_type = "node" },
{ name = "cli", path = "packages/cli", release_type = "rust" },
]
Result: one tag (v1.0.0), one changelog covering everything, one
release — with package.json (web) and Cargo.toml (cli) updated
independently. Reach for this when a group of packages must always ship
together with the same version.
Sub-packages vs. separate packages: separate
[[package]]entries are versioned and tagged independently;sub_packagesshare the parent’s single tag and changelog.
Prereleases
Publish alpha/beta/rc/snapshot versions before a stable release. Configure
globally with [prerelease] or per-package with a prerelease table.
[prerelease]
suffix = "alpha"
strategy = "versioned" # or "static"
[[package]]
path = "."
release_type = "node"
Strategies
versioned(default) — appends an incrementing counter:1.1.0-alpha.1,1.1.0-alpha.2, …static— appends the suffix as-is, with no counter:1.0.1-SNAPSHOT(common in Java).
Lifecycle
Change behavior by editing the config and opening a new release PR:
| From | Config change | Result |
|---|---|---|
v1.0.0 | suffix = "alpha" (+ feature commit) | v1.1.0-alpha.1 |
v1.1.0-alpha.1 | unchanged (+ fix commit) | v1.1.0-alpha.2 |
v1.0.0-alpha.3 | suffix = "beta" (+ feature) | v1.1.0-beta.1 |
v1.0.0-alpha.5 | remove [prerelease] (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
[prerelease]
suffix = "beta"
strategy = "versioned"
[[package]]
path = "./stable"
release_type = "rust"
# inherits the global beta prerelease
[[package]]
path = "./experimental"
release_type = "rust"
prerelease = { suffix = "alpha", strategy = "versioned" }
Aggregating Prerelease Notes
When graduating to stable, include the changelog entries from every prior prerelease:
[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).
Testing Your Configuration
Validate any config change locally before pushing — no token, no remote changes:
releasaurus release-pr --forge local --repo "."
Check that packages are detected, tag prefixes match, and the combined/separate PR strategy behaves as expected. See Local Repository Mode.
Next Steps
- Changelog Customization — filter commits and customize the template.
- Configuration Reference — every option, default, and the full example config.
Changelog Customization
Releasaurus generates changelogs from conventional commits. Control what
appears with the filtering options, and how it’s formatted with a Tera
template — both in the [changelog] section of releasaurus.toml.
Filtering Commits
Each skip_* flag drops a category of commit from the changelog; the
remaining flags adjust what’s shown. Set them in [changelog]:
[changelog]
skip_ci = true
skip_chore = true
skip_miscellaneous = true
include_author = true
[[package]]
path = "."
release_type = "node"
| Option | Default | Effect |
|---|---|---|
skip_ci | false | Excludes ci: commits (e.g. ci: update workflow) |
skip_chore | false | Excludes chore: commits (e.g. chore: update deps) |
skip_doc | false | Excludes docs: commits |
skip_test | false | Excludes test: commits |
skip_style | false | Excludes style: commits |
skip_refactor | false | Excludes refactor: commits |
skip_perf | false | Excludes perf: commits |
skip_revert | false | Excludes revert: commits |
skip_miscellaneous | false | Excludes non-conventional commits (no recognized type prefix) |
skip_merge_commits | true | Excludes merge commits |
include_author | false | Adds the commit author’s name to each entry |
aggregate_prereleases | false | When graduating a prerelease to stable, folds in the changelog entries from all prior prereleases (see Prereleases) |
Dropping or rewriting individual commits
skip_shas removes specific commits by SHA prefix (use 7+ characters).
Handy for commits that shouldn’t affect versioning or appear in the
changelog:
[changelog]
skip_shas = ["abc123d", "def456e"]
reword rewrites a commit’s message in the changelog. The new message
affects both the changelog text and the version bump — changing
fix: to feat:, for example, bumps minor instead of patch:
[[changelog.reword]]
sha = "abc123d"
message = "feat: added user authentication"
Both have CLI equivalents for one-off runs: --skip-sha <sha> and
--reword <sha>=<message>. The
Configuration Reference lists
these options again in terse lookup form.
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:
[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 commit.body -%}
> {{ commit.body }}
{% endif -%}
{% if commit.breaking_description -%}
> {{ commit.breaking_description }}
{% endif -%}
{% else -%}
- {% if commit.scope %}_({{ commit.scope }})_ {% endif %}{{ commit.title }} [_({{ commit.short_id }})_]({{ commit.link }}){% if include_author %} ({{ commit.author_name }}){% endif %}
{% endif -%}
{% endfor %}
{% endfor %}"""
A simpler custom template:
[changelog]
body = """## Release v{{ version }} — {{ timestamp | date(format="%Y-%m-%d") }}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group }}
{% for commit in commits %}
- {{ commit.title }} ({{ commit.short_id }}){% if include_author %} by {{ commit.author_name }}{% endif %}
{% endfor %}
{% endfor %}"""
Template Variables
Release
| Variable | Description |
|---|---|
version | Semantic version (e.g. 1.2.3) |
tag_name | Full tag including prefix/suffix |
link | URL to the release |
tag_compare_link | Diff vs. previous tag (empty for first release) |
sha_compare_link | Diff vs. previous tag, by commit SHA (empty for first release) |
sha | Release commit SHA |
timestamp | Unix timestamp |
include_author | Whether author display is enabled |
Commit (each item in commits)
| Variable | Description |
|---|---|
id / short_id | Full / abbreviated SHA |
group | Category (Features, Bug Fixes, …) |
scope | Optional conventional-commit scope |
title | Message without type/scope |
body | Optional extended description |
link | URL to the commit |
breaking / breaking_description | Breaking-change flag and details |
merge_commit | Whether it’s a merge commit |
timestamp | Commit timestamp |
author_name / author_email | Commit author |
raw_title / raw_message | Original unprocessed title / message |
Tips
Filter merge commits and conditionally show authors:
{% for commit in commits | filter(attribute="merge_commit", value=false) %}
- {{ commit.title }}{% if include_author %} <{{ commit.author_name }}>{% endif %}
{% endfor %}
Test any template change locally before committing it:
releasaurus release-pr --forge local --repo "."
See the Tera documentation for advanced filtering and formatting.
Editing Release Notes
Releasaurus lets you customize the release notes for a specific release
directly in the pull request body — without touching releasaurus.toml
or the CHANGELOG.md.
How It Works
When release-pr creates or updates a release PR, it renders the PR
body with a structured layout per package:
<details open>
<summary>v1.2.3</summary>
<div id="my-package-header"></div>
<div id="my-package" data-tag="v1.2.3">
<!--{"metadata":{"sha_compare_link":"...","tag_compare_link":"..."}}-->
## [v1.2.3](...) - 2026-04-10 ### Features - feat: some new feature
(abc1234)
</div>
<div id="my-package-footer"></div>
</details>
At release time, releasaurus release reads the notes directly from
the PR body rather than regenerating them from commit history. This
means any edits you make before merging are reflected in the published
forge release.
Note: Edits to the PR body affect only the forge release notes.
CHANGELOG.mdis generated from commit history and is not affected.
Editing the Release Notes
Open the PR body and edit the text inside the notes <div>. The
metadata comment (<!--{...}-->) must be left intact — it carries
the tag and link information needed at publish time. For example, adding
a summary paragraph above the generated entries:
<div id="my-package" data-tag="v1.2.3">
<!--{"metadata":{"sha_compare_link":"...","tag_compare_link":"..."}}-->
## [v1.2.3](...) - 2026-04-10 This release improves startup performance and
fixes a crash on empty input. See the [migration guide](https://example.com)
for details. ### Features - feat: some new feature (abc1234)
</div>
Persistent Header and Footer
For content that should survive re-runs of release-pr (for example,
if you run the command again after new commits land), place it in the
dedicated header and footer <div>s.
<div id="my-package-header">
## Highlights This is a major stability release. All users on v1.x are
encouraged to upgrade.
</div>
<div id="my-package-footer">
Full migration guide: https://example.com/migrate
</div>
When release-pr regenerates the PR body, it reads back the content
of these <div>s and re-embeds it. The header is prepended and the
footer is appended to the final release notes at publish time.
Tip: Leave the header and footer
<div>s empty (the default) if you have nothing to add. They will not appear in the published release notes.
Monorepo: Multiple Packages
In a monorepo, each package gets its own set of sections. The id
attributes are derived from the package name with any characters
outside [a-zA-Z0-9-_] replaced by -.
For example, a package named @scope/my-pkg gets:
<div id="-scope-my-pkg">— notes<div id="-scope-my-pkg-header">— header<div id="-scope-my-pkg-footer">— footer
Edit each package’s sections independently.
Backward Compatibility
PRs created by an older version of Releasaurus use a different body
format. The release command detects the format automatically and
falls back to reading release notes from the hidden metadata for those
PRs — no manual migration required.
Limits
- The metadata comment (
<!--{...}-->) inside the notes<div>must not be removed or modified. - Do not write a literal
</div>anywhere inside the notes, header, or footer sections. Releasaurus parses the PR body as HTML and a bare</div>will close the section early, truncating any content that follows. - Header and footer content is preserved verbatim. Markdown is supported by all major forge platforms.
- Re-running
release-prregenerates the notes from commit history and overwrites any direct edits to the notes<div>. Use the header/footer sections for content you want to survive re-runs.
CI/CD Integration
Releasaurus provides official integrations for GitHub Actions, Gitea Actions, and Forgejo Actions. For GitLab CI and Azure Pipelines, use the Docker image directly.
Note on fetch depth: When using
--local-path(hybrid mode), Releasaurus reads commit history and tags directly from the local clone. Most CI systems shallow-clone by default, which will cause missing commits or tags. Configure your CI checkout for full depth when using--local-path. Platform-specific instructions are in each section below.
Required token scopes
| Forge | Scopes / permissions |
|---|---|
| GitHub (classic) | repo |
| GitHub (fine-grained) | Contents, Issues, Pull requests — all read & write. Add Actions/Workflows read & write only if using the Action to modify workflow files. |
| GitLab | api, write_repository |
| Gitea | repository (read/write), issue (read/write), misc (read/write) management |
| Forgejo | repository (read/write), issue (read/write), misc (read/write) management |
| Azure DevOps | Code: Read & Write, Pull Request Threads: Read & Write |
GitHub Actions, Gitea Actions & Forgejo Actions
A single action works for GitHub Actions, Gitea Actions, and Forgejo
Actions workflows. See the
action README
for inputs, usage examples, and fetch depth configuration for
--local-path.
GitLab CI
Use the Releasaurus Docker image directly in your .gitlab-ci.yml.
You may provide an authentication token either by specifying a CI/CD
variable named GITLAB_TOKEN, or by directly passing the --token
option with a reference to your defined variable, e.g.
--token $RELEASE_TOKEN.
Required Scopes:
api(full API access)write_repository(repository write access)
Run both commands in a single job so they execute sequentially:
release first (it tags any merged release PR), then release-pr (it
opens or updates the next one). This matches the order used by the
GitHub, Gitea, and Forgejo action. Defining them as two separate jobs
with the same rules: lets GitLab schedule them in the same stage
concurrently, which races: release-pr may observe a merged but
not-yet-tagged release PR and abort with
must finish previous release first.
Example
releasaurus:
image:
name: rgonnella/releasaurus:vX.X.X
entrypoint: [""]
script:
# Assumes a CI/CD variable named $GITLAB_TOKEN for authentication.
# Alternatively, pass `--token $RELEASE_TOKEN` to each command.
#
# Run `release` BEFORE `release-pr`: `release` tags any merged
# release PR, then `release-pr` opens/updates the next one. The
# reverse order (or two parallel jobs) lets `release-pr` see a
# merged-but-untagged release PR and abort with
# "must finish previous release first".
- releasaurus release --forge gitlab --repo $CI_PROJECT_URL
- releasaurus release-pr --forge gitlab --repo $CI_PROJECT_URL
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Using --local-path
When using --local-path, Releasaurus reads commit history and tags
from the local clone and requires a full checkout. Configure
GIT_DEPTH: 0 to ensure a full clone when the runner starts fresh:
variables:
GIT_DEPTH: 0
If the runner reuses an existing workspace from a prior job
(i.e. GIT_STRATEGY: fetch), GIT_DEPTH has no effect on the
already-shallow repository. Unshallow explicitly in before_script:
before_script:
- git fetch --unshallow || true # no-op if already full-depth
Using both together is safe and covers all runner states.
Azure Pipelines (EXPERIMENTAL)
Azure DevOps support is experimental. No first-party Azure Pipelines
task is provided — use the Releasaurus Docker image directly in your
pipeline. Note that the release command only pushes the git tag
(the changelog commit lands when the release PR is merged); Azure
DevOps Git has no native release object, so no release notes page is
created.
Provide a PAT via the AZURE_DEVOPS_TOKEN pipeline secret variable.
The PAT needs Code: Read & Write and Pull Request Threads: Read & Write scopes.
The release branch (typically releasaurus-release-*) must have
Allow rewriting history
enabled for the build service identity — releasaurus performs
a non-fast-forward reset to the base branch when updating an existing
release PR. See the Azure DevOps known limitation for
the exact setting.
Run release first (it tags any merged release PR), then release-pr (it
opens or updates the next one). This matches the order used by the
GitHub, Gitea, and Forgejo action. Running release-pr first may observe a
merged but not-yet-tagged release PR and abort with
must finish previous release first.
trigger:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
container: rgonnella/releasaurus:vX.X.X
steps:
- checkout: self
fetchDepth: 0 # required if you also pass --local-path
- script: |
releasaurus release \
--forge azure-devops \
--repo "$(Build.Repository.Uri)"
env:
AZURE_DEVOPS_TOKEN: $(AZURE_DEVOPS_TOKEN)
- script: |
releasaurus release-pr \
--forge azure-devops \
--repo "$(Build.Repository.Uri)"
env:
AZURE_DEVOPS_TOKEN: $(AZURE_DEVOPS_TOKEN)
Troubleshooting
Common issues and how to diagnose them. If your problem isn’t covered here, check the GitHub issues.
Inspect Before You Run
get next-release shows exactly what Releasaurus would do — version,
included commits, and release notes — without making any changes. It’s
the fastest way to debug version detection, tag matching, and config:
releasaurus get next-release --repo "https://github.com/owner/repo"
# Or fully offline against a local checkout
releasaurus get next-release --forge local --repo "."
For deeper diagnostics, add --debug (or --dry-run, which also enables
debug). See Testing Modes.
Releasaurus doesn’t find existing tags
Usually a tag prefix mismatch — the package’s tag_prefix must match
your existing tags:
[[package]]
path = "."
tag_prefix = "v" # for v1.0.0; use "api-v" for api-v1.0.0; "" for 1.0.0
If no matching tag exists, Releasaurus treats it as a first release and
analyzes up to first_release_search_depth commits (default 400). Raise
it for a fuller first changelog, or lower it for speed. This affects
only the first release — once a matching tag exists, all commits back
to that tag are analyzed. To control how many tags are fetched while
searching, use tag_search_depth.
“Authentication failed” / 401 Unauthorized
- Confirm the token is set for the forge you’re targeting
(
echo $GITHUB_TOKEN), or pass--tokenexplicitly. - Check the scopes — see required token scopes.
- Check expiration — regenerate if expired.
“Repository not found” with a valid repo
The token lacks access, or the URL is wrong.
-
Verify the URL format, e.g.
--repo "https://github.com/owner/repository". -
Confirm the token’s account has access to the repository.
-
Reproduce offline to rule out config issues:
git clone https://github.com/owner/repo && cd repo releasaurus get next-release --forge local --repo "."
“must finish previous release first”
release-pr found a merged-but-not-yet-tagged release PR. Run release
first to tag it, then release-pr. In CI, always order the two commands
release → release-pr in a single job — see
CI/CD Integration.
Getting Help
When opening an issue, include: debug output (with secrets removed), your
repository structure, the exact command, expected vs. actual behavior,
your OS, releasaurus --version, and your forge platform and hosting
type.
Configuration Reference
Complete reference for releasaurus.toml, environment variables, and
supported languages. For guidance and examples, see
Configuration.
Global Settings
Top-level keys, all optional:
| Key | Type | Default | Description |
|---|---|---|---|
base_branch | string | repo default | Branch targeted for PRs, tagging, and releases. Override: --base-branch. |
first_release_search_depth | integer | 400 | Commits to analyze for the first release (when no matching tag exists). |
tag_search_depth | integer | 100 | Max tags fetched when searching for a previous release. 0 = all tags. |
separate_pull_requests | bool | false | One PR per package (true) vs. a single combined PR (false). |
auto_start_next | bool | false | Bump patch versions automatically after a release (see start-next). |
breaking_always_increment_major | bool | true | Breaking changes (feat!:, BREAKING CHANGE:) bump major. |
features_always_increment_minor | bool | true | feat: commits bump minor. |
custom_major_increment_regex | string | none | Additional regex that triggers a major bump. |
custom_minor_increment_regex | string | none | Additional regex that triggers a minor bump. |
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 (\\):
custom_major_increment_regex = "\\[MAJOR\\]" # matches "[MAJOR]"
custom_minor_increment_regex = "FEATURE" # no escaping needed
[prerelease]
Global prerelease config; can be overridden per package via a package
prerelease table. See Prereleases.
| Key | Type | Default | Description |
|---|---|---|---|
suffix | string | none (stable) | Identifier such as alpha, beta, rc, SNAPSHOT. Override: --prerelease-suffix. |
strategy | string | versioned | versioned (adds .1, .2, …) or static (suffix as-is). Override: --prerelease-strategy. |
[prerelease]
suffix = "beta"
strategy = "versioned"
[changelog]
Controls changelog generation. See Changelog Customization for the template and variables.
| Key | Type | Default | Description |
|---|---|---|---|
skip_ci | bool | false | Exclude ci: commits. |
skip_chore | bool | false | Exclude chore: commits. |
skip_doc | bool | false | Exclude docs: commits. |
skip_test | bool | false | Exclude test: commits. |
skip_style | bool | false | Exclude style: commits. |
skip_refactor | bool | false | Exclude refactor: commits. |
skip_perf | bool | false | Exclude perf: commits. |
skip_revert | bool | false | Exclude revert: commits. |
skip_miscellaneous | bool | false | Exclude non-conventional commits. |
skip_merge_commits | bool | true | Exclude merge commits. |
include_author | bool | false | Include commit author names. |
aggregate_prereleases | bool | false | On graduation, fold prior prerelease notes into the stable release. |
skip_shas | string[] | none | Skip commits by SHA prefix (7+ chars). CLI: --skip-sha. |
reword | object[] | none | Rewrite commit messages (affects changelog and version bump). CLI: --reword. |
body | string | standard template | Tera template for the changelog body. |
[changelog]
skip_ci = true
skip_chore = true
[[changelog.reword]]
sha = "abc123d"
message = "fix: corrected description"
[[package]]
One entry per package; repeatable.
| Key | Type | Default | Description |
|---|---|---|---|
path | string | . | Package directory, relative to workspace_root. |
workspace_root | string | . | Workspace root, relative to repo root. |
name | string | derived from path | Explicit package name; must be unique. |
release_type | string | none | Language for version updates (see Supported Languages). Omit for changelog/tagging only. |
tag_prefix | string | v (root) / <name>-v (nested) | Git tag prefix. Override: --tag-prefix or --set-package <name>.tag_prefix=. |
prerelease | table | inherits global | Per-package prerelease override. Override: --set-package <name>.prerelease.suffix=. |
sub_packages | object[] | none | Group packages under one shared tag/changelog (see Grouped Releases). |
additional_paths | string[] | none | Extra directories whose changes trigger a release for this package. |
additional_manifest_files | string[] / object[] | none | Extra files to version-bump (see below). |
auto_start_next | bool | inherits global | Per-package auto_start_next override. |
breaking_always_increment_major | bool | inherits global | Per-package override. |
features_always_increment_minor | bool | inherits global | Per-package override. |
custom_major_increment_regex | string | inherits global | Per-package override. |
custom_minor_increment_regex | string | inherits global | Per-package override. |
sub_packages entries take name, path, and release_type.
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.
Complete Example
# Global settings
base_branch = "main"
first_release_search_depth = 400
separate_pull_requests = false
auto_start_next = false
breaking_always_increment_major = true
features_always_increment_minor = true
[prerelease]
suffix = "beta"
strategy = "versioned"
[changelog]
skip_ci = true
skip_chore = true
include_author = false
[[package]]
name = "frontend"
path = "./apps/web"
release_type = "node"
tag_prefix = "web-v"
[[package]]
name = "backend"
path = "./services/api"
release_type = "rust"
tag_prefix = "api-v"
prerelease = { suffix = "alpha", strategy = "versioned" }
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.
| Variable | Purpose |
|---|---|
GITHUB_TOKEN | GitHub auth token |
GITLAB_TOKEN | GitLab auth token |
GITEA_TOKEN | Gitea auth token |
FORGEJO_TOKEN | Forgejo auth token |
AZURE_DEVOPS_TOKEN | Azure DevOps PAT (experimental) |
RELEASAURUS_FORGE | Default --forge |
RELEASAURUS_REPO | Default --repo |
RELEASAURUS_LOCAL_PATH | Default --local-path (hybrid mode) |
RELEASAURUS_DEBUG | Enable debug logging when set to any non-empty value |
RELEASAURUS_DRY_RUN | Enable dry-run (auto-enables debug) when set to any non-empty value |
Required token scopes
| Forge | Scopes / permissions |
|---|---|
| GitHub (classic) | repo |
| GitHub (fine-grained) | Contents, Issues, Pull requests — all read & write. Add Actions/Workflows read & write only if using the Action to modify workflow files. |
| GitLab | api, write_repository |
| Gitea | repository (read/write), issue (read/write), misc (read/write) management |
| Forgejo | repository (read/write), issue (read/write), misc (read/write) management |
| Azure DevOps | Code: Read & Write, Pull Request Threads: Read & Write |
RELEASAURUS_DEBUG and RELEASAURUS_DRY_RUN are enabled by any
non-empty value (including false or 0); unset or empty to disable.
The --debug / --dry-run flags always enable regardless of the
variable.
Supported Languages
Set release_type on a package and Releasaurus updates the matching
manifest and lock files. Lock files are updated when present, and all
languages support workspace/monorepo layouts.
release_type | Files updated |
|---|---|
generic | Custom files via additional_manifest_files |
go | version.go, version/version.go, internal/version.go, internal/version/version.go |
java | pom.xml, build.gradle, build.gradle.kts, gradle.properties, gradle/libs.versions.toml |
node | package.json, package-lock.json, yarn.lock |
php | composer.json, composer.lock |
python | pyproject.toml, setup.py, setup.cfg |
ruby | *.gemspec, Gemfile, Gemfile.lock |
rust | Cargo.toml, Cargo.lock |
Library API
The releasaurus-core crate exposes the full release pipeline as a
public Rust API — use it to embed release automation in your own tooling
instead of shelling out to the CLI. (For CI/CD and simple automation, the
CLI is the better choice.)
Adding the Dependency
[dependencies]
releasaurus-core = "0.14"
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 three pieces:
- A
ForgeManagerwrapping a concreteForge. - A
ResolvedConfig— built byResolver::builder()from the loaded TOML plus any runtime overrides. - A
ResolvedPackageHash— the resolved packages, produced alongsideResolvedConfigbyResolver::resolve().
See the crate-level quick start on docs.rs for the full builder chain with per-step comments.
Internally each call drives packages through typed stages —
ResolvedPackage → PreparedPackage → AnalyzedPackage → ReleasablePackage → ReleasePRPackage. The stage name appears in most error contexts, which
helps when reading errors.
Constructing a RepoUrl
Forge constructors (Github::new, Gitlab::new, Gitea::new) take a
RepoUrl defined in this crate rather than a third-party URL type, so
your dependency tree stays stable. Build it from your parsed URL’s
components:
#![allow(unused)]
fn main() {
use releasaurus_core::forge::{RepoUrl, config::Scheme};
let url = RepoUrl {
scheme: Scheme::Https,
host: "github.com".into(),
owner: "my-org".into(),
name: "my-repo".into(),
// Full project path — nested GitLab groups may be "group/subgroup/repo"
path: "my-org/my-repo".into(),
port: None,
token: None,
};
}
Set token only when the credential is embedded in the URL
(https://TOKEN@host/...); otherwise leave it None and pass the token
as Option<secrecy::SecretString> to the forge constructor (add
secrecy to construct one).
The Forge Trait
Forge is the extension point for platform support. The crate ships four
implementations:
| Type | When to use |
|---|---|
forge::github::Github | GitHub (cloud or Enterprise) |
forge::gitlab::Gitlab | GitLab (cloud or self-hosted) |
forge::gitea::Gitea | Gitea self-hosted |
forge::local::LocalRepo | Local git2 operations (testing) |
To target a custom platform, implement Forge from
releasaurus_core::forge::traits and pass it to
ForgeManager::new(Box::new(my_forge), ...):
#![allow(unused)]
fn main() {
use async_trait::async_trait;
use releasaurus_core::{
config::Config,
forge::{
request::Tag,
request::{
Commit, CreateCommitRequest, CreatePrRequest,
CreateReleaseBranchRequest, ForgeCommit,
GetFileContentRequest, GetPrRequest, PrLabelsRequest,
PullRequest, ReleaseByTagResponse, UpdatePrRequest,
},
traits::Forge,
},
result::Result,
};
use std::any::Any;
use url::Url;
pub struct MyForge { /* ... */ }
#[async_trait]
impl Forge for MyForge {
fn repo_name(&self) -> String { todo!() }
fn release_link_base_url(&self) -> Url { todo!() }
fn compare_link_base_url(&self) -> Url { todo!() }
fn default_branch(&self) -> String { todo!() }
async fn load_config(
&self,
branch: Option<String>,
) -> Result<Config> { todo!() }
async fn get_file_content(
&self,
req: GetFileContentRequest,
) -> Result<Option<String>> { todo!() }
// ... remaining trait methods (see docs.rs for the full list)
async fn get_release_by_tag(&self, _: &str)
-> Result<ReleaseByTagResponse> { todo!() }
async fn create_release_branch(&self, _: CreateReleaseBranchRequest)
-> Result<Commit> { todo!() }
async fn create_commit(&self, _: CreateCommitRequest)
-> Result<Commit> { todo!() }
async fn tag_commit(&self, _: &str, _: &str)
-> Result<()> { todo!() }
async fn get_latest_tags_for_prefix(&self, _: &str, _: &str)
-> Result<Vec<Tag>> { todo!() }
async fn get_commits(&self, _: Option<String>, _: Option<String>)
-> Result<Vec<ForgeCommit>> { todo!() }
async fn get_open_release_pr(&self, _: GetPrRequest)
-> Result<Option<PullRequest>> { todo!() }
async fn get_merged_release_pr(&self, _: GetPrRequest)
-> Result<Option<PullRequest>> { todo!() }
async fn create_pr(&self, _: CreatePrRequest)
-> Result<PullRequest> { todo!() }
async fn update_pr(&self, _: UpdatePrRequest)
-> Result<()> { todo!() }
async fn replace_pr_labels(&self, _: PrLabelsRequest)
-> Result<()> { todo!() }
async fn create_release(&self, _: &str, _: &str, _: &str)
-> Result<()> { todo!() }
}
}
Dry-Run & Testing
Pass ForgeOptions { dry_run: true } to ForgeManager::new to skip all
write operations (logged at WARN) while read operations proceed
normally. For tests, LocalRepo runs everything against a local git2
repository; the Forge trait is also #[cfg_attr(test, automock)], so
mockall’s MockForge is available under #[cfg(test)].
Contributing
Thanks for your interest in contributing to Releasaurus! Bug reports, feature requests, code, docs, tests, and community support are all welcome. Bugs and feature requests go through GitHub Issues; general questions through GitHub Discussions.
Development Setup
Prerequisites: Rust 1.92+ (rustup), Git, and a GitHub/GitLab/Gitea account for testing.
This project uses Mise to manage the Rust version and dev tools (see mise.toml). After installing and activating mise:
git clone https://github.com/your-username/releasaurus.git
cd releasaurus
mise trust && mise install
This installs the correct Rust toolchain and tools (including just),
switches to them whenever you cd into the repo, and auto-loads any
variables from a local .env.
A Justfile provides common recipes:
just build # build (add --release for a release build)
just run --help # = cargo run -p releasaurus -- --help
just help # list all recipes
To build and install from source directly:
cargo install --path crates/cli
Running Tests
There are two kinds of tests:
Unit tests use mocks and never touch a real forge:
just test # run unit tests
just test-cov # with coverage
Integration tests run against real forges and require per-forge
environment variables (*_TEST_REPO, *_TEST_TOKEN, *_RESET_SHA for
GITHUB, GITLAB, GITEA, FORGEJO, and AZURE_DEVOPS). You can put
them in .env for mise to load automatically.
⚠️ The configured test repositories WILL be overwritten. All PRs, tags, releases, and branches are deleted and the repo is hard-reset to the configured reset SHA at the start of the suite. Use dedicated, disposable repositories with minimal-permission tokens.
just test-all # all tests, including integration
just test-github-integration # a single forge's integration tests
# (also: gitlab, gitea, forgejo, azure-devops)
Azure DevOps test setup: the test repo’s default branch must have no branch policies (the reset routine force-resets history via a temporary branch swap). The PAT needs
Code: Read & WriteandPull Request Threads: Read & Write.
Coding Standards
- Format with
cargo fmtand lint withcargo clippy. - Write documentation comments for public APIs.
- Test outcomes, not implementation; keep tests minimal and use the
existing
test_helpers.rspatterns; name tests descriptively (returns_all_manifest_targets, nottest_1).
Adding a New Language Updater
Each language updater lives under crates/core/src/updater/<lang>/ and
consists of a ReleaseType variant, a manifests module, an updater
module, file parsers, tests, and docs. To add one:
- Add the
ReleaseTypevariant incrates/core/src/config/release_type.rs. - Create the manifests module
(
crates/core/src/updater/<lang>/manifests.rs, implementingManifestTargets) and register it incrates/core/src/updater/manager.rsunderrelease_type_manifest_targets(). - Create the updater (
crates/core/src/updater/<lang>/updater.rsimplementingPackageUpdater, plus per-format file parsers), declare the module incrates/core/src/updater.rs, and register it in theupdater()function inmanager.rs. - Add tests for manifest generation, updater integration, and each file parser.
- Update the docs — add the language to the Supported Languages
table in
book/src/configuration-reference.md.
Reference implementations: PHP and Python are good simple starting points; Node, Rust, and Java show workspace support, lock files, and multiple build tools. Verify your work end-to-end with the local and hybrid modes:
just run release-pr --forge local --repo "/path/to/test/project" --debug
Code of Conduct
This project follows the Rust Code of Conduct. Report unacceptable behavior to the project maintainers.
Thank you for contributing to Releasaurus!