From 4bf095c69d471fa3d38d848ef3753fa18b99e3ad Mon Sep 17 00:00:00 2001 From: spacedevin Date: Mon, 3 Aug 2026 15:05:57 -0700 Subject: [PATCH] fix(ci): let the crates.io publish bootstrap a crate that does not exist yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v1.2.1 release failed to publish deckfile: Status: 400. No Trusted Publishing config found for repository `spacedevin/deck`. Trusted Publishing config is attached to a crate you already own, so it cannot publish a crate that has never been published — there is nothing to attach the config to. The workflow only had that path, so the very first publish, the one that claims the name, was the one case it could not do. Adds a CARGO_REGISTRY_TOKEN fallback used only to bootstrap: set the secret, release once to claim `deckfile`, configure Trusted Publishing, then delete the secret and this reverts to OIDC on its own. The long-lived token exists only for as long as the bootstrap takes, which keeps the steady state the same as npm-release.yml's. The token presence is detected in a step that reads it through `env` and writes an output, because the `secrets` context is not available in a step-level `if` — gating directly on `secrets.CARGO_REGISTRY_TOKEN` there would have silently evaluated empty and always taken the OIDC path. Also fails with an actionable message naming both options rather than whatever cargo says about a missing credential. --- .github/workflows/crates-release.yml | 61 +++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/.github/workflows/crates-release.yml b/.github/workflows/crates-release.yml index 689f199..295ed27 100644 --- a/.github/workflows/crates-release.yml +++ b/.github/workflows/crates-release.yml @@ -5,14 +5,20 @@ # rather than committed: one source, and the published crate cannot drift from the published npm # package because both come out of the same tag. # -# AUTH: crates.io Trusted Publishing (OIDC, no long-lived token), matching npm-release.yml. -# One-time setup on crates.io for `deckfile`: Settings > Trusted Publishing > GitHub Actions: -# Repository owner: spacedevin -# Repository name: deck -# Workflow filename: crates-release.yml -# Environment: (leave blank) -# Trusted Publishing must be configured before the FIRST publish; if the crate name is not yet -# claimed, do the initial `cargo publish` locally, then wire OIDC and let CI own it from then on. +# AUTH: crates.io Trusted Publishing (OIDC, no long-lived token), matching npm-release.yml — with a +# CARGO_REGISTRY_TOKEN fallback used only to bootstrap. +# +# Trusted Publishing config is attached to a crate you already own, so it cannot publish a crate that +# does not exist yet. Bootstrapping, once: +# 1. Add CARGO_REGISTRY_TOKEN as a repo secret and run this workflow — that claims `deckfile`. +# 2. Configure Trusted Publishing on crates.io: +# deckfile > Settings > Trusted Publishing > GitHub Actions +# Repository owner: spacedevin +# Repository name: deck +# Workflow filename: crates-release.yml +# Environment: (leave blank) +# 3. DELETE the secret. The step below falls back to OIDC automatically, so the long-lived token +# exists only for as long as the bootstrap takes. name: Crates.io release @@ -89,8 +95,33 @@ jobs: working-directory: crate run: cargo test - - name: Authenticate to crates.io (Trusted Publishing) + # Two auth paths, because Trusted Publishing alone cannot bootstrap a crate that does not exist + # yet: a config is attached to a crate you already own, so there is nothing to attach to before + # the first publish. That is exactly how this failed on v1.2.1 — + # Status: 400. No Trusted Publishing config found for repository `spacedevin/deck`. + # + # So: if a CARGO_REGISTRY_TOKEN secret is set, use it. Otherwise use Trusted Publishing. + # To claim the name, add the secret and release once; then configure Trusted Publishing on + # crates.io (deckfile > Settings > Trusted Publishing > owner `spacedevin`, repo `deck`, + # workflow `crates-release.yml`) and DELETE the secret — this falls back to OIDC on its own, + # so the long-lived token exists only for as long as it takes to bootstrap. + # `secrets` is not available in a step-level `if`, only in `env` — so the presence of the token + # has to be turned into a step output before it can gate anything. + - name: Detect a crates.io token + id: cred if: steps.check.outputs.already == 'false' + env: + TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -n "${TOKEN:-}" ]; then + echo "have_token=true" >> "$GITHUB_OUTPUT" + echo "::notice::Publishing with CARGO_REGISTRY_TOKEN. Once deckfile exists, configure Trusted Publishing and delete this secret." + else + echo "have_token=false" >> "$GITHUB_OUTPUT" + fi + + - name: Authenticate to crates.io (Trusted Publishing) + if: ${{ steps.check.outputs.already == 'false' && steps.cred.outputs.have_token == 'false' }} uses: rust-lang/crates-io-auth-action@v1 id: auth @@ -99,5 +130,13 @@ jobs: if: steps.check.outputs.already == 'false' working-directory: crate env: - CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} - run: cargo publish --allow-dirty + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || steps.auth.outputs.token }} + run: | + if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then + echo "No crates.io credential." + echo "Either set the CARGO_REGISTRY_TOKEN repo secret (needed for the FIRST publish of a" + echo "crate, which Trusted Publishing cannot bootstrap), or configure Trusted Publishing" + echo "for \`deckfile\` at https://crates.io/crates/deckfile/settings." + exit 1 + fi + cargo publish --allow-dirty