From d9bbadbe158ea50944ac32c3364c620b11020bbc Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 21 Oct 2026 00:05:57 +0800 Subject: [PATCH] ci: reduce Rust cache size and duplicate keys --- .github/actions/setup-rust/action.yml | 11 ++ .github/actions/setup-rust/prepare-cache.mjs | 45 ++++++++ .../rust-cache-compression/results.json | 101 ++++++++++++++++++ .../scripts/__tests__/prepare-rust-cache.mjs | 71 ++++++++++++ .github/workflows/ci.yml | 3 + 5 files changed, 231 insertions(+) create mode 100644 .github/actions/setup-rust/prepare-cache.mjs create mode 100644 .github/benchmark/rust-cache-compression/results.json create mode 100644 .github/scripts/__tests__/prepare-rust-cache.mjs diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index fa255cdace..1522b08a80 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -36,8 +36,19 @@ runs: restore-cache: false target-dir: ${{ inputs.target-dir }} + - name: Normalize Rust cache environment + if: inputs.restore-cache == 'true' + shell: bash + run: node "$SETUP_RUST_ACTION_PATH/prepare-cache.mjs" + env: + SETUP_RUST_ACTION_PATH: ${{ github.action_path }} + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 if: inputs.restore-cache == 'true' + env: + # Spend a few more seconds when saving to reduce every later download. + # This also applies to the action's post step, which creates the archive. + ZSTD_CLEVEL: '9' with: # Existing entries are immutable and can lack dependencies removed # by v2.9.1. Start a new namespace so the corrected action saves them. diff --git a/.github/actions/setup-rust/prepare-cache.mjs b/.github/actions/setup-rust/prepare-cache.mjs new file mode 100644 index 0000000000..0440ea2bf3 --- /dev/null +++ b/.github/actions/setup-rust/prepare-cache.mjs @@ -0,0 +1,45 @@ +import { execFileSync } from 'node:child_process'; +import { appendFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +export function prepareRustCache({ + run = (args) => execFileSync('rustup', args, { encoding: 'utf8' }).trim(), + env = process.env, + home = homedir(), +} = {}) { + const active = run(['show', 'active-toolchain']).split(/\s+/)[0]; + const installed = run(['toolchain', 'list', '--quiet']).split(/\s+/).filter(Boolean); + if (!active || !installed.includes(active)) { + throw new Error('The active Rust toolchain must be installed before cache setup'); + } + + // Cached jobs build with the workspace toolchain. rust-cache also hashes + // unused toolchains from the runner image, which vary across image updates. + // Keep the workspace compiler as the default before removing those extras. + run(['default', active]); + for (const toolchain of installed) { + if (toolchain !== active) { + run(['toolchain', 'uninstall', toolchain]); + } + } + + // GitHub runners leave these defaults unset; Namespace sets them explicitly. + // rust-cache hashes nonempty environment variables, so use the same values. + // Preserve custom paths, including Windows Dev Drives. + return { + CARGO_HOME: env.CARGO_HOME || join(home, '.cargo'), + RUSTUP_HOME: env.RUSTUP_HOME || join(home, '.rustup'), + }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + const paths = prepareRustCache(); + appendFileSync( + process.env.GITHUB_ENV, + Object.entries(paths) + .map(([key, value]) => `${key}=${value}\n`) + .join(''), + ); +} diff --git a/.github/benchmark/rust-cache-compression/results.json b/.github/benchmark/rust-cache-compression/results.json new file mode 100644 index 0000000000..d4a481c090 --- /dev/null +++ b/.github/benchmark/rust-cache-compression/results.json @@ -0,0 +1,101 @@ +{ + "run": "https://github.com/voidzero-dev/vite-plus/actions/runs/39453647412", + "benchmark_commit": "31b8c67f1", + "source_commit": "0cbfb6d6bf24ceb8b99b8645c613f4d27fadf359", + "scope": "Restored main cache payloads: Cargo registry, Cargo git, and target. Excludes Cargo bin and its installation metadata. Each level compresses the same tar stream; SHA-256 of the decompressed bytes must match. One timing sample per level and job.", + "samples": [ + { + "name": "linux-lint", + "source_bytes": 1776875520, + "source_sha256": "fbb377c81a91bef220e723ec4483e273e1850ae997d395443f3a077a705e48a8", + "paths": [ + "/home/runner/.cargo/registry", + "/home/runner/.cargo/git", + "/home/runner/work/vite-plus/vite-plus/target" + ], + "zstd": "*** Zstandard CLI (64-bit) v1.5.7, by Yann Collet ***", + "samples": [ + { + "level": 3, + "bytes": 513505284, + "compress_seconds": 2.6415996039999996, + "decompress_and_hash_seconds": 1.804909391999999 + }, + { + "level": 6, + "bytes": 493021878, + "compress_seconds": 6.507066184999999, + "decompress_and_hash_seconds": 1.7851912130000045 + }, + { + "level": 9, + "bytes": 457478458, + "compress_seconds": 9.765904976000002, + "decompress_and_hash_seconds": 1.7819855579999881 + } + ] + }, + { + "name": "linux-test", + "source_bytes": 3391518720, + "source_sha256": "e8b067a35ef016642e388aaf3dfb4e30e94018ea6ebbd68a7a7f56f6d08f5b60", + "paths": [ + "/home/runner/.cargo/registry", + "/home/runner/.cargo/git", + "/home/runner/work/vite-plus/vite-plus/target" + ], + "zstd": "*** Zstandard CLI (64-bit) v1.5.7, by Yann Collet ***", + "samples": [ + { + "level": 3, + "bytes": 845734789, + "compress_seconds": 6.381204795999992, + "decompress_and_hash_seconds": 4.233851232000006 + }, + { + "level": 6, + "bytes": 805789463, + "compress_seconds": 15.717300264999977, + "decompress_and_hash_seconds": 4.200105497999999 + }, + { + "level": 9, + "bytes": 705263026, + "compress_seconds": 22.005928431, + "decompress_and_hash_seconds": 4.200576061999982 + } + ] + }, + { + "name": "mac-test", + "source_bytes": 4381358080, + "source_sha256": "40ff23bc23b03fc05171ba01b9ce4896e4821429c6807b7bebc8bcdddaf23abd", + "paths": [ + "/Users/runner/.cargo/registry", + "/Users/runner/.cargo/git", + "/Users/runner/work/vite-plus/vite-plus/target" + ], + "zstd": "*** Zstandard CLI (64-bit) v1.5.7, by Yann Collet ***", + "samples": [ + { + "level": 3, + "bytes": 1127662872, + "compress_seconds": 1.3496105419999935, + "decompress_and_hash_seconds": 3.172481375000004 + }, + { + "level": 6, + "bytes": 1076874052, + "compress_seconds": 3.5355037500000037, + "decompress_and_hash_seconds": 3.1357934999999912 + }, + { + "level": 9, + "bytes": 926059853, + "compress_seconds": 5.343295082999987, + "decompress_and_hash_seconds": 2.978743083000012 + } + ] + } + ] +} diff --git a/.github/scripts/__tests__/prepare-rust-cache.mjs b/.github/scripts/__tests__/prepare-rust-cache.mjs new file mode 100644 index 0000000000..e3941ea5f6 --- /dev/null +++ b/.github/scripts/__tests__/prepare-rust-cache.mjs @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { prepareRustCache } from '../../actions/setup-rust/prepare-cache.mjs'; + +const active = 'nightly-2026-08-02-x86_64-unknown-linux-gnu'; + +function fixture(installed, env = {}) { + const commands = []; + const paths = prepareRustCache({ + env, + home: '/home/runner', + run(args) { + commands.push(args); + if (args[0] === 'show') { + return `${active} (overridden by '/workspace/rust-toolchain.toml')`; + } + if (args[1] === 'list') { + return installed.join('\n'); + } + return ''; + }, + }); + return { commands, paths }; +} + +test('keeps the pinned compiler and removes unused runner toolchains', () => { + const extra = ['stable-x86_64-unknown-linux-gnu', '1.99.0-x86_64-unknown-linux-gnu']; + const { commands } = fixture([...extra, active]); + assert.deepEqual(commands.slice(2), [ + ['default', active], + ...extra.map((toolchain) => ['toolchain', 'uninstall', toolchain]), + ]); +}); + +test('does not uninstall a toolchain when only the active one is installed', () => { + const { commands } = fixture([active]); + assert.deepEqual(commands.slice(2), [['default', active]]); +}); + +test('unset and explicit default paths produce the same cache environment', () => { + const implicit = fixture([active]); + const explicit = fixture([active], { + CARGO_HOME: '/home/runner/.cargo', + RUSTUP_HOME: '/home/runner/.rustup', + }); + assert.deepEqual(implicit.paths, explicit.paths); +}); + +test('preserves custom Cargo and Rustup paths', () => { + const paths = { + CARGO_HOME: 'D:\\Dev Drive\\.cargo', + RUSTUP_HOME: 'D:\\Dev Drive\\.rustup', + }; + assert.deepEqual(fixture([active], paths).paths, paths); +}); + +test('fails before changing toolchains when the active compiler is missing', () => { + const commands = []; + assert.throws( + () => + prepareRustCache({ + run(args) { + commands.push(args); + return args[0] === 'show' ? active : 'stable-x86_64-unknown-linux-gnu'; + }, + }), + /must be installed/, + ); + assert.equal(commands.length, 2); +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8142f1db2..0e80069b83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -326,6 +326,9 @@ jobs: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 - uses: ./.github/actions/clone + - name: Test Rust cache helpers + run: node --test .github/scripts/__tests__/*rust-cache*.mjs + - uses: ./.github/actions/setup-ci-node - uses: ./.github/actions/setup-rust