Problem
Long-lived API tokens are the wrong default for a CI publishing tool in 2026. Shipper's value proposition is safe CI publishing, yet today's release.yml relies on secrets.CARGO_REGISTRY_TOKEN—a broad, persistent credential stored in GitHub Secrets that grants permanent publish authority. If leaked, it stays valid until manually rotated. For a tool whose entire mission is publishing safety, this is backwards.
What Trusted Publishing is
Trusted Publishing replaces long-lived tokens with ephemeral credentials. During a GitHub Actions workflow, the platform issues a short-lived OIDC identity token that proves workflow provenance; Shipper exchanges it for a temporary crates.io token (valid minutes, not years). No token stored in secrets; no permanent publish authority needed.
Reference: RFC 3691: Trusted Publishing for crates.io
Current state in Shipper
Token resolution (crates/shipper/src/ops/auth/):
mod.rs lines 18–31: Resolution order (env vars → credentials.toml → legacy credentials)
oidc.rs: OIDC detection (is_trusted_publishing_available()) checking for ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN
resolver.rs: Resolves actual tokens; integrates with Cargo's credential chain
Workflow (.github/workflows/release.yml):
- Line 229: Preflight uses
env: { CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} }
- Line 259: Publish uses the same long-lived token secret
- Lines 407, 485: Rehearse and resume modes also depend on the secret
- No
permissions: { id-token: write } (required for OIDC)
Proposed change
-
Detect Trusted Publishing in preflight — Check ACTIONS_ID_TOKEN_REQUEST_URL + ACTIONS_ID_TOKEN_REQUEST_TOKEN presence. If both set, route through OIDC exchange instead of falling back to CARGO_REGISTRY_TOKEN.
-
Validate workflow permissions — If OIDC env is detected but id-token: write is not explicitly set in the workflow, preflight should emit a clear, actionable error ("Workflow has OIDC env but missing permissions: { id-token: write }; token exchange will fail at publish time") rather than silently proceeding to a 401 at publish time.
-
Diagnostic clarity — When Trusted Publishing is configured but misfire (missing repo trust link on crates.io, missing workflow permissions, malformed ID token, etc.), Shipper must surface the specific failure mode. Generic "401 Unauthorized" is unhelpful; we need "crates.io has not linked this GitHub repo; re-publish one crate manually to establish trust" or similar.
-
Fallback ordering — Try OIDC first (via the rust-lang/crates-io-auth-action in the workflow); if unavailable/absent, fall back to existing token resolution (CARGO_REGISTRY_TOKEN → env-registry vars → credentials.toml).
-
Documentation — Runbook (likely docs/operator-release-runbook.md) gets a new "Trusted Publishing Setup" section covering: (a) one-time crate registration on crates.io, (b) one-time repo link in crates.io UI, (c) updated release.yml with action invocation + permissions block, (d) troubleshooting common setup mistakes.
Cargo's role
Cargo itself does not yet handle OIDC exchange; per RFC 3691, this is delegated to a separate rust-lang/crates-io-auth-action GitHub Action (already exists and widely adopted, e.g. pyo3, cc crate). The action exchanges the ID token for a short-lived access token and writes it to the environment or a temp file. Shipper (and Cargo) consume the token via standard CARGO_REGISTRY_TOKEN env var.
Future: RFC notes that Cargo may eventually embed the exchange, but that's not the default today. Shipper should not block on Cargo integration; the action-based flow is stable and production-ready.
Acceptance criteria
Problem
Long-lived API tokens are the wrong default for a CI publishing tool in 2026. Shipper's value proposition is safe CI publishing, yet today's
release.ymlrelies onsecrets.CARGO_REGISTRY_TOKEN—a broad, persistent credential stored in GitHub Secrets that grants permanent publish authority. If leaked, it stays valid until manually rotated. For a tool whose entire mission is publishing safety, this is backwards.What Trusted Publishing is
Trusted Publishing replaces long-lived tokens with ephemeral credentials. During a GitHub Actions workflow, the platform issues a short-lived OIDC identity token that proves workflow provenance; Shipper exchanges it for a temporary crates.io token (valid minutes, not years). No token stored in secrets; no permanent publish authority needed.
Reference: RFC 3691: Trusted Publishing for crates.io
Current state in Shipper
Token resolution (
crates/shipper/src/ops/auth/):mod.rslines 18–31: Resolution order (env vars → credentials.toml → legacy credentials)oidc.rs: OIDC detection (is_trusted_publishing_available()) checking forACTIONS_ID_TOKEN_REQUEST_URLandACTIONS_ID_TOKEN_REQUEST_TOKENresolver.rs: Resolves actual tokens; integrates with Cargo's credential chainWorkflow (
.github/workflows/release.yml):env: { CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} }permissions: { id-token: write }(required for OIDC)Proposed change
Detect Trusted Publishing in preflight — Check
ACTIONS_ID_TOKEN_REQUEST_URL+ACTIONS_ID_TOKEN_REQUEST_TOKENpresence. If both set, route through OIDC exchange instead of falling back toCARGO_REGISTRY_TOKEN.Validate workflow permissions — If OIDC env is detected but
id-token: writeis not explicitly set in the workflow, preflight should emit a clear, actionable error ("Workflow has OIDC env but missingpermissions: { id-token: write }; token exchange will fail at publish time") rather than silently proceeding to a 401 at publish time.Diagnostic clarity — When Trusted Publishing is configured but misfire (missing repo trust link on crates.io, missing workflow permissions, malformed ID token, etc.), Shipper must surface the specific failure mode. Generic "401 Unauthorized" is unhelpful; we need "crates.io has not linked this GitHub repo; re-publish one crate manually to establish trust" or similar.
Fallback ordering — Try OIDC first (via the
rust-lang/crates-io-auth-actionin the workflow); if unavailable/absent, fall back to existing token resolution (CARGO_REGISTRY_TOKEN→ env-registry vars → credentials.toml).Documentation — Runbook (likely
docs/operator-release-runbook.md) gets a new "Trusted Publishing Setup" section covering: (a) one-time crate registration on crates.io, (b) one-time repo link in crates.io UI, (c) updatedrelease.ymlwith action invocation +permissionsblock, (d) troubleshooting common setup mistakes.Cargo's role
Cargo itself does not yet handle OIDC exchange; per RFC 3691, this is delegated to a separate
rust-lang/crates-io-auth-actionGitHub Action (already exists and widely adopted, e.g. pyo3, cc crate). The action exchanges the ID token for a short-lived access token and writes it to the environment or a temp file. Shipper (and Cargo) consume the token via standardCARGO_REGISTRY_TOKENenv var.Future: RFC notes that Cargo may eventually embed the exchange, but that's not the default today. Shipper should not block on Cargo integration; the action-based flow is stable and production-ready.
Acceptance criteria
permissions: { id-token: write }is set (or emits clear error)release.ymlworkflow usesrust-lang/crates-io-auth-action@v1(or latest stable)release.ymlhaspermissions: { contents: write, id-token: write }id-token: writeor the action still work viaCARGO_REGISTRY_TOKEN