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.
5+import { execFileSync } from "node:child_process";
6+import { existsSync } from "node:fs";
7+import { resolve } from "node:path";
8+import { fileURLToPath } from "node:url";
9+
10+const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
11+
12+const 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+
17+run("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.
22+run("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.
26+run("playwright chromium", "bunx", ["playwright", "install", "chromium"]);
27+
28+if (!existsSync(resolve(repoRoot, "node_modules/.bin/vitest"))) {
29+ throw new Error("bootstrap: vitest missing after install — bun install likely failed");
30+}
31+
32+console.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.
7+import { execFileSync } from "node:child_process";
8+import { existsSync } from "node:fs";
9+
10+const args = new Set(process.argv.slice(2));
11+const dryRun = args.has("--dry-run");
12+const killAll = args.has("--all");
13+
14+const DEV_PATTERN = /vite dev|dev-db\.ts|scripts\/dev\.ts/;
15+
16+const ps = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" });
17+
18+interface Candidate {
19+ readonly pid: number;
20+ readonly command: string;
21+ readonly checkout: string | undefined;
22+ readonly orphan: boolean;
23+}
24+
25+const candidates: Candidate[] = [];
26+for (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+
40+if (candidates.length === 0) {
41+ console.log("reap: no dev-stack processes found.");
42+ process.exit(0);
43+}
44+
45+for (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+
63+if (!killAll && candidates.some((c) => !c.orphan)) {
64+ console.log("\nLive-checkout servers were kept (another session may own them); --all kills those too.");
65+}