fix(release): keep evidence reads on cached gh (#121619)

This commit is contained in:
Peter Steinberger
2026-08-10 07:40:55 -07:00
committed by GitHub
parent 1f591bba56
commit ed39f6540f
2 changed files with 166 additions and 21 deletions
+21 -11
View File
@@ -10,7 +10,7 @@ import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { plainGhEnv, resolvePlainGhBin } from "./lib/plain-gh.mjs";
import { execGhRead, plainGhEnv, resolvePlainGhBin } from "./lib/plain-gh.mjs";
const DEFAULT_REPO = process.env.OPENCLAW_RELEASE_REPO || "openclaw/openclaw";
const RELEASE_EVIDENCE_SCHEMA = "openclaw.release-validation-evidence/v3";
@@ -98,14 +98,17 @@ export function runReleaseCiGh(args, params = {}) {
const execFileSyncImpl = params.execFileSyncImpl ?? execFileSync;
const timeoutMs = params.timeoutMs ?? GH_COMMAND_TIMEOUT_MS;
const stdio = params.stdio ?? ["ignore", "pipe", "pipe"];
return execFileSyncImpl(resolvePlainGhBin(), args, {
encoding: "utf8",
env: plainGhEnv(),
killSignal: "SIGKILL",
maxBuffer: 64 * 1024 * 1024,
stdio,
timeout: timeoutMs,
});
return execGhRead(
args,
{
encoding: "utf8",
killSignal: "SIGKILL",
maxBuffer: 64 * 1024 * 1024,
stdio,
timeout: timeoutMs,
},
{ execFileSyncImpl },
);
}
function gh(args) {
@@ -131,8 +134,12 @@ export function artifactDownloadArgs(artifactId, repository = DEFAULT_REPO) {
function downloadArtifactZip(artifactId, destination, repository = DEFAULT_REPO) {
const output = openSync(destination, "w");
try {
runReleaseCiGh(artifactDownloadArgs(artifactId, repository), {
execFileSync(resolvePlainGhBin(), artifactDownloadArgs(artifactId, repository), {
env: plainGhEnv(),
killSignal: "SIGKILL",
maxBuffer: 64 * 1024 * 1024,
stdio: ["ignore", output, "pipe"],
timeout: GH_COMMAND_TIMEOUT_MS,
});
} finally {
closeSync(output);
@@ -1045,6 +1052,9 @@ function validateCompletedParentRun(parentView, parentRest, repository, runId) {
export function createReleaseEvidenceClient(repository = DEFAULT_REPO) {
const normalizedRepository = normalizeRepository(repository);
return {
compareCommitLineage(base, head) {
return githubRestJson(`compare/${base}...${head}?per_page=1&page=2`, normalizedRepository);
},
compareCommits(base, head) {
return githubRestJson(`compare/${base}...${head}`, normalizedRepository);
},
@@ -1149,7 +1159,7 @@ export function validateTrustedProducerIdentity(evidence, client, verifier, trus
workflowRefProof = shaPinned ? "manifest-v3-sha-pinned-main-ancestry" : "manifest-v3-branch";
}
const comparison = client.compareCommits(manifest.workflowSha, verifier.sourceSha);
const comparison = client.compareCommitLineage(manifest.workflowSha, verifier.sourceSha);
if (
!["ahead", "identical"].includes(String(comparison.status)) ||
comparison.merge_base_commit?.sha !== manifest.workflowSha
+145 -10
View File
@@ -1,6 +1,6 @@
import { execFileSync, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
@@ -51,6 +51,128 @@ describe("GitHub API commands", () => {
"repos/owner/repo/actions/artifacts/456/zip",
]);
});
it.skipIf(!hasUnzip)("uses cached gh for evidence reads and plain gh only for ZIP bytes", () => {
const root = mkdtempSync(join(tmpdir(), "release-ci-gh-routing-"));
const workflowSha = "0".repeat(40);
const targetSha = "8".repeat(40);
const verifierSha = "c".repeat(40);
const fixture = trustedMainPackageFixture({ targetSha, workflowSha });
const runId = fixture.runId;
const childRunId = String(fixture.childRun.id);
const artifactId = fixture.artifact.id;
const archive = makeStoredZip({
[MANIFEST_ARTIFACT_ENTRY]: JSON.stringify(fixture.manifest),
});
const archivePath = join(root, "manifest.zip");
const fixturesPath = join(root, "fixtures.json");
const shimLog = join(root, "shim.log");
const plainLog = join(root, "plain.log");
const shimGh = join(root, "gh");
const plainGh = join(root, "plain-gh");
fixture.artifact.digest = artifactDigest(archive);
fixture.artifact.size_in_bytes = archive.length;
writeFileSync(archivePath, archive);
writeFileSync(
fixturesPath,
JSON.stringify({
artifact: fixture.artifact,
artifactList: { artifacts: [fixture.artifact] },
child: fixture.childRun,
jobLog: `TARGET_SHA: ${targetSha}\nDispatched: https://github.com/openclaw/openclaw/actions/runs/${childRunId}`,
jobs: { jobs: [fixture.parentJob] },
lineage: { merge_base_commit: { sha: workflowSha }, status: "ahead" },
parent: fixture.parentRun,
parentView: fixture.parentView,
rate: { resources: { core: { limit: 5000, remaining: 4999, reset: 2_000_000_000 } } },
}),
);
writeFileSync(
shimGh,
`#!/usr/bin/env node
import { appendFileSync, readFileSync } from "node:fs";
const args = process.argv.slice(2);
appendFileSync(process.env.SHIM_LOG, JSON.stringify(args) + "\\n");
const fixtures = JSON.parse(readFileSync(process.env.FIXTURES, "utf8"));
const endpoint = args[1] ?? "";
let output;
if (args[0] === "run" && args[1] === "view") output = fixtures.parentView;
else if (endpoint === "rate_limit") output = fixtures.rate;
else if (endpoint === "repos/openclaw/openclaw/actions/runs/${runId}") output = fixtures.parent;
else if (endpoint.startsWith("repos/openclaw/openclaw/actions/runs/${runId}/artifacts?")) output = fixtures.artifactList;
else if (endpoint === "repos/openclaw/openclaw/actions/artifacts/${artifactId}") output = fixtures.artifact;
else if (endpoint.startsWith("repos/openclaw/openclaw/actions/runs/${runId}/jobs?")) output = fixtures.jobs;
else if (endpoint === "repos/openclaw/openclaw/actions/runs/${childRunId}") output = fixtures.child;
else if (endpoint === "repos/openclaw/openclaw/actions/jobs/${fixture.parentJob.id}/logs") output = fixtures.jobLog;
else if (endpoint === "repos/openclaw/openclaw/compare/${workflowSha}...${verifierSha}?per_page=1&page=2") output = fixtures.lineage;
else { console.error("unexpected cached gh request: " + args.join(" ")); process.exit(43); }
process.stdout.write(typeof output === "string" ? output : JSON.stringify(output));
`,
);
writeFileSync(
plainGh,
`#!/usr/bin/env node
import { appendFileSync, readFileSync } from "node:fs";
const args = process.argv.slice(2);
appendFileSync(process.env.PLAIN_LOG, JSON.stringify(args) + "\\n");
if (args[0] !== "api" || args[1] !== "repos/openclaw/openclaw/actions/artifacts/${artifactId}/zip") {
console.error("plain gh used for evidence read: " + args.join(" "));
process.exit(42);
}
process.stdout.write(readFileSync(process.env.ARCHIVE));
`,
);
chmodSync(shimGh, 0o755);
chmodSync(plainGh, 0o755);
try {
const env = {
...process.env,
ARCHIVE: archivePath,
FIXTURES: fixturesPath,
OPENCLAW_GH_BIN: plainGh,
PATH: `${root}:${process.env.PATH ?? ""}`,
PLAIN_LOG: plainLog,
SHIM_LOG: shimLog,
};
const lineageResult = spawnSync(
process.execPath,
[
"--input-type=module",
"--eval",
`import { createReleaseEvidenceClient } from ${JSON.stringify(pathToFileURL(resolve(SCRIPT)).href)};
process.stdout.write(JSON.stringify(createReleaseEvidenceClient("openclaw/openclaw").compareCommitLineage("${workflowSha}", "${verifierSha}")));`,
],
{ encoding: "utf8", env },
);
expect(lineageResult.status).toBe(0);
expect(JSON.parse(lineageResult.stdout)).toEqual({
merge_base_commit: { sha: workflowSha },
status: "ahead",
});
const result = spawnSync(process.execPath, [SCRIPT, runId], { encoding: "utf8", env });
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
expect(result.stdout).toContain(
`child: ${childRunId} OpenClaw Release Checks completed/success`,
);
const shimCalls = readFileSync(shimLog, "utf8");
const plainCalls = readFileSync(plainLog, "utf8");
expect(shimCalls).toContain('"run","view"');
expect(shimCalls).toContain(`"repos/openclaw/openclaw/actions/runs/${runId}"`);
expect(shimCalls).toContain(
`"repos/openclaw/openclaw/compare/${workflowSha}...${verifierSha}?per_page=1&page=2"`,
);
expect(shimCalls).not.toContain(`/actions/artifacts/${artifactId}/zip`);
expect(plainCalls.trim()).toBe(
JSON.stringify(["api", `repos/openclaw/openclaw/actions/artifacts/${artifactId}/zip`]),
);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
});
describe("runReleaseCiGh", () => {
@@ -367,14 +489,16 @@ function trustedMainPackageFixture({
id: Number(runId),
},
};
const compareCommits = (base: string, head: string) => {
expect(base).toBe(workflowSha);
return {
merge_base_commit: { sha: workflowSha },
status: base === head ? "identical" : "ahead",
};
};
const client = {
compareCommits(base: string, head: string) {
expect(base).toBe(workflowSha);
return {
merge_base_commit: { sha: workflowSha },
status: base === head ? "identical" : "ahead",
};
},
compareCommitLineage: compareCommits,
compareCommits,
getJobLog(jobId: number) {
expect(jobId).toBe(parentJob.id);
return [
@@ -406,7 +530,18 @@ function trustedMainPackageFixture({
},
};
return { artifact, childRun, client, manifest, parentRun, runId, targetSha, workflowSha };
return {
artifact,
childRun,
client,
manifest,
parentJob,
parentRun,
parentView,
runId,
targetSha,
workflowSha,
};
}
describe("release CI summary child correlation", () => {
@@ -889,7 +1024,7 @@ describe("release CI summary child correlation", () => {
it("rejects a legacy producer outside the trusted main verifier lineage", () => {
const fixture = trustedMainPackageFixture({ workflowSha: "a".repeat(40) });
fixture.client.compareCommits = () => ({
fixture.client.compareCommitLineage = () => ({
merge_base_commit: { sha: "d".repeat(40) },
status: "diverged",
});