A single-binary, zero-dependency semantic release tool for any language.
The npm semantic-release ecosystem is battle-tested but comes with friction:
- Requires Node.js — even for Go, Rust, Python, and Java projects.
- Complex plugin config — wiring together
@semantic-release/*packages is error-prone. - Coupled to CI runtime — plugins shell out to language-specific toolchains at release time.
sr solves this:
- Single static binary — no runtime, no package manager, minimal dependencies.
- Language-agnostic — works with any project that uses git tags for versioning.
- Zero-config defaults — conventional commits + semver + GitHub releases out of the box.
- Structured JSON output — pipe
sr releasetojqfor custom CI pipelines.
- Conventional Commits parsing (built-in, configurable via
commit_pattern) - Semantic versioning bumps (major / minor / patch)
- Automatic version file bumping (
Cargo.toml,package.json,pyproject.toml) - Changelog generation (Jinja2 templates via
minijinja) - GitHub Releases (via REST API — no external tools needed)
- Structured JSON output for CI piping (
sr release | jq .version) - Trunk-based workflow (tag + release from
main)
curl -fsSL https://raw.githubusercontent.com/urmzd/semantic-release/main/install.sh | shThe installer automatically adds ~/.local/bin to your PATH in your shell profile (.zshrc, .bashrc, or config.fish).
- uses: urmzd/semantic-release@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}Minimal — release on every push to main:
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: urmzd/semantic-release@v1Dry-run on pull requests:
- uses: urmzd/semantic-release@v1
with:
command: release
dry-run: "true"Use outputs in subsequent steps:
- uses: urmzd/semantic-release@v1
id: sr
- if: steps.sr.outputs.released == 'true'
run: echo "Released ${{ steps.sr.outputs.version }}"Upload artifacts to the release:
# Build artifacts are downloaded into release-assets/
- uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- uses: urmzd/semantic-release@v1
with:
artifacts: "release-assets/*"The artifacts input accepts glob patterns (newline or comma separated). All matching files are uploaded to the GitHub release. This keeps artifact handling self-contained in the action — no separate upload steps needed.
Run a build step between version bump and commit (useful for lock files, codegen, etc.):
- uses: urmzd/semantic-release@v1
with:
build-command: "cargo build --release"The command runs with SR_VERSION and SR_TAG environment variables set, so you can reference the new version in your build scripts.
Manual re-trigger with workflow_dispatch (useful when a previous release partially failed):
name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
force:
description: "Re-release the current tag"
type: boolean
default: false
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: urmzd/semantic-release@v1
with:
force: ${{ github.event.inputs.force || 'false' }}| Input | Description | Default |
|---|---|---|
command |
The sr subcommand to run (release, plan, changelog, version, config, completions) |
release |
dry-run |
Preview changes without executing them | false |
force |
Re-release the current tag (use when a previous release partially failed) | false |
config |
Path to the config file | .urmzd.sr.yml |
github-token |
GitHub token for creating releases | ${{ github.token }} |
git-user-name |
Git user name for tag creation | semantic-release[bot] |
git-user-email |
Git user email for tag creation | semantic-release[bot]@urmzd.com |
artifacts |
Glob patterns for artifact files to upload (newline or comma separated) | "" |
build-command |
Shell command to run after version bump, before commit (SR_VERSION and SR_TAG env vars available) |
"" |
| Output | Description |
|---|---|
version |
The released version (empty if no release) |
previous-version |
The previous version before this release (empty if first release) |
tag |
The git tag created for this release (empty if no release) |
bump |
The bump level applied (major/minor/patch, empty if no release) |
floating-tag |
The floating major tag (e.g. v3, empty if disabled or no release) |
commit-count |
Number of commits included in this release |
released |
Whether a release was created (true/false) |
json |
Full release metadata as JSON (empty if no release) |
Download the latest release for your platform from Releases:
| Target | File |
|---|---|
| Linux x86_64 (glibc) | sr-x86_64-unknown-linux-gnu |
| Linux aarch64 (glibc) | sr-aarch64-unknown-linux-gnu |
| Linux x86_64 (musl/static) | sr-x86_64-unknown-linux-musl |
| Linux aarch64 (musl/static) | sr-aarch64-unknown-linux-musl |
| macOS x86_64 | sr-x86_64-apple-darwin |
| macOS aarch64 | sr-aarch64-apple-darwin |
| Windows x86_64 | sr-x86_64-pc-windows-msvc.exe |
The MUSL variants are statically linked and work on any Linux distribution (Alpine, Debian, RHEL, etc.). Prefer these for maximum compatibility.
mkdir -p ~/.local/bin
chmod +x sr-* && mv sr-* ~/.local/bin/srEnsure ~/.local/bin is on your $PATH.
cargo install --path crates/sr-clisr release calls the GitHub REST API directly — no external tools are needed. Authentication is via an environment variable:
export GH_TOKEN=ghp_xxxxxxxxxxxx # or GITHUB_TOKENThe GitHub Action sets this automatically via the github-token input. Dry-run mode (sr release --dry-run) works without a token.
sr works with GitHub Enterprise Server out of the box. The hostname is auto-detected from your git remote URL — changelog links, compare URLs, and API calls will point to the correct host automatically.
Set your GH_TOKEN (or GITHUB_TOKEN) environment variable with a token that has access to your GHES instance:
export GH_TOKEN=ghp_xxxxxxxxxxxxNo additional host configuration is needed — sr derives the API base URL from the git remote hostname automatically (e.g. ghes.example.com → https://ghes.example.com/api/v3).
srreads theoriginremote URL and extracts the hostname (e.g.ghes.example.com).- Changelog links and compare URLs use
https://<hostname>/owner/repo/...instead of hardcodedgithub.com. - REST API calls are routed to
https://<hostname>/api/v3/...automatically.
# Generate a default config file
sr init
# Preview what the next release would look like (includes changelog)
sr plan
# Dry-run a release (no side effects)
sr release --dry-run
# Execute the release
sr release
# Set up shell completions (bash)
sr completions bash >> ~/.bashrcsr ships a commit-msg git hook that enforces Conventional Commits at commit time. It reads allowed types and patterns from .urmzd.sr.yml, falling back to built-in defaults.
Option 1 — Native git hooks:
# Copy the hook into your project
curl -o .githooks/commit-msg https://raw.githubusercontent.com/urmzd/semantic-release/main/.githooks/commit-msg
chmod +x .githooks/commit-msg
git config core.hooksPath .githooks/Option 2 — pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/urmzd/semantic-release
rev: v0.5.0
hooks:
- id: conventional-commit-msgThe hook validates the first line of each commit message against the pattern <type>(<scope>): <description>. Merge commits and rebase-generated commits (fixup!, squash!, amend!) are always allowed through.
commit (hook validates) → push → sr plan (preview) → sr release (execute)
- Commit — the commit-msg hook ensures every commit follows the conventional format (
feat:,fix:,feat!:, etc.). - Preview — run
sr planto see the next version, included commits, and a changelog preview. - Dry-run — run
sr release --dry-runto simulate the full release without side effects (no tags created). - Release — run
sr releaseto execute the full pipeline:- Bumps version in configured manifest files
- Runs
build_commandif configured (withSR_VERSIONandSR_TAGenv vars) - Generates and commits the changelog (with version files)
- Creates and pushes the git tag
- Creates a GitHub release
- Outputs structured JSON to stdout (pipe to
jqfor custom workflows)
sr outputs structured JSON to stdout, making it easy to trigger post-release actions.
Use the action outputs to run steps conditionally:
- uses: urmzd/semantic-release@v1
id: sr
- if: steps.sr.outputs.released == 'true'
run: ./deploy.sh ${{ steps.sr.outputs.version }}
- if: steps.sr.outputs.released == 'true'
run: |
curl -X POST "$SLACK_WEBHOOK" \
-d "{\"text\": \"Released v${{ steps.sr.outputs.version }}\"}"Pipe sr release output to downstream scripts:
# Extract the version
VERSION=$(sr release | jq -r '.version')
# Feed JSON into a custom script
sr release | my-post-release-hook.sh
# Publish to a package registry after release
VERSION=$(sr release | jq -r '.version')
if [ -n "$VERSION" ]; then
npm publish
fisr release prints a JSON object to stdout on success:
{
"version": "1.2.3",
"previous_version": "1.2.2",
"tag": "v1.2.3",
"bump": "patch",
"floating_tag": "v1",
"commit_count": 4
}All diagnostic messages go to stderr, so stdout is always clean JSON (or empty on exit code 2).
| Command | Description |
|---|---|
sr release |
Execute a release (tag + GitHub release) |
sr plan |
Show what the next release would look like |
sr changelog |
Generate or preview the changelog |
sr version |
Show the next version |
sr config |
Validate and display resolved configuration |
sr init |
Create a default .urmzd.sr.yml config file |
sr completions |
Generate shell completions (bash, zsh, fish, powershell, elvish) |
sr release --dry-run— preview without making changessr release --force— re-release the current tag (for partial failure recovery)sr release --build-command 'npm run build'— run a command after version bump, before commitsr plan --format json— machine-readable outputsr changelog --write— write changelog to disksr version --short— print only the version numbersr config --resolved— show config with defaults appliedsr init --force— overwrite existing config filesr completions bash— generate Bash completions
| Code | Meaning |
|---|---|
0 |
Success — a release was created (or dry-run completed). The released version is printed to stdout. |
1 |
Real error — configuration issue, git failure, VCS provider error, etc. |
2 |
No releasable changes — no new commits or no releasable commit types since the last tag. |
Use --force to re-run a release that partially failed (e.g. the tag was created but artifact upload failed). Force mode only works when HEAD is exactly at the latest tag — it re-executes the release pipeline for that tag without bumping the version.
# Re-release the current tag after a partial failure
sr release --forceForce mode will error if:
- There are no tags yet (nothing to re-release)
- HEAD is not at the latest tag (there are new commits — use a normal release instead)
sr looks for .urmzd.sr.yml in the repository root. All fields are optional and have sensible defaults.
# Branches that trigger releases
branches:
- main
# Prefix for git tags
tag_prefix: "v"
# Changelog settings
changelog:
file: CHANGELOG.md # Path to the changelog file (optional)
template: null # Custom Jinja2 template (optional)
# Version files to bump automatically
version_files:
- Cargo.toml
# - package.json
# - pyproject.toml
# Shell command to run after version bump, before commit
# SR_VERSION and SR_TAG env vars are available
build_command: null
# Example: "cargo build --release"
# Override commit-type to bump-level mapping (merged with defaults)
commit_types: {}
# Example:
# docs: patch
# refactor: patch| Filename | Key updated | Notes |
|---|---|---|
Cargo.toml |
package.version (or workspace.package.version) |
Preserves formatting and comments |
package.json |
version |
Pretty-printed JSON output |
pyproject.toml |
project.version (or tool.poetry.version) |
Preserves formatting and comments |
| Type | Bump |
|---|---|
feat |
minor |
fix |
patch |
perf |
patch |
Breaking change (!) |
major |
All other types (e.g. chore, docs, ci) do not trigger a release unless overridden in commit_types.
| Crate | Description |
|---|---|
sr-core |
Pure domain logic — traits, config, versioning, changelog |
sr-git |
Git implementation (native git CLI) |
sr-github |
GitHub VCS provider (REST API) |
sr-cli |
CLI binary (clap) — wires everything together |
action.yml in the repo root is the GitHub Action composite wrapper.
sr uses a pluggable VcsProvider trait and currently ships with GitHub support. GitLab, Bitbucket, and other providers can be added as separate crates implementing the same trait.
| Trait | Purpose |
|---|---|
GitRepository |
Tag discovery, commit listing, tag creation, push |
VcsProvider |
Remote release creation (GitHub, GitLab, etc.) |
CommitParser |
Raw commit to conventional commit |
ChangelogFormatter |
Render changelog entries to text |
ReleaseStrategy |
Orchestrate plan + execute |
- Trunk-based flow — releases happen from a single branch; no release branches.
- Conventional commits as source of truth — commit messages drive versioning.
- Zero-config — works out of the box with reasonable defaults.
- Focused scope — sr handles versioning, tagging, changelog, and publishing. Pre-release validation and downstream actions belong in CI pipeline steps.
- Language-agnostic — sr knows about git and semver, not about cargo or npm.
Requires just for task running.
just init # Install clippy + rustfmt
just check # Run all checks (format, lint, test)
just build # Build workspace
just test # Run tests
just lint # Run clippy
just fmt # Format code
just run plan # Run the CLISee the Justfile for all available recipes.
See CONTRIBUTING.md for development setup, code style, and PR guidelines.