Files
reverse-engineering-anything/tests/boundary/filesystem/ciCachePolicy.test.ts
T
0add6a5aae ci: split fast and deep static checks (#5634)
* ci: split fast and deep static checks

Fast PRs run typecheck, lint, and format before provider admission.
Knip, duplication, architecture guards, and Python stay on the full tier.
Shared-boundary fallbacks keep the Linux x64 baseline, and Turbo jobs
export remote-cache credentials when they are configured.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: keep pull requests from writing the remote cache

Trusted default-branch runs still receive TURBO_TOKEN. Pull requests get an
empty token and remote read-only mode. Knip saves a per-commit cache key so
the next trusted full run can refresh the restored snapshot.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: forward the release cache token and upload deep static reports

Release qualification now passes TURBO_TOKEN into the called workflow.
Labeled pull requests still do not. The deep static job records its task
context and uploads duplication reports after a failed gate.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* style: format the turbo cache policy test

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: keep fast checks exclusive and trusted cache writes explicit

Declare the static artifact name and context script so typecheck matches the
implementations. Lock check:ci:fast around dist, and publish the remote cache
only for release qualification or default-branch push, schedule, and dispatch.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: record static task graphs without nesting the artifacts lock

Keep check:ci:fast locked, and point graph recording at the unlocked
check:ci:fast:tasks script so check:pr and check:test can record hashes
while they already hold that lock.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: keep unauthenticated Turbo runs on the local cache

Open pull requests do not receive TURBO_TOKEN, so a remote read mode cannot
authenticate. Those runs now use the local cache. Trusted runs still publish.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* test(ida): treat unchanged invalid requests as nonretryable

Main already stopped marking invalid_request retryable. The pull request
merge runs that projector, while the IDA lookup tests still expected
retryable true. Match the merged behavior.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-21 03:29:31 +08:00

372 lines
14 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { chmod, readFile, readdir, writeFile } from "node:fs/promises";
import { delimiter, join } from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const repository = process.cwd();
const actionPath = join(repository, ".github/actions/npm-cache/action.yml");
const readAction = async () =>
z
.object({
outputs: z.record(z.string(), z.object({ value: z.string() })),
runs: z.object({
steps: z.array(
z.object({
id: z.string().optional(),
if: z.string().optional(),
uses: z.string().optional(),
with: z.record(z.string(), z.string()).optional(),
run: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
}),
),
}),
})
.parse(parse(await readFile(actionPath, "utf8")));
const readWorkflows = async () => {
const directory = join(repository, ".github/workflows");
const files = (await readdir(directory)).filter((file) =>
/\.ya?ml$/u.test(file),
);
return Promise.all(
files.map(async (file) => ({
file,
workflow: z
.object({
jobs: z
.record(
z.string(),
z.object({
steps: z.array(z.record(z.string(), z.unknown())).optional(),
}),
)
.optional(),
})
.parse(parse(await readFile(join(directory, file), "utf8"))),
})),
);
};
describe("hosted npm download-cache policy", () => {
it("only allows shared writes for trusted original default-branch events", async () => {
const action = await readAction();
const policy = action.runs.steps.find((step) => step.id === "context");
const command = z.string().parse(policy?.run);
const outputDirectory = await createTestTempDirectory(
"rea-ci-cache-policy-",
);
const npmStub = join(outputDirectory, "npm");
await writeFile(
npmStub,
String.raw`#!/bin/sh
printf '%s\n' 'C:\Users\runner with spaces\AppData\Local\npm-cache'
`,
);
await chmod(npmStub, 0o755);
for (const [event, ref, defaultBranch, expected] of [
["push", "refs/heads/main", "main", "true"],
["schedule", "refs/heads/main", "main", "true"],
["workflow_dispatch", "refs/heads/main", "main", "true"],
["push", "refs/heads/stable", "stable", "true"],
["pull_request", "refs/heads/main", "main", "false"],
["pull_request_target", "refs/heads/main", "main", "false"],
["workflow_call", "refs/heads/main", "main", "false"],
["push", "refs/heads/feature/cache", "main", "false"],
["push", "refs/heads/main", "stable", "false"],
["push", "refs/heads/main", "", "false"],
]) {
const outputPath = join(
outputDirectory,
`policy-${Date.now()}-${Math.random()}`,
);
execFileSync("bash", ["-euo", "pipefail", "-c", command], {
env: {
...process.env,
PATH: `${outputDirectory}${delimiter}${process.env.PATH ?? ""}`,
GITHUB_EVENT_NAME: event,
GITHUB_REF: ref,
GITHUB_OUTPUT: outputPath,
DEFAULT_BRANCH: defaultBranch,
LOCKFILES_HASH: "lock-hash",
RUNNER_OS: "Windows",
},
});
const output = await readFile(outputPath, "utf8");
expect(output).toContain(`can-save=${expected}\n`);
expect(output).toContain(
`path=${String.raw`C:\Users\runner with spaces\AppData\Local\npm-cache`}\n`,
);
expect(output).toContain(
`key=npm-download-Windows-${process.arch}-lock-hash\n`,
);
}
});
it("uses a lockfile-, npmrc-, OS-, and runtime-architecture-scoped download cache", async () => {
const action = await readAction();
const policy = action.runs.steps.find((step) => step.id === "context");
const restoreAndSave = action.runs.steps.find((step) =>
step.uses?.startsWith("actions/cache@"),
);
const restoreOnly = action.runs.steps.find((step) =>
step.uses?.startsWith("actions/cache/restore@"),
);
expect(action.outputs["can-save"]?.value).toBe(
"${{ steps.context.outputs.can-save }}",
);
expect(action.outputs.path?.value).toBe(
"${{ steps.context.outputs.path }}",
);
expect(policy?.env?.DEFAULT_BRANCH).toBe(
"${{ github.event.repository.default_branch }}",
);
expect(policy?.env?.LOCKFILES_HASH).toBe(
"${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json', '.npmrc') }}",
);
expect(policy?.run).toContain(
'cache_key="npm-download-${RUNNER_OS}-${runtime_arch}-${LOCKFILES_HASH}"',
);
expect(restoreAndSave?.uses).toBe(
"actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830",
);
expect(restoreAndSave?.if).toBe("steps.context.outputs.can-save == 'true'");
expect(restoreOnly?.uses).toBe(
"actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830",
);
expect(restoreOnly?.if).toBe("steps.context.outputs.can-save != 'true'");
expect(restoreAndSave?.with?.path).toBe(
"${{ steps.context.outputs.path }}",
);
expect(restoreOnly?.with?.path).toBe(restoreAndSave?.with?.path);
expect(restoreAndSave?.with?.key).toBe("${{ steps.context.outputs.key }}");
expect(restoreOnly?.with?.key).toBe(restoreAndSave?.with?.key);
expect(restoreAndSave?.with).not.toHaveProperty("restore-keys");
expect(restoreOnly?.with).not.toHaveProperty("restore-keys");
});
it("replaces every inline setup-node npm cache immediately and preserves conditions", async () => {
const workflows = await readWorkflows();
let npmCacheSteps = 0;
for (const { file, workflow } of workflows) {
const source = await readFile(
join(repository, ".github/workflows", file),
"utf8",
);
expect(source).not.toMatch(/^\s+cache:\s*npm\s*$/mu);
for (const [jobName, job] of Object.entries(workflow.jobs ?? {})) {
const steps = job.steps ?? [];
for (let index = 0; index < steps.length; index++) {
const step = steps[index];
if (step?.uses !== "./.github/actions/npm-cache") continue;
npmCacheSteps++;
const setup = steps[index - 1];
expect(
typeof setup?.uses === "string" &&
setup.uses.startsWith("actions/setup-node@"),
`${file}:${jobName} npm cache action must immediately follow setup-node`,
).toBe(true);
expect(step.if).toBe(setup?.if);
expect(setup?.with).not.toHaveProperty("cache", "npm");
const npmCiIndex = steps.findIndex(
(candidate, candidateIndex) =>
candidateIndex > index && candidate.run === "npm ci",
);
expect(
npmCiIndex,
`${file}:${jobName} retains npm ci after cache`,
).toBeGreaterThan(index);
}
}
}
expect(npmCacheSteps).toBeGreaterThan(0);
});
});
it("shares static task snapshots only after validation using the npm cache owner's write authority", async () => {
const workflows = await readWorkflows();
const ci = workflows.find(({ file }) => file === "ci.yml");
const steps = ci?.workflow.jobs?.static?.steps ?? [];
expect(
steps.find((step) => step.uses === "./.github/actions/npm-cache")?.id,
).toBe("npm-cache");
const restore = steps.find((step) => step.id === "static-cache");
const save = steps.find(
(step) => step.name === "Save validated static task cache",
);
expect(restore?.uses).toBe(
"actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830",
);
expect(restore?.if).toBe("needs.changes.outputs.code == 'true'");
expect(restore?.with).toMatchObject({ path: ".turbo/cache/" });
expect(save?.uses).toBe(
"actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830",
);
expect(save?.if).toContain("success()");
expect(save?.if).toContain("steps.npm-cache.outputs.can-save == 'true'");
expect(save?.with).toEqual({
path: ".turbo/cache/",
key: "${{ steps.static-cache.outputs.cache-primary-key }}",
});
expect(steps.indexOf(save!)).toBeGreaterThan(
steps.findIndex((step) => step.run === "npm run check:ci:fast"),
);
const deep = ci?.workflow.jobs?.["static-deep"]?.steps ?? [];
const deepSave = deep.find(
(step) => step.name === "Save validated deep static task cache",
);
const knipRestore = deep.find((step) => step.id === "knip-cache");
const knipSave = deep.find(
(step) => step.name === "Save Knip analysis cache",
);
expect(deepSave?.uses).toBe(
"actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830",
);
expect(deepSave?.if).toContain("success()");
expect(deepSave?.if).toContain("steps.npm-cache.outputs.can-save == 'true'");
expect(deepSave?.if).toContain(
"steps.static-cache.outputs.cache-hit != 'true'",
);
expect(deep.indexOf(deepSave!)).toBeGreaterThan(
deep.findIndex((step) => step.run === "npm run check:ci:deep"),
);
const knipWith = z
.object({
path: z.string(),
key: z.string(),
"restore-keys": z.string(),
})
.parse(knipRestore?.with);
expect(knipWith.path).toBe("node_modules/.cache/knip");
expect(knipWith.key).toContain("${{ github.sha }}");
expect(knipWith["restore-keys"]).not.toContain("github.sha");
expect(knipWith["restore-keys"].startsWith("knip-meta-v1-")).toBe(true);
expect(knipSave?.if).toContain(
"steps.knip-cache.outputs.cache-hit != 'true'",
);
expect(knipSave?.with).toEqual({
path: "node_modules/.cache/knip",
key: "${{ steps.knip-cache.outputs.cache-primary-key }}",
});
});
it("passes the npm cache owner's selected path to package installation", async () => {
const workflows = await readWorkflows();
const ci = workflows.find(({ file }) => file === "ci.yml");
const steps = ci?.workflow.jobs?.["package-e2e"]?.steps ?? [];
const cacheIndex = steps.findIndex(
(step) => step.uses === "./.github/actions/npm-cache",
);
expect(steps[cacheIndex]?.id).toBe("npm-cache");
const consumerIndex = steps.findIndex(
(step) => step.run === "npm run verify:package",
);
expect(consumerIndex).toBeGreaterThan(cacheIndex);
expect(
z.record(z.string(), z.unknown()).parse(steps[consumerIndex]?.env)
.npm_config_cache,
).toBe("${{ steps.npm-cache.outputs.path }}");
});
it("exports turbo remote-cache credentials wherever a workflow runs turbo", async () => {
const directory = join(repository, ".github/workflows");
const files = (await readdir(directory)).filter((file) =>
/\.ya?ml$/u.test(file),
);
const turboCommand =
/\bturbo\b|build:cached|check:ci|docs:check|docs:generate|docs:prepare|test:prepare/u;
const workflowSchema = z.object({
env: z.record(z.string(), z.unknown()).optional(),
jobs: z.record(
z.string(),
z
.object({
env: z.record(z.string(), z.unknown()).optional(),
steps: z.array(z.object({ run: z.string().optional() })).optional(),
})
.passthrough(),
),
});
for (const file of files) {
const workflow = workflowSchema.parse(
parse(await readFile(join(directory, file), "utf8")),
);
for (const [name, job] of Object.entries(workflow.jobs)) {
const runsTurbo = job.steps?.some(
(step) => step.run !== undefined && turboCommand.test(step.run),
);
if (!runsTurbo) continue;
const env = { ...workflow.env, ...job.env };
const token = z.string().parse(env.TURBO_TOKEN);
const cache = z.string().parse(env.TURBO_CACHE);
expect(token, `${file}:${name}`).toContain("secrets.TURBO_TOKEN");
expect(token, `${file}:${name}`).toContain("|| ''");
expect(token, `${file}:${name}`).not.toBe("${{ secrets.TURBO_TOKEN }}");
const grantsReleaseWrite = token.includes(
"inputs.remote_cache_write == true",
);
const grantsCalledEvent = token.includes(
"github.event_name == 'workflow_call'",
);
expect(grantsReleaseWrite || grantsCalledEvent, `${file}:${name}`).toBe(
true,
);
if (grantsReleaseWrite)
expect(token, `${file}:${name}`).not.toContain(
"github.event_name == 'workflow_call'",
);
for (const event of ["push", "schedule", "workflow_dispatch"])
expect(token, `${file}:${name}`).toContain(
`github.event_name == '${event}'`,
);
expect(token, `${file}:${name}`).not.toContain(
"github.event_name == 'pull_request'",
);
if (grantsReleaseWrite) {
expect(cache, `${file}:${name}`).toContain(
"inputs.remote_cache_write == true",
);
expect(cache, `${file}:${name}`).toContain("|| 'local:rw'");
expect(cache, `${file}:${name}`).not.toContain("local:rw,remote:r'");
} else expect(cache, `${file}:${name}`).toContain("local:rw,remote:r'");
expect(cache, `${file}:${name}`).toContain("local:rw,remote:rw");
expect(env.TURBO_TEAM, `${file}:${name}`).toBe("${{ vars.TURBO_TEAM }}");
}
}
});
it("bounds the rolling static snapshot with Turbo age and size eviction", async () => {
const turbo = z
.object({
cacheMaxAge: z.string(),
cacheMaxSize: z.string(),
})
.parse(JSON.parse(await readFile(join(repository, "turbo.json"), "utf8")));
// The static job restores the previous snapshot by prefix and saves the
// whole .turbo/cache directory again. Without local eviction every main
// commit carries all historical entries into a new immutable GitHub cache,
// growing transfers toward the 10 GB repository limit.
expect(turbo.cacheMaxAge).toMatch(/^[1-9]\d*(s|m|h|d|w)$/u);
expect(turbo.cacheMaxAge).not.toBe("0");
const size = /^(\d+(?:\.\d+)?)(MB|GB|TB)$/iu.exec(turbo.cacheMaxSize);
expect(size, "cacheMaxSize must use an explicit MB/GB/TB budget").not.toBe(
null,
);
const value = Number(size?.[1] ?? "0");
const unit = (size?.[2] ?? "MB").toUpperCase();
const megabytes =
unit === "TB" ? value * 1024 * 1024 : unit === "GB" ? value * 1024 : value;
expect(megabytes).toBeGreaterThan(0);
expect(megabytes).toBeLessThanOrEqual(1024);
});