Rust release, CI, security and versioning plumbing

Merged
JE
2mo ago
+791-24

Release engineering, CI, security and versioning plumbing for the Rust SDK. Stacked on the conformance runner PR (#160), which sits on rust-sdk (the crate): the Rust CI jobs here exercise both workspaces. Card: W4 releng.

What

  • release-rust.yml — shaped after basecamp-sdk's release-python.yml: smithy → test → package → publish.
    • package verifies the tag is on main and [package] version == tag (read with cargo metadata, not a regex), runs cargo package --locked under SOURCE_DATE_EPOCH, asserts .cargo_vcs_info.json (sha1 == HEAD, path_in_vcs == rust/fizzy-sdk) before anything irreversible, and uploads the .crate.
    • publish (if: push, environment release-crates, id-token: write) downloads the artifact, repackages the same commit and refuses unless the bytes match; then asks crates.io for the version and publishes only on 404. A 200 must carry the same checksum (rerun after a partial failure) or fail; a yanked version fails; any other status fails closed. OIDC via rust-lang/crates-io-auth-action (SHA-pinned). After publishing, the registry checksum is compared to the packaged sha256.
    • workflow_dispatch is a packaging rehearsal (cargo publish --dry-run) and never mints a token.
    • Verified locally that cargo package is byte-reproducible across runs (with and without SOURCE_DATE_EPOCH; cargo normalizes mtimes itself), so the publish-side comparison is sound.
  • test.yml — test-rust matrix {msrv, stable} sets RUSTUP_TOOLCHAIN per leg (the MSRV from cargo metadata's rust_version, stable from rust-toolchain.toml) so the toolchain file cannot make both legs test stable, and asserts rustc --version in both workspaces. Stable: rs-lint, rs-test, rs-doc, rs-deny, rs-check-drift, rs-publish-check, semver-checks (advisory, skipped until a crates.io baseline exists); MSRV: the library alone across feature sets. Both: lockfiles unchanged. conformance-rust needs: test-rust.
  • security.yml — Trivy over both workspaces; cargo deny weekly (the same make rs-deny PR CI runs).
  • Dependabot — cargo for /rust and /conformance/runner/rust.
  • release-github.yml — release-rust.yml in WORKFLOWS, cargo add fizzy-sdk@X in the install body, and --event push on the waiter so a workflow_dispatch rehearsal can never satisfy the publish gate (this was a latent hole for all six workflows).
  • Makefiles / scripts — root rs-* targets delegating to rust/Makefile (both workspaces: fmt, clippy, tests, deny under one deny.toml; the conformance targets themselves are in #164); make release reads the crate version via cargo metadata and refuses a stale lock in either workspace; bump-version.sh edits only the [package] table and refreshes both Cargo.locks; sync-api-version.sh seds the generated API_VERSION (toolchain-free; drift check catches disagreement); scripts/check-rs-service-drift.sh is the operationId census.
  • Hygiene — .editorconfig, .gitignore, pre-commit cargo fmt, CONTRIBUTING, AGENTS (six generated service layers).

crates.io bootstrap (human step)

Trusted publishing needs the crate to exist first. The exact values (crate fizzy-sdk, owner basecamp, repo fizzy-sdk, workflow release-rust.yml, environment release-crates, owner github:basecamp:cli) and the procedure are on the W4 card. The release-crates environment must be restricted to v* tags with required reviewers.

Gates

actionlint clean; zizmor: only the pre-existing by-design self-repository finding (7 across all workflows, 0 low+). Locally at rust-sdk @ 607730b with the pinned 1.98.1: rs-check-drift, rs-deny, rs-publish-check green; make conformance-rust 127/127.

Adversarial review (Codex, gpt-6-astra, xhigh, read-only)

Accepted: --event push on the waiter; dead-code lint failures in the runner draft (fixed there); document the bootstrap on the publish job. Declined: none on this PR's surface.

Review round 1: restacked so the runner precedes this PR; dropped --offline from the lockfile refresh.