mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
fix(ci): handle cache eviction and composed cleanup assertions (#4824)
* fix(ci): handle eviction when staging portable build cache
* docs(readme): fix French heading spacing
(cherry picked from commit 0ccd0e6999)
* test(ci): verify observed cache coverage during eviction
* test(electron): assert composed launch cleanup diagnostics
(cherry picked from commit e1466dc480e715b427cddfc3569b2baf03574405)
---------
Co-authored-by: ShuaixinHuang <1836656514@qq.com>
This commit is contained in:
1 parent
7016951394
commit
3ddb2e4cf7
7 files changed
+508
-67
No files matched your search
@@ -1,10 +1,11 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { copyFile, lstat, mkdir, rm } from "node:fs/promises";
|
||||
import { copyFile, lstat, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
const output = ".cache/ci-build-cache";
|
||||
const plan = ".cache/ci-build-plan.json";
|
||||
|
||||
function readTasks() {
|
||||
const { tasks } = z
|
||||
@@ -29,37 +30,93 @@ function readTasks() {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
async function copyEntry(hash) {
|
||||
const entries = [];
|
||||
for (const suffix of [".tar.zst", "-meta.json", "-manifest.json"]) {
|
||||
const name = `${hash}${suffix}`;
|
||||
const source = join(".turbo/cache", name);
|
||||
const entry = await lstat(source).catch((error) => {
|
||||
// Eviction can remove a cache hit even during the successful build.
|
||||
// Manifests are also optional in supported local cache entries.
|
||||
if (error.code === "ENOENT") return undefined;
|
||||
throw error;
|
||||
});
|
||||
if (!entry) continue;
|
||||
if (!entry.isFile())
|
||||
throw new Error(
|
||||
`Portable build cache entry must be a regular file: ${source}`,
|
||||
);
|
||||
entries.push({ suffix, name, source, size: entry.size });
|
||||
}
|
||||
// Publish whole reusable entries only. Consumers rebuild missing tasks.
|
||||
if (
|
||||
![".tar.zst", "-meta.json"].every((suffix) =>
|
||||
entries.some((entry) => entry.suffix === suffix),
|
||||
)
|
||||
)
|
||||
return undefined;
|
||||
const copied = [];
|
||||
let missing = false;
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
await copyFile(entry.source, join(output, entry.name));
|
||||
copied.push(entry);
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
if (entry.suffix === "-manifest.json") continue;
|
||||
missing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (missing) {
|
||||
for (const entry of copied) await rm(join(output, entry.name));
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
files: copied.length,
|
||||
bytes: copied.reduce((sum, entry) => sum + entry.size, 0),
|
||||
};
|
||||
}
|
||||
|
||||
async function snapshot(tasks) {
|
||||
await mkdir(output, { recursive: true });
|
||||
let files = 0;
|
||||
let bytes = 0;
|
||||
const available = new Set();
|
||||
try {
|
||||
for (const hash of new Set(tasks.map((task) => task.hash))) {
|
||||
for (const suffix of [".tar.zst", "-meta.json", "-manifest.json"]) {
|
||||
const name = `${hash}${suffix}`;
|
||||
const source = join(".turbo/cache", name);
|
||||
const entry = await lstat(source).catch((error) => {
|
||||
// Manifests are not present in every supported local cache entry.
|
||||
if (suffix === "-manifest.json" && error.code === "ENOENT")
|
||||
return undefined;
|
||||
throw error;
|
||||
});
|
||||
if (!entry) continue;
|
||||
if (!entry.isFile())
|
||||
throw new Error(
|
||||
`Portable build cache entry must be a regular file: ${source}`,
|
||||
);
|
||||
await copyFile(source, join(output, name));
|
||||
files++;
|
||||
bytes += entry.size;
|
||||
}
|
||||
const entry = await copyEntry(hash);
|
||||
if (!entry) continue;
|
||||
available.add(hash);
|
||||
files += entry.files;
|
||||
bytes += entry.bytes;
|
||||
}
|
||||
// A fully evicted cache still has an explicit artifact and coverage record;
|
||||
// required build outputs and downstream verification remain separate gates.
|
||||
const manifest = `${JSON.stringify({
|
||||
version: 1,
|
||||
tasks: tasks.map((task) => ({
|
||||
...task,
|
||||
available: available.has(task.hash),
|
||||
})),
|
||||
})}\n`;
|
||||
await writeFile(join(output, "portable-build-cache.json"), manifest);
|
||||
files++;
|
||||
bytes += Buffer.byteLength(manifest);
|
||||
} catch (error) {
|
||||
await rm(output, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ tasks: tasks.length, files, bytes, path: output }),
|
||||
JSON.stringify({
|
||||
tasks: tasks.length,
|
||||
files,
|
||||
bytes,
|
||||
path: output,
|
||||
missing_tasks: tasks
|
||||
.filter((task) => !available.has(task.hash))
|
||||
.map((task) => task.taskId),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,9 +125,10 @@ if (extra.length || !["key", "snapshot"].includes(mode))
|
||||
throw new Error(
|
||||
"Usage: portable-build-cache.mjs key|snapshot < turbo-dry-plan.json",
|
||||
);
|
||||
// Remove any earlier snapshot before parsing: a failed second invocation must
|
||||
// never leave an old successful artifact eligible for upload.
|
||||
// Invalidate this mode's previous result before parsing its input. Failed
|
||||
// validation must not leave stale plans or snapshots for downstream reuse.
|
||||
if (mode === "snapshot") await rm(output, { recursive: true, force: true });
|
||||
else await rm(plan, { force: true });
|
||||
const tasks = readTasks();
|
||||
if (mode === "key") {
|
||||
const prefix = z
|
||||
@@ -80,6 +138,10 @@ if (mode === "key") {
|
||||
.parse(process.env.BUILD_CACHE_PREFIX);
|
||||
const hashes = tasks.map(({ taskId, hash }) => `${taskId}:${hash}`).sort();
|
||||
const digest = createHash("sha256").update(hashes.join("\n")).digest("hex");
|
||||
// Retain only validated identities for the later snapshot. Running Turbo
|
||||
// again after the build can evict cache entries before they are copied.
|
||||
await mkdir(".cache", { recursive: true });
|
||||
await writeFile(plan, `${JSON.stringify({ tasks })}\n`);
|
||||
console.log(`key=${prefix}-${digest}\nprefix=${prefix}-`);
|
||||
} else {
|
||||
await snapshot(tasks);
|
||||
|
||||
Reference in new issue
Block a user