Public: anyone can read this; the crew can change it.

Contained checks and gates install from the repository's shared bun cache, filled by the forge and bound read-only into the cell #7549

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/7543-contained-checks-and-gates-install-from → main 4 commits fork 5 threads · 5 unresolved

  • Merged. scout/7543-contained-checks-and-gates-install-from was merged into main as da600e5.

Review

Sign in to review this change.
5 files changed +425 −9
src/checks/install.ts +129 −1
@@ -16,7 +16,8 @@
1616 * the check again and the merge queue reads it as cut off, never as red.
1717 */
1818
19 import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
19 import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
20 import { tmpdir } from "node:os";
2021 import { dirname, join } from "node:path";
2122 import { err, ok, type Result } from "../kernel/result.ts";
2223
@@ -48,6 +49,133 @@ export function checkInstallCache(dataDir: string, repoId: string): string {
4849 return join(dataDir, "check-install", repoId.replace(/[^A-Za-z0-9_-]/g, "_"), "bun");
4950 }
5051
52 /** The registry a fill fetches from unless the forge's own environment names another. */
53 export const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
54
55 /**
56 * A contained `bun install` links nothing from the read-only cache: hardlinks
57 * cannot cross a bind onto another device, so it copies instead. Applied to
58 * every contained install, on purpose: whether cache and cell share a device
59 * is the host's layout, not something to probe per check, and copying a warm
60 * cache costs milliseconds against a network install. Every `bun install` in
61 * the command is asked, so `cd x && bun install` copies too.
62 */
63 export function copying(command: string): string {
64 return command.replace(/\bbun install\b(?![^&;|]*--backend)/g, "bun install --backend=copyfile");
65 }
66
67 /** What a contained install runs: the command, and the cache env it reads. */
68 export function cellInstall(
69 command: string,
70 env: Record<string, string>,
71 warm: string | undefined,
72 ): { readonly command: string; readonly env: Record<string, string> } {
73 return warm === undefined
74 ? { command, env }
75 : { command: copying(command), env: { ...env, BUN_INSTALL_CACHE_DIR: warm } };
76 }
77
78 /**
79 * How long a fill may run: never past half the lock's staleness, or another
80 * process would take the lock mid-fill and two fills would write the cache.
81 */
82 export function fillTimeoutMs(requested: number | undefined): number {
83 return Math.min(requested ?? Number.POSITIVE_INFINITY, LOCK_STALE_MS / 2);
84 }
85
86 /**
87 * Why a lockfile may not fill the shared cache, or undefined when it may: the
88 * fill runs outside a cell, so it fetches only from the pinned registry. A
89 * binary lockfile cannot be read, and a URL elsewhere or a git dependency
90 * would make the forge fetch from a host the pull chose.
91 */
92 export function lockfileRefusal(dir: string, registry: string): string | undefined {
93 if (existsSync(join(dir, "bun.lockb")) && !existsSync(join(dir, "bun.lock")))
94 return "only a binary bun.lockb, which cannot be checked against the registry";
95 if (!existsSync(join(dir, "bun.lock"))) return "no bun lockfile in the checkout";
96 const text = readFileSync(join(dir, "bun.lock"), "utf8");
97 for (const url of text.match(/[a-z+]+:\/\/[^"\s]+/gi) ?? [])
outdated

This copies bunfig.toml and .npmrc from the untrusted pull into the scratch dir, and the install then runs on the host with the pull's registry and URLs. Strip those files, pin the registry, and keep pull-written entries out of the cache main uses.

98 if (!url.startsWith(registry)) return `the lockfile names ${url.slice(0, 80)}, not the registry`;
outdated

This runs bun on the host with the lockfile's URLs. --ignore-scripts stops lifecycle scripts but not arbitrary fetches (SSRF) or cache poisoning. See the body.

99 if (/[@"'/](?:github|gitlab|bitbucket|git|ssh|git\+\w+):/i.test(text))
100 return "the lockfile has a git dependency";
101 return undefined;
102 }
103
104 /**
105 * Fill the shared cache on the host before a contained install (#7543): the
106 * cell binds the cache read-only, so only the forge writes it. Installs the
107 * commit's lockfile into a scratch copy of the checkout (no `node_modules`,
108 * no `.git`, and no `bunfig.toml` or `.npmrc`: the pull does not choose the
109 * registry) with `--frozen-lockfile --ignore-scripts`, so no lifecycle script
110 * runs outside a cell and the only host fetched is the pinned registry
111 * (`BUN_CONFIG_REGISTRY` of `env`, else {@link DEFAULT_REGISTRY}). Holds the
112 * same lock uncontained installs hold. A package the forge cannot fetch fails
113 * here, with the install's own output, and releases the lock.
114 *
115 * The cell reads the cache without the lock while a later fill may write
116 * it. That is safe because bun writes each package to a temp name in the
117 * cache and renames it into place, so a reader sees a whole entry or none,
118 * and a package the lockfile names was filled before this cell started.
119 *
120 * Answers whether it filled. False (a lockfile it will not trust, or none) is
121 * not a failure: the cell then installs into a cache of its own.
122 */
123 export async function prefillInstallCache(options: {
124 readonly cache: string;
125 readonly dir: string;
126 readonly env: Record<string, string>;
127 readonly log: (line: string) => void;
128 readonly signal: AbortSignal;
129 readonly timeoutMs?: number | undefined;
130 }): Promise<Result<boolean, string>> {
131 const { cache, dir, log, signal } = options;
132 const registry = (options.env["BUN_CONFIG_REGISTRY"] ?? DEFAULT_REGISTRY).replace(/\/?$/, "/");
133 const refusal = lockfileRefusal(dir, registry);
134 if (refusal !== undefined) {
135 log(`cache not filled: ${refusal}`);
136 return ok(false);
137 }
138 mkdirSync(cache, { recursive: true });
139 const held = await withLock(
140 `${cache}.lock`,
141 async (): Promise<Result<boolean, string>> => {
142 const scratch = mkdtempSync(join(tmpdir(), "prefill-"));
143 try {
144 cpSync(dir, scratch, {
145 recursive: true,
146 filter: (path) => !/(^|\/)(node_modules|\.git|bunfig\.toml|\.npmrc)$/.test(path),
147 });
148 const proc = Bun.spawn(["bun", "install", "--frozen-lockfile", "--ignore-scripts"], {
149 cwd: scratch,
150 env: { ...options.env, BUN_CONFIG_REGISTRY: registry, BUN_INSTALL_CACHE_DIR: cache },
151 stdin: "ignore",
152 stdout: "pipe",
153 stderr: "pipe",
154 });
155 const stop = (): void => proc.kill();
156 signal.addEventListener("abort", stop, { once: true });
157 const timer = setTimeout(stop, fillTimeoutMs(options.timeoutMs));
158 const lines = async (stream: ReadableStream<Uint8Array>): Promise<void> => {
159 const text = await new Response(stream).text();
160 for (const line of text.split("\n")) if (line.trim() !== "") log(line);
161 };
162 try {
163 await Promise.all([lines(proc.stdout), lines(proc.stderr)]);
164 const code = await proc.exited;
165 return code === 0 ? ok(true) : err(`filling the install cache exited ${code}`);
166 } finally {
167 clearTimeout(timer);
168 signal.removeEventListener("abort", stop);
169 }
170 } finally {
171 rmSync(scratch, { recursive: true, force: true });
172 }
173 },
174 { signal },
175 );
176 return held.ok ? held.value : held;
177 }
178
51179 /**
52180 * Run `attempt` (one install, its output lines handed to `log`), and once
53181 * more after `retryMs` when it failed with a {@link TRANSIENT_INSTALL} line.
src/checks/service.ts +32 −6
@@ -95,7 +95,7 @@ import { summariseCheckFailure } from "./summary.ts";
9595 import type { Result } from "../kernel/result.ts";
9696 import { placementFromEnv, type Placement } from "./placement.ts";
9797 import { JOB_VANISHED, runJobStep } from "./job_step.ts";
98 import { checkInstallCache, installWithRetry, withLock } from "./install.ts";
98 import { cellInstall, checkInstallCache, installWithRetry, prefillInstallCache, withLock } from "./install.ts";
9999 import { checkStage, type GateStage } from "./gate_reason.ts";
100100 import { extraGates, gateQueued, gateRunId, gatesInFlight, watchGates } from "./queue.ts";
101101 import type { JobOptions } from "../cluster/jobs.ts";
@@ -995,18 +995,44 @@ export function startChecks(app: App, options: ChecksOptions): ChecksHandle {
995995 // cache is shared, so installs into it take turns (`checks/install.ts`).
996996 const shared =
997997 cell === undefined ? checkInstallCache(app.config.dataDir, repo.id) : undefined;
998 const once = (log: (line: string) => void): Promise<Result<void, string>> =>
999 runStep("install", install, {
998 // A cell installs from that same cache, bound read-only (#7543):
999 // the forge fills it first, on the host, so no pull writes what
1000 // the gate on main installs. Cluster jobs (`runJobStep`) are
1001 // unchanged: a Job has its own cache and never takes this path.
1002 const warm = cell === undefined ? undefined : checkInstallCache(app.config.dataDir, repo.id);
1003 // Set once the fill has run: a cell installs from the shared cache
1004 // only when the forge filled it; otherwise it keeps a cache of its own.
1005 let filled = false;
1006 const once = (log: (line: string) => void): Promise<Result<void, string>> => {
1007 const base = shared === undefined ? env : { ...env, BUN_INSTALL_CACHE_DIR: shared };
1008 const from = cellInstall(install, base, filled ? warm : undefined);
1009 const revealed = cell !== undefined && filled && warm !== undefined ? { ...cell, reveal: [warm] } : cell;
1010 return runStep("install", from.command, {
10001011 cwd: dir,
1001 env: shared === undefined ? env : { ...env, BUN_INSTALL_CACHE_DIR: shared },
1012 env: from.env,
10021013 log,
10031014 signal,
10041015 timeoutMs: manifest.timeout * 1000,
1005 cell,
1016 cell: revealed,
10061017 });
outdated

With no lockfile the fill is skipped, then once() installs into a read-only cache and fails. Handle that case.

1018 };
10071019 const installed = await installWithRetry(
10081020 async (log) => {
1009 if (shared === undefined) return once(log);
1021 if (shared === undefined) {
1022 if (warm !== undefined) {
1023 const fill = await prefillInstallCache({
1024 cache: warm,
1025 dir,
1026 env,
1027 log,
1028 signal,
1029 timeoutMs: manifest.timeout * 1000,
1030 });
1031 if (!fill.ok) return fill;
1032 filled = fill.value;
1033 }
1034 return once(log);
1035 }
10101036 mkdirSync(shared, { recursive: true });
10111037 const held = await withLock(`${shared}.lock`, () => once(log), { signal });
10121038 return held.ok ? held.value : held;
test/check-install.test.ts +224 −2
@@ -3,8 +3,8 @@
33 * install failure is retried once, and failing the same way twice is the
44 * machine's — queued again, never the pull's red.
55 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6 import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
7 import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
88 import { mkdtemp, rm } from "node:fs/promises";
99 import { tmpdir } from "node:os";
1010 import { join } from "node:path";
@@ -12,6 +12,11 @@ import { createApp, type App } from "../src/app.ts";
1212 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
1313 import {
1414 checkInstallCache,
15 cellInstall,
16 copying,
17 fillTimeoutMs,
18 LOCK_STALE_MS,
19 prefillInstallCache,
1520 INFRASTRUCTURE,
1621 installWithRetry,
1722 transientLine,
@@ -228,3 +233,220 @@ describe("a check whose install fails on the machine", () => {
228233 expect(runs.length).toBe(2);
229234 });
230235 });
236
237 describe("prefillInstallCache (#7543)", () => {
238 setDefaultTimeout(60_000);
239 const scratch = async (): Promise<string> => mkdtemp(join(tmpdir(), "prefill-test-"));
240
241 /** A registry on loopback serving one package, `[email protected]`; counts its requests. */
242 const registry = async (): Promise<{ url: string; hits: string[]; stop: () => void; dir: string }> => {
243 const dir = await scratch();
244 mkdirSync(join(dir, "pkg", "package"), { recursive: true });
245 writeFileSync(join(dir, "pkg", "package", "package.json"), JSON.stringify({ name: "fixture-pkg", version: "1.0.0" }));
246 const made = Bun.spawnSync(["tar", "-czf", join(dir, "fixture.tgz"), "-C", join(dir, "pkg"), "package"]);
247 if (made.exitCode !== 0) throw new Error("tar is needed for the fixture registry");
248 const tarball = new Uint8Array(await Bun.file(join(dir, "fixture.tgz")).arrayBuffer());
249 const hits: string[] = [];
250 const server: ReturnType<typeof Bun.serve> = Bun.serve({
251 port: 0,
252 hostname: "127.0.0.1",
253 fetch(req): Response {
254 const path = new URL(req.url).pathname;
255 hits.push(path);
256 if (path === "/fixture-pkg/-/fixture-pkg-1.0.0.tgz") return new Response(tarball);
257 if (path === "/fixture-pkg")
258 return Response.json({
259 name: "fixture-pkg",
260 "dist-tags": { latest: "1.0.0" },
261 versions: {
262 "1.0.0": {
outdated

This returns early when the registry is unreachable, so the test can pass while proving nothing. Use a local fixture registry.

263 name: "fixture-pkg",
264 version: "1.0.0",
265 dist: { tarball: `http://127.0.0.1:${server.port}/fixture-pkg/-/fixture-pkg-1.0.0.tgz` },
266 },
267 },
268 });
269 return new Response("not found", { status: 404 });
270 },
271 });
272 return { url: `http://127.0.0.1:${server.port}/`, hits, stop: () => void server.stop(true), dir };
273 };
274
275 /** A checkout depending on the fixture, with the lockfile bun writes against it. */
276 const checkout = async (reg: string, dir: string): Promise<string> => {
277 const co = join(dir, "co");
278 mkdirSync(co);
279 writeFileSync(join(co, "package.json"), JSON.stringify({ name: "x", version: "1.0.0", dependencies: { "fixture-pkg": "1.0.0" } }));
280 const env = { PATH: process.env["PATH"] ?? "", HOME: dir, BUN_CONFIG_REGISTRY: reg };
281 // Async: the fixture registry answers from this process's event loop.
282 const made = Bun.spawn(["bun", "install", "--lockfile-only"], {
283 cwd: co,
284 env: { ...env, BUN_INSTALL_CACHE_DIR: join(dir, "lock-cache") },
285 stdout: "pipe",
286 stderr: "pipe",
outdated

A bun.lock of {} likely fails on a lockfile mismatch and not on an unfetchable package. Assert the failure is the fetch and not the mismatch.

287 });
288 if ((await made.exited) !== 0) throw new Error(`fixture lockfile: ${await new Response(made.stderr).text()}`);
289 return co;
290 };
291
292 const envFor = (dir: string, reg: string): Record<string, string> => ({
293 PATH: process.env["PATH"] ?? "",
294 HOME: dir,
295 BUN_CONFIG_REGISTRY: reg,
296 });
297
298 test("copying asks every bun install in a contained command for the copyfile backend, once", () => {
299 expect(copying("bun install --frozen-lockfile")).toBe("bun install --backend=copyfile --frozen-lockfile");
300 expect(copying("cd x && bun install")).toBe("cd x && bun install --backend=copyfile");
301 expect(copying("bun install --backend=hardlink")).toBe("bun install --backend=hardlink");
302 expect(copying("npm ci")).toBe("npm ci");
303 });
304
305 test("a contained install reads the shared cache; an unfilled one keeps its own", () => {
306 const filled = cellInstall("bun install", { A: "1" }, "/c/bun");
307 expect(filled.command).toBe("bun install --backend=copyfile");
308 expect(filled.env).toEqual({ A: "1", BUN_INSTALL_CACHE_DIR: "/c/bun" });
309 expect(cellInstall("bun install", { A: "1" }, undefined)).toEqual({ command: "bun install", env: { A: "1" } });
310 });
311
312 test("a checkout with no lockfile fills nothing, says so, and is not a failure", async () => {
313 const dir = await scratch();
314 const lines: string[] = [];
315 const filled = await prefillInstallCache({ cache: join(dir, "cache"), dir, env: {}, log: (l) => lines.push(l), signal: never });
316 expect(filled).toEqual({ ok: true, value: false });
317 expect(lines.join("\n")).toContain("no bun lockfile");
318 await rm(dir, { recursive: true, force: true });
319 });
320
321 test("a fill needs only the pinned registry, and a warm cache needs none", async () => {
322 const reg = await registry();
323 try {
324 const co = await checkout(reg.url, reg.dir);
325 const cache = join(reg.dir, "cache");
326 const first = await prefillInstallCache({ cache, dir: co, env: envFor(reg.dir, reg.url), log: () => {}, signal: never });
327 expect(first).toEqual({ ok: true, value: true });
328 expect(reg.hits).toContain("/fixture-pkg/-/fixture-pkg-1.0.0.tgz");
329 reg.stop();
330 const again = await prefillInstallCache({
331 cache,
332 dir: co,
333 env: envFor(reg.dir, reg.url),
334 log: () => {},
335 signal: never,
336 });
337 expect(again).toEqual({ ok: true, value: true });
338 expect(existsSync(`${cache}.lock`)).toBe(false);
339 } finally {
340 reg.stop();
341 await rm(reg.dir, { recursive: true, force: true });
342 }
343 });
344
345 test("a fill's timeout is clamped under the lock's staleness", () => {
346 expect(fillTimeoutMs(undefined)).toBe(LOCK_STALE_MS / 2);
347 expect(fillTimeoutMs(LOCK_STALE_MS * 4)).toBe(LOCK_STALE_MS / 2);
348 expect(fillTimeoutMs(1000)).toBe(1000);
349 });
350
351 test("bun installs from a filled cache that is read-only, copying, with no registry", async () => {
352 const reg = await registry();
353 try {
354 const co = await checkout(reg.url, reg.dir);
355 const cache = join(reg.dir, "cache");
356 const filled = await prefillInstallCache({ cache, dir: co, env: envFor(reg.dir, reg.url), log: () => {}, signal: never });
357 expect(filled).toEqual({ ok: true, value: true });
358 reg.stop();
359 Bun.spawnSync(["chmod", "-R", "a-w", cache]);
360 const probe = join(cache, "probe");
361 let writable = true;
362 try {
363 writeFileSync(probe, "x");
364 } catch {
365 writable = false;
366 }
367 // (root ignores modes; the install is then merely run, not proven read-only)
368 if (process.getuid?.() !== 0) expect(writable).toBe(false);
369 const proc = Bun.spawn(["bun", "install", "--frozen-lockfile", "--backend=copyfile"], {
370 cwd: co,
371 env: { ...envFor(reg.dir, "http://127.0.0.1:1/"), BUN_INSTALL_CACHE_DIR: cache },
372 stdout: "pipe",
373 stderr: "pipe",
374 });
375 const out = (await new Response(proc.stdout).text()) + (await new Response(proc.stderr).text());
376 const code = await proc.exited;
377 Bun.spawnSync(["chmod", "-R", "u+w", cache]);
378 expect({ code, out }).toMatchObject({ code: 0 });
379 expect(existsSync(join(co, "node_modules", "fixture-pkg", "package.json"))).toBe(true);
380 } finally {
381 reg.stop();
382 await rm(reg.dir, { recursive: true, force: true });
383 }
384 });
385
386 test("a package the registry will not serve fails on the fetch, not the lockfile, and frees the lock", async () => {
387 const reg = await registry();
388 try {
389 const co = await checkout(reg.url, reg.dir);
390 reg.stop();
391 const lines: string[] = [];
392 const cache = join(reg.dir, "empty-cache");
393 const filled = await prefillInstallCache({ cache, dir: co, env: envFor(reg.dir, reg.url), log: (l) => lines.push(l), signal: never });
394 expect(filled.ok).toBe(false);
395 expect(lines.join("\n")).not.toMatch(/lockfile had changes|frozen/i);
396 expect(reg.hits.filter((h) => h.endsWith(".tgz"))).toEqual([]); // the tarball was asked for and never served
397 expect(existsSync(`${cache}.lock`)).toBe(false);
398 } finally {
399 await rm(reg.dir, { recursive: true, force: true });
400 }
401 });
402
403 test("the pull's own bunfig.toml and .npmrc do not choose where the forge fetches", async () => {
404 const reg = await registry();
405 const decoy = await registry();
406 try {
407 const co = await checkout(reg.url, reg.dir);
408 writeFileSync(join(co, "bunfig.toml"), `[install]\nregistry = "${decoy.url}"\n`);
409 writeFileSync(join(co, ".npmrc"), `registry=${decoy.url}\n`);
410 const filled = await prefillInstallCache({
411 cache: join(reg.dir, "cache"),
412 dir: co,
413 env: envFor(reg.dir, reg.url),
414 log: () => {},
415 signal: never,
416 });
417 expect(filled).toEqual({ ok: true, value: true });
418 expect(decoy.hits).toEqual([]);
419 expect(reg.hits.length).toBeGreaterThan(0);
420 } finally {
421 reg.stop();
422 decoy.stop();
423 await rm(reg.dir, { recursive: true, force: true });
424 await rm(decoy.dir, { recursive: true, force: true });
425 }
426 });
427
428 test("a lockfile naming another host, or a git dependency, is not filled from", async () => {
429 const reg = await registry();
430 try {
431 const co = await checkout(reg.url, reg.dir);
432 const cache = join(reg.dir, "cache");
433 const lock = join(co, "bun.lock");
434 const good = readFileSync(lock, "utf8");
435 for (const bad of [
436 `${good}\n// ["x", "http://169.254.169.254/x.tgz"]`,
437 `${good}\n// ["y", "y@github:someone/repo#abc"]`,
438 ]) {
439 writeFileSync(lock, bad);
440 const lines: string[] = [];
441 const filled = await prefillInstallCache({ cache, dir: co, env: envFor(reg.dir, reg.url), log: (l) => lines.push(l), signal: never });
442 expect(filled).toEqual({ ok: true, value: false });
443 expect(lines.join("\n")).toContain("cache not filled");
444 }
445 expect(reg.hits.filter((h) => h.endsWith(".tgz"))).toEqual([]);
446 expect(existsSync(cache)).toBe(false);
447 } finally {
448 reg.stop();
449 await rm(reg.dir, { recursive: true, force: true });
450 }
451 });
452 });
test/contain.test.ts +39
@@ -1077,3 +1077,42 @@ describe.skipIf(noCells)(
10771077 }, 240_000);
10781078 },
10791079 );
1080
1081 describe.skipIf(noCells)("cells: the shared install cache is read-only (#7543)", () => {
1082 test("a revealed cache under a hidden data dir is readable and a write into it fails", async () => {
1083 const data = await mkdtemp(join(tmpdir(), "forge-cell-cache-"));
1084 const cache = join(data, "check-install", "r", "bun");
1085 mkdirSync(cache, { recursive: true });
1086 writeFileSync(join(cache, "pkg"), "x");
1087 const dir = join(data, "..", `co-${Date.now()}`);
1088 mkdirSync(dir);
1089 const lines: string[] = [];
1090 const result = await runStep(
1091 "check",
1092 `cat ${cache}/pkg && (echo y > ${cache}/new && echo WROTE || echo REFUSED)`,
1093 {
1094 cwd: dir,
1095 env: childEnv({}),
1096 log: (line) => lines.push(line),
1097 signal: new AbortController().signal,
1098 timeoutMs: 60_000,
1099 cell: {
1100 dir,
1101 writable: [],
1102 home: join(dir, ".home"),
1103 hide: [data],
1104 reveal: [cache],
1105 egress: [],
1106 reach: [],
1107 limits: { memoryMiB: 1024, cpus: 2, diskMiB: 1024 },
1108 },
1109 },
1110 );
1111 expect(result.ok).toBe(true);
1112 expect(lines).toContain("x");
1113 expect(lines).toContain("REFUSED");
1114 expect(lines).not.toContain("WROTE");
1115 await rm(data, { recursive: true, force: true });
1116 await rm(dir, { recursive: true, force: true });
1117 }, 60_000);
1118 });
test/spawn-pipes.test.ts +1
@@ -33,6 +33,7 @@ const PIPING: Record<string, { sites: string[]; reason: string }> = {
3333 "src/release/devtools.ts": { sites: ["stderr"], reason: "stderr read for the DevTools address, then drained in a background loop; stdout ignored" },
3434 "src/release/boot.ts": { sites: ["stderr"], reason: "stderr read to its end in a background loop, awaited before the reason is read; stdout ignored" },
3535 "src/release/checkout.ts": { sites: ["stdout+stderr"], reason: "stdout and stderr both read via Response.text()" },
36 "src/checks/install.ts": { sites: ["stdout+stderr"], reason: "prefillInstallCache reads stdout and stderr to the end via Response.text() while the exit is awaited" },
3637 "src/cli/config.ts": { sites: ["stdout"], reason: "stdout awaited with the exit; stderr ignored" },
3738 };
3839

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs