#!/usr/bin/env node import { execFileSync, spawnSync } from "node:child_process"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { isRecord, readStringField } from "@openclaw/normalization-core/record-coerce"; import { minimatch } from "minimatch"; import { parse } from "yaml"; import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; import { execGhApiRead, plainGhEnv } from "./lib/plain-gh.mjs"; const SCHEDULED_HOSTED_WORKFLOW_PATHS = new Map([ ["Blacksmith Testbox", ".github/workflows/ci-check-testbox.yml"], ["Blacksmith ARM Testbox", ".github/workflows/ci-check-arm-testbox.yml"], ["Blacksmith Build Artifacts Testbox", ".github/workflows/ci-build-artifacts-testbox.yml"], ["Workflow Sanity", ".github/workflows/workflow-sanity.yml"], ]); export const SCHEDULED_HOSTED_WORKFLOWS = [...SCHEDULED_HOSTED_WORKFLOW_PATHS.keys()]; const CI_WORKFLOW_PATH = ".github/workflows/ci.yml"; const BUILD_ARTIFACTS_WORKFLOW = "Blacksmith Build Artifacts Testbox"; const ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS = [ "Blacksmith Testbox", "Blacksmith ARM Testbox", "Workflow Sanity", ]; // Full workflow-run objects are large enough for a 100-row response to exceed // the Octopool relay cap on busy SHAs. Keep each REST page bounded and retain // the existing 1,000-result search window through pagination. const WORKFLOW_RUNS_PAGE_SIZE = 30; const MAX_WORKFLOW_RUN_SEARCH_RESULTS = 1_000; const COMPARE_COMMITS_PAGE_SIZE = 100; export const HOSTED_GATE_MAX_AGE_HOURS = 24; const HOSTED_GATE_MAX_AGE_MS = HOSTED_GATE_MAX_AGE_HOURS * 60 * 60 * 1_000; const HOSTED_GATE_CLOCK_SKEW_MS = 5 * 60 * 1_000; const MAX_CI_REUSE_CANDIDATES = 5; const CI_REUSE_RUN_LIST_LIMIT = 50; const GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024; const PATH_GLOB_OPTIONS = { dot: true, nocomment: true, nonegate: true }; type WorkflowRun = Partial>; type CiGateJob = ReturnType; type ExecGit = (args: string[], options?: { input?: string }) => string; type PullRequestPathFilter = { paths?: string[]; "paths-ignore"?: string[] }; type CollectHostedGateEvidenceParams = { sha: string; pr?: number; recentSha?: string; pullRequestCommitShas?: string[]; pullRequestHeadBranch?: string; pullRequestHeadRepository?: string; workflowRuns: WorkflowRun[]; ciGateJobs?: CiGateJob[]; loadCiReuseCandidates?: () => WorkflowRun[]; execGit?: ExecGit; notApplicableScheduledWorkflows?: string[]; changelogOnly?: boolean; nowMs?: number; }; type HostedGateEvidence = { headSha: string; evidenceHeadSha?: string; reusedFromSha?: string; reusedRunId?: number; patchIdMatched?: true; workflows: Array<{ id: number | undefined; name: string | undefined; event: string | undefined; headSha: string | undefined; headBranch: string | undefined; status: string | undefined; conclusion: string | null | undefined; createdAt: string | undefined; updatedAt: string | undefined; url: string | undefined; }>; fallbackCoveredWorkflows?: Array<{ name: string; coveredBy: string; reason: string }>; notApplicableWorkflows?: string[]; }; function optionalNumber(record: Record, key: string): number | undefined { const value = record[key]; return typeof value === "number" ? value : undefined; } function optionalNullableString( record: Record, key: string, ): string | null | undefined { const value = record[key]; return value === null || typeof value === "string" ? value : undefined; } function toWorkflowRun(run: Record) { const headRepository = isRecord(run.head_repository) ? { full_name: readStringField(run.head_repository, "full_name") } : undefined; const pullRequests = Array.isArray(run.pull_requests) ? run.pull_requests.filter(isRecord).map((pullRequest) => ({ number: optionalNumber(pullRequest, "number"), })) : undefined; return { id: optionalNumber(run, "id"), run_number: optionalNumber(run, "run_number"), run_attempt: optionalNumber(run, "run_attempt"), name: readStringField(run, "name"), event: readStringField(run, "event"), head_sha: readStringField(run, "head_sha"), head_branch: readStringField(run, "head_branch"), head_repository: headRepository, path: readStringField(run, "path"), display_title: readStringField(run, "display_title"), status: readStringField(run, "status"), conclusion: optionalNullableString(run, "conclusion"), created_at: readStringField(run, "created_at"), updated_at: readStringField(run, "updated_at"), html_url: readStringField(run, "html_url"), pull_requests: pullRequests, }; } function toCiGateJob(job: Record) { return { name: readStringField(job, "name"), run_id: optionalNumber(job, "run_id"), run_attempt: optionalNumber(job, "run_attempt"), status: readStringField(job, "status"), conclusion: readStringField(job, "conclusion"), completed_at: readStringField(job, "completed_at"), }; } export function parseArgs(argv: readonly string[]) { const args = { repo: "", sha: "", pr: 0, recentSha: "", output: "", changelogOnly: false, }; parseFlagArgs( argv, args, [ ...( [ ["--repo", "repo"], ["--sha", "sha"], ["--recent-sha", "recentSha"], ["--output", "output"], ] as const ).map(([flag, key]) => stringFlag(flag, key, { allowInline: false, missingValueMessage: `Expected ${flag} .`, rejectShortOptions: true, }), ), stringFlag("--pr", "pr", { allowInline: false, missingValueMessage: "Expected --pr .", rejectShortOptions: true, transform(value: string) { const parsed = Number(value); if (!Number.isSafeInteger(parsed) || parsed <= 0) { throw new Error("Expected --pr ."); } return parsed; }, }), booleanFlag("--changelog-only", "changelogOnly"), ], { duplicateOptionMessage: (flag: string) => `${flag} was provided more than once.`, ignoreDoubleDash: false, onUnhandledArg(arg: string) { throw new Error(`Unknown option: ${arg}`); }, }, ); if (!args.repo || !args.sha || !args.pr || !args.output) { throw new Error( "Usage: node scripts/verify-pr-hosted-gates.mjs --repo --sha --pr [--recent-sha ] --output ", ); } return args; } function matchesOrderedPathPatterns(changedPath: string, patterns: string[]) { let included = false; for (const declaredPattern of patterns ?? []) { const excluded = declaredPattern.startsWith("!"); const pattern = excluded ? declaredPattern.slice(1) : declaredPattern; if (minimatch(changedPath, pattern, PATH_GLOB_OPTIONS)) { included = !excluded; } } return included; } function pullRequestPathFilterApplies(pullRequest: PullRequestPathFilter, changedPaths: string[]) { if (changedPaths.length === 0) { return false; } const includedPatterns = pullRequest.paths ?? []; if (includedPatterns.length > 0) { return changedPaths.some((changedPath) => matchesOrderedPathPatterns(changedPath, includedPatterns), ); } const ignoredPatterns = pullRequest["paths-ignore"] ?? []; if (ignoredPatterns.length > 0) { return changedPaths.some((changedPath: string) => ignoredPatterns.every( (pattern: string) => !minimatch(changedPath, pattern, PATH_GLOB_OPTIONS), ), ); } return true; } export function notApplicableScheduledHostedWorkflows(changedPaths: string[]) { return [...SCHEDULED_HOSTED_WORKFLOW_PATHS] .filter(([expectedName, workflowPath]) => { const workflow = parse(readFileSync(workflowPath, "utf8")); if (workflow?.name !== expectedName) { throw new Error(`${workflowPath} must declare workflow name ${expectedName}.`); } if (!Object.hasOwn(workflow?.on ?? {}, "pull_request")) { throw new Error(`${workflowPath} must declare a pull_request trigger.`); } const pullRequest = workflow.on.pull_request ?? {}; if (Object.hasOwn(pullRequest, "branches") || Object.hasOwn(pullRequest, "branches-ignore")) { throw new Error(`${workflowPath} pull_request branch filters are not supported.`); } return !pullRequestPathFilterApplies(pullRequest, changedPaths); }) .map(([workflowName]) => workflowName); } function formatObservedRuns(runs: WorkflowRun[]) { if (runs.length === 0) { return "none"; } return runs .map( (run) => `${run.id ?? "unknown"}:${run.status ?? "unknown"}/${run.conclusion ?? "unknown"}`, ) .join(", "); } function isReleaseGateCiRun(run: WorkflowRun, sha: string) { return ( run?.event === "workflow_dispatch" && run?.head_sha === sha && (run.path ?? "").split("@", 1)[0] === CI_WORKFLOW_PATH && run?.display_title === `CI release gate ${sha}` ); } function matchingAuthoritativeRuns( runs: WorkflowRun[], workflowName: string, sha: string, allowManual = true, ) { return runs.filter((run) => { if (run?.head_sha !== sha) { return false; } if (run?.event === "pull_request") { return run?.name === workflowName; } return allowManual && workflowName === "CI" && isReleaseGateCiRun(run, sha); }); } function latestRun(runs: WorkflowRun[]) { // GitHub run_number is creation order; updated_at moves as jobs finish. return runs.toSorted( (left, right) => Number(right.run_number ?? right.id ?? 0) - Number(left.run_number ?? left.id ?? 0), )[0]; } function runUpdatedAtMs(run: Pick | undefined) { const value = Date.parse(run?.updated_at ?? ""); return Number.isFinite(value) ? value : null; } function isRecentRun(run: WorkflowRun | undefined, nowMs: number) { const updatedAtMs = runUpdatedAtMs(run); return ( updatedAtMs !== null && updatedAtMs >= nowMs - HOSTED_GATE_MAX_AGE_MS && updatedAtMs <= nowMs + HOSTED_GATE_CLOCK_SKEW_MS ); } function isSuccessfulRecentRun(run: WorkflowRun | undefined, nowMs: number) { return run?.status === "completed" && run.conclusion === "success" && isRecentRun(run, nowMs); } function runGit(args: string[], { input }: { input?: string } = {}) { const result = spawnSync("git", args, { encoding: "utf8", input, maxBuffer: GIT_MAX_BUFFER_BYTES, stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"], }); if (result.error) { throw result.error; } if (result.status !== 0) { throw new Error(`git ${args[0]} failed with exit code ${result.status ?? "unknown"}`); } return result.stdout; } function parseSingleObjectId(raw: unknown, label: string) { const values = String(raw).trim().split(/\s+/u).filter(Boolean); const value = values[0]; if (values.length !== 1 || !value || !/^[0-9a-f]{40,64}$/u.test(value)) { throw new Error(`Expected one ${label} object id.`); } return value; } function computePatchId(sha: string, mainRef: string, execGit: ExecGit) { const mergeBase = parseSingleObjectId( execGit(["merge-base", mainRef, sha]), `merge base for ${sha}`, ); const diff = execGit(["diff", mergeBase, sha]); const patchIdLines = execGit(["patch-id", "--stable"], { input: diff }) .trim() .split(/\r?\n/u) .filter(Boolean); if (patchIdLines.length !== 1) { throw new Error(`Expected one patch id for ${sha}.`); } const patchIdLine = patchIdLines[0]; if (!patchIdLine) { throw new Error(`Invalid patch-id output for ${sha}.`); } const [patchId, commitId, ...rest] = patchIdLine.trim().split(/\s+/u); if ( rest.length > 0 || !/^[0-9a-f]{40,64}$/u.test(patchId ?? "") || !/^[0-9a-f]{40,64}$/u.test(commitId ?? "") ) { throw new Error(`Invalid patch-id output for ${sha}.`); } return patchId; } function ensureCommitAvailable(sha: string, execGit: ExecGit) { try { execGit(["cat-file", "-e", `${sha}^{commit}`]); return true; } catch { try { execGit(["fetch", "origin", sha]); execGit(["cat-file", "-e", `${sha}^{commit}`]); return true; } catch { return false; } } } function isQualifyingCiReuseRun(run: WorkflowRun) { if (run?.event === "pull_request") { return run?.name === "CI"; } return typeof run.head_sha === "string" && isReleaseGateCiRun(run, run.head_sha); } function findPatchIdenticalCiReuse({ sha, candidateRuns, nowMs, mainRef = "origin/main", execGit = runGit, }: { sha: string; candidateRuns: WorkflowRun[]; nowMs: number; mainRef?: string; execGit?: ExecGit; }) { if (!Array.isArray(candidateRuns)) { return undefined; } const candidates = candidateRuns .filter( (run) => run?.head_sha !== sha && /^[0-9a-f]{40,64}$/u.test(run.head_sha ?? "") && isQualifyingCiReuseRun(run) && isSuccessfulRecentRun(run, nowMs), ) .toSorted((left, right) => (right.updated_at ?? "").localeCompare(left.updated_at ?? "")) .slice(0, MAX_CI_REUSE_CANDIDATES); if (candidates.length === 0) { return undefined; } let currentPatchId; try { currentPatchId = computePatchId(sha, mainRef, execGit); } catch { return undefined; } for (const run of candidates) { if (!run.head_sha || !ensureCommitAvailable(run.head_sha, execGit)) { continue; } try { if (computePatchId(run.head_sha, mainRef, execGit) === currentPatchId) { return { run, reusedFromSha: run.head_sha, reusedRunId: run.id, patchIdMatched: true, }; } } catch { // A candidate is reusable only when every local git proof step succeeds. } } return undefined; } type PatchIdenticalReuse = NonNullable>; const CI_GATE_CHECK_NAME = "openclaw/ci-gate"; /** * True when this run's own openclaw/ci-gate job already succeeded on the * run's CURRENT attempt. The gate job needs every selected lane and fails on * any non-success result, so a successful gate proves the merge-relevant * outcome minutes before post-gate stragglers (timing summaries, artifact * uploads) let the run itself reach completed. Check suites survive reruns, * so binding goes through the attempt-scoped jobs listing: the job must carry * the run's own run_attempt — a prior attempt's gate success can never vouch * for a rerun that has not reached its gate yet. */ function hasSuccessfulCiGateJob(run: WorkflowRun, ciGateJobs: CiGateJob[], nowMs: number) { if (!run?.id || !Array.isArray(ciGateJobs)) { return false; } const runAttempt = run.run_attempt ?? 1; return ciGateJobs.some((job) => { if (job?.name !== CI_GATE_CHECK_NAME) { return false; } // Workflow attempts share a run id and filter=latest keeps a not-yet-rerun // job's prior-attempt execution, so bind to the attempt explicitly: the // REST job payload exposes run_attempt, and jobs are fetched from the // attempt-specific endpoint. Both must agree with the run's attempt. if (job?.run_id !== run.id || (job?.run_attempt ?? runAttempt) !== runAttempt) { return false; } if (job?.status !== "completed" || job?.conclusion !== "success") { return false; } const completedAtMs = Date.parse(job?.completed_at ?? ""); return ( Number.isFinite(completedAtMs) && completedAtMs >= nowMs - HOSTED_GATE_MAX_AGE_MS && completedAtMs <= nowMs + HOSTED_GATE_CLOCK_SKEW_MS ); }); } function isGateProvenInProgressRun( run: WorkflowRun | undefined, ciGateJobs: CiGateJob[], nowMs: number, ) { return ( (run?.status === "in_progress" || run?.status === "queued") && isRecentRun(run, nowMs) && hasSuccessfulCiGateJob(run, ciGateJobs, nowMs) ); } function preferredCiRun(runs: WorkflowRun[], nowMs: number) { const scheduledRuns = runs.filter((run) => run.event === "pull_request"); const latestScheduledRun = latestRun(scheduledRuns); const latestDecision = latestRun( scheduledRuns.filter( (run) => run.status === "completed" && typeof run.conclusion === "string" && !["cancelled", "skipped"].includes(run.conclusion), ), ); const latestManualRun = latestRun(runs.filter((run) => run.event === "workflow_dispatch")); // Manual proof may replace stale scheduled success or a pending run, // never an unresolved terminal non-success. if (latestDecision && latestDecision.conclusion !== "success") { return latestDecision; } if (latestScheduledRun && latestScheduledRun.status !== "completed") { return latestRun( [latestScheduledRun, latestManualRun].filter((run): run is WorkflowRun => run !== undefined), ); } if (latestScheduledRun?.status === "completed" && isSuccessfulRecentRun(latestDecision, nowMs)) { return latestDecision; } return latestManualRun ?? latestScheduledRun; } function successfulRunOrThrow( runs: WorkflowRun[], workflowName: string, sha: string, { allowManual = true, nowMs = Date.now(), ciGateJobs = [], }: { allowManual?: boolean; nowMs?: number; ciGateJobs?: CiGateJob[] } = {}, ) { const matchingRuns = matchingAuthoritativeRuns(runs, workflowName, sha, allowManual); const run = workflowName === "CI" ? preferredCiRun(matchingRuns, nowMs) : latestRun(matchingRuns); if (run && isSuccessfulRecentRun(run, nowMs)) { return run; } if (workflowName === "CI") { if (run && isGateProvenInProgressRun(run, ciGateJobs, nowMs)) { return run; } // A terminal non-success stays blocking unless a NEWER pending SCHEDULED // rerun on the same head has already passed its own gate — the gate needs // every selected lane, so that attempt is authoritative proof the failure // is re-resolved. The newer-than bound stops a stalled older run's gate // from masking a later failure, and manual runs can never mask one. if (run?.status === "completed" && run.conclusion !== "success") { const failedRunCreatedAtMs = Date.parse(run?.created_at ?? ""); const gateProvenRerun = matchingRuns.find((candidate) => { if (candidate === run || candidate.event !== "pull_request") { return false; } const candidateCreatedAtMs = Date.parse(candidate?.created_at ?? ""); if ( !Number.isFinite(candidateCreatedAtMs) || !Number.isFinite(failedRunCreatedAtMs) || candidateCreatedAtMs <= failedRunCreatedAtMs ) { return false; } return isGateProvenInProgressRun(candidate, ciGateJobs, nowMs); }); if (gateProvenRerun) { return gateProvenRerun; } } } throw new Error( `Missing successful recent ${workflowName} workflow for ${sha}. Observed: ${formatObservedRuns(matchingRuns)}`, ); } function hasSuccessfulRecentReleaseGate(workflowRuns: WorkflowRun[], sha: string, nowMs: number) { const releaseGate = latestRun(workflowRuns.filter((run) => isReleaseGateCiRun(run, sha))); return isSuccessfulRecentRun(releaseGate, nowMs); } function runBelongsToPullRequest( run: WorkflowRun, pr: number, pullRequestCommitShas: Set, pullRequestHeadBranch: string, pullRequestHeadRepository: string, ) { if (run.pull_requests?.some((pullRequest) => pullRequest.number === pr)) { return true; } if (Array.isArray(run?.pull_requests) && run.pull_requests.length > 0) { return false; } // Fork pull_request runs currently arrive with pull_requests: []. Require // the immutable commit plus its PR head identity; branch identity alone is // mutable, while ancestry alone can include commits from merged branches. return ( typeof run.head_sha === "string" && pullRequestCommitShas.has(run.head_sha) && run?.head_branch === pullRequestHeadBranch && run?.head_repository?.full_name?.toLowerCase() === pullRequestHeadRepository.toLowerCase() ); } function canCoverQueuedBuildArtifacts( workflowRuns: WorkflowRun[], sha: string, nowMs: number, notApplicableScheduledWorkflowNames: Set | undefined, ) { if (!hasSuccessfulRecentReleaseGate(workflowRuns, sha, nowMs)) { return false; } const supportingGatesPassed = ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS.every((workflowName) => { const matchingRuns = matchingAuthoritativeRuns(workflowRuns, workflowName, sha, false); if (matchingRuns.length === 0 && notApplicableScheduledWorkflowNames?.has(workflowName)) { return true; } const run = latestRun(matchingRuns); return isSuccessfulRecentRun(run, nowMs); }); if (!supportingGatesPassed) { return false; } const buildArtifactRuns = matchingAuthoritativeRuns( workflowRuns, BUILD_ARTIFACTS_WORKFLOW, sha, false, ); const latestBuildArtifactRun = latestRun(buildArtifactRuns); return ( latestBuildArtifactRun?.status === "queued" && isRecentRun(latestBuildArtifactRun, nowMs) && buildArtifactRuns.every( (run) => run.status === "queued" || (run.status === "completed" && run.conclusion === "success"), ) ); } function stripAnsi(raw: string) { const escape = String.fromCharCode(27); return raw.replace(new RegExp(`${escape}\\[[0-?]*[ -/]*[@-~]`, "gu"), ""); } export function parseWorkflowRunPage(raw: string) { const page = JSON.parse(stripAnsi(raw)) as unknown; if (!isRecord(page)) { throw new Error("Expected workflow run page to be an object."); } const totalCount = typeof page.total_count === "number" ? page.total_count : 0; const workflowRuns = Array.isArray(page.workflow_runs) ? page.workflow_runs.filter(isRecord).map(toWorkflowRun) : []; return { totalCount, workflowRuns, }; } export function workflowRunPageCount(totalCount: number) { return Math.min( Math.ceil(totalCount / WORKFLOW_RUNS_PAGE_SIZE), Math.ceil(MAX_WORKFLOW_RUN_SEARCH_RESULTS / WORKFLOW_RUNS_PAGE_SIZE), ); } export function collectHostedGateEvidence({ sha, pr = 0, recentSha = "", pullRequestCommitShas = [], pullRequestHeadBranch = "", pullRequestHeadRepository = "", workflowRuns, ciGateJobs = [], loadCiReuseCandidates = () => [], execGit = runGit, notApplicableScheduledWorkflows, changelogOnly = false, nowMs = Date.now(), }: CollectHostedGateEvidenceParams): HostedGateEvidence { if (!Array.isArray(workflowRuns)) { throw new Error("workflowRuns must be an array."); } const notApplicableScheduledWorkflowNames = notApplicableScheduledWorkflows === undefined ? undefined : new Set(notApplicableScheduledWorkflows); const pullRequestCommitShaSet = new Set(pullRequestCommitShas); const collectForSha = ( evidenceSha: string, { allowManual, requiredScheduledWorkflows = new Set(), ciRun, }: { allowManual: boolean; ciRun?: WorkflowRun; requiredScheduledWorkflows?: Set; }, ) => { const workflows: WorkflowRun[] = []; const fallbackCoveredWorkflows: Array<{ name: string; coveredBy: string; reason: string }> = []; if (!changelogOnly) { workflows.push( ciRun ?? successfulRunOrThrow(workflowRuns, "CI", evidenceSha, { allowManual, nowMs, // Gate proof only vouches for the exact head under verification. ciGateJobs: evidenceSha === sha ? ciGateJobs : [], }), ); } for (const workflowName of SCHEDULED_HOSTED_WORKFLOWS) { const matchingRuns = matchingAuthoritativeRuns( workflowRuns, workflowName, evidenceSha, allowManual, ); if ( matchingRuns.length === 0 && !requiredScheduledWorkflows.has(workflowName) && (notApplicableScheduledWorkflowNames === undefined || notApplicableScheduledWorkflowNames.has(workflowName)) ) { continue; } if ( allowManual && workflowName === BUILD_ARTIFACTS_WORKFLOW && canCoverQueuedBuildArtifacts( workflowRuns, evidenceSha, nowMs, notApplicableScheduledWorkflowNames, ) ) { fallbackCoveredWorkflows.push({ name: workflowName, coveredBy: "CI release gate", reason: "scheduled workflow is queued", }); continue; } workflows.push( successfulRunOrThrow(workflowRuns, workflowName, evidenceSha, { allowManual, nowMs, }), ); } return { workflows, fallbackCoveredWorkflows }; }; let ciRun: WorkflowRun | undefined; let ciReuse: PatchIdenticalReuse | undefined; if (!changelogOnly) { try { ciRun = successfulRunOrThrow(workflowRuns, "CI", sha, { allowManual: true, nowMs, ciGateJobs, }); } catch (exactCiError) { let candidateRuns: WorkflowRun[]; try { candidateRuns = loadCiReuseCandidates(); } catch { candidateRuns = []; } ciReuse = findPatchIdenticalCiReuse({ sha, candidateRuns, nowMs, execGit, }); if (!ciReuse) { throw exactCiError; } ciRun = ciReuse.run; } } let evidenceSha = sha; let selected: | { workflows: WorkflowRun[]; fallbackCoveredWorkflows: Array<{ name: string; coveredBy: string; reason: string }>; } | undefined; try { selected = collectForSha(sha, { allowManual: true, ciRun }); } catch (exactError) { // Scheduled hosted workflows retain their existing recent-cohort fallback. // CI itself is either exact-head proof or the patch-identical run selected // above; never silently replace it with an unverified prior-head run. const targetScheduledWorkflows = new Set( SCHEDULED_HOSTED_WORKFLOWS.filter( (workflowName) => (notApplicableScheduledWorkflowNames !== undefined && !notApplicableScheduledWorkflowNames.has(workflowName)) || matchingAuthoritativeRuns(workflowRuns, workflowName, sha, false).length > 0, ), ); const fallbackShas = [ ciReuse?.reusedFromSha, recentSha, ...workflowRuns .filter( (run) => run?.event === "pull_request" && run?.head_sha !== sha && runBelongsToPullRequest( run, pr, pullRequestCommitShaSet, pullRequestHeadBranch, pullRequestHeadRepository, ) && isRecentRun(run, nowMs), ) .toSorted((left, right) => String(right.updated_at ?? "").localeCompare(String(left.updated_at ?? "")), ) .map((run) => run.head_sha), ].filter((value): value is string => typeof value === "string" && value.length > 0); let fallbackError; for (const fallbackSha of new Set(fallbackShas)) { try { selected = collectForSha(fallbackSha, { allowManual: false, requiredScheduledWorkflows: targetScheduledWorkflows, ciRun, }); evidenceSha = fallbackSha; break; } catch (error) { fallbackError ??= error; } } if (!selected) { throw fallbackError ?? exactError; } } const evidence: HostedGateEvidence = { headSha: sha, ...(evidenceSha !== sha ? { evidenceHeadSha: evidenceSha } : {}), ...(ciReuse ? { reusedFromSha: ciReuse.reusedFromSha, reusedRunId: ciReuse.reusedRunId, patchIdMatched: true satisfies true, } : {}), ...(selected.fallbackCoveredWorkflows.length > 0 ? { fallbackCoveredWorkflows: selected.fallbackCoveredWorkflows } : {}), workflows: selected.workflows.map((run) => ({ id: run.id, name: run.name, event: run.event, headSha: run.head_sha, headBranch: run.head_branch, status: run.status, conclusion: run.conclusion, createdAt: run.created_at, updatedAt: run.updated_at, url: run.html_url, })), }; const notApplicableWorkflows = (notApplicableScheduledWorkflows ?? []).filter( (workflowName) => matchingAuthoritativeRuns(workflowRuns, workflowName, sha, false).length === 0, ); return notApplicableWorkflows.length > 0 ? { ...evidence, notApplicableWorkflows } : evidence; } export function workflowRunQueryPaths( repo: string, { sha, recentSha, headBranch }: { sha: string; recentSha: string; headBranch?: string }, page = 1, ) { const pageSuffix = `per_page=${WORKFLOW_RUNS_PAGE_SIZE}&page=${page}`; const shas = [...new Set([sha, recentSha].filter(Boolean))]; const queries = shas.map( (headSha) => `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&${pageSuffix}`, ); if (headBranch) { queries.push( `repos/${repo}/actions/runs?branch=${encodeURIComponent(headBranch)}&event=pull_request&${pageSuffix}`, ); } return queries; } function loadWorkflowRunsForQuery(queryForPage: (page: number) => string) { const loadPage = (page: number) => parseWorkflowRunPage( execGhApiRead(queryForPage(page), { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }), ); // Bound every SHA query to GitHub's documented search window. const firstPage = loadPage(1); const workflowRuns = [...firstPage.workflowRuns]; for (let page = 2; page <= workflowRunPageCount(firstPage.totalCount); page += 1) { workflowRuns.push(...loadPage(page).workflowRuns); } return workflowRuns; } function loadWorkflowRuns(repo: string, sha: string, recentSha: string, headBranch?: string) { const queries = workflowRunQueryPaths(repo, { sha, recentSha, headBranch }); const withPage = (query: string, page: number) => query.replace(/page=1$/u, `page=${page}`); const workflowRuns = queries.flatMap((query) => loadWorkflowRunsForQuery((page) => withPage(query, page)), ); return [...new Map(workflowRuns.map((run) => [run.id, run])).values()]; } function loadCiReuseCandidateRuns(repo: string, headBranch: string) { const raw = execFileSync( "gh", [ "run", "list", "--repo", repo, "--workflow", "ci.yml", "--branch", headBranch, "--limit", String(CI_REUSE_RUN_LIST_LIMIT), "--json", "databaseId,workflowName,headSha,headBranch,event,status,conclusion,createdAt,updatedAt,url,displayTitle", ], { encoding: "utf8", env: plainGhEnv(), stdio: ["ignore", "pipe", "pipe"], }, ); const runs = JSON.parse(stripAnsi(raw)) as unknown; if (!Array.isArray(runs)) { throw new Error("Expected gh run list to return an array."); } if (!runs.every(isRecord)) { throw new Error("Expected every gh run list entry to be an object."); } // The workflow selector above supplies the path identity that the REST // release-gate matcher normally reads from each full workflow-run object. return runs.map( (run): WorkflowRun => ({ id: optionalNumber(run, "databaseId"), name: readStringField(run, "workflowName"), event: readStringField(run, "event"), status: readStringField(run, "status"), conclusion: optionalNullableString(run, "conclusion"), head_sha: readStringField(run, "headSha"), head_branch: readStringField(run, "headBranch"), path: CI_WORKFLOW_PATH, created_at: readStringField(run, "createdAt"), updated_at: readStringField(run, "updatedAt"), html_url: readStringField(run, "url"), display_title: readStringField(run, "displayTitle"), }), ); } export function compareCommitPageCount(totalCommits: unknown) { if (typeof totalCommits !== "number" || !Number.isSafeInteger(totalCommits) || totalCommits < 0) { throw new Error("Expected comparison total_commits to be a non-negative integer."); } return Math.max(1, Math.ceil(totalCommits / COMPARE_COMMITS_PAGE_SIZE)); } function loadPullRequestCommitShas( repo: string, { baseSha, headSha }: { baseSha: string; headSha: string }, ) { const loadPage = (page: number) => { const comparison = JSON.parse( execGhApiRead( `repos/${repo}/compare/${baseSha}...${headSha}?per_page=${COMPARE_COMMITS_PAGE_SIZE}&page=${page}`, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }, ), ) as unknown; if (!isRecord(comparison)) { throw new Error(`Expected comparison commit page ${page} to be an object.`); } return comparison; }; // The PR commits endpoint stops at 250. GitHub's paginated comparison is // equivalent to git log BASE..HEAD and keeps the membership proof complete. const firstPage = loadPage(1); const pages = [firstPage]; for (let page = 2; page <= compareCommitPageCount(firstPage?.total_commits); page += 1) { pages.push(loadPage(page)); } const shas = pages.flatMap((comparison, index) => { if (!Array.isArray(comparison?.commits)) { throw new Error(`Expected comparison commit page ${index + 1} to be an array.`); } if (!comparison.commits.every(isRecord)) { throw new Error(`Expected comparison commit page ${index + 1} entries to be objects.`); } return comparison.commits .map((commit) => commit.sha) .filter((sha: unknown): sha is string => typeof sha === "string"); }); if (shas.length !== firstPage.total_commits) { throw new Error( `Expected ${firstPage.total_commits} comparison commits, received ${shas.length}.`, ); } return shas; } function loadPullRequestChangedPaths(baseSha: string, headSha: string) { return runGit(["diff", "--name-only", `${baseSha}...${headSha}`]) .trim() .split(/\r?\n/u) .filter(Boolean); } function loadCiGateJobs( repo: string, workflowRuns: WorkflowRun[], sha: string, nowMs = Date.now(), ) { // Only an in-progress exact-head CI run can benefit from gate proof. const candidates = workflowRuns.filter( (run) => run?.name === "CI" && run?.head_sha === sha && (run?.status === "in_progress" || run?.status === "queued") && isRecentRun(run, nowMs), ); return candidates.flatMap((run) => { const attempt = run.run_attempt ?? 1; // The jobs endpoint pages at 100 and full-scope runs already sit near // that; page until the gate job is visible so growth past one page can // never silently disable the early-proof path. const jobs: CiGateJob[] = []; for (let page = 1; page <= 5; page += 1) { const payload = JSON.parse( execGhApiRead( `repos/${repo}/actions/runs/${run.id}/attempts/${attempt}/jobs?per_page=100&page=${page}`, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ), ) as unknown; const pageJobs = isRecord(payload) && Array.isArray(payload.jobs) ? payload.jobs.filter(isRecord).map(toCiGateJob) : []; jobs.push(...pageJobs); const totalCount = Number(isRecord(payload) ? (payload.total_count ?? 0) : 0); if ( pageJobs.length === 0 || jobs.length >= totalCount || jobs.some((job) => job.name === CI_GATE_CHECK_NAME) ) { break; } } // Re-read the run after fetching its attempt jobs and drop the evidence if // the attempt advanced in between: otherwise a rerun starting in that // window would let the just-fetched prior-attempt gate vouch for an // attempt that has not reached its own gate. Same-attempt completion is // fine — a run that finished successfully still proves this attempt, and // a non-success completion must not be blessed by its own earlier gate. const current = JSON.parse( execGhApiRead(`repos/${repo}/actions/runs/${run.id}`, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }), ) as unknown; if (!isRecord(current)) { return []; } const sameAttempt = (current.run_attempt ?? attempt) === attempt; const stillPending = current.status === "in_progress" || current.status === "queued"; const completedSuccess = current.status === "completed" && current.conclusion === "success"; if (!sameAttempt || (!stillPending && !completedSuccess)) { return []; } return jobs; }); } function main(argv = process.argv.slice(2)) { const args = parseArgs(argv); const pullRequest = JSON.parse( execGhApiRead(`repos/${args.repo}/pulls/${args.pr}`, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }), ) as unknown; const head = isRecord(pullRequest) && isRecord(pullRequest.head) ? pullRequest.head : undefined; const base = isRecord(pullRequest) && isRecord(pullRequest.base) ? pullRequest.base : undefined; const headRepo = head && isRecord(head.repo) ? head.repo : undefined; const headBranch = head ? readStringField(head, "ref") : undefined; const headRepository = headRepo ? readStringField(headRepo, "full_name") : undefined; const baseSha = base ? readStringField(base, "sha") : undefined; const headSha = head ? readStringField(head, "sha") : undefined; if (!headBranch || !headRepository || !baseSha || !headSha) { throw new Error(`PR #${args.pr} is missing head or base metadata.`); } if (headSha !== args.sha) { throw new Error(`PR #${args.pr} head changed from ${args.sha} to ${headSha}.`); } const changedPaths = loadPullRequestChangedPaths(baseSha, headSha); const workflowRuns = loadWorkflowRuns(args.repo, args.sha, args.recentSha, headBranch); const evidence = collectHostedGateEvidence({ sha: args.sha, pr: args.pr, recentSha: args.recentSha, pullRequestCommitShas: loadPullRequestCommitShas(args.repo, { baseSha, headSha }), pullRequestHeadBranch: headBranch, pullRequestHeadRepository: headRepository, workflowRuns, ciGateJobs: loadCiGateJobs(args.repo, workflowRuns, args.sha), loadCiReuseCandidates: () => loadCiReuseCandidateRuns(args.repo, headBranch), notApplicableScheduledWorkflows: notApplicableScheduledHostedWorkflows(changedPaths), changelogOnly: args.changelogOnly, }); const evidenceHeadSha = evidence.evidenceHeadSha ?? args.sha; const notApplicableWorkflows = "notApplicableWorkflows" in evidence ? evidence.notApplicableWorkflows : undefined; const manifest = { schemaVersion: 2, generatedAt: new Date().toISOString(), repo: args.repo, pullRequestNumber: args.pr, selection: { mode: evidence.patchIdMatched ? "patch-identical-pre-rebase" : evidenceHeadSha === args.sha ? "exact-head" : "recent-pr-head", maxAgeHours: HOSTED_GATE_MAX_AGE_HOURS, }, ...evidence, }; mkdirSync(path.dirname(args.output), { recursive: true }); writeFileSync(args.output, `${JSON.stringify(manifest, null, 2)}\n`); if (evidence.patchIdMatched) { const reusedRun = manifest.workflows.find((workflow) => workflow.id === evidence.reusedRunId); const updatedAtMs = runUpdatedAtMs({ updated_at: reusedRun?.updatedAt }); const ageHours = updatedAtMs === null ? "unknown" : `${(Math.max(0, Date.now() - updatedAtMs) / (60 * 60 * 1_000)).toFixed(1)}h`; console.log( `hosted CI reused from patch-identical pre-rebase head ${evidence.reusedFromSha} (run ${evidence.reusedRunId}, age ${ageHours})`, ); } console.log( `Hosted gates passed for PR #${args.pr} at ${args.sha} using ${evidenceHeadSha}: ${manifest.workflows .map((workflow) => `${workflow.name}#${workflow.id}`) .join(", ")}${ notApplicableWorkflows?.length ? `; not applicable: ${notApplicableWorkflows.join(", ")}` : "" }`, ); } if (isDirectRunUrl(process.argv[1], import.meta.url)) { main(); }