* 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>
112 lines
3.6 KiB
JavaScript
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();
|