Note
This repository is vibe coded with Claude.
A template for a tested, linted and publishable Rust command-line program:
a Cargo workspace with one example program (a library plus a thin binary),
rustfmt, clippy -D warnings, a 100% line-coverage gate
(cargo-llvm-cov), cargo-deny, cargo-semver-checks, and a release that
publishes the crate to crates.io through trusted publishing and then ships the
binary: native archives for Linux (static), macOS and Windows on x64 and
arm64, wheels on PyPI, npm packages, a Homebrew formula, a container image and
a pre-commit hook, all with build provenance attestations. It is itself an
instance of
template-rust-crate (so
it gets the quality gates and the crates.io release from there, and through
it the devcontainer, CI, release workflow and template sync from
template-base) and is itself a
template: a CLI created from it receives fixes through template sync. See
docs/TEMPLATE.md for the conventions shared by every
instance ("Rust CLI template notes" for what this template adds), and
docs/adrs/0002-rust-cli-template.md
for why it is built this way.
-
On GitHub, use "Use this template" on this repository (or
gh repo create <org>/<name> --template craftainer/template-rust-cli). -
Rename the program. Decide the names first: the repository, the crate and binary (usually the same), the PyPI distribution, the npm root package and the npm scope for the six platform packages. These places move together:
Cargo.toml:[package] name(template-rust-cli), the description,[workspace.package] repository(it must be thehttps://github.com/<owner>/<repo>URL of the repository that releases: npm provenance and the Homebrew URLs derive from it), andkeywordsandcategoriesfor crates.io. If the binary should be named differently from the crate, add a[[bin]]table with itsname;src/lib.rs: thetemplate_rust_cli::path in the doctest and the program name inUSAGEand in the--versionoutput;src/main.rs: thetemplate_rust_cli::runpath;tests/cli.rs: theCARGO_BIN_EXE_template-rust-clivariable (it carries the binary's name) and everytemplate_rust_cli::path;pyproject.toml:[project] name,description, the URL;packaging.toml:[npm] package(the root npm package, which the CLI's users install) andscope(the organisation of the platform packages, for example@craftainer-labs; they become<scope>/<binary>-<os>-<cpu>);.pre-commit-hooks.yaml: the hookid,name,description,entryandargs;- this file's title and description.
Then run
cargo update --workspaceto refreshCargo.lockfor the new name. Nothing else names the CLI: the workflows,dist-workspace.tomlandscripts/binaries/read the name fromCargo.tomlandpackaging.toml, and are template-owned (the container image's/<binary>included), as isscripts/test_release_binaries.py, which derives every name and npm scope it expects from those two files. -
Point template sync at this template.
.github/template-sync-state.jsonhere records thetemplate-rust-cratetag this template itself last synced from; the copy in a new CLI must instead name this repository and the tag it was created at (the latest release ofcraftainer/template-rust-cli):{"template_repo": "craftainer/template-rust-cli", "synced_tag": "vX.Y.Z"}Add a
TEMPLATE_SYNC_TOKENsecret too (see.github/workflows/README.md, "Template sync"). Files the CLI owns beyond the manifest'signoretier go in.github/template-sync-manifest.local.ymlunderignore:. -
Open the repository in the devcontainer, then run
prek run --all-files --hook-stage manual; everything should pass before you write any code. -
Keep
LICENSE(GPL-3.0) anddeny.toml's license allow-list, or change both together (andlicenseinCargo.tomlandpyproject.toml).
make publish-packages is a no-op (with a ::warning:: in the log) until you opt in, per repository, by setting the
repository variable CARGO_PUBLISH to any non-empty value (Settings,
Secrets and variables, Actions, Variables). The version lives only in the
git tag release.yml computes; Cargo.toml stays at 0.0.0 and
make build stamps RELEASE_VERSION into the build without committing it.
crates.io has no "pending publisher": a trusted publisher can only be added to a crate that already exists. So the first release of each crate uses a short-lived bootstrap token, and trusted publishing takes over after it:
- Create a crates.io API token scoped to the new crate name
(
publish-newonly for the first release) with a short expiry, and store it as the repository secretCARGO_REGISTRY_TOKEN. - Set
CARGO_PUBLISH=1and run the release workflow.make publish-packagessees the token and publishes with it. - On crates.io, open the crate's Settings, Trusted Publishing, and add a
GitHub publisher: the owner, the repository, the workflow
release.yml, and no environment. - Delete the
CARGO_REGISTRY_TOKENsecret and revoke the token. From the next release,make publish-packagesfinds no token and exchanges the job's GitHub OIDC identity for a 30-minute crates.io token, publishes with it and revokes it.
The Release workflow (above) does not build the binaries. Right after it
succeeds, dispatch the second workflow with the same tag, and watch it:
gh workflow run release-binaries.yml -f tag=vX.Y.Z
gh run watchIt builds the six targets on native runners, attests everything
(SLSA Build L3, verify with
gh attestation verify <file> --repo <owner>/<repo>), attaches the archives,
their checksums and the Homebrew formula to the GitHub release, and then
publishes to whichever of these you switched on with a repository variable:
| Variable | Publishes | Needs |
|---|---|---|
CLI_PYPI_PUBLISH = pypi or testpypi |
wheels and sdist, with PEP 740 attestations | a PyPI trusted publisher (a pending one works for the first release) naming workflow release-binaries.yml and environment release |
CLI_NPM_PUBLISH = any value |
the six <scope>/<binary>-<os>-<cpu> packages, then the root package |
an npm trusted publisher per package naming release-binaries.yml and release; the first publish of all seven is staged with the stage-only secret NPM_TOKEN and approved by hand, as for the family's npm packages |
HOMEBREW_TAP = <owner>/homebrew-<tap> |
commits Formula/<binary>.rb to the tap |
the secret HOMEBREW_TAP_TOKEN (write access to the tap) |
CLI_IMAGE_PUBLISH = any value |
ghcr.io/<owner>/<repo>:<version> (and latest for a stable release), multi-arch |
nothing beyond GITHUB_TOKEN |
Create the GitHub environment release first (the PyPI, npm and Homebrew
jobs run in it, so you can add required reviewers there). The workflow only
runs from the default branch, for a published release whose tag is on it, and
can be re-run for the same tag. Details, and why it is a manual second step,
are in docs/TEMPLATE.md, "Rust CLI template notes".
Using the CLI as a hook in other repositories (.pre-commit-hooks.yaml):
repos:
- repo: https://github.com/<owner>/<repo>
rev: vX.Y.Z
hooks:
- id: <hook id>Licensed under the GNU General Public License v3.0.