scrat 0.1.2

Release management tooling focused on sanity retention
Documentation

scrat

CI Crates.io docs.rs MSRV

Release management tooling focused on sanity retention.

scrat is a batteries-included release pipeline for projects that use git tags and GitHub releases. It detects your ecosystem, diffs your dependencies, collects stats, renders release notes, bumps versions, commits, tags, pushes, and creates a GitHub release—all in one command. Every step is on by default, every step is skippable, and hooks let you bolt on anything custom.

Think of it as np for any ecosystem, with built-in release notes via git-cliff.

Table of Contents

Why

Every release I've ever done by hand has the same steps: run tests, bump versions, update the changelog, commit, tag, push, create a GitHub release, attach assets, and try not to forget something. Most tools automate one or two of those steps. scrat automates all of them—and the ones it doesn't know about, you can wire up with hooks.

Design principles:

  • Batteries included, everything optional. Every built-in step is on by default. Pass --no-<step> to skip it. Set a value in config to override the built-in behavior. Or disable it entirely.
  • Thin CLI, fat core. The binary is a thin UI layer. All orchestration lives in scrat-core so other tools can embed it.
  • Hooks over built-ins for custom stuff. scrat doesn't know about your postcard generator or your quote corpus. Declare shell commands as hooks—they run at every phase boundary with variable interpolation.
  • Zero config works. Override what you want. Auto-detection handles ecosystem, version strategy, test commands, and publish commands. Config files only exist to override the defaults.

Quick Start

# Install
cargo install scrat

# See what scrat detects about your project
scrat info

# Check if you're ready to release
scrat preflight

# Preview release notes without shipping
scrat notes

# Dry run the full pipeline
scrat ship --dry-run

# Ship it
scrat ship

scrat ship is the main event. It runs every stage below, shows you the plan, asks for confirmation, and executes.

The Pipeline

scrat ship runs these stages in order. Each stage feeds structured data into a PipelineContext that flows through the whole pipeline. Hooks can read and mutate this context at every phase boundary.

1. Preflight

Checks release readiness before anything else runs.

  • Clean working directory (no uncommitted changes)
  • On the correct release branch (main or master, configurable)
  • git-cliff installed (required for release notes)

If any check fails, the pipeline stops. Run scrat preflight standalone to diagnose issues.

2. Version Resolution

Determines the next version. Three strategies, auto-detected:

Strategy When How
Conventional Commits git-cliff installed Analyzes commit messages to determine major/minor/patch
Explicit --version 1.2.3 passed Uses exactly what you give it
Interactive Fallback Shows recent commits, offers version candidates, you pick

scrat reads the current version from your project files (Cargo.toml, package.json, composer.json, pyproject.toml) and computes candidates from there. Go, Swift, and Ruby don't have a standard version file, so they use the interactive or explicit strategy.

3. Test

Runs your test suite. The command is auto-detected per ecosystem:

Ecosystem Detected Via Default Command
Rust Cargo.toml cargo test
Node package.json npm test
Go go.mod go test ./...
PHP composer.json composer test
Python pyproject.toml pytest
Ruby Gemfile bundle exec rake test
Swift Package.swift swift test
Generic (manual selection) (none — set commands.test)

Override with commands.test in config. Skip with --no-test.

4. Bump

Updates version numbers in project files and generates the changelog.

  • Writes the new version to Cargo.toml (Rust), package.json (Node), composer.json (PHP), or pyproject.toml (Python) — only if a version field already exists in the file
  • Go, Swift, and Ruby skip version-file rewrite (version lives in git tags or ecosystem-specific files like version.rb)
  • Runs git-cliff to update CHANGELOG.md
  • Reports which files were modified

Skip changelog generation with --no-changelog. Run scrat bump standalone to bump without shipping.

5. Dependency Diff

Diffs lockfiles between the previous tag and HEAD to find what changed.

Ecosystem Lockfile Parser
Rust Cargo.lock State machine over [[package]] blocks
Python uv.lock Same as Cargo.lock (identical TOML [[package]] format)
Go go.mod Line-oriented collect-and-merge (not go.sum — cleaner, no checksums)
PHP composer.lock State machine over JSON "name"/"version" pairs
Ruby Gemfile.lock Collect-and-merge on 4-space-indent gem lines
Swift Package.resolved JSON state machine on "identity"/"version"
Node package-lock.json (stub — returns empty, full parser planned)
Generic (none) Skipped

All parsers work on git diff output—not the full lockfile—so they're fast and don't need heavyweight format-specific dependencies. Results feed into release notes automatically.

Skip with --no-deps.

6. Stats Collection

Gathers release statistics from git:

  • Commit count
  • Files changed, insertions, deletions
  • Contributors and their commit counts

Uses git diff --shortstat and git shortlog. Results feed into release notes.

Skip with --no-stats.

7. Release Notes

Renders release notes using a two-pass git-cliff pattern:

  1. git-cliff --unreleased --context produces JSON with commits grouped by type
  2. scrat injects extra data (deps, stats, metadata) into the context's extra field
  3. git-cliff --from-context - --body <template> renders the final markdown

