Skip to content

Commit 14eb460

Browse files
authored
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
1 parent c00d79a commit 14eb460

5 files changed

Lines changed: 939 additions & 16 deletions

File tree

scripts/AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
1818
- Metadata-only or explicitly narrow commands may skip the lock when the existing helper logic says that is safe.
1919
- If you change the lock heuristics, add or update the narrow tests under `test/scripts/`.
2020

21+
## PR Prepare Gates
22+
23+
- `scripts/pr prepare-gates` holds the heavy-check lock for its whole local gate block (`scripts/pr-gates-lock.mjs`), so concurrent gate runs across `.worktrees` queue as units instead of dying on child lock timeouts or vitest no-output watchdog kills.
24+
- `OPENCLAW_PR_GATES_REMOTE=testbox` runs the full-suite `pnpm test` gate on a Blacksmith Testbox through `scripts/crabbox-wrapper.mjs` (same delegation as `check:changed`); `pnpm build`/`pnpm check` stay local. The `tbx_` lease id and Actions run URL land in `.local/gates.env` (`REMOTE_GATES_*`) and `.local/prep.md`. Use it for reviewed trusted code when a loaded host makes the local 88-shard run stall-kill; contributor/fork code stays on secretless CI or sanitized AWS unless a maintainer explicitly approves credentialed execution.
25+
2126
## Generated Outputs
2227

2328
- If a script writes generated artifacts, keep the source-of-truth generator, the package script, and the matching verification/check command aligned.

scripts/pr-gates-lock.mjs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Holds the shared local heavy-check lock for a whole scripts/pr gate block so
2+
// concurrent gate runs across .worktrees serialize before their first command
3+
// instead of dying mid-test on child lock timeouts or no-output watchdog kills.
4+
import fs from "node:fs";
5+
import {
6+
acquireLocalHeavyCheckLockSync,
7+
resolveLocalHeavyCheckEnv,
8+
} from "./lib/local-heavy-check-runtime.mjs";
9+
10+
// A queued gate block legitimately waits out another full gate run; the
11+
// 10-minute per-command default in the lock runtime is far too short for that.
12+
const DEFAULT_GATE_LOCK_TIMEOUT_MS = 2 * 60 * 60 * 1000;
13+
const PARENT_WATCH_INTERVAL_MS = 2_000;
14+
15+
function parseArgs(argv) {
16+
const args = { statusFile: "" };
17+
for (let index = 0; index < argv.length; index += 1) {
18+
if (argv[index] === "--status-file") {
19+
args.statusFile = argv[index + 1] ?? "";
20+
index += 1;
21+
continue;
22+
}
23+
throw new Error(`Unknown option: ${argv[index]}`);
24+
}
25+
if (!args.statusFile) {
26+
throw new Error("Usage: node scripts/pr-gates-lock.mjs --status-file <path>");
27+
}
28+
return args;
29+
}
30+
31+
function main() {
32+
const { statusFile } = parseArgs(process.argv.slice(2));
33+
const baseEnv = resolveLocalHeavyCheckEnv(process.env);
34+
const env = baseEnv.OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS
35+
? baseEnv
36+
: { ...baseEnv, OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS: String(DEFAULT_GATE_LOCK_TIMEOUT_MS) };
37+
38+
const parentPid = process.ppid;
39+
const release = acquireLocalHeavyCheckLockSync({
40+
cwd: process.cwd(),
41+
env,
42+
toolName: "pr-gates",
43+
});
44+
let released = false;
45+
const releaseOnce = () => {
46+
if (released) {
47+
return;
48+
}
49+
released = true;
50+
release();
51+
};
52+
53+
process.on("exit", releaseOnce);
54+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
55+
process.on(signal, () => {
56+
releaseOnce();
57+
process.exit(0);
58+
});
59+
}
60+
61+
fs.writeFileSync(statusFile, "acquired\n");
62+
63+
// The owner-pid reclaim in the lock runtime already covers a SIGKILLed
64+
// holder; this watch releases promptly when the gate shell dies mid-block.
65+
// ppid 1 also counts as dead: orphans reparent to init/launchd, and a
66+
// helper that started orphaned has no gate block to hold the lock for.
67+
setInterval(() => {
68+
if (process.ppid !== parentPid || process.ppid === 1) {
69+
releaseOnce();
70+
process.exit(0);
71+
}
72+
}, PARENT_WATCH_INTERVAL_MS);
73+
}
74+
75+
main();

0 commit comments

Comments
 (0)