mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Admit full source verification only for exact-source Docker or npm release work, while superseded pending sources complete metadata-only admission. Restart only strictly deferred runs; failed or cancelled checks require an explicit request. Route canonical master native images to dedicated 32-core Spot profiles and keep source checks on protected verification capacity. Use the four-slot GitHub-hosted publication pool for registry work and signing, with proof waits outside that pool and a fresh source-proof check before tagging. Trim historical checkout blobs and prepared-runner disk cleanup while preserving tag and fork publication behavior. Validation: release/source and source/artifact regressions, 665 workflow contracts, actionlint, complete latest-head PR CI, and Greptile 5/5 with all comments resolved. Existing CLI and large-file repository timing tests also passed independently; their CI shards received one scoped retry. Co-Authored-By: Paperclip <noreply@paperclip.ing>
124 lines
8.2 KiB
JavaScript
124 lines
8.2 KiB
JavaScript
import { appendFile } from "node:fs/promises";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const repository = "paperclipai/paperclip";
|
|
const repositoryId = 1170821064;
|
|
const workflows = new Set(["docker.yml", "release.yml", "cloud-readiness.yml", "cloud-migrator-artifacts.yml"]);
|
|
export const sourceAdmissionJob = "Cloud candidate admission v1";
|
|
export const migratorAdmissionJob = "Cloud image candidate admission v1";
|
|
|
|
function assertSha(sha) {
|
|
if (!/^[a-f0-9]{40}$/.test(sha ?? "")) throw new Error("Admission requires a full source SHA.");
|
|
}
|
|
|
|
function trusted(run, sha, workflow) {
|
|
return run.workflow_id === workflow.id && run.path === workflow.path && run.head_sha === sha &&
|
|
run.repository?.id === repositoryId && run.head_repository?.id === repositoryId &&
|
|
run.repository?.full_name === repository && run.head_repository?.full_name === repository &&
|
|
run.head_branch === "master" && run.event === "push" &&
|
|
Number.isSafeInteger(run.id) && run.id > 0 && Number.isSafeInteger(run.run_attempt) && run.run_attempt > 0;
|
|
}
|
|
|
|
async function producer(file, sha, api, status) {
|
|
assertSha(sha);
|
|
if (!workflows.has(file)) throw new Error("Unknown release workflow.");
|
|
const workflow = await api(`/repos/${repository}/actions/workflows/${file}`);
|
|
if (workflow.path !== `.github/workflows/${file}` || !Number.isSafeInteger(workflow.id) || workflow.id < 1) throw new Error("Release workflow identity mismatch.");
|
|
const list = await api(`/repos/${repository}/actions/workflows/${workflow.id}/runs?branch=master&event=push&head_sha=${sha}&per_page=100${status ? `&status=${status}` : ""}`);
|
|
if (!Array.isArray(list.workflow_runs) || !Number.isSafeInteger(list.total_count) || list.total_count < 0 ||
|
|
list.total_count > 100 || list.workflow_runs.length !== list.total_count) throw new Error("Incomplete release workflow listing.");
|
|
const run = list.workflow_runs.filter(run => trusted(run, sha, workflow) && (!status || run.status === status)).sort((a, b) => b.id - a.id)[0];
|
|
return { workflow, run };
|
|
}
|
|
|
|
// A retry is an explicit request for this exact immutable source. Normal push
|
|
// work starts only for a candidate admitted by an image build or npm canary.
|
|
export async function releaseAdmitted(sha, { api, event = "push", attempt = 1, imageOnly = false }) {
|
|
assertSha(sha);
|
|
if (!Number.isSafeInteger(attempt) || attempt < 1) throw new Error("Invalid release attempt.");
|
|
if (!["push", "workflow_dispatch"].includes(event)) return false;
|
|
if (event === "workflow_dispatch" || (Number.isSafeInteger(attempt) && attempt > 1)) return true;
|
|
for (const file of imageOnly ? ["docker.yml"] : ["docker.yml", "release.yml"]) {
|
|
const { workflow, run } = await producer(file, sha, api, "in_progress");
|
|
if (!run) continue;
|
|
const current = await api(`/repos/${repository}/actions/runs/${run.id}`);
|
|
if (current.id === run.id && trusted(current, sha, workflow) && current.run_attempt === run.run_attempt && current.status === "in_progress") return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function deferredProducer(jobs, run, admissionJob) {
|
|
const admission = jobs.filter(job => job.name === admissionJob);
|
|
const remaining = jobs.filter(job => job.name !== admissionJob);
|
|
return run.status === "completed" && run.conclusion === "success" && admission.length === 1 && admission[0].status === "completed" && admission[0].conclusion === "success" && remaining.length > 0 &&
|
|
remaining.every(job => job.status === "completed" && job.conclusion === "skipped") &&
|
|
jobs.every(job => job.run_id === run.id && job.run_attempt === run.run_attempt && job.head_sha === run.head_sha);
|
|
}
|
|
|
|
// Only skipped admission is restartable here. A failed check or build never
|
|
// becomes an automatic retry, and every write revalidates canonical identity.
|
|
export async function requestDeferredProducer({ file, sha, admissionJob, api, rerun, requested = new Set() }) {
|
|
if (({ "cloud-readiness.yml": sourceAdmissionJob, "cloud-migrator-artifacts.yml": migratorAdmissionJob })[file] !== admissionJob) throw new Error("Unknown deferred producer contract.");
|
|
const { workflow, run } = await producer(file, sha, api);
|
|
if (!run) return "pending";
|
|
const key = `${run.id}/${run.run_attempt}`;
|
|
if (requested.has(key)) return "pending";
|
|
const jobs = [];
|
|
let total;
|
|
for (let page = 1; page <= 10; page++) {
|
|
const batch = await api(`/repos/${repository}/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100&page=${page}`);
|
|
if (!Array.isArray(batch.jobs) || !Number.isSafeInteger(batch.total_count) || batch.total_count < 0) throw new Error("Invalid release producer job list.");
|
|
if ((total !== undefined && total !== batch.total_count) ||
|
|
batch.jobs.length !== Math.min(100, Math.max(0, batch.total_count - (page - 1) * 100))) throw new Error("Incomplete release producer job list.");
|
|
total = batch.total_count;
|
|
jobs.push(...batch.jobs);
|
|
if (page * 100 >= total) break;
|
|
if (page === 10) throw new Error("Incomplete release producer job list.");
|
|
}
|
|
if (run.status !== "completed") {
|
|
const current = jobs.filter(job => job.run_id === run.id && job.run_attempt === run.run_attempt && job.head_sha === sha);
|
|
// Admission itself succeeds for both outcomes. Only an actual build is
|
|
// evidence that its output admitted work; queued jobs may still be skipped.
|
|
return current.some(job => job.name !== admissionJob && job.labels?.length > 0 && ["in_progress", "completed"].includes(job.status) && job.conclusion !== "skipped")
|
|
? "admitted" : "pending";
|
|
}
|
|
if (!deferredProducer(jobs, run, admissionJob)) return "finished";
|
|
const current = await api(`/repos/${repository}/actions/runs/${run.id}`);
|
|
if (current.id !== run.id || !trusted(current, sha, workflow) || current.run_attempt !== run.run_attempt || current.status !== "completed" || current.conclusion !== "success") return "pending";
|
|
try {
|
|
await rerun(`/repos/${repository}/actions/runs/${run.id}/rerun`);
|
|
} catch (error) {
|
|
// Docker and npm may select the same source at the same time. A rejected
|
|
// duplicate write is harmless only when a fresh read proves a retry began.
|
|
const latest = await api(`/repos/${repository}/actions/runs/${run.id}`);
|
|
if (latest.id !== run.id || !trusted(latest, sha, workflow)) throw error;
|
|
if (!(latest.run_attempt > run.run_attempt || (latest.run_attempt === run.run_attempt &&
|
|
["queued", "in_progress", "requested", "pending", "waiting"].includes(latest.status)))) {
|
|
// GitHub may reject a duplicate before its read API shows the retry.
|
|
// Leave it pending for the next poll; this is not proof of admission.
|
|
if ([409, 422].includes(error.status) && latest.run_attempt === run.run_attempt && latest.status === "completed" && latest.conclusion === "success") return "pending";
|
|
throw error;
|
|
}
|
|
}
|
|
requested.add(key);
|
|
return "pending";
|
|
}
|
|
|
|
export function createReleaseRerunner(token, fetchImpl = fetch) {
|
|
return async path => {
|
|
if (!/^\/repos\/paperclipai\/paperclip\/actions\/runs\/[1-9]\d*\/rerun$/.test(path)) throw new Error("Invalid release retry path.");
|
|
const response = await fetchImpl(`https://api.github.com${path}`, { method: "POST", redirect: "error",
|
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(30_000) });
|
|
if (!response.ok) throw Object.assign(new Error(`Could not admit deferred release work: HTTP ${response.status}.`), { status: response.status });
|
|
};
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
if (process.env.GITHUB_REPOSITORY !== repository || process.env.GITHUB_REF !== "refs/heads/master" || !process.env.GITHUB_TOKEN) throw new Error("Canonical master admission context required.");
|
|
const { createActionsReader } = await import("./cloud-source-verification.mjs");
|
|
const admitted = await releaseAdmitted(process.env.GITHUB_SHA, { api: createActionsReader({ token: process.env.GITHUB_TOKEN }),
|
|
event: process.env.GITHUB_EVENT_NAME, attempt: Number(process.env.GITHUB_RUN_ATTEMPT), imageOnly: process.argv[2] === "image-only" });
|
|
console.log(admitted ? "Release candidate admitted." : "Full build deferred until a release consumer admits this source.");
|
|
if (process.env.GITHUB_OUTPUT) await appendFile(process.env.GITHUB_OUTPUT, `admitted=${admitted}\n`);
|
|
}
|