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.
- 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
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 .# 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
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
-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
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
- 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
| Commit | Bump |
|---|---|
fix: ... |
patch |
feat: ... |
minor |
feat!: ... or BREAKING CHANGE: in footer |
major |
perf: ... |
patch |
chore: ..., docs: ..., ci: ..., refactor: ... |
no release |
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.jsonOr, if you installed it locally directly via ./node_modules/super-release/config-schema.json
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: originDefines 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) |
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 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.
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"]
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 compatGlobal file dependency patterns. When a commit changes any matching file, ALL packages are considered affected.
dependencies:
- yarn.lock
- pnpm-lock.yaml
- package.json
- '.github/**'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'Filter which packages are released. packages is an allow-list (glob patterns), exclude is a deny-list.
packages:
- '@acme/*'
exclude:
- my-monorepo-rootOrdered 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
- 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 commitEach 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 |
|---|---|---|
changelog |
Generates/updates changelog per package (parallel) | -- |
npm |
-- | Publishes packages (parallel within dependency levels) |
exec |
Runs custom shell command per package | Runs custom shell command per package |
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 ..
Default: [changelog, npm]
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/core@1.1.0, @acme/utils@1.0.1{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.
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.
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.
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 packagePackages 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.
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.yamlOn 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 branch- 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
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