Brings GoReleaser's declarative release workflow to Rust with native Cargo workspace awareness. Exposes build orchestration, cross-compilation via cargo-zigbuild or cross, deterministic artifact generation, and multi-platform packaging (deb, rpm, snap, DMG, MSI). Handles the full pipeline: bump Cargo.toml and Cargo.lock atomically, build for multiple targets, generate checksums and SBOMs, sign with cosign or GPG, publish to crates.io with dependency-ordered polling, push GitHub releases, and announce to Discord or Slack. The tag and bump commands rewrite version files and commit in one shot, avoiding the usual lockfile drift. If you ship Rust binaries and want reproducible releases without stitching together a dozen CI jobs, this wires the whole stack together from a single YAML config.
The release pipeline built for Rust — workspace-aware, reproducible, and signed by default.
Anodizer reads a declarative config file and runs your entire release from a single anodizer release command: build, archive, checksum, changelog, sign, release, publish, and announce. It's built around the Rust ecosystem — Cargo workspaces, Cargo.lock-aware version bumps, crates.io, and byte-reproducible artifacts.
Written by Claude; maintained by us.
See What works (with proof) for a per-feature status — every "live" claim links to a real published artifact you can verify yourself.
Your release is a Cargo workspace — not a bag of loose binaries. anodizer is built that way from the ground up.
anodizer tag and anodizer bump rewrite Cargo.toml and Cargo.lock, then commit and tag — locally by default, and (on --push) push the bump commit and tag atomically — no orphaned bump commit, no hand-rolled git push, no lockfile drift.cargo-zigbuild or cross. No rustup target add rituals, no per-target CI shards to babysit.anodizer check determinism rebuilds them and byte-compares. "Reproducible" becomes a fact your CI enforces, not a claim in your release notes.Then the long tail that Rust authors actually hit: generated per-crate READMEs, cargo-binstall metadata derived straight from your config (no hand-maintained pkg-url that 404s), version_files to pin your docs and install scripts to the released version, and post-release install smoke tests that catch a broken artifact before your users do.
Already know GoReleaser? anodizer's {{ .Field }} template syntax will feel right at home. Moving from cargo-dist, release-plz, or cargo-release? The migration guides map your setup straight over.
Build
cargo-zigbuild, cross, or native cargo buildPackage
curl | sh install-script generation — a per-platform asset table derived from your build/archive config (no hand-maintained URLs to 404), with a checksum-verify gateshasum -c) formatSign
Publish
homebrew-core formula)docker buildxAnnounce
Advanced
{{ .Field }} syntaxmod_timestamp and builds_infoversion_files) to keep docs, scripts, and manifests in lockstep at taganodizer promote) — flip an already-published artifact to a stable track (snapcraft channels, npm dist-tags, OCI floating tags, GitHub prerelease) with no rebuildbrew install tj-smith47/tap/anodizer
cargo install anodizer
git clone https://github.com/tj-smith47/anodizer.git
cd anodizer
cargo install --path crates/cli
# Generate a starter config from your Cargo workspace
anodizer init > .anodizer.yaml
# Validate your config
anodizer check
# Check that required tools are available
anodizer healthcheck
# Build a snapshot (no publishing)
anodizer release --snapshot
# Dry run (full pipeline, no side effects)
anodizer release --dry-run
# Bump crate versions from Conventional Commits — rewrites Cargo.toml + Cargo.lock,
# no tag (tag separately, or let the release workflow tag for you)
anodizer bump --dry-run --output json # preview the per-crate plan as JSON
anodizer bump # infer patch/minor/major per crate since its last tag
anodizer bump minor --package mycrate # force a level for one crate
anodizer bump --workspace --commit --changelog # bump every member, commit, refresh CHANGELOG.md
anodizer bump --pre rc.1 # append a prerelease identifier (0.5.0-rc.1)
# Auto-tag from commit directives
# (Conventional Commits: feat: → minor, fix: → patch, BREAKING CHANGE: → major)
anodizer tag --dry-run # preview what tag would be created
anodizer tag --push --changelog # tag + refresh changelog + push (triggers the release)
anodizer tag --sign --push # signed annotated tag (uses git user.signingkey)
anodizer tag --custom-tag v0.1.0 # force a tag value, bypassing bump derivation
anodizer tag --version 1.2.3 --crate mycrate # pin an exact version for recovery (per-crate mode)
anodizer tag rollback "$GITHUB_SHA" # delete anodize-managed tags at a SHA + revert the bump
# Resume a stalled release from a populated dist/ — no rebuild
anodizer continue # resume the publish-only pipeline after a transient failure
anodizer continue --merge # fan-in split-build workers, then sign/checksum/release/publish
anodizer continue --skip npm,announce # resume but skip named stages/publishers
# Run only the publish stages (release / blob / publish) from a completed dist/
anodizer publish --publishers homebrew,scoop # publish to a subset of configured publishers
anodizer publish --dry-run # preview the publish chain with no side effects
# Promote an already-published artifact to a stable track — no rebuild
anodizer promote --to stable # promote the newest prerelease artifact
anodizer promote --to stable --from candidate --dry-run # preview promoting from a named source track
anodizer promote --to stable --version v1.2.0 \
--publishers snapcraft,npm # promote a pinned version on chosen publishers
For CI-based releases, set GITHUB_TOKEN (or ANODIZER_GITHUB_TOKEN) as a secret — the release pipeline picks it up automatically.
Anodizer uses .anodizer.yaml (or .anodizer.toml) in your project root. Add a schema comment for editor autocomplete:
# yaml-language-server: $schema=https://tj-smith47.github.io/anodizer/schema.json
project_name: myapp
defaults:
targets:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
- x86_64-apple-darwin
- aarch64-apple-darwin
- x86_64-pc-windows-msvc
cross: auto
crates:
- name: myapp
path: "."
tag_template: "v{{ Version }}"
builds:
- binary: myapp
archives:
- name_template: "{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}"
files: [LICENSE, README.md]
release:
github:
owner: myorg
name: myapp
publish:
cargo: {}
homebrew:
repository:
owner: myorg
name: homebrew-tap
See the full configuration reference and the template reference for all available fields, variables, and filters.
cfgd — declarative, GitOps-style machine configuration management — is anodizer's first real-world adopter and dogfoods every shipped publisher. It's a 4-crate workspace (shared lib + CLI + Kubernetes operator + CSI driver) that ships to crates.io (dependency-aware ordering), GitHub Releases, Homebrew, Scoop, Chocolatey, Winget, the Snap Store, Krew, GHCR, and via cargo binstall — all from one .anodizer.yaml and one tag push.
A condensed slice of cfgd's .anodizer.yaml:
workspaces:
- name: cfgd-core
crates:
- name: cfgd-core
tag_template: "core-v{{ Version }}"
version_sync: { enabled: true, mode: cargo }
- name: cfgd
crates:
- name: cfgd
depends_on: [cfgd-core]
version_sync: { enabled: true, mode: cargo }
universal_binaries:
- name_template: "{{ ProjectName }}"
replace: false
binstall:
enabled: true # pkg-url + per-target overrides derived from archive.name_template
# ... cfgd-operator, cfgd-csi
Every cell of What works (with proof) links to a real published cfgd artifact for the feature in question — that's the verification surface.
Anodizer ships a first-party action, tj-smith47/anodizer-action, which is what this repo dogfoods in its own release.yml:
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- name: Release
uses: tj-smith47/anodizer-action@v1
with:
version: latest # accepts `latest`, `nightly`, or an exact tag (e.g. `v0.5.0`). Pin in production.
auto-install: true # auto-detect nfpm/makeself/snapcraft/cosign/etc from .anodizer.yaml
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
For split/merge fan-out, GPG key import, registry login, and per-platform variants, see anodizer-action and the live .github/workflows/release.yml in this repo.
anodizer release Full release pipeline (--snapshot, --dry-run, --split/--merge, --publish-only)
anodizer tag Auto-tag from commit directives
anodizer tag rollback Delete anodize-managed tags at a SHA and revert the bump commit
anodizer bump Bump crate versions (Conventional Commits → semver level) without tagging
anodizer promote Promote a published artifact to a stable track without rebuilding
anodizer continue Resume a stalled or split release from a populated dist/ (--merge to fan-in)
anodizer publish Run only the publish stages (release, blob, publish) from a completed dist/
anodizer check Validate configuration + run determinism harness
anodizer preflight Verify the environment can run the configured release (tools, secrets, key material)
anodizer init Generate starter .anodizer.yaml
anodizer healthcheck Probe external tools (nfpm, cosign, ...)
Failure handling is in-process: a failed anodizer release executes the
release.on_failure policy itself (rollback by default — delete the tag,
revert the bump — auto-degrading to hold once a one-way-door publisher like
crates.io has landed), so release workflows need no if: failure() recovery
steps. anodizer tag rollback "$GITHUB_SHA" is the manual recovery command
for killed or held runs. See
Release resilience
for the flag matrix and recovery flows.
Full reference: anodizer --help or the docs site.
Full documentation is available at tj-smith47.github.io/anodizer.
Operator guides:
anodizer check determinism harness, runtime allow-listLicensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.