Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions docs/local-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,20 @@ examples/release/micro-eval gates, so it is a fast signal, not a merge gate.
## What it runs

Every leg is an isolated git worktree pinned to the HEAD commit (uncommitted
changes are not covered — the runner warns), with its own `node_modules`.
Legs live under `.worktrees/local-ci/` (gitignored), are reused across runs
for warm caches, and can be recreated with `--fresh`.
changes are not covered — the runner warns), with its own `node_modules` and
its own `TMPDIR` (`<system tmp>/abci-<hash8>-<leg>`, where `<hash8>` is
derived from the repo root path; recreated every run). The temp roots live
under the short system temp directory rather than the repo worktree because
Chrome creates AF_UNIX sockets inside `TMPDIR` and the kernel caps socket
paths at 108 bytes; the hash keeps concurrent runs from different checkouts
from colliding. The private temp root keeps concurrent legs from observing
each other's temp traffic: suites that assert temp-root hygiene (for example
`cli.test.ts` scans `os.tmpdir()` for leaked `agent-bundle-artifact-*`
directories) only ever see their own leg's directories, so a sibling leg's
in-flight work cannot fail them — while a directory the leg itself leaks
still fails its own scan. Legs live under `.worktrees/local-ci/`
(gitignored), are reused across runs for warm caches, and can be recreated
with `--fresh`.

| Local leg | Node | Steps | Mirrors hosted job |
| --- | --- | --- | --- |
Expand Down
31 changes: 28 additions & 3 deletions scripts/local-ci.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,23 @@
* be shared across Node ABIs. The shared pnpm store is content-addressed
* (and side-effects caches are keyed by engine), so concurrent per-leg
* installs stay cheap. Legs live under .worktrees/local-ci/ (gitignored) and
* are reused across runs for warm caches; `--fresh` recreates them.
* are reused across runs for warm caches; `--fresh` recreates them. Each leg
* also gets a private TMPDIR (os.tmpdir()/abci-<hash8>-<leg>, where <hash8>
* is derived from the repo root path; recreated every run): concurrent legs
* would otherwise share /tmp, and suites that assert temp-root hygiene
* (cli.test.ts scans os.tmpdir() for leaked agent-bundle-artifact-*
* directories) would see a sibling leg's in-flight temp traffic and fail on
* it (#110). The temp roots deliberately live under the SYSTEM temp
* directory, not the repo worktree: Chrome creates AF_UNIX sockets inside
* TMPDIR, and the kernel caps socket paths at 108 bytes — a repo-nested
* TMPDIR overflows that and crashes every browser test at launch.
*/
import { createHash } from 'node:crypto';
import { spawn } from 'node:child_process';
import { execFile as executeFile } from 'node:child_process';
import { existsSync } from 'node:fs';
import { chmod, mkdir, readdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { availableParallelism, homedir } from 'node:os';
import { availableParallelism, homedir, tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
Expand Down Expand Up @@ -380,11 +390,26 @@ const main = async () => {
const directory = join(legsRoot, plan.name);
await ensureLegWorktree(directory, sha);
const syntheticBinDirectory = await createSyntheticBinDirectory(plan, pnpmEntrypoint);
// Private per-leg temp root (see the isolation model above). Recreating
// it keeps every run's hygiene scans free of a crashed prior run's
// leftovers, while a leak WITHIN a run still fails its own leg's scan.
// It must be a SHORT path under the system temp root (never under the
// repo worktree): Chrome creates AF_UNIX sockets in TMPDIR and the
// kernel's sun_path limit is 108 bytes. The hash keys the directory to
// this repo root, so concurrent runs from different checkouts cannot
// collide while reruns from the same checkout reuse (and reset) it.
const repositoryHash = createHash('sha256').update(repositoryRoot).digest('hex').slice(0, 8);
const temporaryDirectory = join(tmpdir(), `abci-${repositoryHash}-${plan.name}`);
await rm(temporaryDirectory, { recursive: true, force: true });
await mkdir(temporaryDirectory, { recursive: true });
legs.push({
...plan,
directory,
syntheticBinDirectory,
environment: buildLegEnvironment(syntheticBinDirectory, plan.environmentOverrides),
environment: buildLegEnvironment(syntheticBinDirectory, {
...plan.environmentOverrides,
TMPDIR: temporaryDirectory,
}),
});
}

Expand Down
Loading