super-release
v1.12.1
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 }}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. |
| 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
Conventional Commits
| Commit | Bump |
| ----------------------------------------------------- | ---------- |
| fix: ... | patch |
| feat: ... | minor |
| feat!: ... or BREAKING CHANGE: in footer | major |
| perf: ... | patch |
| chore: ..., docs: ..., ci: ..., refactor: ... | no release |
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. Defaults: the grouped conventional
changelog for changelog, and that plus GitHub attribution for github.
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.
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
