Files
rea/scripts/ci/static-task-context.mjs
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

112 lines
3.6 KiB
JavaScript

import { execFile } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { z } from "zod";
const execute = promisify(execFile);
const STATIC_CONTEXT_SCRIPTS = new Set([
"check:ci:fast:tasks",
"check:ci:deep:tasks",
]);
const contextSchema = z.object({
version: z.literal(1),
sourceSha: z.string().regex(/^[a-f0-9]{40}$/u),
sourceClean: z.boolean(),
prHeadSha: z
.string()
.regex(/^[a-f0-9]{40}$/u)
.optional(),
nodeVersion: z.string().min(1),
npmVersion: z.string().min(1),
turboVersion: z.string().min(1),
platform: z.string().min(1),
architecture: z.string().min(1),
taskHashes: z.record(z.string(), z.string().regex(/^[a-f0-9]{16}$/u)),
});
/** Parse the producing check graph's receipt; incomplete provenance is unknown. */
export const parseStaticTaskContext = (value) => contextSchema.parse(value);
/** Retain only named task hashes and tool/source identity, never the environment. */
export async function recordStaticTaskContext({
root = process.cwd(),
npmCli = process.env.npm_execpath,
outputDirectory = join(root, ".cache/static-checks"),
script = process.env.CI_STATIC_CONTEXT_SCRIPT ?? "check:ci:fast:tasks",
} = {}) {
if (!STATIC_CONTEXT_SCRIPTS.has(script))
throw new Error("Static context script is not a known check");
if (typeof npmCli !== "string" || !npmCli)
throw new Error(
"Run static context recording through npm run ci:static-context",
);
const options = {
cwd: root,
encoding: "utf8",
timeout: 30_000,
maxBuffer: 32 * 1024 * 1024,
};
const [graph, source, status, npmVersion, turbo] = await Promise.all([
execute(
process.execPath,
[npmCli, "run", "--silent", script, "--", "--dry=json"],
options,
),
execute("git", ["rev-parse", "HEAD"], options),
execute(
"git",
["status", "--porcelain", "--untracked-files=normal"],
options,
),
execute(process.execPath, [npmCli, "--version"], options),
readFile(join(root, "node_modules/turbo/package.json"), "utf8"),
]);
const tasks = z
.object({
tasks: z
.array(
z.object({
task: z.string().min(1),
hash: z.string().regex(/^[a-f0-9]{16}$/u),
}),
)
.min(1),
})
.parse(JSON.parse(graph.stdout)).tasks;
if (new Set(tasks.map(({ task }) => task)).size !== tasks.length)
throw new Error("Static graph contains duplicate task identities");
const context = parseStaticTaskContext({
version: 1,
sourceSha: source.stdout.trim(),
sourceClean: status.stdout.length === 0,
...(process.env.CI_PR_HEAD_SHA
? { prHeadSha: process.env.CI_PR_HEAD_SHA }
: {}),
nodeVersion: process.version,
npmVersion: npmVersion.stdout.trim(),
turboVersion: z.object({ version: z.string() }).parse(JSON.parse(turbo))
.version,
platform: process.platform,
architecture: process.arch,
taskHashes: Object.fromEntries(tasks.map(({ task, hash }) => [task, hash])),
});
if (process.env.GITHUB_SHA && process.env.GITHUB_SHA !== context.sourceSha)
throw new Error(
"Static context source does not match the workflow revision",
);
await mkdir(outputDirectory, { recursive: true });
await writeFile(
join(outputDirectory, "static-task-context.json"),
JSON.stringify(context, null, 2) + "\n",
);
return context;
}
if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
await recordStaticTaskContext();