Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions .github/scripts/publish-crate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand All @@ -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

Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 9 additions & 31 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -301,30 +288,21 @@ 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
echo "::group::publish $crate"
.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
Expand Down
29 changes: 10 additions & 19 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,18 @@ serves against the attestation and creates the GitHub Release.
`CHANGELOG.md` and the `docs/MIGRATION-<minor>.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

Expand Down Expand Up @@ -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.

Expand Down
Loading