Files
openclaw/scripts/testbox-lease-freshness.mjs
Vincent Koc c47ceb0f3d improve(release): reuse exact-SHA validation evidence (#104162)
* 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
2026-07-11 12:48:27 +08:00

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);
}