From cf3855e10b9165800574f8eec92c303779d697da Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 31 Jul 2026 01:27:13 +0200 Subject: [PATCH 1/3] fix(ci): install server deps in mobile showcase workflow The filtered install added in #4802 only covers @t3tools/mobile... and @t3tools/scripts..., but scripts/mobile-showcase.ts boots the T3 server from apps/server/src/bin.ts (package name "t3"). Its dependencies were never installed, so the server crashed on startup with ERR_MODULE_NOT_FOUND for @effect/platform-node and every capture job timed out waiting for the server port. Add --filter=t3... to both jobs. Co-Authored-By: Claude Fable 5 --- .github/workflows/mobile-showcase-screenshots.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/mobile-showcase-screenshots.yml b/.github/workflows/mobile-showcase-screenshots.yml index 0aa9f30a2ff..3eaaf508e31 100644 --- a/.github/workflows/mobile-showcase-screenshots.yml +++ b/.github/workflows/mobile-showcase-screenshots.yml @@ -52,6 +52,7 @@ jobs: args: - --filter=@t3tools/mobile... - --filter=@t3tools/scripts... + - --filter=t3... - name: Expose pnpm run: | @@ -100,6 +101,7 @@ jobs: args: - --filter=@t3tools/mobile... - --filter=@t3tools/scripts... + - --filter=t3... - name: Expose pnpm run: | From 62648685af8924a62f7819343d7e60a551076ff3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 31 Jul 2026 01:47:43 +0200 Subject: [PATCH 2/3] fix(scripts): poll for environment-id instead of one-shot read The showcase server starts accepting connections before its ServerEnvironment layer persists userdata/environment-id, so reading the file immediately after waitForPort races server startup. The fast macOS runner lost that race (ENOENT); poll for non-empty content with the same timeout discipline as waitForPort. Co-Authored-By: Claude Fable 5 --- scripts/mobile-showcase.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/mobile-showcase.ts b/scripts/mobile-showcase.ts index 35d6ffd5202..56bb37cc061 100644 --- a/scripts/mobile-showcase.ts +++ b/scripts/mobile-showcase.ts @@ -500,6 +500,23 @@ async function waitForPort(port: number, label = "Process", timeoutMs = 60_000): throw new Error(`${label} did not begin listening on port ${port} within ${timeoutMs}ms.`); } +async function waitForFileContent( + filePath: string, + label: string, + timeoutMs = 60_000, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const content = await NodeFSP.readFile(filePath, "utf8").then( + (value) => value.trim(), + () => "", + ); + if (content) return content; + await delay(250); + } + throw new Error(`${label} was not written to ${filePath} within ${timeoutMs}ms.`); +} + async function reserveAvailablePort(): Promise { return await new Promise((resolve, reject) => { const server = NodeNet.createServer(); @@ -1225,12 +1242,12 @@ async function main(): Promise { showcaseServers.push(server); await waitForPort(port, `${environment.label} server`); await seedShowcaseEnvironment({ baseDir, projectIds: environment.projectIds }); - const environmentId = ( - await NodeFSP.readFile(NodePath.join(baseDir, "userdata", "environment-id"), "utf8") - ).trim(); - if (!environmentId) { - throw new Error(`${environment.label} did not persist an environment id.`); - } + // The server begins listening before the ServerEnvironment layer + // persists the environment id, so poll rather than read once. + const environmentId = await waitForFileContent( + NodePath.join(baseDir, "userdata", "environment-id"), + `${environment.label} environment id`, + ); showcaseEnvironments.push({ baseDir, environmentId, label: environment.label, port }); } From 0465c85970e537a51b4be67c81051d10dea5b499 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 31 Jul 2026 02:07:17 +0200 Subject: [PATCH 3/3] fix(scripts): wait for server migrations before reseeding the showcase db MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seedDatabase assumed the projection tables existed as soon as the environment server's port opened, but the server starts listening before its database bootstrap finishes — on fast runners the seed hit 'no such table: projection_pending_approvals'. Poll sqlite_master for the seeded tables before deleting from them. Co-Authored-By: Claude Fable 5 --- scripts/mobile-showcase-environment.ts | 59 ++++++++++++++++++++------ 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/scripts/mobile-showcase-environment.ts b/scripts/mobile-showcase-environment.ts index 9c04c7e9dd1..f7854675351 100644 --- a/scripts/mobile-showcase-environment.ts +++ b/scripts/mobile-showcase-environment.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics nodeBuiltinImport:off globalDate:off - This host-side fixture creates an isolated local T3 environment. +// @effect-diagnostics nodeBuiltinImport:off globalTimers:off globalDate:off - This host-side fixture creates an isolated local T3 environment. import * as NodeChildProcess from "node:child_process"; import * as NodeFSP from "node:fs/promises"; import * as NodePath from "node:path"; @@ -387,6 +387,48 @@ function insertThread( .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt); } +const SEEDED_PROJECTION_TABLES = [ + "projection_pending_approvals", + "projection_thread_proposed_plans", + "projection_thread_activities", + "projection_thread_messages", + "projection_thread_sessions", + "projection_turns", + "projection_threads", + "projection_projects", + "projection_state", +] as const; + +function hasSeedableSchema(dbPath: string): boolean { + let database: NodeSqlite.DatabaseSync; + try { + database = new NodeSqlite.DatabaseSync(dbPath, { readOnly: true }); + } catch { + return false; + } + try { + const row = database + .prepare( + `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`, + ) + .get(...SEEDED_PROJECTION_TABLES) as { count: number }; + return row.count === SEEDED_PROJECTION_TABLES.length; + } catch { + return false; + } finally { + database.close(); + } +} + +async function waitForSeedableSchema(dbPath: string, timeoutMs = 60_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (hasSeedableSchema(dbPath)) return; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`The environment server did not migrate ${dbPath} within ${timeoutMs}ms.`); +} + function seedDatabase( dbPath: string, workspaceRoots: ReadonlyMap, @@ -401,17 +443,7 @@ function seedDatabase( const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 }); try { database.exec("BEGIN IMMEDIATE"); - for (const table of [ - "projection_pending_approvals", - "projection_thread_proposed_plans", - "projection_thread_activities", - "projection_thread_messages", - "projection_thread_sessions", - "projection_turns", - "projection_threads", - "projection_projects", - "projection_state", - ]) { + for (const table of SEEDED_PROJECTION_TABLES) { database.exec(`DELETE FROM ${table}`); } const insertProject = database.prepare( @@ -594,6 +626,9 @@ export async function seedShowcaseEnvironment(input: { }); }), ); + // The environment server begins listening before it finishes migrating the + // database, so wait for the schema before deleting from and reseeding it. + await waitForSeedableSchema(dbPath); seedDatabase(dbPath, workspaceRoots, projects, threads, now); const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals");