From 9ec7682850743732eda558e1c23f0b9622a1f7d1 Mon Sep 17 00:00:00 2001 From: Tom Wieland Date: Wed, 21 Oct 2026 12:39:45 +0200 Subject: [PATCH 1/2] ci(release): publish from a pushed tag with the registry token, as in 0.10.1 0.11.0 made publish.yml require a GitHub-verified signed tag and publish through crates.io trusted publishing only. Neither was ever set up (no signing key registered, no trusted publishers, and trusted publishing cannot create the new id_effect_rpc_macros and id_effect_lints), so the v0.11.0 and v0.12.0 runs failed at their first job. Back to the 0.10.1 flow: any pushed v* tag starts the release and the publish job logs in with the CARGO_REGISTRY_TOKEN secret (crates-io environment), which can also publish a crate's first version. The rest of the 0.11 pipeline stays: pre-publish tests, packaging validation, SBOMs, SLSA provenance, digest checks against crates.io and the GitHub Release. Signed-off-by: Tom Wieland Co-Authored-By: Claude Opus 5.5 --- .github/scripts/publish-crate.sh | 15 ++++-------- .github/workflows/publish.yml | 40 +++++++------------------------- docs/RELEASING.md | 29 ++++++++--------------- 3 files changed, 24 insertions(+), 60 deletions(-) diff --git a/.github/scripts/publish-crate.sh b/.github/scripts/publish-crate.sh index c15c1add..ee478a9c 100755 --- a/.github/scripts/publish-crate.sh +++ b/.github/scripts/publish-crate.sh @@ -7,11 +7,10 @@ # VERSION the version being published (required) # DRY_RUN=true pass --dry-run to cargo publish # PLAN_ONLY=true print "publish" or "skip" from the crates.io index and exit (read-only); -# a crate that was never published fails the plan unless DRY_RUN=true: -# trusted publishing cannot create a crate (docs/RELEASING.md) +# a crate that was never published is planned as a new crate # SHA256SUMS optional sha256sum file of the attested .crate files: the .crate that # cargo publish uploaded must match its line -# CRATES_IO_TOKEN the short-lived trusted-publishing token (rust-lang/crates-io-auth-action); +# CRATES_IO_TOKEN the crates.io token (the CARGO_REGISTRY_TOKEN secret); # handed to cargo on stdin by `cargo login`, never on the command line set -euo pipefail @@ -50,11 +49,7 @@ if published; then fi if [ "${PLAN_ONLY:-false}" = "true" ]; then if new_crate; then - if [ "${DRY_RUN:-false}" != "true" ]; then - echo "::error::$pkg has never been published: trusted publishing cannot create a crate. Reserve the name and add the trusted publisher first (docs/RELEASING.md, owner steps)." - exit 1 - fi - echo "publish $pkg $version (new crate: a real release needs the owner steps in docs/RELEASING.md)" + echo "publish $pkg $version (new crate)" exit 0 fi echo "publish $pkg $version" @@ -65,7 +60,7 @@ args=(--package "$pkg" --no-verify "$@") if [ "${DRY_RUN:-false}" = "true" ]; then args+=(--dry-run) else - : "${CRATES_IO_TOKEN:?set CRATES_IO_TOKEN (trusted publishing) for a real publish}" + : "${CRATES_IO_TOKEN:?set CRATES_IO_TOKEN (the CARGO_REGISTRY_TOKEN secret) for a real publish}" cargo login <<<"$CRATES_IO_TOKEN" fi @@ -80,7 +75,7 @@ if [ "$code" -ne 0 ]; then exit 0 fi if grep -qiE "required permissions|403|not authorized|trusted publish" <<<"$output"; then - echo "::error::$pkg publish denied — trusted publishing needs Industrial/id_effect publish.yml (environment crates-io) as a trusted publisher on this crate (docs/RELEASING.md)" + echo "::error::$pkg publish denied — the CARGO_REGISTRY_TOKEN secret (environment crates-io) may not publish it" exit 1 fi if grep -qiE "readme .* does not appear to exist" <<<"$output"; then diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7501bdca..829a9b3c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,8 @@ name: Publish to crates.io -# Trigger: push a signed semver tag, e.g. git tag -s v0.11.0 -m v0.11.0 && git push origin v0.11.0 +# Trigger: push a semver tag, e.g. git tag v0.12.0 && git push origin v0.12.0 # The tag must match the version in every publishable Cargo.toml. docs/RELEASING.md has the flow. # -# verify-versions versions match the tag; the tag is annotated and its signature verified (S6) +# verify-versions versions match the tag # test pre-publish tests # validate-packaging publish order, path-only dev-deps, cargo package per crate # build cargo package --locked of every published crate + one CycloneDX SBOM each @@ -13,10 +13,8 @@ name: Publish to crates.io # verify downloads every crate from crates.io: sha256 == attested digest, slsa-verifier # release GitHub Release with the .crate files, SBOMs and the *.intoto.jsonl provenance # -# crates.io auth: trusted publishing only (rust-lang/crates-io-auth-action exchanges the job's -# GitHub OIDC token for a crates.io token that lives 30 minutes). No registry secret is read (S3). -# Every crate must already exist on crates.io with this workflow as its trusted publisher; the -# publish plan refuses a release that contains a never-published crate (docs/RELEASING.md). +# crates.io auth: the CARGO_REGISTRY_TOKEN secret of the crates-io environment, as up to +# 0.10.1 (it can also publish a crate's first version). on: push: tags: @@ -71,7 +69,7 @@ jobs: echo "dry-run=$DRY_RUN" } >> "$GITHUB_OUTPUT" echo "Publishing version: $VERSION (tag: ${TAG:-none}, dry run: $DRY_RUN)" - - name: Require a signed tag (s6_release_requires_signed_tag) + - name: Require a release tag env: GH_TOKEN: ${{ github.token }} TAG: ${{ steps.version.outputs.tag }} @@ -83,20 +81,10 @@ jobs: echo "Dry run from a branch: no tag to check." exit 0 fi - echo "::error::A real publish runs from a signed v* tag (push the tag, or dispatch on the tag ref)." + echo "::error::A real publish runs from a v* tag (push the tag, or dispatch on the tag ref)." exit 1 fi - ref=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --jq '.object.type + " " + .object.sha') - if [ "${ref%% *}" != "tag" ]; then - echo "::error::$TAG is a lightweight tag; releases need a signed annotated tag (git tag -s)." - exit 1 - fi - verification=$(gh api "repos/$GITHUB_REPOSITORY/git/tags/${ref#* }" --jq '(.verification.verified | tostring) + " " + .verification.reason') - if [ "${verification%% *}" != "true" ]; then - echo "::error::$TAG signature is not verified by GitHub (reason: ${verification#* }). Sign it with a key registered on the tagger's GitHub account." - exit 1 - fi - echo "$TAG is an annotated tag with a verified signature." + echo "Publishing from tag $TAG." - name: Check all crate versions match env: VERSION: ${{ steps.version.outputs.version }} @@ -260,7 +248,6 @@ jobs: environment: crates-io permissions: contents: read - id-token: write # crates.io trusted publishing (OIDC) env: VERSION: ${{ needs.verify-versions.outputs.version }} DRY_RUN: ${{ needs.verify-versions.outputs.dry-run }} @@ -301,15 +288,10 @@ jobs: cp lints/id_effect_lints/target/package/tmp-crate/*.crate target/package/tmp-crate/ grep '\.crate$' "$SHA256SUMS" > crates.sha256 (cd target/package/tmp-crate && sha256sum --check --strict "$GITHUB_WORKSPACE/crates.sha256") - # A trusted-publishing token lives 30 minutes, so each half of the order gets its own. - - name: crates.io token (trusted publishing, first half) - id: auth-1 - if: needs.verify-versions.outputs.dry-run != 'true' - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 - name: Publish (first half of the order) if: needs.verify-versions.outputs.dry-run != 'true' env: - CRATES_IO_TOKEN: ${{ steps.auth-1.outputs.token }} + CRATES_IO_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: | set -euo pipefail for crate in $(./scripts/validate-crate-packaging.sh --print-order | sed -n '1,15p'); do @@ -317,14 +299,10 @@ jobs: .github/scripts/publish-crate.sh "$crate" --locked echo "::endgroup::" done - - name: crates.io token (trusted publishing, second half) - id: auth-2 - if: needs.verify-versions.outputs.dry-run != 'true' - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 - name: Publish (second half of the order, then id_effect_lints) if: needs.verify-versions.outputs.dry-run != 'true' env: - CRATES_IO_TOKEN: ${{ steps.auth-2.outputs.token }} + CRATES_IO_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: | set -euo pipefail for crate in $(./scripts/validate-crate-packaging.sh --print-order | sed -n '16,$p'); do diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 7994a52e..254f09f9 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -11,22 +11,18 @@ serves against the attestation and creates the GitHub Release. `CHANGELOG.md` and the `docs/MIGRATION-.md` guide. 2. `bash scripts/validate-crate-packaging.sh` (publish order and packaging) and `cargo build --workspace --locked` (the committed `Cargo.lock` is current). -3. Merge to `main`, then tag the merge commit with a **signed annotated** tag and push it: +3. Merge to `main`, then tag the merge commit and push the tag; GitHub Actions does the release: ```bash - git tag -s v0.11.0 -m "v0.11.0" - git push origin v0.11.0 + git tag -a v0.12.0 -m "v0.12.0" + git push origin v0.12.0 ``` - The signing key (GPG or SSH) must be registered as a signing key on the tagger's GitHub - account: the workflow asks GitHub whether the tag's signature is verified and refuses a - lightweight or unverified tag (`s6_release_requires_signed_tag`). - To rehearse, run the workflow by hand (Actions → Publish to crates.io → Run workflow) with **Dry run** checked. A dry run packages and attests everything, runs `cargo publish --dry-run` over the whole order, checks the packages against the attested digests and runs `slsa-verifier` on them; it uploads nothing and creates no release. A run without -**Dry run** must be dispatched on a tag ref and passes the same tag check. +**Dry run** must be dispatched on a tag ref. ## What the workflow does @@ -63,20 +59,15 @@ slsa-verifier verify-artifact id_effect-0.11.0.crate \ ## crates.io authentication -The `publish` job uses [crates.io trusted publishing](https://crates.io/docs/trusted-publishing) -and nothing else: [`rust-lang/crates-io-auth-action`](https://github.com/rust-lang/crates-io-auth-action) -exchanges the job's GitHub OIDC token (`id-token: write`, publish job only) for a crates.io token -that lives 30 minutes and is revoked when the job ends. The order is published in two halves with -a fresh token each, so a slow index never outlives a token. `publish-crate.sh` hands the token to -cargo with `cargo login` on stdin. No workflow reads a registry secret (S3). - -Trusted publishing cannot create a crate: crates.io only accepts it for a crate that already -exists and lists this workflow as a trusted publisher. The publish plan step therefore fails a -real release, before anything is uploaded, if any crate in the order has never been published -(a dry run only warns). A new crate gets its name reserved by the owner first (step 2 below). +The `publish` job publishes with the `CARGO_REGISTRY_TOKEN` secret of the `crates-io` +environment, as up to 0.10.1; `publish-crate.sh` hands it to cargo with `cargo login` on stdin. +The token also publishes a crate's first version, so a new crate needs no owner step. ### Owner steps (D15) +Not needed for the token flow above: they belong to the signed-tag and trusted-publishing flow +of 0.11.0, which 0.12.0 replaced with the 0.10.1 flow. + These need the owner's own crates.io and GitHub logins; the agent does not do them. Record the date of each in the r11-sc-publish task evidence. From 1e99b7010f679b12fb023eb6271e681105c4ebcb Mon Sep 17 00:00:00 2001 From: Tom Wieland Date: Wed, 21 Oct 2026 12:52:13 +0200 Subject: [PATCH 2/2] ci: pin Biome to 2.5.15 Biome 2.6.0 and 2.6.1 (published 2026-10-21) install from npm without their platform binary ("Cannot find module '@biomejs/cli-linux-x64/biome'"), which fails effect:ci-format and with it every Moon CI run. Pin the last version the green runs used. Signed-off-by: Tom Wieland Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4ccaec0..41d01cd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,9 @@ jobs: with: node-version: '22' - name: Install Biome (treefmt formatter for JS/JSON) - run: npm install -g @biomejs/biome + # Pinned: 2.6.0/2.6.1 (2026-10-21) install without the platform binary + # (Cannot find module '@biomejs/cli-linux-x64/biome'). + run: npm install -g @biomejs/biome@2.5.15 - name: Install Rust (pinned, rust-toolchain.toml) uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master with: