mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): keep evidence reads on cached gh (#121619)
This commit is contained in:
committed by
GitHub
parent
1f591bba56
commit
ed39f6540f
@@ -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
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user