ci: stabilize release validation tests (#108420)

* test(codex): remove turn-watch timing race

* ci: pin workflow sanity shellcheck

* ci: run workflow sanity on blacksmith

* ci: serialize workflow sanity lint

* ci: bound workflow lint stalls

* ci: cap actionlint process fanout

* ci: report stalled workflow shellcheck

* ci: stabilize release validation checks

* ci: restore release workflow shellcheck

* test: preserve codex turn watch reset proof
This commit is contained in:
Peter Steinberger
2026-07-15 13:22:31 -07:00
committed by GitHub
parent ff8a015981
commit 28d30e9dae
9 changed files with 459 additions and 145 deletions

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
import hashlib
import stat
import sys
import zipfile
from pathlib import PurePosixPath
MAX_ENTRIES = 4096
MAX_FILE_BYTES = 64 * 1024 * 1024
MAX_TOTAL_BYTES = 256 * 1024 * 1024
def archive_tree(path: str) -> dict[str, tuple[int, str]]:
files: dict[str, tuple[int, str]] = {}
total_bytes = 0
with zipfile.ZipFile(path) as archive:
infos = archive.infolist()
if len(infos) > MAX_ENTRIES:
raise ValueError("too many dependency evidence archive entries")
seen: set[str] = set()
for info in infos:
name = info.filename
pure = PurePosixPath(name)
normalized = str(pure) + ("/" if info.is_dir() else "")
if (
not name
or "\\" in name
or name != normalized
or len(pure.parts) < 2
or pure.parts[0] != "dependency-evidence"
or any(part in (".", "..") for part in pure.parts)
or name in seen
):
raise ValueError(f"unsafe dependency evidence archive entry: {name!r}")
seen.add(name)
file_type = stat.S_IFMT(info.external_attr >> 16)
allowed_types = (0, stat.S_IFDIR) if info.is_dir() else (0, stat.S_IFREG)
if file_type not in allowed_types or info.flag_bits & 0x1:
raise ValueError(f"unsupported dependency evidence archive entry: {name!r}")
if info.is_dir():
continue
if info.file_size > MAX_FILE_BYTES:
raise ValueError(f"oversized dependency evidence archive entry: {name!r}")
total_bytes += info.file_size
if total_bytes > MAX_TOTAL_BYTES:
raise ValueError("dependency evidence archive exceeds the size limit")
digest = hashlib.sha256()
with archive.open(info) as source:
while chunk := source.read(1024 * 1024):
digest.update(chunk)
files[name] = (info.file_size, digest.hexdigest())
return files
def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: compare-release-evidence-zip.py <source.zip> <existing.zip>", file=sys.stderr)
return 2
try:
matches = archive_tree(argv[0]) == archive_tree(argv[1])
except (OSError, ValueError, zipfile.BadZipFile, RuntimeError) as error:
print(f"dependency evidence ZIP comparison failed: {error}", file=sys.stderr)
return 1
return 0 if matches else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

View File

@@ -0,0 +1,52 @@
export interface OpenClawNpmResumeRunRecord {
conclusion?: unknown;
event?: unknown;
head_branch?: unknown;
head_sha?: unknown;
html_url?: unknown;
path?: unknown;
workflow_id?: unknown;
}
export interface OpenClawNpmResumeTagRecord {
object?: {
sha?: unknown;
type?: unknown;
};
verification?: {
verified?: unknown;
};
}
export interface OpenClawNpmResumeJobRecord {
conclusion?: unknown;
name?: unknown;
}
export interface OpenClawNpmResumeValidationInput {
canonicalWorkflowId: unknown;
compareStatus: unknown;
jobs: OpenClawNpmResumeJobRecord[];
run: OpenClawNpmResumeRunRecord;
tag: OpenClawNpmResumeTagRecord;
tagRef: OpenClawNpmResumeTagRecord;
}
export interface OpenClawNpmResumeIdentity {
tagObjectSha: string;
url: string;
workflowRef: string;
workflowSha: string;
}
export function validateOpenClawNpmResumeRun(
input: OpenClawNpmResumeValidationInput,
): OpenClawNpmResumeIdentity;
export function resolveOpenClawNpmResumeRun(options: {
repo: string;
runId: string;
runGh?: (args: string[]) => string;
}): OpenClawNpmResumeIdentity;
export function main(argv?: string[]): void;

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
const RELEASE_PUBLISH_REF_PATTERN = /^release-publish\/([a-f0-9]{12})-([1-9][0-9]*)$/u;
const WORKFLOW_PATH = ".github/workflows/openclaw-npm-release.yml";
function fail(message) {
throw new Error(message);
}
function parseJson(raw, label) {
try {
return JSON.parse(raw);
} catch (error) {
throw new Error(`${label} returned invalid JSON.`, { cause: error });
}
}
function requiredString(value, label) {
if (typeof value !== "string" || value.length === 0) {
fail(`OpenClaw npm resume run is missing ${label}.`);
}
return value;
}
function requiredSha(value, label) {
const sha = requiredString(value, label);
if (!SHA_PATTERN.test(sha)) {
fail(`OpenClaw npm resume run has invalid ${label}.`);
}
return sha;
}
function trustedWorkflowPath(path, branch) {
return new Set([
WORKFLOW_PATH,
`${WORKFLOW_PATH}@${branch}`,
`${WORKFLOW_PATH}@refs/tags/${branch}`,
]).has(path);
}
export function validateOpenClawNpmResumeRun({
canonicalWorkflowId,
compareStatus,
jobs,
run,
tag,
tagRef,
}) {
const url = requiredString(run?.html_url, "html_url");
const branch = requiredString(run?.head_branch, "head_branch");
const branchMatch = RELEASE_PUBLISH_REF_PATTERN.exec(branch);
if (!branchMatch) {
fail(`OpenClaw npm resume run has an untrusted workflow ref: ${url}`);
}
const sha = requiredSha(run?.head_sha, "head_sha");
const path = requiredString(run?.path, "path");
if (
run?.conclusion !== "success" ||
run?.event !== "workflow_dispatch" ||
!trustedWorkflowPath(path, branch) ||
run?.workflow_id !== canonicalWorkflowId ||
sha.slice(0, 12) !== branchMatch[1]
) {
fail(`OpenClaw npm resume run has an untrusted workflow identity: ${url}`);
}
const tagObjectSha = requiredSha(tagRef?.object?.sha, "tooling tag object SHA");
if (tagRef?.object?.type !== "tag") {
fail(`OpenClaw npm resume run tooling ref is not a signed annotated tag: ${url}`);
}
const tagCommitSha = requiredSha(tag?.object?.sha, "tooling tag commit SHA");
if (
tag?.object?.type !== "commit" ||
tagCommitSha !== sha ||
tag?.verification?.verified !== true ||
(compareStatus !== "ahead" && compareStatus !== "identical")
) {
fail(
`OpenClaw npm resume run is not bound to a real, main-reachable protected tooling tag: ${url}`,
);
}
if (
!Array.isArray(jobs) ||
!jobs.some((job) => job?.name === "validate_publish_request" && job?.conclusion === "success")
) {
fail(`OpenClaw npm resume run lacks successful parent release approval validation: ${url}`);
}
return {
url,
workflowRef: `refs/tags/${branch}`,
workflowSha: sha,
tagObjectSha,
};
}
function defaultRunGh(args) {
return execFileSync("gh", args, {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
});
}
export function resolveOpenClawNpmResumeRun({ repo, runId, runGh = defaultRunGh }) {
if (!/^[1-9][0-9]*$/u.test(runId)) {
fail("OpenClaw npm resume run id must be a positive integer.");
}
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repo)) {
fail("OpenClaw npm resume repository must be owner/name.");
}
const api = (endpoint) =>
parseJson(runGh(["api", `repos/${repo}/${endpoint}`, "--method", "GET"]), endpoint);
const run = api(`actions/runs/${runId}`);
const canonicalWorkflow = api(`actions/workflows/${WORKFLOW_PATH.split("/").at(-1)}`);
const branch = requiredString(run?.head_branch, "head_branch");
const tagRef = api(`git/ref/tags/${branch}`);
const tagObjectSha = requiredSha(tagRef?.object?.sha, "tooling tag object SHA");
const tag = api(`git/tags/${tagObjectSha}`);
const sha = requiredSha(run?.head_sha, "head_sha");
const comparison = api(`compare/${sha}...main`);
const jobs = parseJson(
runGh(["run", "view", runId, "--repo", repo, "--json", "jobs", "--jq", ".jobs"]),
"resume run jobs",
);
return validateOpenClawNpmResumeRun({
canonicalWorkflowId: canonicalWorkflow?.id,
compareStatus: comparison?.status,
jobs,
run,
tag,
tagRef,
});
}
function parseArgs(argv) {
const options = { repo: "", runId: "" };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--repo") {
options.repo = argv[(index += 1)] ?? "";
} else if (arg === "--run-id") {
options.runId = argv[(index += 1)] ?? "";
} else {
fail(`Unknown argument: ${arg}`);
}
}
return options;
}
export function main(argv = process.argv.slice(2)) {
const result = resolveOpenClawNpmResumeRun(parseArgs(argv));
process.stdout.write(`${JSON.stringify(result)}\n`);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}