scrat ships a built-in template with: breaking changes, grouped commits with emoji, dependency changes (updated/added/removed), a stats table, and a "nerd drawer" with contributor details.

Point to your own template with release.notes_template in config or --template on scrat notes.

Skip with --no-notes. Falls back to --generate-notes (GitHub's auto-generated notes) if rendering fails.

8. Git

Commits, tags, and pushes.

  • git add . && git commit -m "chore: release {version}"
  • git tag -a v{version} -m "Release {version}"
  • git push origin {branch} && git push origin --tags

Fine-grained control:

Flag Effect
--no-git Skip entire phase (commit, tag, push)
--no-tag Commit and push, but don't create a tag
--no-push Commit and tag locally, don't push

9. GitHub Release

Creates (or updates) a GitHub release using gh.

  • Auto-detects edit vs. create: if a release already exists for the tag, it edits and re-uploads assets instead of failing. This makes scrat ship safe to re-run after a partial failure.
  • Draft by default: releases are created as drafts so you can review before publishing. Publish with gh release edit <tag> --draft=false.
  • Configurable title: release.title = "{repo} {tag}" with hook-style variable interpolation.
  • Assets: declare release.assets = ["dist/app.tar.gz", "checksums.txt"] in config. Hook commands produce these files; scrat attaches them.

Skip with --no-release. Override draft behavior with --draft / --no-draft.

10. Publish

Publishes to a package registry. Auto-detected:

Ecosystem Default Command
Rust cargo publish
Node npm publish
Python twine upload dist/*
Ruby gem push
Go (none — Go modules publish via git push)
Swift (none — Swift packages distribute via git URLs)
PHP (none — set commands.publish if needed)

Skip with --no-publish. Override with commands.publish in config.

Commands

scrat ship

The full release pipeline. Runs all stages above, with confirmation prompt.

scrat ship                    # interactive — asks for confirmation
scrat ship --dry-run          # preview without changes
scrat ship --version 2.0.0    # explicit version
scrat ship --no-publish -y    # skip publish, skip confirmation
scrat ship --draft            # force draft mode (overrides config)

scrat notes

Renders release notes without shipping. Useful for previewing what the notes will look like.

scrat notes                          # preview notes for current version
scrat notes --from v1.0.0            # diff against specific tag
scrat notes --version 2.0.0          # render as if releasing 2.0.0
scrat notes --template my-notes.tera # use custom template
scrat notes --json                   # output raw context as JSON

scrat bump

Bumps version and generates changelog without shipping.

scrat bump                    # interactive version selection
scrat bump --version 1.2.3    # explicit version
scrat bump --dry-run          # preview without changes
scrat bump --no-changelog     # skip changelog generation

scrat preflight

Checks release readiness.

scrat preflight               # run all checks
scrat preflight --json        # machine-readable output

scrat info

Shows project information: detected ecosystem, version, tools, config paths.

scrat info                    # human-readable
scrat info --json             # machine-readable

scrat init

Generates a scrat config file for a project.

scrat init                    # interactive — asks for ecosystem, version strategy

scrat doctor

Diagnoses configuration and environment issues.

Configuration

Config files are discovered automatically. Precedence (highest first):

  1. Explicit file via --config <path>
  2. .config/scrat.toml in current directory (walks up to .git boundary)
  3. .scrat.toml / scrat.toml in current directory (walks up to .git boundary)
  4. ~/.config/scrat/config.toml (user config)
  5. Built-in defaults

Supported formats: TOML, YAML, JSON.

Zero config works. Everything below is optional—only set what you want to override.

Full Reference

# Log level: debug, info, warn, error
log_level = "info"

# Directory for JSONL log files (default: platform-specific)
# log_dir = "/var/log/scrat"

[project]
# Override detected ecosystem: rust, node, go, php, python, ruby, swift, generic
# type = "rust"
# Override release branch (default: auto-detect main/master)
# release_branch = "main"

[version]
# Override version strategy: conventional-commits, interactive, explicit
# strategy = "conventional-commits"
# Use your own cliff.toml for version computation instead of scrat's built-in
# cliff_config = "cliff.toml"

[commands]
# Override per-phase commands (default: auto-detected per ecosystem)
# test = "just test"
# build = "cargo build --release"
# publish = "cargo publish"
# clean = "cargo clean"

[release]
# Create GitHub releases (default: true)
# github_release = true

# Create as draft — review before publishing (default: true)
# draft = true

# Title format with variable interpolation (default: tag name)
# title = "{repo} {tag}"

# GitHub Discussions category (only for new releases)
# discussion_category = "releases"

# Custom git-cliff template for release notes
# notes_template = "templates/my-notes.tera"

# Files to attach to the GitHub release
# assets = ["dist/release-card.png", "dist/checksums.txt"]

[hooks]
# Shell commands at each phase boundary.
# See the Hooks section for details.
# post_bump = ["ll-graphics generate --version {version} --output dist/release-card.png"]

[ship]
# Prompt for confirmation before executing (default: true)
# Set to false for CI/scripted use. --yes/-y flag also skips.
# confirm = true

# Skip pipeline phases permanently (equivalent to --no-* CLI flags).
# CLI flags override these — passing --no-publish on a run where
# no_publish is already true in config is harmless.
# no_changelog = false
# no_publish = false
# no_push = false
# no_release = false
# no_deps = false
# no_stats = false
# no_notes = false
# no_test = false
# no_tag = false
# no_git = false

Hooks

Hooks are shell commands that run at phase boundaries during the ship workflow. Declare them in config as lists of strings.

Hook Points

12 hook points across 6 phases:

Hook When
pre_ship / post_ship Before/after the entire workflow
pre_test / post_test Before/after the test phase
pre_bump / post_bump Before/after version bump + changelog
pre_tag / post_tag Before/after git commit + tag + push
pre_release / post_release Before/after GitHub release creation
pre_publish / post_publish Before/after registry publish

Variable Interpolation

Commands support {var} placeholders:

Variable Example Value
{version} 1.2.3
{prev_version} 1.1.0
{tag} v1.2.3
{changelog_path} CHANGELOG.md
{owner} claylo
{repo} scrat

Execution Model

Commands run in parallel by default. Two prefixes alter execution:

sync: — barrier. All prior commands finish, the sync command runs alone, then subsequent commands resume in parallel.

[hooks]
post_bump = [
    "generate-image --version {version}",
    "generate-checksums",
    "sync: validate-artifacts",  # waits for both above, runs alone
    "upload-to-cdn",             # resumes parallel
]

filter: — barrier + JSON piping. Like sync:, but the command also receives the full PipelineContext as JSON on stdin and must return valid JSON on stdout. The output replaces the pipeline context. This lets you mutate built-in step output without replacing the whole step.

[hooks]
post_bump = [
    "filter: jq '[.dependencies[] | select(.name != \"dev-dep\")]'",
]

Example: Full Hook Setup

[hooks]
post_bump = [
    "ll-graphics release-postcard --tag {tag} --output dist/release-card.png",
]
pre_release = [
    "sync: test -f dist/release-card.png",
]

[release]
assets = ["dist/release-card.png"]

CLI Reference

Global Options

Flag Description
-c, --config <FILE> Explicit config file path
-C, --chdir <DIR> Run as if started in DIR
-v, --verbose More detail (repeatable: -vv)
-q, --quiet Only print errors
--json Machine-readable JSON output
--color <auto|always|never> Colorize output

scrat ship Flags

Step control — every pipeline step is skippable:

Flag Skips
--no-test Test phase
--no-changelog Changelog generation (during bump)
--no-publish Registry publish
--no-deps Dependency diff
--no-stats Stats collection
--no-notes Release notes rendering
--no-tag Git tag (still commits and pushes)
--no-push Git push (still commits and tags locally)
--no-git Entire git phase (commit, tag, push)
--no-release GitHub release creation

Other options:

Flag Description
--version <VERSION> Set version explicitly
--draft Force draft mode (overrides config)
--no-draft Force published mode (overrides config)
--dry-run Preview without making changes
-y, --yes Skip confirmation prompt

Crate Organization

  • scrat - The CLI binary. Handles argument parsing, command dispatch, and user interaction.
  • scrat-core - The core library. Contains configuration loading, error types, and shared functionality.

Installation

Homebrew (macOS and Linux)

brew install claylo/brew/scrat

Pre-built Binaries

Download from the releases page.

Binaries available for:

  • macOS (Apple Silicon and Intel)
  • Linux (x86_64 and ARM64, glibc and musl)
  • Windows (x86_64 and ARM64)

From Source

cargo install scrat

Shell Completions

Shell completions for Bash, Zsh, and Fish are included in release archives and Homebrew installs. They are generated at build time via xtask, not a runtime subcommand.

Prerequisites

scrat shells out to these tools when their features are used:

Tool Required For Install
git-cliff Changelog + release notes cargo install git-cliff
gh GitHub release creation brew install gh

Development

Workspace layout:

crates/
  scrat/       # CLI binary — thin UI layer
  scrat-core/  # Core library — all orchestration and logic
xtask/         # Build automation (man pages, completions, install)

Requirements

Build Tasks

Command Description
just check Format + clippy + deny + test + doc-test
just test Run tests with nextest
just clippy Run clippy lints
just fmt Format with rustfmt
just cov Coverage report
just deny Security/license audit

Architecture

  • Thin CLI, fat core: the binary parses args, calls core, maybe prompts the user, displays results. If you're importing deps, stats, detect, git, or pipeline in the CLI crate, it belongs in core.
  • Plan/execute pattern: plan_ship() returns Ready or NeedsInteraction. The CLI only prompts on the latter. ReadyShip::execute() runs the pipeline with event callbacks for progress display.
  • Error handling: thiserror in the library, anyhow in the binary.
  • Safe Rust only: #![deny(unsafe_code)] workspace-wide.

License

Licensed under either of:

at your option.