authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-31 10:11:12+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-31 10:11:12+02:00
log50a3d6b872406bb15af72d439740dd63e89f820d
treee1bfc83cb1e8ee59324333c30059924f8cab3842
parente259dd23c7ef67a7f215389762a1ef5622c79088
signature Signed by PGP key B5690EEEBB952194

fix(ci): repair the mobile showcase screenshots workflow (#5057)


3 files changed, 72 insertions(+), 18 deletions(-)

.github/workflows/mobile-showcase-screenshots.yml+2
...@@ -52,6 +52,7 @@ jobs:...@@ -52,6 +52,7 @@ jobs:
52 args:52 args:
53 - --filter=@t3tools/mobile...53 - --filter=@t3tools/mobile...
54 - --filter=@t3tools/scripts...54 - --filter=@t3tools/scripts...
55 - --filter=t3...
5556
56 - name: Expose pnpm57 - name: Expose pnpm
57 run: |58 run: |
...@@ -100,6 +101,7 @@ jobs:...@@ -100,6 +101,7 @@ jobs:
100 args:101 args:
101 - --filter=@t3tools/mobile...102 - --filter=@t3tools/mobile...
102 - --filter=@t3tools/scripts...103 - --filter=@t3tools/scripts...
104 - --filter=t3...
103105
104 - name: Expose pnpm106 - name: Expose pnpm
105 run: |107 run: |
scripts/mobile-showcase-environment.ts+47-12
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off globalDate:off - This host-side fixture creates an isolated local T3 environment.1// @effect-diagnostics nodeBuiltinImport:off globalTimers:off globalDate:off - This host-side fixture creates an isolated local T3 environment.
2import * as NodeChildProcess from "node:child_process";2import * as NodeChildProcess from "node:child_process";
3import * as NodeFSP from "node:fs/promises";3import * as NodeFSP from "node:fs/promises";
4import * as NodePath from "node:path";4import * as NodePath from "node:path";
...@@ -387,6 +387,48 @@ function insertThread(...@@ -387,6 +387,48 @@ function insertThread(
387 .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt);387 .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt);
388}388}
389389
390const SEEDED_PROJECTION_TABLES = [
391 "projection_pending_approvals",
392 "projection_thread_proposed_plans",
393 "projection_thread_activities",
394 "projection_thread_messages",
395 "projection_thread_sessions",
396 "projection_turns",
397 "projection_threads",
398 "projection_projects",
399 "projection_state",
400] as const;
401
402function hasSeedableSchema(dbPath: string): boolean {
403 let database: NodeSqlite.DatabaseSync;
404 try {
405 database = new NodeSqlite.DatabaseSync(dbPath, { readOnly: true });
406 } catch {
407 return false;
408 }
409 try {
410 const row = database
411 .prepare(
412 `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`,
413 )
414 .get(...SEEDED_PROJECTION_TABLES) as { count: number };
415 return row.count === SEEDED_PROJECTION_TABLES.length;
416 } catch {
417 return false;
418 } finally {
419 database.close();
420 }
421}
422
423async function waitForSeedableSchema(dbPath: string, timeoutMs = 60_000): Promise<void> {
424 const deadline = Date.now() + timeoutMs;
425 while (Date.now() < deadline) {
426 if (hasSeedableSchema(dbPath)) return;
427 await new Promise((resolve) => setTimeout(resolve, 250));
428 }
429 throw new Error(`The environment server did not migrate ${dbPath} within ${timeoutMs}ms.`);
430}
431
390function seedDatabase(432function seedDatabase(
391 dbPath: string,433 dbPath: string,
392 workspaceRoots: ReadonlyMap<string, string>,434 workspaceRoots: ReadonlyMap<string, string>,
...@@ -401,17 +443,7 @@ function seedDatabase(...@@ -401,17 +443,7 @@ function seedDatabase(
401 const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 });443 const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 });
402 try {444 try {
403 database.exec("BEGIN IMMEDIATE");445 database.exec("BEGIN IMMEDIATE");
404 for (const table of [446 for (const table of SEEDED_PROJECTION_TABLES) {
405 "projection_pending_approvals",
406 "projection_thread_proposed_plans",
407 "projection_thread_activities",
408 "projection_thread_messages",
409 "projection_thread_sessions",
410 "projection_turns",
411 "projection_threads",
412 "projection_projects",
413 "projection_state",
414 ]) {
415 database.exec(`DELETE FROM ${table}`);447 database.exec(`DELETE FROM ${table}`);
416 }448 }
417 const insertProject = database.prepare(449 const insertProject = database.prepare(
...@@ -594,6 +626,9 @@ export async function seedShowcaseEnvironment(input: {...@@ -594,6 +626,9 @@ export async function seedShowcaseEnvironment(input: {
594 });626 });
595 }),627 }),
596 );628 );
629 // The environment server begins listening before it finishes migrating the
630 // database, so wait for the schema before deleting from and reseeding it.
631 await waitForSeedableSchema(dbPath);
597 seedDatabase(dbPath, workspaceRoots, projects, threads, now);632 seedDatabase(dbPath, workspaceRoots, projects, threads, now);
598633
599 const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals");634 const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals");
scripts/mobile-showcase.ts+23-6
...@@ -500,6 +500,23 @@ async function waitForPort(port: number, label = "Process", timeoutMs = 60_000):...@@ -500,6 +500,23 @@ async function waitForPort(port: number, label = "Process", timeoutMs = 60_000):
500 throw new Error(`${label} did not begin listening on port ${port} within ${timeoutMs}ms.`);500 throw new Error(`${label} did not begin listening on port ${port} within ${timeoutMs}ms.`);
501}501}
502502
503async function waitForFileContent(
504 filePath: string,
505 label: string,
506 timeoutMs = 60_000,
507): Promise<string> {
508 const deadline = Date.now() + timeoutMs;
509 while (Date.now() < deadline) {
510 const content = await NodeFSP.readFile(filePath, "utf8").then(
511 (value) => value.trim(),
512 () => "",
513 );
514 if (content) return content;
515 await delay(250);
516 }
517 throw new Error(`${label} was not written to ${filePath} within ${timeoutMs}ms.`);
518}
519
503async function reserveAvailablePort(): Promise<number> {520async function reserveAvailablePort(): Promise<number> {
504 return await new Promise<number>((resolve, reject) => {521 return await new Promise<number>((resolve, reject) => {
505 const server = NodeNet.createServer();522 const server = NodeNet.createServer();
...@@ -1225,12 +1242,12 @@ async function main(): Promise<void> {...@@ -1225,12 +1242,12 @@ async function main(): Promise<void> {
1225 showcaseServers.push(server);1242 showcaseServers.push(server);
1226 await waitForPort(port, `${environment.label} server`);1243 await waitForPort(port, `${environment.label} server`);
1227 await seedShowcaseEnvironment({ baseDir, projectIds: environment.projectIds });1244 await seedShowcaseEnvironment({ baseDir, projectIds: environment.projectIds });
1228 const environmentId = (1245 // The server begins listening before the ServerEnvironment layer
1229 await NodeFSP.readFile(NodePath.join(baseDir, "userdata", "environment-id"), "utf8")1246 // persists the environment id, so poll rather than read once.
1230 ).trim();1247 const environmentId = await waitForFileContent(
1231 if (!environmentId) {1248 NodePath.join(baseDir, "userdata", "environment-id"),
1232 throw new Error(`${environment.label} did not persist an environment id.`);1249 `${environment.label} environment id`,
1233 }1250 );
1234 showcaseEnvironments.push({ baseDir, environmentId, label: environment.label, port });1251 showcaseEnvironments.push({ baseDir, environmentId, label: environment.label, port });
1235 }1252 }
12361253