History

Make fresh-worktree e2e turnkey: per-checkout ports, port-squat fail-fast, bootstrap script

- e2e ports are now derived from a hash of the checkout root (42000-45999 blocks) so concurrent worktrees never collide; E2E_*_PORT/URL still override. - globalsetups fail fast with the squatting PID when a port is taken instead of silently attaching to a foreign checkout's dev server (the cause of 31-scenario auth-error cascades). - scripts/bootstrap.ts: one idempotent command for submodules + install + playwright; scripts/reap-dev-servers.ts kills orphaned dev stacks. - e2e: bun run ports prints this checkout's derived ports. - sources-api scenario: align with payload-first OpenAPI results (PR #956 changed the product, the cloud-only scenario was not updated). - AGENTS.md: fresh-worktree setup + environment gotchas (fish shell, /tmp scripts, bun.lock conflicts, per-checkout ports).

Rhys Sullivan committed 5 months ago

Commit a2bb24e1 parent cbf936515 files · +242 −16Browse files

Showing the changes to scripts (2 of 15 files). Show all

Unified · Split

2 files
  1. scripts/bootstrap.ts +32 −0
  2. scripts/reap-dev-servers.ts +65 −0
scripts/bootstrap.ts+32−0
@@ -0,0 +1,32 @@
1// One-command setup for a fresh checkout or agent worktree: submodules,
2// dependencies (whose prepare hook builds the internal packages dev servers
3// need), and the Playwright browser the e2e suite drives. Idempotent and
4// safe to re-run; each step prints what it is doing.
5import { execFileSync } from "node:child_process";
6import { existsSync } from "node:fs";
7import { resolve } from "node:path";
8import { fileURLToPath } from "node:url";
9
10const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
11
12const run = (label: string, cmd: string, args: ReadonlyArray<string>) => {
13 console.log(`\n[bootstrap] ${label}: ${cmd} ${args.join(" ")}`);
14 execFileSync(cmd, [...args], { cwd: repoRoot, stdio: "inherit" });
15};
16
17run("vendor submodules", "git", ["submodule", "update", "--init", "--recursive"]);
18
19// `bun install` runs the workspace prepare hook, which builds
20// @executor-js/vite-plugin and @executor-js/react — the two artifacts the
21// apps' vite dev servers fail without in a fresh worktree.
22run("dependencies (+ prepare builds)", "bun", ["install"]);
23
24// e2e browser scenarios need Playwright's chromium; the cache is shared
25// per-machine so this is a fast no-op when already present.
26run("playwright chromium", "bunx", ["playwright", "install", "chromium"]);
27
28if (!existsSync(resolve(repoRoot, "node_modules/.bin/vitest"))) {
29 throw new Error("bootstrap: vitest missing after install — bun install likely failed");
30}
31
32console.log("\n[bootstrap] done — `cd e2e && bun run test` runs the full suite.");
scripts/reap-dev-servers.ts+65−0
@@ -0,0 +1,65 @@
1// Find (and optionally kill) leaked dev-stack processes — vite dev servers,
2// PGlite dev-dbs, e2e emulators — left behind when a session dies without
3// running its teardown. Default: kill only ORPHANS (processes whose checkout
4// path no longer exists, e.g. a removed worktree) and list the rest.
5// `--all` also kills live-checkout servers (do this only when you know no
6// other agent session is using them). `--dry-run` lists without killing.
7import { execFileSync } from "node:child_process";
8import { existsSync } from "node:fs";
9
10const args = new Set(process.argv.slice(2));
11const dryRun = args.has("--dry-run");
12const killAll = args.has("--all");
13
14const DEV_PATTERN = /vite dev|dev-db\.ts|scripts\/dev\.ts/;
15
16const ps = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" });
17
18interface Candidate {
19 readonly pid: number;
20 readonly command: string;
21 readonly checkout: string | undefined;
22 readonly orphan: boolean;
23}
24
25const candidates: Candidate[] = [];
26for (const line of ps.split("\n")) {
27 const match = line.match(/^\s*(\d+)\s+(.*)$/);
28 if (!match) continue;
29 const [, pidText, command] = match;
30 if (!DEV_PATTERN.test(command!)) continue;
31 if (Number(pidText) === process.pid) continue;
32 // The checkout root is whatever absolute path prefixes node_modules/ or a
33 // workspace dir in the command line.
34 const pathMatch = command!.match(/(\/[^ ]*?)\/(?:node_modules|apps|packages|e2e)\//);
35 const checkout = pathMatch?.[1];
36 const orphan = checkout !== undefined && !existsSync(checkout);
37 candidates.push({ pid: Number(pidText), command: command!.slice(0, 160), checkout, orphan });
38}
39
40if (candidates.length === 0) {
41 console.log("reap: no dev-stack processes found.");
42 process.exit(0);
43}
44
45for (const c of candidates) {
46 const shouldKill = !dryRun && (c.orphan || killAll);
47 const tag = c.orphan ? "ORPHAN" : "live ";
48 console.log(`${shouldKill ? "KILL " : "keep "} ${tag} pid=${c.pid} ${c.command}`);
49 if (shouldKill) {
50 try {
51 // Group kill (detached boots) with a direct-pid fallback.
52 try {
53 process.kill(-c.pid, "SIGTERM");
54 } catch {
55 process.kill(c.pid, "SIGTERM");
56 }
57 } catch (error) {
58 console.error(` failed to kill ${c.pid}: ${String(error)}`);
59 }
60 }
61}
62
63if (!killAll && candidates.some((c) => !c.orphan)) {
64 console.log("\nLive-checkout servers were kept (another session may own them); --all kills those too.");
65}