feat: queue concurrent local:gate runs on a machine-wide lease (#2339)

`npm run local:gate` now runs its stages under `scripts/gate-lease.mjs`, a
`proper-lockfile` lease under `os.tmpdir()` (or `$XDG_RUNTIME_DIR`), so a gate
started in a second worktree waits for the first instead of overlapping it.

Measured on v2/main @ aa56551b: a quiet gate is 257s green; two started
together had one fail at 279s (`smoke:web:chromium` found port 6298 held by
the other gate's smoke) and the survivor take 338s. Under the lease, three
gates started within seconds ran back to back (259s, then 262s, then the
third), each green with zero timeouts.

The lease grants rather than polls a threshold, so two waiters cannot
deadlock on each other; a holder that dies stops refreshing and is taken over
after 30s; a signal to the wrapper stops the whole process group, releases,
and exits 128+signal; the wait names the holder's pid and worktree and gives
up after 45 minutes. `INSPECTOR_SKIP_GATE_LEASE=1` bypasses it. A lock that
cannot be created runs the gate unleased with a warning.

`scriptChainRuns` looks through the wrapper (and only it) so the existing
vouches about what `local:gate` runs keep reading the real chain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-12 22:00:33 -04:00
commit f99402918d
2 changed files with 849 additions and 12 deletions

View file

@ -50,6 +50,16 @@ export function reachableScripts(scripts, entry = "validate") {
return reached;
}
/** The lease wrapper `local:gate` runs under (#2339); see `scripts/gate-lease.mjs`. */
export const GATE_LEASE_WRAPPER = "node scripts/gate-lease.mjs ";
/** `node scripts/gate-lease.mjs npm run X` → `npm run X`; anything else unchanged. */
function unwrapGateLease(segment) {
return segment.startsWith(GATE_LEASE_WRAPPER)
? segment.slice(GATE_LEASE_WRAPPER.length).trim()
: segment;
}
/** The command strings of every script reachable from the root `validate`. */
function rootReachedCommands(rootScripts) {
return [...reachableScripts(rootScripts)]
@ -108,6 +118,12 @@ export function rootReachesScript(rootScripts, scriptName) {
* flags, as are npm's implicit `pre`/`post` hooks. Only the target is matched
* exactly, which suits the argument-less scripts a vouch asks about.
*
* One wrapper is transparent: `node scripts/gate-lease.mjs npm run X` runs
* `npm run X` under the local:gate lease (#2339) and nothing else, so a vouch
* about what `local:gate` runs looks through it. It is matched by its exact
* leading tokens — a wrapper with flags, or any other wrapper, still hides the
* invocation, because this helper cannot know what an arbitrary wrapper does.
*
* @param {Record<string, string>} scripts
* @param {string} entry
* @param {string} target
@ -124,7 +140,9 @@ export function scriptChainRuns(scripts, entry, target) {
if (typeof scripts?.[hook] === "string") queue.push(hook);
const body = scripts?.[name];
if (typeof body !== "string") continue;
for (const segment of body.split(/\n|;|&&/).map((part) => part.trim())) {
for (const segment of body
.split(/\n|;|&&/)
.map((part) => unwrapGateLease(part.trim()))) {
if (segment === `npm run ${target}`) return true;
const tokens = segment.split(/\s+/);
if (tokens[0] === "npm" && tokens[1] === "run" && tokens[2]) {

View file

@ -23,7 +23,11 @@ import {
findWorkflowViolations,
formatWorkflowViolations,
} from "./workflow-gate.mjs";
import { reachableScripts } from "./npm-scripts.mjs";
import {
GATE_LEASE_WRAPPER,
reachableScripts,
scriptChainRuns,
} from "./npm-scripts.mjs";
const repoRoot = join(import.meta.dirname, "..", "..");
const workflowDir = join(repoRoot, ".github", "workflows");
@ -607,7 +611,22 @@ describe("the gate's name", () => {
it("is `local:gate`, and runs the local-only Storybook step", () => {
assert.ok(scripts["local:gate"], "the pre-push gate must exist");
assert.match(scripts["local:gate"], /local:storybook/);
assert.match(scripts["local:gate:stages"], /local:storybook/);
});
it("runs its stages under the lease wrapper, and nothing else (#2339)", () => {
// The wrapper serializes gates across worktrees. It must be the ONLY thing
// `local:gate` does — a stage placed beside it would run outside the
// lease — and it must run exactly the stages script, so every assertion
// below about what the gate runs can read `local:gate:stages` directly.
assert.equal(
scripts["local:gate"],
`${GATE_LEASE_WRAPPER}npm run local:gate:stages`,
);
assert.ok(
scriptChainRuns(scripts, "local:gate", "local:storybook"),
"the wrapper is transparent to a chain vouch",
);
});
describe("runs each client's test suite once, not twice (#2341)", () => {
@ -631,8 +650,11 @@ describe("the gate's name", () => {
);
it("starts from `local:validate`, and never from `validate`", () => {
assert.match(scripts["local:gate"], /^npm run local:validate && /);
assert.doesNotMatch(scripts["local:gate"], /\brun validate(?=$|[\s&;])/);
assert.match(scripts["local:gate:stages"], /^npm run local:validate && /);
assert.doesNotMatch(
scripts["local:gate:stages"],
/\brun validate(?=$|[\s&;])/,
);
// Nothing reachable from the WHOLE gate — not just its first stage —
// may run `validate` or a client's `validate:*`: those are the doors
// through which the bare `test` leg would come back, and a later stage