Repository navigation
Conversation
All Rust jobs shared one rust-cache key, so the fastest job saved it and every slower job restored a cache built for a different target, logged "Cache up-to-date", and rebuilt its dependencies on every run. Use a per-job shared key, restrict saves to main so PR-scoped caches stop evicting main's, and uninstall the runner image's preinstalled stable toolchain so image rollouts no longer rotate the cache key. Integration tests restore the axum job's cache, which builds the same artifacts. Closes #1258 Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Review summary
Reviewed b9235ca43d6e7dd1cd484eb5a69daeb3eae671db against 7a0ecb4cbfa39af3d83f7e1ed2733d7eabdc4cb1. No actionable issues introduced by this PR were found.
Inspected all three changed files, every affected job, all four integration-action consumers, and the resolved setup/cache action implementations.
Safety proof
- CI logs show all 12 affected job executions removing
stable, installing pinned Rust 1.95.0, and completing successfully. Coverage includes native Linux/macOS, both WASM targets, Clippy, parity, and integration tests. - Logs from runs
37701852034,37701851968, and37701851955confirm separate OS/job cache namespaces, matching Axum/integration keys, andsave-if: falseforwarded in every affected PR execution. No Rust cache saves occurred.
Validation and remaining uncertainty
- All 22 PR checks pass. The CI merge tree is identical to the reviewed head tree.
- Diff whitespace checks and actionlint without ShellCheck pass. Actionlint with ShellCheck reports 12 unchanged SC2086 diagnostics, confirmed against the base revision. All new removal scripts pass ShellCheck.
- In-memory assertions pass for all setup definitions, key uniqueness, intentional integration sharing, Rust pins, shell syntax, the absent-rustup branch, and CI log observations.
- Existing reviews, inline comments, issue comments, and review threads were empty.
- Main-branch cache saving, subsequent warm restores, and the claimed storage/performance improvements remain unverified on GitHub after merge.
aram356
left a comment
There was a problem hiding this comment.
Summary
Gives each Rust CI job its own rust-cache key, saves Rust caches only from main, and removes the runner image's stable so image updates stop changing the keys. This PR's CI logs confirm the mechanics: cache-save-if reaches rust-cache v2.9.1 as save-if, every job's key now lists only Rust 1.95.0, the four integration jobs compute test-axum's exact key, and nothing saved from the PR ref. One gap remains: the integration action can still save on a workflow_dispatch run from main.
1 of the inline comments below carries a one-click GitHub
suggestion: use Commit suggestion to apply it. The other three describe the change in prose because it spans several files or adds a new one.
Blocking
🔧 wrench
- The save gate is open on
workflow_dispatch: see inline at.github/actions/setup-integration-test-env/action.yml:69
Non-blocking
🌱 seedling / 🤔 thinking / ♻️ refactor
- 🌱 Most of the integration jobs' Rust compile isn't in
cargo-axum: see inline at.github/actions/setup-integration-test-env/action.yml:68 - 🤔 Removing the default toolchain lets rustup reinstall
stablesilently: see inline at.github/workflows/test.yml:40 - ♻️ One shared Rust setup action instead of nine copies: see inline at
.github/workflows/test.yml:36
CI Status
- cargo fmt: PASS (required)
- format-typescript: PASS (required)
- cargo test: PASS (required)
- format-docs: PASS (required)
- CLAUDE.md symlink guard: PASS
- cargo test (axum native): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- vitest: PASS
- prepare integration artifacts: PASS
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- browser integration tests: PASS
- CodeQL: PASS
- Analyze (rust): PASS
- Analyze (actions): PASS
- Analyze (javascript-typescript) (CodeQL Advanced): PASS
- Analyze (javascript-typescript) (CodeQL - Code Quality): PASS
- Analyze (python): PASS
| # Restore the axum job's main cache: it builds the same axum debug and | ||
| # Fastly release WASM artifacts. Integration tests run on PRs only, and | ||
| # PR caches are scoped to their ref and only evict main's shared caches. | ||
| cache-shared-key: cargo-axum-${{ runner.os }} | ||
| cache-save-if: ${{ github.ref == 'refs/heads/main' }} |
There was a problem hiding this comment.
🔧 wrench: The save gate is open on workflow_dispatch
Integration Tests also runs on workflow_dispatch (integration-tests.yml:11), so "Integration tests run on PRs only" doesn't hold. A dispatch from main makes this expression true in all four jobs that use the action, and they save under the key test-axum uses. rust-cache never saves over an exact key match ("Cache up-to-date."), so the first job to save owns cargo-axum until the key changes. If a dispatched job saves first, for example in the ~9 minutes before test-axum finishes after a Cargo.lock change lands on main, test-axum restores that partial cache and rebuilds its dependencies on every run. That's the failure #1258 fixes. The integration tests job, for instance, only compiles the integration-tests crate (376 crates in this PR's run) and none of test-axum's axum, bench, or release WASM builds.
All 18 dispatches so far ran on feature branches, so this hasn't happened yet. The action only needs to restore, so a constant closes it:
| # Restore the axum job's main cache: it builds the same axum debug and | |
| # Fastly release WASM artifacts. Integration tests run on PRs only, and | |
| # PR caches are scoped to their ref and only evict main's shared caches. | |
| cache-shared-key: cargo-axum-${{ runner.os }} | |
| cache-save-if: ${{ github.ref == 'refs/heads/main' }} | |
| # Restore the axum job's main cache: it builds the same axum debug and | |
| # Fastly release WASM artifacts. Restore only: test-axum owns this key, | |
| # and rust-cache never replaces an exact-match key, so a partial cache | |
| # saved by a workflow_dispatch run on main would stick. | |
| cache-shared-key: cargo-axum-${{ runner.os }} | |
| cache-save-if: "false" |
Checked in a scratch copy: the YAML parses to cache-save-if: 'false', which fails rust-cache's save === "true" test, and actionlint passes on integration-tests.yml.
| # Restore the axum job's main cache: it builds the same axum debug and | ||
| # Fastly release WASM artifacts. Integration tests run on PRs only, and | ||
| # PR caches are scoped to their ref and only evict main's shared caches. | ||
| cache-shared-key: cargo-axum-${{ runner.os }} |
There was a problem hiding this comment.
🌱 seedling: Most of the integration jobs' Rust compile isn't in cargo-axum
cargo-axum covers prepare-artifacts' axum debug build (277 crates) and Fastly release WASM build (255). The rest of the workflow's compiles aren't in any main cache. Counts are from this PR's run:
integration testsandFastly EC lifecycleruncargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --target x86_64-unknown-linux-gnu(376 crates). test-axum never builds that crate. At most 326 of those crate versions appear in test-axum's explicit-target CLI build, and feature differences will cut that further. test-parity compiles the same 376 crates, but without--target, so they land intarget/debuginstead. The explicit--targetdates from Add integration testing with testcontainers and Playwright #442, when.cargo/config.tomlset a global wasm32 build target. Add trusted-server-adapter-axum native dev server (PR 16) #643 removed that setting.generate-integration-viceroy-configs.shbuilds 228 crates intocrates/trusted-server-integration-tests/target, which rust-cache doesn't save.- The Cloudflare
worker-build --release(198 crates) isn't built by any main job. test-cloudflare only runscargo check.
Possible follow-up: drop --target from the two cargo test steps, give this action a cache-key input, and have those two jobs restore cargo-parity-${{ runner.os }}. None of this is a regression from the shared key, so it can wait.
| # rust-cache hashes every installed toolchain into its key, so the image's | ||
| # preinstalled stable would rotate the key on each runner-image rollout. | ||
| shell: bash | ||
| run: if command -v rustup >/dev/null; then rustup toolchain uninstall stable; fi |
There was a problem hiding this comment.
🤔 thinking: Removing the default toolchain lets rustup reinstall stable silently
After this step rustup's default still points at stable, which is no longer installed. setup-rust-toolchain only sets a directory override for the checkout, and the runner's rustup (1.29.1) auto-installs a missing active toolchain. So a future step that runs cargo or rustc outside the checkout, from $HOME or $RUNNER_TEMP for example, would quietly download the current stable (1.99.0 today) and build with it instead of the pinned 1.95.0. Nothing does that today: none of the 12 jobs that run this step logged an auto-install in this PR's run. The cache key wouldn't move either, because rust-cache saves under the key it computed at restore.
Reproduced in rust:1.95.0-slim: with the default toolchain uninstalled, cd /tmp && cargo --version printed "the missing active toolchain ... has been auto-installed" and ran cargo 1.99.0. Unsetting the default in the same step turns that into an error ("rustup could not choose a version of cargo to run ... no default is configured"). In the same container, the in-repo override and rustup toolchain list (rust-cache's input) were unchanged:
if command -v rustup >/dev/null; then rustup toolchain uninstall stable && rustup default none; fiThis would go in all nine copies of the step.
| run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT | ||
| shell: bash | ||
|
|
||
| - name: Remove runner-image Rust toolchain |
There was a problem hiding this comment.
♻️ refactor: One shared Rust setup action instead of nine copies
This PR adds the same uninstall step and the same cache-save-if line and comment in nine places: seven jobs here, format.yml, and the integration action. Each sits next to a "Retrieve Rust version" step that every job already repeats. A new Rust job that skips the uninstall gets a key that changes with each runner image. One that skips the save gate starts saving PR caches again. Neither fails visibly. A local composite action would hold all three in one place. Sketch:
# .github/actions/setup-rust/action.yml
name: Set up pinned Rust
description: Install the .tool-versions Rust toolchain with a per-job cache that only main saves.
inputs:
cache-key:
description: Per-job cache key prefix.
required: true
target:
description: Comma-separated extra targets.
default: ""
components:
description: Comma-separated extra components.
default: ""
save-cache:
description: Set to "false" for restore-only callers.
default: "true"
runs:
using: composite
steps:
- id: rust-version
shell: bash
run: echo "rust-version=$(awk '$1 == "rust" { print $2 }' .tool-versions)" >> "$GITHUB_OUTPUT"
- name: Remove runner-image Rust toolchain
# rust-cache hashes every installed toolchain into its key.
shell: bash
run: if command -v rustup >/dev/null; then rustup toolchain uninstall stable; fi
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ steps.rust-version.outputs.rust-version }}
target: ${{ inputs.target }}
components: ${{ inputs.components }}
cache-shared-key: ${{ inputs.cache-key }}-${{ runner.os }}
cache-save-if: ${{ inputs.save-cache == 'true' && github.ref == 'refs/heads/main' }}Each job would then use uses: ./.github/actions/setup-rust with cache-key: cargo-fastly and so on, and the integration action would pass save-cache: "false". Optional for this PR.
Summary
cargo-${{ runner.os }}, and a job with an exact key match never saves. The first job to save (cloudflare or parity) owns the cache, and the axum, fastly, spin and clippy jobs rebuild 150–1,000 dependency crates on every run even though they report a full hit.main. PR-scoped caches can't be reused by other PRs and were pushing the repo past its 10 GB limit (12.6 GB, ~10.8 GB of it from PRs), evicting main's caches.stabletoolchain before setup. rust-cache hashes every installed toolchain into its key, so each runner-image update was changing the key.Changes
.github/workflows/test.ymlcargo-fastly,cargo-axum,cargo-cloudflare,cargo-spin,cargo-parity;cargo-cliunchanged),cache-save-ifonmainonly, and a step that uninstalls the image'sstabletoolchain.github/workflows/format.ymlcargo-clippy).github/actions/setup-integration-test-env/action.ymlcargo-axum, which builds the same axum debug and Fastly release WASM artifacts. Same save and toolchain changes.Closes
Closes #1258
Test plan
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest run(ran withNODE_OPTIONS=--no-experimental-webstoragebecause local Node is 26, not the pinned 24.12.0)cd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveactbenchmark ofmain'stest.ymlvs this branch, running the cloudflare, fastly and axum jobs sequentially (one cold main-push run, then two warm PR runs per variant)cargo testWhat the benchmark confirmed:
mainruns save one cache per job.stabletoolchain.Not covered locally:
After merge, check these:
gh cache listshows Rust caches only forrefs/heads/main, under 10 GB.Compilinglines.The first PR runs after merge build cold until
mainhas saved the new keys.Checklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)actbenchmark above.)