diff --git a/.github/scripts/tests/pr-runner-rust-cache.test.mjs b/.github/scripts/tests/pr-runner-rust-cache.test.mjs index 59f3e2698d..1987072442 100644 --- a/.github/scripts/tests/pr-runner-rust-cache.test.mjs +++ b/.github/scripts/tests/pr-runner-rust-cache.test.mjs @@ -53,3 +53,37 @@ test("a pull request never writes to or evicts the master cache entry", () => { assert.doesNotMatch(step, /^\s*if:/m, "the restore must not be conditional; a miss is already free"); assert.doesNotMatch(prWorkflow, /uses: Swatinem\/rust-cache@[0-9a-f]{40}[\s\S]*?save-if: (?!false)/); }); + +// The cache key mixes in every toolchain rust-cache can find, so the runner +// image's own stable Rust lands in it too. The fleets carried 1.98.0 while +// ubuntu-latest carried 1.98.1, which is why GitHub-hosted pull requests +// missed a cache the fleet hit. Both workflows now strip everything but the +// pin. They have to do it the same way: if the reader and the writer disagree, +// the key matches nothing and every run recompiles. +const NORMALIZE = /# rust-cache hashes every installed toolchain[\s\S]*?rustup toolchain list\n/; + +test("reader and writer strip extra toolchains identically before the key is computed", () => { + const mine = pr.match(NORMALIZE); + const theirs = release.match(NORMALIZE); + assert.ok(mine, "pr-trusted.yml must normalize the installed toolchains"); + assert.ok(theirs, "release-verify.yml must normalize the installed toolchains"); + assert.equal(mine[0], theirs[0], "the normalization must be identical in both workflows"); + + for (const [name, body] of [["reader", mine[0]], ["writer", theirs[0]]]) { + // Keep the pin, drop the rest, and never fail the job over it. + assert.match(body, /grep -vx "\$toolchain"/, name); + assert.match(body, /xargs -n1 rustup toolchain uninstall/, name); + assert.match(body, /\|\| true/, name); + } +}); + +test("the toolchain is stripped before the cache step, not after", () => { + for (const [name, body, cacheStep] of [ + ["reader", pr, " - name: Restore Runner Rust dependencies (read only)"], + ["writer", release, " - name: Cache Runner Rust dependencies"], + ]) { + const normalize = body.search(NORMALIZE); + const cache = body.indexOf(cacheStep); + assert.ok(normalize >= 0 && cache > normalize, `${name}: normalization must precede the cache step`); + } +}); diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index fcb8a41f39..81c5d597f6 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -683,6 +683,23 @@ jobs: toolchain="$(rustup show active-toolchain | awk '{print $1}')" echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV" + # rust-cache hashes every installed toolchain into the cache key, not + # only the active one. Each runner image also ships its own stable + # Rust, and those disagree across images: 1.98.0 on the RunsOn fleets + # against 1.98.1 on ubuntu-latest. Two runners that agreed on the pin + # therefore still computed different keys, and every GitHub-hosted + # pull request missed this cache while the fleet hit it. Remove every + # toolchain except the pin, so the key depends on the pinned compiler + # and the lockfile alone rather than on what the image happens to + # carry. Keep this block identical in both workflows: the reader and + # the writer must agree or the key matches nothing. + extra_toolchains="$(rustup toolchain list | awk '{print $1}' | grep -vx "$toolchain" || true)" + if [ -n "$extra_toolchains" ]; then + echo "$extra_toolchains" | xargs -n1 rustup toolchain uninstall \ + || echo '::notice title=Runner Rust cache::could not remove an extra toolchain; the cache key may not match' + fi + rustup toolchain list + - name: Restore Runner Rust dependencies (read only) uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: diff --git a/.github/workflows/release-verify.yml b/.github/workflows/release-verify.yml index 505624a6b0..5144e6b52a 100644 --- a/.github/workflows/release-verify.yml +++ b/.github/workflows/release-verify.yml @@ -302,6 +302,23 @@ jobs: toolchain="$(rustup show active-toolchain | awk '{print $1}')" echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV" + # rust-cache hashes every installed toolchain into the cache key, not + # only the active one. Each runner image also ships its own stable + # Rust, and those disagree across images: 1.98.0 on the RunsOn fleets + # against 1.98.1 on ubuntu-latest. Two runners that agreed on the pin + # therefore still computed different keys, and every GitHub-hosted + # pull request missed this cache while the fleet hit it. Remove every + # toolchain except the pin, so the key depends on the pinned compiler + # and the lockfile alone rather than on what the image happens to + # carry. Keep this block identical in both workflows: the reader and + # the writer must agree or the key matches nothing. + extra_toolchains="$(rustup toolchain list | awk '{print $1}' | grep -vx "$toolchain" || true)" + if [ -n "$extra_toolchains" ]; then + echo "$extra_toolchains" | xargs -n1 rustup toolchain uninstall \ + || echo '::notice title=Runner Rust cache::could not remove an extra toolchain; the cache key may not match' + fi + rustup toolchain list + - name: Cache Runner Rust dependencies # Restore and save only within trusted master-push verification. GitHub # isolates branch/PR caches from master; other callers compile afresh.