mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 18:16:07 +00:00
* perf(release): share changelog verification snapshots * perf(release): reuse exact-SHA validation evidence * feat(release): checkpoint candidate workflow state * feat(release): watch CI transitions compactly * fix(testbox): rotate stale reusable leases * refactor(release): move CI verifier into scripts * fix(release): preserve verifier executable mode * fix(testbox): force noninteractive remote hydration * perf(testbox): skip sync for proven clean heads * fix(testbox): keep changed gates synchronized * fix(testbox): isolate git state probes * fix(testbox): isolate wrapper git commands * fix(testbox): preserve git command contracts * fix(release): validate reused SHA evidence * fix(release): resume serialized plugin selections * fix(testbox): sync source on every lease reuse * fix(release): verify from trusted workflow checkout * fix(release): gate evidence reuse on trusted lineage * fix(release): support legacy verifier checkouts * fix(testbox): export CI across shell snippets * fix(release): revalidate reused evidence before publish * fix(release): reject untrusted reuse before lookup * fix(release): reuse SHA-pinned root evidence * fix(ci): allow unreleased notes in QA packages * fix(release): satisfy script lint contracts * fix(release): handle Unicode workflow refs safely
132 lines
4.2 KiB
JavaScript
132 lines
4.2 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
renameSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
const STATE_VERSION = 1;
|
|
const DEPENDENCY_INPUTS = ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc"];
|
|
const ENVIRONMENT_INPUTS = [
|
|
".crabbox.yaml",
|
|
".github/workflows/ci-check-testbox.yml",
|
|
".node-version",
|
|
"scripts/crabbox-wrapper.mjs",
|
|
];
|
|
|
|
function optionValue(args, name, fallback = "") {
|
|
const shortName = name.replace(/^--/u, "-");
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const argument = args[index];
|
|
if (argument === name || argument === shortName) {
|
|
return args[index + 1] ?? fallback;
|
|
}
|
|
if (argument.startsWith(`${name}=`) || argument.startsWith(`${shortName}=`)) {
|
|
return argument.slice(argument.indexOf("=") + 1);
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function git(repoRoot, args) {
|
|
return execFileSync("git", ["-C", repoRoot, ...args], {
|
|
encoding: "utf8",
|
|
env: { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null" },
|
|
}).trim();
|
|
}
|
|
|
|
function listFiles(path) {
|
|
if (!existsSync(path)) {
|
|
return [];
|
|
}
|
|
if (statSync(path).isFile()) {
|
|
return [path];
|
|
}
|
|
return readdirSync(path, { withFileTypes: true })
|
|
.flatMap((entry) => listFiles(resolve(path, entry.name)))
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function digestInputs(repoRoot, inputs) {
|
|
const hash = createHash("sha256");
|
|
for (const input of inputs) {
|
|
for (const path of listFiles(resolve(repoRoot, input))) {
|
|
hash.update(path.slice(repoRoot.length));
|
|
hash.update("\0");
|
|
hash.update(readFileSync(path));
|
|
hash.update("\0");
|
|
}
|
|
}
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
export function buildTestboxLeaseFingerprint(repoRoot, args) {
|
|
let baseSha;
|
|
try {
|
|
baseSha = git(repoRoot, ["merge-base", "HEAD", "refs/remotes/origin/main"]);
|
|
} catch {
|
|
baseSha = git(repoRoot, ["rev-parse", "HEAD"]);
|
|
}
|
|
return {
|
|
version: STATE_VERSION,
|
|
baseSha,
|
|
headSha: git(repoRoot, ["rev-parse", "HEAD"]),
|
|
workingTreeClean: git(repoRoot, ["status", "--porcelain=v1"]) === "",
|
|
dependencyDigest: digestInputs(repoRoot, [...DEPENDENCY_INPUTS, "patches"]),
|
|
environmentDigest: digestInputs(repoRoot, ENVIRONMENT_INPUTS),
|
|
workflow: optionValue(args, "--blacksmith-workflow", ".github/workflows/ci-check-testbox.yml"),
|
|
job: optionValue(args, "--blacksmith-job", "check"),
|
|
ref: optionValue(args, "--blacksmith-ref", "main"),
|
|
};
|
|
}
|
|
|
|
export function testboxLeaseStaleReasons(saved, current) {
|
|
if (!saved || saved.version !== STATE_VERSION) {
|
|
return ["state schema"];
|
|
}
|
|
return ["baseSha", "dependencyDigest", "environmentDigest", "workflow", "job", "ref"].filter(
|
|
(key) => saved[key] !== current[key],
|
|
);
|
|
}
|
|
|
|
export function prepareTestboxLeaseFreshness({ args, env, provider, repoRoot }) {
|
|
const id = optionValue(args, "--id");
|
|
if (provider !== "blacksmith-testbox" || args[0] !== "run" || !id?.startsWith("tbx_")) {
|
|
return null;
|
|
}
|
|
const configuredStateDir = env.OPENCLAW_TESTBOX_LEASE_STATE_DIR?.trim();
|
|
if (env.VITEST && !configuredStateDir) {
|
|
return null;
|
|
}
|
|
const stateDir = resolve(configuredStateDir || resolve(repoRoot, ".crabbox", "testbox-leases"));
|
|
const path = resolve(stateDir, `${id}.json`);
|
|
const current = buildTestboxLeaseFingerprint(repoRoot, args);
|
|
if (existsSync(path)) {
|
|
const saved = JSON.parse(readFileSync(path, "utf8"));
|
|
const staleReasons = testboxLeaseStaleReasons(saved, current);
|
|
if (staleReasons.length > 0 && env.OPENCLAW_TESTBOX_ALLOW_STALE !== "1") {
|
|
throw new Error(
|
|
`Testbox ${id} is stale (${staleReasons.join(", ")}); stop it and warm a fresh lease, or set OPENCLAW_TESTBOX_ALLOW_STALE=1 for an intentional diagnostic reuse`,
|
|
);
|
|
}
|
|
return { current, path };
|
|
}
|
|
return { current, path };
|
|
}
|
|
|
|
export function recordTestboxLeaseFreshness(prepared) {
|
|
if (!prepared) {
|
|
return;
|
|
}
|
|
mkdirSync(resolve(prepared.path, ".."), { recursive: true });
|
|
const temporaryPath = `${prepared.path}.tmp-${process.pid}`;
|
|
writeFileSync(temporaryPath, `${JSON.stringify(prepared.current, null, 2)}\n`);
|
|
renameSync(temporaryPath, prepared.path);
|
|
}
|