mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 03:25:05 +00:00
fix(ci): preserve complete Rust caches across main and fork builds
This commit is contained in:
@@ -86,6 +86,30 @@ and Java rather than Docker; those matrix entries explicitly retain Java 21.
|
||||
| Cassandra | 3.11.19, 5.0.6 |
|
||||
| RabbitMQ | 3.13, 4.3 |
|
||||
|
||||
## Rust caches and build timings
|
||||
|
||||
Linux package, clippy, Rust test and native Agent jobs use the same pinned sccache
|
||||
compiler wrappers on main and fork PRs. Main uses the existing S3 backend; jobs
|
||||
without repository secrets use sccache's local disk backend. Only backend settings
|
||||
are conditional: changing `CC` or `CXX` invalidates native build-script fingerprints
|
||||
even if a target archive was restored. The cache key retains the real compiler
|
||||
environment; only the CI selection variables are excluded. Full/fast features and
|
||||
test-group selection remain unchanged and can still require additional builds.
|
||||
|
||||
All Rust target caches use the `v1-rust-complete` namespace and save only after a
|
||||
successful job, from main. This bypasses old partial snapshots without deleting
|
||||
caches or changing the S3 namespace. A cancelled or failed first build must not
|
||||
publish a shared snapshot: rust-cache does not update an exact cache hit, so later
|
||||
successful builds would otherwise keep restoring the unfinished snapshot.
|
||||
The first successful main run must populate the new namespace before fork PRs can
|
||||
reuse it. Target caching is not a per-commit snapshot of workspace artifacts.
|
||||
|
||||
Clippy, nextest and native Agent nextest builds emit Cargo HTML timing reports.
|
||||
Their `DBX-*-cargo-timings` artifacts are retained for three days, including after
|
||||
failures when a report exists, but uploads do not delay cancelled runs. Inspect
|
||||
these reports alongside sccache statistics: its hit rate excludes unsupported
|
||||
crate types and does not measure the time spent compiling and linking test binaries.
|
||||
|
||||
## Validation
|
||||
|
||||
Run these from the repository root:
|
||||
|
||||
@@ -277,8 +277,11 @@ test("nextest, doctests and coverage select identical packages and features in e
|
||||
const nextest = rustCommand("test", group, mode);
|
||||
const doctest = rustCommand("doctest", group, mode);
|
||||
assert.deepEqual(nextest.slice(0, 3), ["nextest", "run", "--no-fail-fast"]);
|
||||
assert.deepEqual(doctest, ["test", "--doc", ...nextest.slice(3)]);
|
||||
assert.deepEqual(rustCommand("tree", group, mode), ["tree", ...nextest.slice(3)]);
|
||||
const selection = nextest.slice(3).filter((argument) => argument !== "--timings");
|
||||
assert.deepEqual(doctest, ["test", "--doc", ...selection]);
|
||||
assert.deepEqual(rustCommand("tree", group, mode), ["tree", ...selection]);
|
||||
assert.ok(nextest.includes("--timings"));
|
||||
assert.ok(rustCommand("clippy", "workspace", mode).includes("--timings"));
|
||||
assert.ok(nextest.includes("--no-default-features"));
|
||||
assert.ok(nextest.includes("--locked"));
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function rustCommand(action, group, mode) {
|
||||
] : appFeatures;
|
||||
const packages = group === "workspace" ? ["--workspace"] : rustGroups[group].flatMap((name) => ["--package", name]);
|
||||
const command = action === "test" ? ["nextest", "run", "--no-fail-fast"] : action === "doctest" ? ["test", "--doc"] : [action];
|
||||
return [...command, ...packages, "--locked", ...(action === "clippy" ? ["--all-targets"] : []),
|
||||
return [...command, ...packages, "--locked", ...(["test", "clippy"].includes(action) ? ["--timings"] : []), ...(action === "clippy" ? ["--all-targets"] : []),
|
||||
"--no-default-features", "--features", features.join(","), ...(action === "clippy" ? ["--", "-D", "warnings"] : [])];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const workflow = readFileSync(new URL("../workflows/ci.yml", import.meta.url), "utf8");
|
||||
@@ -83,19 +86,93 @@ test("every old Agent stage has an independent owner and Java packaging remains
|
||||
for (const name of ["agent-rust", "agent-go", "agent-integration", "agent-java"]) assert.doesNotMatch(job(name), /continue-on-error: true/);
|
||||
});
|
||||
|
||||
test("native Rust driver caches exclude failed build artifacts", () => {
|
||||
const content = job("agent-rust");
|
||||
assert.ok(content.includes("shared-key: ci-agent-rust-v2-${{ matrix.driver }}"));
|
||||
assert.ok(content.includes("cache-on-failure: false"));
|
||||
test("shared Rust caches publish only complete builds outside the old cache namespace", () => {
|
||||
for (const name of ["packages", "rust-fmt-clippy", "rust-test", "agent-rust", "agent-integration"]) {
|
||||
const content = job(name);
|
||||
assert.ok(content.includes("prefix-key: v1-rust-complete"), name);
|
||||
assert.ok(content.includes("cache-on-failure: false"), name);
|
||||
assert.doesNotMatch(content, /cache-on-failure: true/);
|
||||
assert.ok(content.includes("save-if: ${{ github.ref == 'refs/heads/main'"), name);
|
||||
}
|
||||
});
|
||||
|
||||
test("main and fork Rust jobs use the same rust-cache fingerprint", () => {
|
||||
const stableCacheEnvironment = /- name: Rust cache\s+env:\s+RUSTC_WRAPPER: ""\s+RUST_FEATURE_MODE: ""\s+RUST_TEST_GROUP: ""\s+CC: ""\s+CXX: ""\s+uses: swatinem\/rust-cache@v2/;
|
||||
const stableCacheEnvironment = /- name: Rust cache\s+env:\s+RUST_FEATURE_MODE: ""\s+RUST_TEST_GROUP: ""\s+uses: swatinem\/rust-cache@v2/;
|
||||
for (const name of ["packages", "rust-fmt-clippy", "rust-test", "agent-rust"]) {
|
||||
assert.match(job(name), stableCacheEnvironment);
|
||||
const content = job(name);
|
||||
assert.match(content, stableCacheEnvironment);
|
||||
assert.match(content, /^ RUSTC_WRAPPER: sccache$/m);
|
||||
assert.ok(content.includes('version: "v0.16.0"'));
|
||||
assert.doesNotMatch(content, /RUSTC_WRAPPER: ""|CC: ""|CXX: ""/);
|
||||
}
|
||||
});
|
||||
|
||||
test("S3 and secretless Rust jobs export identical compiler wrappers", (context) => {
|
||||
const directory = mkdtempSync(path.join(tmpdir(), "dbx-ci-sccache-"));
|
||||
context.after(() => rmSync(directory, { recursive: true, force: true }));
|
||||
const compiler = "/opt/hostedtoolcache/sccache/0.16.0/x64/sccache";
|
||||
const backend = {
|
||||
SCCACHE_BUCKET: "test-bucket",
|
||||
SCCACHE_ENDPOINT: "https://cache.example.test",
|
||||
SCCACHE_REGION: "test-region",
|
||||
SCCACHE_S3_KEY_PREFIX: "test-prefix",
|
||||
SCCACHE_S3_USE_SSL: "true",
|
||||
AWS_ACCESS_KEY_ID: "test-access-key",
|
||||
AWS_SECRET_ACCESS_KEY: "test-secret-key",
|
||||
};
|
||||
for (const name of ["packages", "rust-fmt-clippy", "rust-test", "agent-rust"]) {
|
||||
const setup = job(name).match(/- name: Configure sccache\n([\s\S]*?)(?=\n - name: Rust cache)/)?.[1];
|
||||
assert.ok(setup, name);
|
||||
assert.doesNotMatch(setup, /^\s+if:/m);
|
||||
const script = setup.match(/ run: \|\n([\s\S]*)/)[1].split("\n").map((line) => line.slice(10)).join("\n");
|
||||
for (const remote of [true, false]) {
|
||||
const output = path.join(directory, `${name}-${remote}.env`);
|
||||
const result = spawnSync("bash", ["-e", "-o", "pipefail", "-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
GITHUB_ENV: output,
|
||||
SCCACHE_PATH: compiler,
|
||||
CACHE_BUCKET: remote ? backend.SCCACHE_BUCKET : "",
|
||||
CACHE_ENDPOINT: remote ? backend.SCCACHE_ENDPOINT : "",
|
||||
CACHE_REGION: remote ? backend.SCCACHE_REGION : "",
|
||||
CACHE_KEY_PREFIX: remote ? backend.SCCACHE_S3_KEY_PREFIX : "",
|
||||
CACHE_ACCESS_KEY_ID: remote ? backend.AWS_ACCESS_KEY_ID : "",
|
||||
CACHE_SECRET_ACCESS_KEY: remote ? backend.AWS_SECRET_ACCESS_KEY : "",
|
||||
},
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, "");
|
||||
const exported = Object.fromEntries(readFileSync(output, "utf8").trim().split("\n").map((line) => {
|
||||
const separator = line.indexOf("=");
|
||||
return [line.slice(0, separator), line.slice(separator + 1)];
|
||||
}));
|
||||
assert.deepEqual(exported, {
|
||||
...(remote ? backend : {}),
|
||||
SCCACHE_IDLE_TIMEOUT: "0",
|
||||
CC: `${compiler} cc`,
|
||||
CXX: `${compiler} c++`,
|
||||
}, `${name}: ${remote ? "S3" : "local"}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Rust build timings remain available after failures without waiting for cancelled runs", () => {
|
||||
for (const [name, artifact, location] of [
|
||||
["rust-fmt-clippy", "DBX-clippy-cargo-timings", "target/cargo-timings/"],
|
||||
["rust-test", "DBX-rust-${{ matrix.group }}-cargo-timings", "target/cargo-timings/"],
|
||||
["agent-rust", "DBX-agent-${{ matrix.driver }}-cargo-timings", "agents/drivers/${{ matrix.driver }}/target/cargo-timings/"],
|
||||
]) {
|
||||
const content = job(name);
|
||||
assert.match(content, /name: Upload Rust Cargo timings\s+if: always\(\) && !cancelled\(\)\s+uses: actions\/upload-artifact@v6/);
|
||||
assert.ok(content.includes(`name: ${artifact}`));
|
||||
assert.ok(content.includes(`path: ${location}`));
|
||||
assert.ok(content.includes("retention-days: 3"));
|
||||
assert.ok(content.includes("if-no-files-found: ignore"));
|
||||
}
|
||||
assert.ok(job("agent-rust").includes('--manifest-path "drivers/$DRIVER/Cargo.toml" --locked --no-fail-fast --timings'));
|
||||
});
|
||||
|
||||
test("Linux Rust dependency installs bound apt mirror stalls", () => {
|
||||
for (const name of ["packages", "rust-fmt-clippy", "rust-test", "agent-rust"]) {
|
||||
const content = job(name);
|
||||
|
||||
Reference in New Issue
Block a user