Skip to content

Configurable trunk-based semantic release CLI for Rust — conventional commits, changelog generation, git tagging, and GitHub releases

License

Notifications You must be signed in to change notification settings

urmzd/semantic-release

Repository files navigation

sr — Semantic Release

A single-binary, zero-dependency semantic release tool for any language.

CI

Why?

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 release to jq for custom CI pipelines.

Features

  • 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)

Installation

Shell installer (Linux/macOS)

curl -fsSL https://raw.githubusercontent.com/urmzd/semantic-release/main/install.sh | sh

The installer automatically adds ~/.local/bin to your PATH in your shell profile (.zshrc, .bashrc, or config.fish).

GitHub Action (recommended)

- uses: urmzd/semantic-release@v1
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}

Usage

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@v1

Dry-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' }}

Inputs

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) ""

Outputs

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)

Binary download

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/sr

Ensure ~/.local/bin is on your $PATH.

Build from source

cargo install --path crates/sr-cli

Prerequisites

sr 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_TOKEN

The GitHub Action sets this automatically via the github-token input. Dry-run mode (sr release --dry-run) works without a token.

GitHub Enterprise Server (GHES)

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.

Setup

Set your GH_TOKEN (or GITHUB_TOKEN) environment variable with a token that has access to your GHES instance:

export GH_TOKEN=ghp_xxxxxxxxxxxx

No additional host configuration is needed — sr derives the API base URL from the git remote hostname automatically (e.g. ghes.example.comhttps://ghes.example.com/api/v3).

How it works

  1. sr reads the origin remote URL and extracts the hostname (e.g. ghes.example.com).
  2. Changelog links and compare URLs use https://<hostname>/owner/repo/... instead of hardcoded github.com.
  3. REST API calls are routed to https://<hostname>/api/v3/... automatically.

Quick Start

# 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 >> ~/.bashrc

Developer Workflow

Commit message validation

sr 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-msg

The 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.

End-to-end release flow

commit (hook validates) → push → sr plan (preview) → sr release (execute)
  1. Commit — the commit-msg hook ensures every commit follows the conventional format (feat:, fix:, feat!:, etc.).
  2. Preview — run sr plan to see the next version, included commits, and a changelog preview.
  3. Dry-run — run sr release --dry-run to simulate the full release without side effects (no tags created).
  4. Release — run sr release to execute the full pipeline:
    • Bumps version in configured manifest files
    • Runs build_command if configured (with SR_VERSION and SR_TAG env 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 jq for custom workflows)

Post-release hooks

sr outputs structured JSON to stdout, making it easy to trigger post-release actions.

GitHub 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 }}\"}"

CLI

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
fi

JSON output schema

sr 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).

CLI Reference

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)

Common flags

  • sr release --dry-run — preview without making changes
  • sr 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 commit
  • sr plan --format json — machine-readable output
  • sr changelog --write — write changelog to disk
  • sr version --short — print only the version number
  • sr config --resolved — show config with defaults applied
  • sr init --force — overwrite existing config file
  • sr completions bash — generate Bash completions

Exit codes

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.

--force flag

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 --force

Force 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)

Configuration

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

Supported version files

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

Default commit-type mapping

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.

Architecture

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.

Core traits

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

Design Philosophy

  1. Trunk-based flow — releases happen from a single branch; no release branches.
  2. Conventional commits as source of truth — commit messages drive versioning.
  3. Zero-config — works out of the box with reasonable defaults.
  4. Focused scope — sr handles versioning, tagging, changelog, and publishing. Pre-release validation and downstream actions belong in CI pipeline steps.
  5. Language-agnostic — sr knows about git and semver, not about cargo or npm.

Development

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 CLI

See the Justfile for all available recipes.

Contributing

See CONTRIBUTING.md for development setup, code style, and PR guidelines.

About

Configurable trunk-based semantic release CLI for Rust — conventional commits, changelog generation, git tagging, and GitHub releases

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors