scrat 0.1.2

Release management tooling focused on sanity retention
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# scrat

[![CI](https://github.com/claylo/scrat/actions/workflows/ci.yml/badge.svg)](https://github.com/claylo/scrat/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/scrat.svg)](https://crates.io/crates/scrat)
[![docs.rs](https://docs.rs/scrat/badge.svg)](https://docs.rs/scrat)
[![MSRV](https://img.shields.io/badge/MSRV-1.89.0-blue.svg)](https://github.com/claylo/scrat)

**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](https://github.com/sindresorhus/np) for any ecosystem,
with built-in release notes via [git-cliff](https://git-cliff.org/).


## Table of Contents

- [Why]#why
- [Quick Start]#quick-start
- [The Pipeline]#the-pipeline
- [Commands]#commands
- [Configuration]#configuration
- [Hooks]#hooks
- [CLI Reference]#cli-reference
- [Installation]#installation
- [Development]#development
- [License]#license


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

```bash
# 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.

```bash
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.

```bash
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.

```bash
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.

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

### `scrat info`

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

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

### `scrat init`

Generates a scrat config file for a project.

```bash
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

```toml
# 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.

```toml
[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.

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

### Example: Full Hook Setup

```toml
[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)

```bash
brew install claylo/brew/scrat
```

### Pre-built Binaries

Download from the [releases page](https://github.com/claylo/scrat/releases).

Binaries available for:

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

### From Source

```bash
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]https://git-cliff.org/ | Changelog + release notes | `cargo install git-cliff` |
| [gh]https://cli.github.com/ | 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

- Rust toolchain per `rust-toolchain.toml` (currently 1.94.1)
- [just]https://github.com/casey/just
- [cargo-nextest]https://nexte.st/

### 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:

- Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE)
- MIT license ([LICENSE-MIT]LICENSE-MIT)

at your option.