super-release
v1.17.0
Published
A fast semantic-release alternative for monorepos, written in Rust
Maintainers
Readme
super-release
A fast and opinionated semantic-release alternative for monorepos, written in Rust.
Analyzes conventional commits to determine version bumps, generate changelogs,
update package.json files, publish to npm, and create git tags -- across all packages in a monorepo, in parallel.
Features
- Monorepo-first: discovers all
package.jsonpackages and associates commits by changed files - Prerelease branches (
beta,next, or dynamic from branch name) - Maintenance branches (
1.x,2.x) with major-version capping - Changelog generation powered by git-cliff
- Auto-detects package manager (npm, yarn, pnpm)
- Configurable tag format templates
- Build in steps: changelog, npm, exec
- Global file dependencies and ignore patterns
- Idempotent: safe to rerun after partial failures
- Dry-run mode with pretty, truncated output
Installation
The easiest way -- no install needed:
npx -y super-release --dry-runOr install as a dev dependency:
pnpm add -D super-releaseThe npm package automatically downloads the prebuilt native binary for your platform on first run.
Supported platforms: Linux (x86_64, aarch64, musl/Alpine), macOS (x86_64, Apple Silicon), Windows (x86_64).
Alternatively, build from source:
cargo install --path .GitHub Action
Run super-release with a single uses:. It is a Docker container action published
to GHCR, so it runs on Linux runners only.
name: Release
on:
push:
branches: [main]
permissions:
contents: write # create releases and tags
issues: write # "released" comments/labels (optional)
pull-requests: write # PR preview / released comments (optional)
id-token: write # npm provenance (optional)
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history + tags are required
# Only if you publish to npm:
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- uses: bowlingx/super-release@v1
env:
GITHUB_TOKEN: ${{ github.token }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # only if publishing to npmOn pull requests, set preview: true to post a sticky comment with the next
version and rendered release notes (needs pull-requests: write):
- uses: bowlingx/super-release@v1
with:
preview: true
env:
GITHUB_TOKEN: ${{ github.token }}The preview is evaluated against the PR's base branch. When PRs target an
integration branch (say develop) that is later merged into the release branch,
set base to the branch the release will actually run from:
- uses: bowlingx/super-release@v1
with:
preview: true
base: main
env:
GITHUB_TOKEN: ${{ github.token }}Inputs
| Input | Default | Description |
|---------------------------------|-----------------------|--------------------------------------------------------|
| github-token | ${{ github.token }} | Token for the GitHub API (releases, comments). |
| dry-run | false | Show what would happen without making changes. |
| preview | false | Post/update a PR preview comment instead of releasing. |
| config | | Path to the config file (default .release.yaml). |
| package | | Filter to a specific package. |
| base | | Base branch for preview (default: the PR base). |
| working-directory | | Repository root to operate on. |
| dangerously-skip-config-check | false | Skip config-schema validation. |
| args | | Extra raw CLI flags (escape hatch). |
Requirements
fetch-depth: 0— version bumps are computed from the full history and all tags; a shallow checkout produces wrong versions.contents: writeand a token, to create releases and tags.- npm auth (only if you publish to npm) is configured out of band, exactly like
any npm workflow:
actions/setup-nodewithregistry-urlplusNODE_AUTH_TOKEN, or an.npmrc.
Pin to a major with @v1; the action resolves the matching :v1 image (latest
1.x). For a fully pinned image, use the Docker usage with a :X.Y.Z tag.
Docker
The same image is published to GHCR for use from any CI or locally:
docker run --rm \
-v "$PWD:/repo" -w /repo \
-e GITHUB_TOKEN \
ghcr.io/bowlingx/super-release:v1 --dry-runTags: :v1 (latest 1.x), :latest, :X.Y.Z (exact), :sha-<commit>. The image is
Alpine-based and bundles git and Node.js (for the npm step).
Quick Start
# Preview what would be released
super-release --dry-run
# Run a release
super-release
# Get the next version for a package (useful in CI scripts)
super-release --show-next-version
# Get the next version for a specific package in a monorepo
super-release --show-next-version --package @acme/core
CLI Reference
Usage: super-release [OPTIONS]
Options:
-n, --dry-run Show what would happen without making changes
-C, --path <PATH> Repository root [default: .]
-c, --config <CONFIG> Path to config file [default: .release.yaml]
--show-next-version Print the next version and exit
--preview Render a pull-request release preview and exit
--pr <PR> Pull request number for --preview (auto-detected in CI)
--repo <OWNER/NAME> GitHub repository for --preview (auto-detected)
--base <BRANCH> Base branch to evaluate --preview against
--no-comment With --preview, print Markdown instead of commenting
-p, --package <PACKAGE> Filter to a specific package (for --show-next-version)
-v, --verbose Verbose output
--dangerously-skip-config-check
Skip config file validation against the JSON schema
-h, --help Print help
-V, --version Print version--show-next-version
Outputs only the next version (or the current version if no bump is needed) and exits silently. Useful for CI scripts:
VERSION=$(super-release --show-next-version)
SUPER_RELEASE_VERSION=$VERSION cargo build --releaseIn monorepos, use --package to select which package: super-release --show-next-version -p @acme/core
--preview
Renders a release preview for a pull request: the next version for each package plus a preview of the release notes, without changing anything. When a GitHub token and a pull request are detected (as in GitHub Actions), it posts — and on later runs updates in place — a single "sticky" comment on the PR. Otherwise it prints the Markdown to stdout so you can pipe it into your own comment step.
# In CI (GitHub Actions): auto-detects the PR, repo, base branch, and token
super-release --preview
# Locally / other CI: print the Markdown
super-release --preview --base main --no-commentThe preview evaluates the PR against its base branch and reflects the commits currently on the branch, so the final
release may differ after a squash-merge. Requires pull-requests: write permission and a full checkout
(fetch-depth: 0). A ready-to-use workflow lives in
.github/workflows/pr-preview.yml.
How It Works
- Discover packages -- finds all directories with a
package.json(respects.gitignore) - Resolve tags -- finds the latest release tag per package (filtered by branch context, only reachable from HEAD)
- Walk commits -- only analyzes commits since the oldest tag (not the entire history)
- Associate commits to packages -- maps changed files to their owning package (respects
dependenciesandignoreconfig) - Calculate versions -- determines bump levels from conventional commits
- Run steps -- changelog, npm publish, exec commands
- Git finalize -- commits modified files, creates tags, optionally pushes
In GitHub Actions (and Gitea/Forgejo Actions), GitLab CI and Azure Pipelines, each output section (discovered packages, version bumps, every step including its forwarded command output, git finalize, publishing releases) is printed as a collapsible log group. The release plan and the final result stay visible.
Conventional Commits
| Commit | Bump |
| ----------------------------------------------------- | ---------- |
| fix: ... | patch |
| feat: ... | minor |
| feat!: ... or BREAKING CHANGE: in footer | major |
| perf: ... | patch |
| Revert "..." or revert: ... with a This reverts commit <sha> footer | patch |
| chore: ..., docs: ..., ci: ..., refactor: ... | no release |
Reverts
Revert commits behave exactly like they do in semantic-release:
- Reverting an already-released commit triggers a patch release, and the revert shows up in the changelog and
release notes under a
◀️ Revertsection. - If a commit and its revert both land in the same unreleased range, they cancel each other out (per package) and neither counts toward the release.
- The message
git revertproduces (Revert "feat: ..."plusThis reverts commit <sha>.) works out of the box — no editing needed. The Angular-stylerevert: feat: ...form is supported too. - A
revert:commit without theThis reverts commit <sha>footer does not trigger a release (matching semantic-release's default release rules).
Configuration
Create a .release.yaml in your repository root. JSON (.release.json) and JSONC (.release.jsonc) are also
supported. All fields are optional with sensible defaults.
The config is validated against a bundled JSON Schema at startup. Use --dangerously-skip-config-check
to bypass validation.
For editor autocompletion:
# yaml-language-server: $schema=https://raw.githubusercontent.com/bowlingx/super-release/v1.4.0/schema.json// .release.jsonc
{
"$schema": "https://raw.githubusercontent.com/bowlingx/super-release/v1.4.0/schema.json",
}Or, if you installed it locally directly via ./node_modules/super-release/config-schema.json
Full Example
branches:
- main
- name: next
channel: next # publishes to "next" npm dist-tag
- name: next-major
channel: next-major
- name: beta
prerelease: beta
- name: 'test-*'
prerelease: true
packages: ['@acme/core'] # only release core on test branches
- name: '1.x'
maintenance: true
tag_format: 'v{version}'
tag_format_package: '{name}/v{version}'
packages:
- '@acme/*'
exclude:
- my-monorepo-root
# Files that trigger ALL packages when changed
dependencies:
- yarn.lock
- pnpm-lock.yaml
# Files to ignore -- commits touching only these won't trigger releases
ignore:
- 'README.md'
- 'docs/**'
- '**/*.md'
steps:
- name: changelog
- name: npm
options:
provenance: true
- name: exec
options:
prepare_cmd: 'sed -i'''' -e ''s/^version = .*/version = "{version}"/'' Cargo.toml'
files:
- Cargo.toml
- Cargo.lock
git:
commit_message: 'chore(release): {releases} [skip ci]'
push: false
remote: originReference
branches
Defines which branches can produce releases. Only configured branches are allowed -- running on an unconfigured branch exits cleanly.
| Form | Type | Example versions |
| ----------------------------------------------- | ----------------------------------- | -------------------------------- |
| - main | Stable (primary) | 1.0.0, 1.1.0, 2.0.0 |
| - name: next channel: next | Stable (next channel) | 1.1.0 on next dist-tag |
| - name: next-major channel: next-major | Stable (next-major channel) | 2.0.0 on next-major dist-tag |
| - name: beta prerelease: beta | Prerelease (fixed channel) | 2.0.0-beta.1, 2.0.0-beta.2 |
| - name: "test-*" prerelease: true | Prerelease (branch name as channel) | 2.0.0-test-my-feature.1 |
| - name: "1.x" maintenance: true | Maintenance (major locked) | 1.5.1, 1.6.0 (no 2.x) |
| - name: "1.5.x" maintenance: true | Maintenance (major+minor locked) | 1.5.1, 1.5.2 (no 1.6.x) |
Multiple release branches
You can have multiple stable release branches (e.g. main, next, next-major) that release independently. Each
non-primary branch should set a channel so it publishes to a different npm dist-tag:
branches:
- main # primary: publishes to "latest"
- name: next
channel: next # publishes to "next" dist-tag
- name: next-major
channel: next-major # publishes to "next-major" dist-tagVersion collision detection: If a branch tries to release a version that already exists as a tag (e.g. next
released 1.1.0 and main also tries 1.1.0), super-release will error. Merge the higher branch into the lower one
first, or let the lower branch release a different version.
Maintenance branches
Maintenance branches cap version bumps to stay within a range inferred from the branch name:
1.x-- major is locked:feat:bumps minor,feat!:is capped to minor, no major bumps1.5.x-- major and minor are locked: all bumps become patch only
If the branch name doesn't follow the N.x / N.N.x pattern, set range explicitly:
branches:
- name: legacy-support
maintenance: true
range: '1.5.x' # cap to 1.5.x patch rangeIn monorepos, packages whose version is outside the maintenance range are automatically skipped. For example, on branch
1.x, a package at v3.0.0 will be skipped while a package at v1.2.0 will be released normally.
Branch options
Branches can filter which packages they release with packages:
branches:
- name: 'test-*'
prerelease: true
packages: # only release these on test branches
- '@acme/core'
- '@acme/utils'Tag filtering by branch: Stable branches only see stable tags. Prerelease branches see their own channel's tags plus stable tags. Tags on other branches that haven't been merged are ignored.
Default: ["main", "master"]
tag_format / tag_format_package
Templates for git tag names. Placeholders: {version}, {name}.
tag_format: "v{version}" # root: v1.2.3 (default)
tag_format_package: "{name}/v{version}" # sub-packages: @acme/core/v1.2.3 (default)
tag_format_package: "{name}@{version}" # semantic-release compatdependencies
Global file dependency patterns. When a commit changes any matching file, ALL packages are considered affected.
dependencies:
- yarn.lock
- pnpm-lock.yaml
- package.json
- '.github/**'ignore
Glob patterns for files to ignore. Commits that only touch ignored files will not trigger a release. If a commit touches both ignored and non-ignored files, only the non-ignored files determine which packages are affected.
ignore:
- 'README.md'
- 'docs/**'
- '**/*.md'
- '.prettierrc'packages / exclude
Filter which packages are released. packages is an allow-list (glob patterns), exclude is a deny-list.
packages:
- '@acme/*'
exclude:
- my-monorepo-rootsteps
Ordered list of steps. Each step has a name, optional packages and branches filters (glob patterns), and
options.
steps:
- name: changelog
options:
filename: CHANGELOG.md
preview_lines: 20
# template_file: templates/changelog.tera # custom git-cliff body (path)
# template: '## {{ version }}\n{% for c in commits %}- {{ c.message }}\n{% endfor %}' # or inline
- name: npm
packages: ['@acme/*'] # only publish @acme packages
branches: ['main', 'beta'] # only run on main and beta branches
options:
access: public
provenance: true
registry: https://registry.npmjs.org
tag: next # dist-tag (default: auto from prerelease channel)
publish_args: ['--otp=123456']
package_manager: yarn # force specific PM (default: auto-detect)
check_registry: true # check if version exists before publishing (default: true)
- name: exec
packages: ['my-rust-lib']
options:
prepare_cmd: 'sed -i'''' -e ''s/^version = .*/version = "{version}"/'' Cargo.toml'
publish_cmd: 'cargo publish'
files: [Cargo.toml, Cargo.lock] # include in git commit
- name: github
options:
assets: ['dist/*.tgz'] # glob(s), relative to repo root
draft: false
# prerelease: true # default: true on prerelease branches
# release_name_template: '{name} v{version}' # default: the tag
# github_url: https://ghe.corp/api/v3 # GitHub Enterprise
comment_on_success: true # comment on resolved PRs/issues (default: true)
released_labels: ['released'] # labels added to them (default: ['released'])
# success_comment: '🎉 Shipped in {releases}' # {releases} = the tags, {tag}
# template_file: templates/release.tera # custom git-cliff release body (path)
# template: '## {{ version }}...' # or inlineBoth the changelog and github steps accept a custom git-cliff body template
(template inline, or template_file as a repo-root-relative path; the file wins if both are set). The
github template additionally has GitHub data available — commit.remote.username / commit.remote.pr_number
for @author and PR links, github.contributors (with is_first_time) for a "New Contributors" section, and
extra.repo_url / extra.tag / extra.previous_tag for the compare link. GitHub data is looked up per released
commit, so commits landed by a rebase merge link to their PR too, and an author counts as new when their only commits
are the ones released in this run. Both templates get extra.dependency_chain for a release forced by a dependency
bump: the packages that caused it, root cause first (null for a release with its own commits). Defaults: the grouped
conventional changelog for changelog, and that plus GitHub attribution for github; both add a line to a
dependency-only release naming the dependency that changed.
Each step can be scoped:
packages-- glob patterns to filter which packages the step operates on. If empty, the step runs for all packages. For example,packages: ["@acme/*"]limits an npm publish step to only@acme-scoped packages.branches-- glob patterns for branch names this step runs on. If empty, the step runs on all branches. For example,branches: ["main"]ensures a step only runs on the main branch.
| Step | Prepare/Publish | Release (after tags are pushed) |
| ----------- | ------------------------------------------------------ | -------------------------------------------------------- |
| changelog | Generates/updates changelog per package (parallel) | -- |
| npm | Publishes packages (parallel within dependency levels) | -- |
| exec | Runs custom shell command per package | -- |
| github | -- | Creates a GitHub Release per package with its notes + assets |
Package version bumps (package.json) happen automatically before steps run (part of core).
Steps return the files they modified. The core git step stages exactly those files for the commit -- no git add ..
The github step runs after the commit and tags are pushed, so it needs git.push: true and a
GITHUB_TOKEN (or GH_TOKEN) with contents: write (and issues: write + pull-requests: write for
comments). It creates one GitHub Release per released package (idempotent -- safe to re-run) and comments on the
PRs/issues each release resolved (detected from (#N) squash/merge subjects and closes/fixes #N keywords),
adding a released label. Comments are posted once (marker-guarded), so re-runs don't duplicate them.
Release bodies are enriched with GitHub data when a token is available -- a "What's Changed" list with @author
mentions and PR links, plus a "New Contributors" section -- via git-cliff's GitHub integration (the API calls are
cached on disk, and fall back to the plain git-cliff notes if the fetch fails). The CHANGELOG.md file keeps its
conventional grouped format.
Default: [changelog, npm]
git
Core git behavior after all steps run. Not a step -- always runs.
git:
commit_message: 'chore(release): {releases} [skip ci]'
push: false # push commit + tags to remote
remote: originCommit message placeholders:
{releases}-- comma-separated:@acme/[email protected], @acme/[email protected]{summary}-- one per line:- @acme/core 1.0.0 -> 1.1.0{count}-- number of packages released
The git step:
- Stages files reported by steps (changelogs, exec
files, package.json bumps) - Commits (or skips if nothing changed)
- Creates annotated tags for each release
- Pushes commit + tags if
push: true
Tags are idempotent -- existing tags are skipped. The npm step checks the registry before publishing (npm view) and
skips versions that already exist. Non-404 errors (auth, network) abort the release to prevent partial publishes.
A publish the registry rejects as already published (cannot publish over the previously published versions) is also
treated as skipped; this is how already-published private packages are detected when npm view has no read access,
for example with trusted publishing and no NODE_AUTH_TOKEN.
Monorepo Support
Structure
my-monorepo/
package.json <- root package (tags: v1.0.0)
.release.yaml
packages/
core/
package.json <- @acme/core (tags: @acme/core/v1.0.0)
src/
utils/
package.json <- @acme/utils (tags: @acme/utils/v1.0.0)
src/Packages are discovered by finding package.json files recursively (respects .gitignore). Each commit is associated
to a package based on which files it changed.
Independent versioning
Each package has its own version and release tag. A commit that only touches packages/core/ will only bump
@acme/core. Packages are versioned independently -- @acme/core can be at v3.0.0 while @acme/utils is at
v1.2.0.
Filtering packages
Use packages (allow-list) and exclude (deny-list) at the top level to control which packages are released:
packages:
- '@acme/*' # only release @acme-scoped packages
exclude:
- my-monorepo-root # skip the root packageDependencies and publish order
Packages that depend on each other (via dependencies or devDependencies in package.json) are published in
dependency order. If @acme/utils depends on @acme/core, core is published first. Independent packages publish in
parallel.
Global file dependencies
Files that affect all packages (lock files, shared config) can be declared as global dependencies. A commit that only
changes yarn.lock will trigger releases for all packages:
dependencies:
- yarn.lock
- pnpm-lock.yamlMaintenance branches in monorepos
On a maintenance branch like 1.x, packages whose current version is outside the maintenance range are automatically
skipped. For example, if @acme/core is at v3.0.0 and @acme/utils is at v1.2.0, only @acme/utils will be
released on the 1.x branch.
You can also use per-branch packages filters for explicit control:
branches:
- name: '1.x'
maintenance: true
packages:
- '@acme/utils' # only release utils on this maintenance branchPerformance
- Tag-bounded history walk: only walks commits since the oldest package tag
- Single-pass commit collection: commits fetched once, partitioned per package
- Reachable-only tags: single revwalk to check tag reachability, stops early
Acknowledgements
super-release is inspired by and builds on the ideas of:
- semantic-release -- the original automated release tool that pioneered conventional-commit-based versioning
- git-cliff -- powers changelog generation via
git-cliff-core
