Skill Tracer
Skills › gstack › gstack › cso-staging-run.ts

cso-staging-run.ts

124 lines · scripts/cso-staging-run.ts at 3f79d81

1#!/usr/bin/env -S bun --no-env-file --config=/dev/null
2/** Bind a qualification dispatch to the one successful protected-main staging run that signed its images. */
3import * as fs from 'node:fs';
4import * as path from 'node:path';
5import { canonical } from '../lib/cso/contracts';
6import { committedImageBuildMatrix } from './cso-image-matrix';
7
8export const STAGING_WORKFLOW_PATH = '.github/workflows/cso-runtime-images.yml';
9export const NATIVE_JOB = 'qualify-native';
10const SLSA_PROVENANCE = 'https://slsa.dev/provenance/v1';
11const REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
12const INVOCATION = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/actions\/runs\/([1-9][0-9]*)\/attempts\/[1-9][0-9]*$/;
13const MAX_FILE_BYTES = 4 * 1024 * 1024;
15export interface StagingRunEvidence {
16 repository: string;
17 runId: string;
18 sourceCommit: string;
19 runtimeIds: string[];
20 run: unknown;
21 jobPages: unknown;
24/**
25 * Resolve the staging run from verified SLSA provenance. The predicate's
26 * invocation and the Fulcio certificate's run URI must agree, and every
27 * verified statement for every image must name the same run.
28 */
29export function stagingRunId(repository: string, verifications: unknown[]): string {
30 if (!REPOSITORY.test(repository) || verifications.length === 0) throw new Error('INVALID_STAGING_PROVENANCE');
31 const runs = new Set<string>();
32 for (const verification of verifications) {
33 if (!Array.isArray(verification) || verification.length === 0) throw new Error('INVALID_STAGING_PROVENANCE');
34 for (const item of verification) {
35 const result = (item as any)?.verificationResult;
36 if (result?.statement?.predicateType !== SLSA_PROVENANCE) throw new Error('INVALID_STAGING_PROVENANCE');
37 const invocation = result.statement.predicate?.runDetails?.metadata?.invocationId;
38 const match = typeof invocation === 'string' ? INVOCATION.exec(invocation) : null;
39 if (!match || match[1] !== repository || result.signature?.certificate?.runInvocationURI !== invocation) {
40 throw new Error('STAGING_INVOCATION_MISMATCH');
41 }
42 runs.add(match[2]);
43 }
44 }
45 if (runs.size !== 1) throw new Error('STAGING_RUN_NOT_UNIQUE');
46 return [...runs][0];
49/** Every statement in one qualification dispatch names the same staged source commit. */
50export function dispatchSourceCommit(event: unknown): string {
51 const statements = (event as any)?.client_payload?.statements;
52 if (!Array.isArray(statements) || statements.length === 0) throw new Error('INVALID_QUALIFICATION_DISPATCH');
53 const commits = new Set(statements.map((statement: any) => statement?.sourceCommit));
54 const [commit] = [...commits];
55 if (commits.size !== 1 || typeof commit !== 'string' || !/^[a-f0-9]{40}$/.test(commit)) throw new Error('STAGING_SOURCE_COMMIT_MISMATCH');
56 return commit;
59/** The resolved run must be the protected-main staging dispatch at the statements' commit, with every native gate passed. */
60export function verifyStagingRun(evidence: StagingRunEvidence): void {
61 const { repository, runId, sourceCommit, runtimeIds } = evidence;
62 const run = evidence.run as any;
63 if (!run || typeof run !== 'object' || String(run.id) !== runId) throw new Error('STAGING_RUN_ID_MISMATCH');
64 if (run.path !== STAGING_WORKFLOW_PATH) throw new Error('STAGING_WORKFLOW_MISMATCH');
65 if (run.repository?.full_name !== repository || run.head_repository?.full_name !== repository) throw new Error('STAGING_REPOSITORY_MISMATCH');
66 if (run.event !== 'workflow_dispatch' || run.head_branch !== 'main') throw new Error('STAGING_RUN_NOT_PROTECTED_MAIN_DISPATCH');
67 if (run.status !== 'completed' || run.conclusion !== 'success') throw new Error('STAGING_RUN_NOT_SUCCESSFUL');
68 if (run.head_sha !== sourceCommit) throw new Error('STAGING_SOURCE_COMMIT_MISMATCH');
70 const pages = evidence.jobPages;
71 if (!Array.isArray(pages) || pages.length === 0) throw new Error('INVALID_STAGING_JOBS');
72 const totals = new Set(pages.map((page: any) => page?.total_count));
73 if (pages.some((page: any) => !Array.isArray(page?.jobs)) || totals.size !== 1) throw new Error('INVALID_STAGING_JOBS');
74 const jobs = pages.flatMap((page: any) => page.jobs);
75 if (jobs.length !== [...totals][0]) throw new Error('INCOMPLETE_STAGING_JOBS');
77 const native = jobs.filter((job: any) => typeof job?.name === 'string' && job.name.startsWith(NATIVE_JOB));
78 const expected = runtimeIds.map(id => `${NATIVE_JOB} ${id}`).sort();
79 if (runtimeIds.length === 0 || canonical(native.map((job: any) => job.name).sort()) !== canonical(expected)) {
80 throw new Error('STAGING_NATIVE_JOBS_MISMATCH');
81 }
82 if (native.some((job: any) => String(job.run_id) !== runId || job.status !== 'completed' || job.conclusion !== 'success')) {
83 throw new Error('STAGING_NATIVE_GATE_NOT_PASSED');
84 }
87function readJson(file: string): unknown {
88 const absolute = path.resolve(file);
89 const stat = fs.lstatSync(absolute);
90 if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > MAX_FILE_BYTES) throw new Error('UNSAFE_STAGING_EVIDENCE_FILE');
91 try { return JSON.parse(fs.readFileSync(absolute, 'utf8')); } catch { throw new Error('INVALID_STAGING_EVIDENCE_FILE'); }
94function option(args: string[], name: string): string {
95 const index = args.indexOf(name);
96 const value = index >= 0 ? args[index + 1] : undefined;
97 if (!value || value.startsWith('--')) throw new Error(`Missing ${name}`);
98 return value;
101if (import.meta.main) {
102 try {
103 const [command, ...args] = process.argv.slice(2);
104 if (command === 'run-id' && args[0] === '--repository' && args.length >= 3) {
105 process.stdout.write(`${stagingRunId(args[1], args.slice(2).map(readJson))}\n`);
106 } else if (command === 'verify' && args.length === 10) {
107 const event = readJson(option(args, '--event'));
108 verifyStagingRun({
109 repository: option(args, '--repository'),
110 runId: option(args, '--run-id'),
111 sourceCommit: dispatchSourceCommit(event),
112 runtimeIds: committedImageBuildMatrix().include.map(row => row.runtimeId),
113 run: readJson(option(args, '--run')),
114 jobPages: readJson(option(args, '--jobs')),
115 });
116 process.stdout.write('STAGING RUN VERIFIED\n');
117 } else {
118 throw new Error('Usage: cso-staging-run.ts run-id --repository OWNER/REPO PROVENANCE.json... | verify --repository OWNER/REPO --run-id ID --event EVENT.json --run RUN.json --jobs JOB-PAGES.json');
119 }
120 } catch (error) {
121 process.stderr.write((error instanceof Error ? error.message : 'STAGING_RUN_VERIFICATION_FAILED') + '\n');
122 process.exitCode = 1;
123 }