Publishing
Publishing is plain cargo publish --registry acme. What matters is where it runs: from GitHub
Actions, every version gets provenance you can verify.
Trusted publishing from GitHub Actions
Add a workflow that publishes when you push a version tag:
name: publish
on:
push:
tags: ["v*"]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: cargo-bins/cargo-binstall@<commit sha> # pin to a commit
- run: cargo binstall --no-confirm cargo-credential-privatecrates
- run: cargo publish --registry acmeWhat happens when it runs:
- Cargo packages the crate and asks the credential provider for a publish token, passing the crate’s name, version and SHA-256 checksum.
- The provider requests an Actions OIDC token whose audience binds it to exactly those:
https://acme.privatecrates.dev/publish/story_engine/0.2.0/<sha256>. It is used once and never cached. - The registry accepts the upload only if the checksum of the bytes it received matches the audience, the job ran in the crate’s owning repository, and its workflow (and environment, if required) is allowed by the crate’s owners file.
- The crate is stored as an immutable GitHub release in your storage repository, together with the OIDC
token as
story_engine-0.2.0.provenance.jwt: a statement signed by GitHub, not by us, that this workflow run published exactly these bytes.
Because the token names one version and one checksum, it cannot be replayed to publish anything else.
Publishing a workspace works the same way: Cargo asks for one token per crate. Many crates can live in one
repository: each crate’s package.repository names that repository (a link to the crate’s
directory, such as https://github.com/acme/mono/tree/main/crates/foo, also works), and each
gets its own owners file.
A crate’s first publish
A new crate is first published from CI too, so its first version always has provenance. Its Cargo.toml must name a repository in your organisation, and the workflow must run in that repository:
[package]
name = "story_engine"
version = "0.2.0"
# The owning repository: it must be in your organisation.
# In a monorepo, a link to the crate's directory works too.
repository = "https://github.com/acme/story-engine"
# Publish only to your registry, never to crates.io by accident.
publish = ["acme"]The registry then creates the crate’s owners file, with that repository as the owner and the publishing workflow as the only one allowed.
The owners file
Each crate has owners/<name>.toml in the storage repository. Changing who may publish is an
administrator’s edit to this file, by pull request; there is deliberately no API for it.
repository_id = 123456789 # survives renames
repository = "acme/story-engine" # for humans
publish_workflows = ["publish.yml"] # may publish
publish_environment = "crates" # optional
allow_manual_publish = false # the defaultrepository_idis authoritative, so renaming or transferring the repository inside the organisation breaks nothing.publish_environmentrequires the job to run in a GitHub environment, so its protection rules apply: a required reviewer gives you a two-person rule, enforced by GitHub.
Publishing from a developer’s machine
By default it is refused, with instructions:
error: story_engine is published from CI only, so every version has verifiable provenance.
Add .github/workflows/publish.yml (see https://acme.privatecrates.dev/login#publish) and push a tag.A crate can opt in with allow_manual_publish = true in its owners file. Then anyone with push access
to the owning repository can publish from their machine. Those versions have no provenance, and the verifier and
search label them as manual publishes.
Yanking
Anyone with push access to the owning repository can yank or unyank a version from their machine, without provenance: yanking has to be quick, changes no bytes and can be undone.
cargo yank --registry acme --version 0.2.0 story_engine
cargo yank --registry acme --version 0.2.0 --undo story_engineVersions are permanent
Every version is kept forever: Cargo needs yanked versions to stay downloadable for existing lockfiles, and immutable releases make the history tamper-proof. A version number can never be reused, so fix a bad release by publishing a new version.
Names that exist on crates.io
A private crate whose name also exists on crates.io is a dependency-confusion risk. Publishing such a name
is refused unless name_clash = "warn" is set in privatecrates.toml. Someone could
also claim your crate’s name on crates.io later, so the registry checks daily and the verifier reports
clashes. For names that matter, reserve them on crates.io with a placeholder crate.