Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ jobs:
run: node scripts/check-package-includes.mjs

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
if: needs.changes.result != 'success' || needs.changes.outputs.rust == 'true'

- name: cargo fmt
Expand Down Expand Up @@ -186,6 +188,8 @@ jobs:
- uses: ./.github/actions/setup-rust

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}

- uses: actions/setup-python@v7
with:
Expand Down Expand Up @@ -364,6 +368,8 @@ jobs:
- uses: ./.github/actions/setup-rust

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}

- name: Fetch the pinned sv-tests revision
run: |
Expand Down Expand Up @@ -405,6 +411,7 @@ jobs:

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
shared-key: ci-napi-linux

- uses: pnpm/action-setup@v6
Expand Down Expand Up @@ -449,6 +456,7 @@ jobs:

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
shared-key: ci-napi-wasm

- uses: pnpm/action-setup@v6
Expand Down Expand Up @@ -546,6 +554,8 @@ jobs:
- uses: ./.github/actions/setup-rust

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}

- name: Lint ARM64 backend
run: >-
Expand Down Expand Up @@ -583,6 +593,8 @@ jobs:
- uses: ./.github/actions/setup-rust

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}

- uses: pnpm/action-setup@v6

Expand Down Expand Up @@ -621,6 +633,8 @@ jobs:
targets: aarch64-unknown-linux-gnu

- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}

- uses: pnpm/action-setup@v6

Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/rust-cache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Warm Rust dependency cache

# PR merge-ref caches cannot be reused by other PRs. Populate the default
# branch cache once per toolchain/dependency key, without running tests twice.
on:
push:
branches: [master]
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
- "rust-toolchain.toml"
- ".cargo/**"
- ".github/actions/setup-rust/**"
- ".github/workflows/rust-cache.yml"
- ".github/workflows/ci.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: rust-dependency-cache-${{ github.ref }}
cancel-in-progress: false

env:
CARGO_INCREMENTAL: "0"

jobs:
# Same job ID, runner, toolchain and environment as CI's Rust Tests cache.
rust-tests:
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
name: Populate Rust Tests dependency cache
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: ./.github/actions/setup-rust
- uses: Swatinem/rust-cache@v2
id: cache
- name: Install cargo-nextest
if: steps.cache.outputs.cache-hit != 'true'
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest@0.9.144
- name: Build test targets to populate dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: >-
cargo nextest list --locked --workspace --message-format json
--features celox/systemverilog,celox-test-suite/verilator,celox-test-suite/icarus
> /dev/null
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ See GitHub's [advanced setup migration instructions](https://docs.github.com/en/

### Product checks

Rust caches in `ci.yml` are restored on every branch but saved only on the
default branch. A PR's merge-ref cache cannot be restored by another PR, so
saving one copy per PR consumes the shared cache quota without warming other
queues. `rust-cache.yml` builds the Rust Tests targets on master after toolchain,
manifest, or CI-cache setup changes, or on manual dispatch. It uses the same job
ID, toolchain, environment and CI features as Rust Tests, skips the build on an
exact cache hit, and does not execute tests. The normal daily master CI warms
the other job caches. Caches remain dependency-only; workspace binaries are not
added to the quota. Toolchain changes and cold caches still require compilation.
Mergify-only configuration edits run script validation, not product compilation;
mixed Rust changes and unknown paths retain their existing coverage.


Pull requests use `scripts/ci-changes.mjs` to select jobs and scope Rust tests
by affected crates. Windows and ARM64 checks run in Mergify queue draft PRs,
legacy merge groups, and daily/manual full runs. Queue drafts run the full Rust
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci-changes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export function classifyFiles(files, { releasePlease = false } = {}) {
continue;
}

if (startsWithAny(path, ["scripts/"])) {
if (path === ".mergify.yml" || startsWithAny(path, ["scripts/"])) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify rust-cache workflow edits for script validation

When a PR changes only .github/workflows/rust-cache.yml, this condition does not set scripts, and the later generic .github/workflows/ branch treats the file as neutral. Consequently the lint job skips node --test scripts/*.test.mjs, so the newly added test that reads this workflow never checks the very edits it is meant to protect; a mismatch in the job ID, features, nextest version, or cache policy can therefore merge without validation. Include this workflow in the script-validation classification (or otherwise schedule its focused test).

AGENTS.md reference: AGENTS.md:L14-L16

Useful? React with 👍 / 👎.

affected.scripts = true;
continue;
}
Expand Down
14 changes: 14 additions & 0 deletions scripts/ci-changes.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -487,3 +487,17 @@ test("version-only Release Please PRs defer product tests to queue validation",
assert.deepEqual(classifyFiles(releasePullRequestFiles, { releasePlease: true }), none);
assert.equal(classifyFiles([...releasePullRequestFiles, "crates/celox/src/lib.rs"], { releasePlease: true }).rust, true);
});

test("queue configuration runs script checks without rebuilding the product", () => {
assert.deepEqual(classifyFiles([".mergify.yml"]), { ...none, scripts: true });
assert.deepEqual(classifyFiles([".mergify.yml", "CONTRIBUTING.md"]), {
...none,
scripts: true,
docs: true,
});
assert.deepEqual(classifyFiles([".mergify.yml", "Cargo.lock"]), {
...all,
docs: false,
});
assert.deepEqual(classifyFiles([".mergify.yml", "unknown/new-source"]), all);
});
39 changes: 39 additions & 0 deletions scripts/ci-workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,42 @@ test("queue validation rejects missing coverage, producer failures and detection
for (const flag of ["rust", "napi", "napi_arm64", "javascript", "scripts"]) docs.changes.outputs[flag] = "false";
assert.equal(passes(docs), true);
});

test("PR caches cannot evict the shared default-branch Rust caches", () => {
const cacheSteps = [
...workflow.matchAll(
/ - uses: Swatinem\/rust-cache@v2\n([\s\S]*?)(?=\n -|\n [a-z]|$)/g,
),
];
assert.equal(cacheSteps.length, 8);
for (const [, step] of cacheSteps) {
assert.match(
step,
/save-if: \$\{\{ github.ref == format\('refs\/heads\/\{0\}', github.event.repository.default_branch\) \}\}/,
);
}
});

test("cache warming shares the Rust Tests key and builds without running tests", () => {
const warm = readFileSync(
new URL("../.github/workflows/rust-cache.yml", import.meta.url),
"utf8",
);
assert.match(warm, /branches: \[master\]/);
assert.match(warm, /rust-tests:\n if: github.ref == format/);
assert.match(warm, /CARGO_INCREMENTAL: "0"/);
assert.match(warm, /cancel-in-progress: false/);
assert.doesNotMatch(
warm,
/pull_request:|cargo nextest run|cargo test|shared-key:/,
);
assert.match(warm, /steps.cache.outputs.cache-hit != 'true'/);
assert.match(
warm,
/cargo nextest list --locked --workspace --message-format json/,
);
const features = job("rust-tests").match(/--features (\S+)/)[1];
assert.ok(warm.includes(`--features ${features}`));
const nextest = job("rust-tests").match(/tool: (cargo-nextest@\S+)/)[1];
assert.ok(warm.includes(`tool: ${nextest}`));
});
Loading