105 lines
3.3 KiB
JavaScript
105 lines
3.3 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 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"),
|
|
} = {}) {
|
|
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", "check:ci:tasks", "--", "--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();
|