openclaw/scripts/pr-gates-lock.mjs
Peter Steinberger 14eb460dd2
feat(scripts): serialize pr prepare gates and add remote Testbox test gate (#100242)
* feat(scripts): serialize pr prepare gates and add remote testbox test gate

Concurrent scripts/pr gate runs across .worktrees queued on the shared
heavy-check lock mid-test: the queued run's children hit the 10-minute
lock timeout while its unlocked build stage piled CPU load onto the
holder's vitest shards, which then stalled past the 120s no-output
watchdog and were SIGTERMed with zero real test failures (observed
landing PRs #99935/#100026 on a loaded maintainer Mac).

- scripts/pr-gates-lock.mjs holds the shared heavy-check lock for the
  whole local gate block; gate stages inherit the existing *_LOCK_HELD
  child contract, so concurrent gate runs now queue as whole units
  before their first command.
- OPENCLAW_PR_GATES_REMOTE=testbox runs the full-suite pnpm test gate on
  a Blacksmith Testbox via scripts/crabbox-wrapper.mjs (same delegation
  as check:changed). The tbx_ lease id and Actions run URL from the
  crabbox --timing-json report are recorded in .local/gates.env
  (REMOTE_GATES_*) and .local/prep.md. Local remains the default;
  pnpm build/check stay local.

Formatting verified with the primary checkout's oxfmt (hook bypassed:
linked worktree has no hydrated node_modules).

* fix(scripts): refresh the gate stamp when lease-retry gates rerun for a rebased head

The lease-retry path reran build/check/test for the rebased prep head but
left .local/gates.env describing the pre-push head, so prep.md/prep.env
attributed stale evidence (including the new remote testbox lease id) to
the pushed commit. Extract write_gates_env_stamp as the single stamp
writer, rewrite the stamp from the retry path for all modes, and re-source
gates.env in prepare_push after the push settles.

Found by autoreview (codex/gpt-5.5); formatting verified with the primary
checkout's oxfmt (hook bypassed: linked worktree has no node_modules).

* fix(scripts): harden remote PR gate evidence

* fix(scripts): serialize complete gate setup

* fix(scripts): clear stale docs gate proof
2026-07-05 04:23:58 -07:00

75 lines
2.3 KiB
JavaScript

// Holds the shared local heavy-check lock for a whole scripts/pr gate block so
// concurrent gate runs across .worktrees serialize before their first command
// instead of dying mid-test on child lock timeouts or no-output watchdog kills.
import fs from "node:fs";
import {
acquireLocalHeavyCheckLockSync,
resolveLocalHeavyCheckEnv,
} from "./lib/local-heavy-check-runtime.mjs";
// A queued gate block legitimately waits out another full gate run; the
// 10-minute per-command default in the lock runtime is far too short for that.
const DEFAULT_GATE_LOCK_TIMEOUT_MS = 2 * 60 * 60 * 1000;
const PARENT_WATCH_INTERVAL_MS = 2_000;
function parseArgs(argv) {
const args = { statusFile: "" };
for (let index = 0; index < argv.length; index += 1) {
if (argv[index] === "--status-file") {
args.statusFile = argv[index + 1] ?? "";
index += 1;
continue;
}
throw new Error(`Unknown option: ${argv[index]}`);
}
if (!args.statusFile) {
throw new Error("Usage: node scripts/pr-gates-lock.mjs --status-file <path>");
}
return args;
}
function main() {
const { statusFile } = parseArgs(process.argv.slice(2));
const baseEnv = resolveLocalHeavyCheckEnv(process.env);
const env = baseEnv.OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS
? baseEnv
: { ...baseEnv, OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS: String(DEFAULT_GATE_LOCK_TIMEOUT_MS) };
const parentPid = process.ppid;
const release = acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "pr-gates",
});
let released = false;
const releaseOnce = () => {
if (released) {
return;
}
released = true;
release();
};
process.on("exit", releaseOnce);
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
process.on(signal, () => {
releaseOnce();
process.exit(0);
});
}
fs.writeFileSync(statusFile, "acquired\n");
// The owner-pid reclaim in the lock runtime already covers a SIGKILLed
// holder; this watch releases promptly when the gate shell dies mid-block.
// ppid 1 also counts as dead: orphans reparent to init/launchd, and a
// helper that started orphaned has no gate block to hold the lock for.
setInterval(() => {
if (process.ppid !== parentPid || process.ppid === 1) {
releaseOnce();
process.exit(0);
}
}, PARENT_WATCH_INTERVAL_MS);
}
main();