From 2984cb62f2dc8c7701680fbfc73a6c8d56ed2f0a Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:46:13 -0400 Subject: [PATCH] ci(site): start Chrome in the smoke test without depending on its stderr The deploy after #73 failed before publishing: the smoke test waited 20s for Chrome to print its DevTools address on stderr, and on that runner it printed nothing at all. The same test passed on the pull request, so this was the start-up, not the site. The test now picks a free port itself and polls Chrome's /json/version until it answers, up to a minute per attempt, and starts a fresh Chrome up to three times if one stalls or exits, printing what happened. It also passes --disable-dev-shm-usage, the usual guard for a small /dev/shm on CI machines. Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/smoke_site.mjs | 81 ++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/scripts/smoke_site.mjs b/scripts/smoke_site.mjs index f5112a1..8a46e0d 100644 --- a/scripts/smoke_site.mjs +++ b/scripts/smoke_site.mjs @@ -91,31 +91,66 @@ if (!chromePath) { process.exit(2); } -const profile = mkdtempSync(path.join(tmpdir(), "plumbline-smoke-")); -const chrome = spawn(chromePath, [ - "--headless=new", - "--remote-debugging-port=0", - `--user-data-dir=${profile}`, - "--no-first-run", - "--no-default-browser-check", - "--disable-gpu", - "--window-size=1280,900", - "about:blank", -], { stdio: ["ignore", "ignore", "pipe"] }); +// A free port, chosen here rather than by Chrome, so the DevTools address is +// known without reading Chrome's stderr (which a cold start on a CI runner has +// been seen to leave empty for longer than any sensible wait). +async function freePort() { + const probe = createServer(); + await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve)); + const { port } = probe.address(); + await new Promise((resolve) => probe.close(resolve)); + return port; +} -const browserUrl = await new Promise((resolve, reject) => { - let seen = ""; - const timer = setTimeout(() => reject(new Error(`Chrome did not report a DevTools address:\n${seen}`)), 20000); - chrome.stderr.on("data", (chunk) => { - seen += chunk; - const match = /DevTools listening on (ws:\/\/\S+)/.exec(seen); - if (match) { - clearTimeout(timer); - resolve(match[1]); +// Starts Chrome and waits until its DevTools endpoint answers. A cold start is +// slow and occasionally stalls, so each attempt waits up to a minute and a +// stalled Chrome is killed and started again, three times at most. +async function launchChrome() { + let last = ""; + for (let attempt = 1; attempt <= 3; attempt++) { + const port = await freePort(); + const profile = mkdtempSync(path.join(tmpdir(), "plumbline-smoke-")); + const child = spawn(chromePath, [ + "--headless=new", + `--remote-debugging-port=${port}`, + `--user-data-dir=${profile}`, + "--no-first-run", + "--no-default-browser-check", + "--disable-gpu", + "--disable-dev-shm-usage", + "--window-size=1280,900", + "about:blank", + ], { stdio: ["ignore", "ignore", "pipe"] }); + let stderr = ""; + let exited = null; + child.stderr.on("data", (chunk) => { stderr += chunk; }); + child.on("exit", (code) => { exited = code; }); + const deadline = Date.now() + 60000; + while (Date.now() < deadline && exited === null) { + try { + const response = await fetch(`http://127.0.0.1:${port}/json/version`); + if (response.ok) { + const { webSocketDebuggerUrl } = await response.json(); + return { child, profile, browserUrl: webSocketDebuggerUrl }; + } + } catch { + // Not listening yet. + } + await new Promise((resolve) => setTimeout(resolve, 250)); } - }); - chrome.on("exit", (code) => reject(new Error(`Chrome exited with ${code}:\n${seen}`))); -}); + last = exited === null ? "did not answer within 60s" : `exited with ${exited}`; + console.log(` Chrome attempt ${attempt} ${last}; ${attempt < 3 ? "starting another" : "giving up"}`); + child.kill(); + try { + rmSync(profile, { recursive: true, force: true }); + } catch { + // Held briefly after exit; the OS cleans temp. + } + if (attempt === 3) throw new Error(`Chrome ${last}:\n${stderr}`); + } +} + +const { child: chrome, profile, browserUrl } = await launchChrome(); // ---- a minimal DevTools client ---------------------------------------------