View File

@@ -1058,6 +1058,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
],
["scripts/mobile-release-ref.ts", ["test/scripts/mobile-release-ref.test.ts"]],
["scripts/apple-release-source-check.sh", ["test/scripts/apple-release-source-check.test.ts"]],
["scripts/compare-release-evidence-zip.py", ["test/scripts/package-acceptance-workflow.test.ts"]],
["scripts/android-release.sh", ["test/scripts/android-release-wrapper-args.test.ts"]],
["scripts/android-release-signing.mjs", ["test/scripts/android-release-signing.test.ts"]],
["scripts/android-release-upload.sh", ["test/scripts/android-release-wrapper-args.test.ts"]],
@@ -1092,6 +1093,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
["test/scripts/release-workflow-matrix-plan.test.ts"],
],
["scripts/release-fast-pretag-check.sh", ["test/scripts/package-acceptance-workflow.test.ts"]],
["scripts/openclaw-npm-resume-run.mjs", ["test/scripts/openclaw-npm-resume-run.test.ts"]],
["scripts/plugin-clawhub-release-check.ts", ["test/scripts/release-wrapper-scripts.test.ts"]],
["scripts/plugin-clawhub-release-plan.ts", ["test/scripts/release-wrapper-scripts.test.ts"]],
["scripts/plugin-npm-release-check.ts", ["test/scripts/release-wrapper-scripts.test.ts"]],
@@ -2108,6 +2110,7 @@ const TOOLING_DECLARATION_SOURCE_MIRRORS = [
["scripts/ci-changed-scope.d.mts", "scripts/ci-changed-scope.mjs"],
["scripts/copy-bundled-plugin-metadata.d.mts", "scripts/copy-bundled-plugin-metadata.mjs"],
["scripts/docs-link-audit.d.mts", "scripts/docs-link-audit.mjs"],
["scripts/openclaw-npm-resume-run.d.mts", "scripts/openclaw-npm-resume-run.mjs"],
["scripts/periphery-intersection.d.mts", "scripts/periphery-intersection.mjs"],
[
"scripts/lib/bundled-plugin-build-entries.d.mts",