Seed Rust as a template language, extracted from basecamp-sdk
Part 2 of the Rust work on the seed: Rust becomes a template language on par with Go, TypeScript, Ruby, Swift and Kotlin. Source of truth is basecamp/basecamp-sdk main at 9159204 (#859, the crate that landed first, as the program sequenced it); everything here is extracted from that tree and parameterised the seed's way ({{.AppName}}, {{.AppTitle}}, {{.GithubOrg}}, {{.GithubRepo}}, {{.AppName | upper}} — no new variables).
Base. Stacked on #11 (rustS1/seed-self-ci, head 5fadd13), which is the top of the part-1 stack: it carries #9 and #10 and the self-CI this PR extends (render smoke, actionlint/zizmor over rendered templates). #12 and #13 are independent of these files. Retarget to main once #11 merges.
What is in the seed now
seed/rust/ — a Cargo workspace (Cargo.toml.tmpl, members = ["sdk"]) whose sdk/ crate is complete: unlike the other language scaffolds it builds, tests, documents and packages as shipped, with an empty src/generated/ tree in place of the generator's output. The module split is basecamp-sdk's runtime — client (builder, account scope, the SPEC §7 attempt loop with the 401 replay), auth (coalesced refresh), config (from_env over {{.AppName | upper}}_*), error (SPEC §6 record, Retry-After and field-error parsing), hooks, http (the HttpClient seam + the rustls reqwest client), operation, pagination (Link header, collect_all, pages/items streams), retry, route, security, types, url, version, services — with the Basecamp-only parts left out: OAuth, the two-hop download, webhooks, the SPEC §18 composites, the template-library confirmation projection, AuthRoutableUrl. Tests that named generated routes now use route-local fixtures; 47 unit + 4 integration tests + the README doctest pass on the rendered crate. rust-toolchain.toml, rustfmt.toml, clippy.toml, deny.toml and the workspace [lints] are the devex standard verbatim.
Templates — release-rust.yml.tmpl is basecamp-sdk's workflow shape: smithy → test (fmt, clippy -D warnings, tests, docs, drift, cargo deny over both workspaces) → package (tag on main, version via cargo metadata, cargo package --locked under SOURCE_DATE_EPOCH, .cargo_vcs_info.json sha and path_in_vcs asserted) → publish (if: push + repository predicate, environment: release-crates, id-token: write, rust-lang/crates-io-auth-action, publish only on a crates.io 404 for the version and fail closed on anything else, repackage-and-compare, post-publish checksum compare, idempotent re-run). release-github.yml.tmpl waits on rust; test.yml.tmpl gets the rust conformance arm (the action arm is #10's); security.yml.tmpl gains trivy-rust (both lockfiles) and cargo-deny; dependabot.yml.tmpl gets cargo for /rust and /conformance/runner/rust. New Rust jobs are SHA-pinned and persist-credentials: false, per the seed's direction of travel, even where the surrounding file is not.
Makefile + scripts — the rs-* block (rs-build rs-test rs-lint rs-doc rs-deny rs-generate-services rs-check-drift rs-publish-check rs-check rs-clean), conformance-rust, conformance-runner-tests-rust, and rs-* in the generate-services, check, check-full and clean aggregates; sync-api-version-check covers the generated API_VERSION; make release reads the crate version back through cargo metadata, fails on a stale lockfile in either workspace and runs the publish dry-run. sync-version.sh.tmpl writes the [package] version bounded to that table, reads it back through cargo, and refreshes both Cargo.lock files. scripts/check-rust-service-drift.sh.tmpl is the regenerate-into-tmpdir-and-diff gate (the seed ships no other language's drift script; this one is small and release-rust.yml calls it).
Docs — seed README/CONTRIBUTING (the generator port; "Releasing the Rust crate for the first time": manual first publish, owners, Trusted Publishing on release-rust.yml + release-crates, revoke, make release on the same SHA)/AGENTS/SECURITY, root README language matrix, prompts/seed-sdk.md Phase 4 Rust + smoke row + environment setup, MAKEFILE-CONVENTION.md "Rust Crate Policy" (no rust/v* sub-tag; 404-only publish; first publish by hand), root AGENTS.md, skills/rubric-audit profile detection.
Seed self-CI — a rust-scaffold job renders seed/ and, on stable (1.98.1), runs cargo fmt --check, clippy -D warnings, the tests (all-features and --no-default-features), the examples, cargo doc -D warnings, cargo deny check and cargo publish --dry-run on the rendered workspace, and on the MSRV (1.88) builds and tests the library; the seed job also shellchecks the rendered scripts. That surfaced a pre-existing -f on a glob in sync-version.sh.tmpl, fixed here.
Deliberately per-repo (not in the seed)
- The generator. The seed ships no generator for any language (Go's
cmd/generate-services, TS'sscripts/generate-services.tsetc. are referenced by the Makefile but not shipped), and the program decision was a per-repo port of basecamp-sdk'srust/generator.rs-generate-servicesand the drift script name{{.AppName}}-sdk-generator; CONTRIBUTING and the prompt say where it comes from.membersgains"generator"when it is ported. - The conformance runner. basecamp-sdk's
conformance/runner/rustis a 130 KB dispatch over that repo's 262 generated operations; no rendering makes it build against a freshly seeded crate, and the seed's existing runners (basecamp-sdk copies) have already diverged from their consumers, which is the failure this would repeat. The action arm (#10), the Makefile targets, Dependabot and Trivy all point atconformance/runner/rustfor when a repo ports it. - A per-language
test-rustjob intest.yml.tmpl: the seed's test template has no per-language job for any language (smithy-verify, conformance matrix, rubric-check), so thers-checkcontract lives inrelease-rust.yml's test job and the Makefile, where a consumer wires it into its own test workflow (basecamp-sdk'stest-rustmsrv × stable matrix is the reference). - CodeQL and labeler rows — the seed carries neither workflow; basecamp-sdk's
codeql.ymlalready has therustmatrix arm. native-tls. The exemplar offers it as an additive feature; the devex standard says rustls-only at launch, and the seed'sdeny.tomlbansopenssl-sysaccordingly. A repo that needs it removes the ban and adds the lane.Cargo.lock. Not shipped (the crate name is a placeholder until rendering, and no other seed language ships a lockfile); the prompt's Rust phase iscargo generate-lockfileand commit, since everyrs-*target passes--locked.uses: ./.github/workflows/smithy-verify.ymlkeeps the seed's current spelling; #14 moves every template to the self-repository form.
Verified locally
hack/render-seed.sh (no leftover placeholders), then on the rendered tree: actionlint clean; zizmor --min-severity high clean under hack/zizmor-seed.yml (release-rust.yml has no finding at any severity beyond the self-repository one #14 owns); shellcheck clean; cargo fmt --check, clippy -D warnings, cargo test (52 tests), --no-default-features build + lib tests, examples, cargo doc -D warnings, cargo deny check, cargo publish --dry-run all green on rustc 1.97.1 (MSRV 1.88). bats actions/release-orchestrate/test: 28/28.
Review round 1 (dc5e2bc)
Copilot + Codex, 15 threads: 13 fixed (runner audit guarded so the Phase 4 rs-check checkpoint holds; make bump runs the script the seed ships; sync-api-version writes the Rust constant; userinfo base URLs refused; request id bounded; multipart backslash encoded; total-count header from route metadata; IPv6 brackets; HttpClient redirect contract restated; README MSRV claim; bootstrap CARGO_REGISTRY_TOKEN; MSRV lane in ci.yml), 2 declined in-thread (SDK-level redirect following — the fail-closed 3xx is intended and the follower is the app-specific download flow; conformance-swift in the aggregate — pre-existing, macOS-bound, not a Rust surface). Four of the fixes are shared with basecamp-sdk's exemplar and are flagged there.