Skip to content

Commit 30f4d63

Browse files
t3dotggclaude
andauthored
ci(web): releases fail when the built web app cannot boot (#25929)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3087e60 commit 30f4d63

4 files changed

Lines changed: 70 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@ jobs:
344344
- name: Test
345345
run: vp run --filter @t3tools/web test
346346

347+
# Browser tests run source modules; this boots the production chunk graph.
348+
- name: Boot the built web client
349+
run: |
350+
vp run --filter @t3tools/web build
351+
node apps/web/scripts/verify-built-boot.ts
352+
347353
# apps/server sets `fileParallelism: false`, so its test files run strictly
348354
# one at a time. Sharding spreads them over separate runners instead of
349355
# separate workers, so no two server test files ever share a machine and the

‎.github/workflows/release.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@ jobs:
344344
- name: Test
345345
run: vp run --filter @t3tools/web test
346346

347+
# Browser tests run source modules; this boots the production chunk graph.
348+
- name: Boot the built web client
349+
run: |
350+
vp run --filter @t3tools/web build
351+
node apps/web/scripts/verify-built-boot.ts
352+
347353
# Server test files run one at a time, so more small runners beat fewer big ones.
348354
test_server:
349355
name: Release server tests ${{ matrix.shard }}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// @effect-diagnostics nodeBuiltinImport:off - plain Node script CI runs against the built client.
2+
// Boots the built web client in headless Chromium and fails if it reaches showBootError.
3+
// Source-level browser tests never run the production chunk graph, where boot breaks hide.
4+
import * as NodeFS from "node:fs";
5+
import * as NodePath from "node:path";
6+
import * as NodeURL from "node:url";
7+
import { chromium } from "playwright";
8+
9+
const distDir = NodeURL.fileURLToPath(new URL("../dist/", import.meta.url));
10+
const origin = "http://t3-boot.test";
11+
// With no server, the client retries its session for about 15s before it renders.
12+
const timeoutMs = 60_000;
13+
14+
if (!NodeFS.existsSync(NodePath.join(distDir, "index.html"))) {
15+
console.error(`No web build at ${distDir}. Run the web build first.`);
16+
process.exit(1);
17+
}
18+
19+
const browser = await chromium.launch();
20+
try {
21+
const page = await browser.newPage();
22+
const errors: string[] = [];
23+
page.on("pageerror", (error) => errors.push(error.stack ?? error.message));
24+
page.on("console", (message) => {
25+
if (message.type() === "error") errors.push(message.text());
26+
});
27+
28+
// Serve dist with an SPA fallback; the client has no server here, so other requests fail fast.
29+
await page.route("**/*", (route) => {
30+
const url = new URL(route.request().url());
31+
if (url.origin !== origin) return route.abort();
32+
const file = NodePath.join(distDir, decodeURIComponent(url.pathname));
33+
const isAsset =
34+
file.startsWith(distDir) && NodeFS.statSync(file, { throwIfNoEntry: false })?.isFile();
35+
if (isAsset) return route.fulfill({ path: file });
36+
if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/.well-known/")) {
37+
return route.fulfill({ status: 503 });
38+
}
39+
return route.fulfill({ path: NodePath.join(distDir, "index.html") });
40+
});
41+
42+
await page.goto(`${origin}/`);
43+
// React replaces the static #boot-shell splash when it mounts; showBootError fills it instead.
44+
const settled = await page.waitForSelector("#root > :not(#boot-shell), #boot-error-reload", {
45+
timeout: timeoutMs,
46+
});
47+
48+
if (await settled.evaluate((node) => node.id === "boot-error-reload")) {
49+
console.error("The built web client failed to boot:");
50+
for (const error of errors) console.error(error);
51+
process.exitCode = 1;
52+
} else {
53+
console.log("The built web client booted and mounted React.");
54+
}
55+
} finally {
56+
await browser.close();
57+
}

‎apps/web/tsconfig.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"vite.config.ts",
3232
"vercel.ts",
3333
"test",
34+
"scripts/verify-built-boot.ts",
3435
"scripts/warm-dep-cache.ts",
3536
"../../scripts/lib/public-config.ts",
3637
"../../scripts/lib/third-party-licenses.ts"

0 commit comments

Comments
 (0)