@runa-ai/runa-cli 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/{build-HUDIP6KU.js → build-HQMSVN6N.js} +3 -3
  2. package/dist/{check-LOMVIRHX.js → check-PCSQPYDM.js} +2 -2
  3. package/dist/{chunk-QM53IQHM.js → chunk-2QX7T24B.js} +1 -1
  4. package/dist/{chunk-CCW3PLQY.js → chunk-3JO6YP3T.js} +1 -1
  5. package/dist/{chunk-XDCHRVE3.js → chunk-4XHZQRRK.js} +2 -2
  6. package/dist/{chunk-7B5C6U2K.js → chunk-A6A7JIRD.js} +35 -2
  7. package/dist/{chunk-AFY3TX4I.js → chunk-AO554K3G.js} +1 -1
  8. package/dist/{chunk-Z4Z5DNW4.js → chunk-B3POLMII.js} +12 -0
  9. package/dist/chunk-CKRLVEIO.js +119 -0
  10. package/dist/{chunk-HD74F6W2.js → chunk-FWMGC5FP.js} +1 -0
  11. package/dist/{chunk-FHG3ILE4.js → chunk-OBYZDT2E.js} +38 -8
  12. package/dist/{chunk-H2AHNI75.js → chunk-PAWNJA3N.js} +1 -1
  13. package/dist/{chunk-VM3IWOT5.js → chunk-QSEF4T3Y.js} +13 -5
  14. package/dist/{chunk-NPSRD26F.js → chunk-UHDAYPHH.js} +1 -1
  15. package/dist/{chunk-2APB25TT.js → chunk-VSH3IXDQ.js} +7 -3
  16. package/dist/{chunk-644FVGIQ.js → chunk-WPMR7RQ4.js} +9 -2
  17. package/dist/{chunk-EMB6IZFT.js → chunk-XVNDDHAF.js} +20 -1
  18. package/dist/{risk-detector-plpgsql-HWKS4OLR.js → chunk-Y5ANTCKE.js} +3 -412
  19. package/dist/{ci-XY6IKEDC.js → ci-Z4525QW6.js} +2150 -488
  20. package/dist/{cli-UZA4RBNQ.js → cli-SVXOSMW6.js} +72 -54
  21. package/dist/commands/ci/commands/ci-prod-db-operations.d.ts +6 -4
  22. package/dist/commands/ci/commands/ci-prod-types.d.ts +3 -0
  23. package/dist/commands/ci/commands/ci-prod-workflow.d.ts +1 -1
  24. package/dist/commands/ci/commands/ci-resolvers.d.ts +1 -1
  25. package/dist/commands/ci/commands/ci-supabase-local.d.ts +4 -0
  26. package/dist/commands/ci/machine/actors/build/build-and-playwright.d.ts +1 -1
  27. package/dist/commands/ci/machine/actors/db/collect-schema-stats.d.ts +11 -1
  28. package/dist/commands/ci/machine/actors/db/production-preview.d.ts +22 -4
  29. package/dist/commands/ci/machine/actors/db/schema-canonical-diff.d.ts +8 -1
  30. package/dist/commands/ci/machine/actors/db/sync-schema.d.ts +1 -0
  31. package/dist/commands/ci/machine/actors/finalize/index.d.ts +0 -1
  32. package/dist/commands/ci/machine/actors/index.d.ts +1 -1
  33. package/dist/commands/ci/machine/actors/setup/local.d.ts +2 -0
  34. package/dist/commands/ci/machine/actors/setup/pr-common.d.ts +3 -0
  35. package/dist/commands/ci/machine/actors/setup/pr-local.d.ts +2 -0
  36. package/dist/commands/ci/machine/commands/machine-runner.d.ts +5 -1
  37. package/dist/commands/ci/machine/commands/step-telemetry.d.ts +16 -0
  38. package/dist/commands/ci/machine/contract.d.ts +40 -0
  39. package/dist/commands/ci/machine/formatters/github-comment-types.d.ts +7 -2
  40. package/dist/commands/ci/machine/formatters/github-comment.d.ts +2 -1
  41. package/dist/commands/ci/machine/formatters/sections/final-comment.d.ts +2 -1
  42. package/dist/commands/ci/machine/formatters/sections/index.d.ts +1 -1
  43. package/dist/commands/ci/machine/formatters/summary.d.ts +4 -4
  44. package/dist/commands/ci/machine/guards.d.ts +4 -0
  45. package/dist/commands/ci/machine/helpers.d.ts +25 -0
  46. package/dist/commands/ci/machine/machine-state-helpers.d.ts +1 -1
  47. package/dist/commands/ci/machine/machine.d.ts +15 -8
  48. package/dist/commands/ci/machine/types.d.ts +9 -0
  49. package/dist/commands/ci/utils/ci-diagnostics.d.ts +67 -0
  50. package/dist/commands/ci/utils/ci-summary.d.ts +118 -0
  51. package/dist/commands/ci/utils/db-url-utils.d.ts +4 -77
  52. package/dist/commands/ci/utils/github-api.d.ts +14 -0
  53. package/dist/commands/db/apply/contract.d.ts +73 -0
  54. package/dist/commands/db/apply/helpers/alter-statement-parsers.d.ts +95 -0
  55. package/dist/commands/db/apply/helpers/data-compatibility-checker.d.ts +0 -61
  56. package/dist/commands/db/apply/helpers/function-plan-false-positive-filter.d.ts +36 -0
  57. package/dist/commands/db/apply/helpers/hazard-handler.d.ts +4 -4
  58. package/dist/commands/db/apply/helpers/index.d.ts +14 -5
  59. package/dist/commands/db/apply/helpers/partition-acl-cleaner.d.ts +3 -1
  60. package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts +69 -6
  61. package/dist/commands/db/apply/helpers/plan-ast.d.ts +56 -0
  62. package/dist/commands/db/apply/helpers/plan-check-filter.d.ts +26 -0
  63. package/dist/commands/db/apply/helpers/plan-drop-protection.d.ts +43 -0
  64. package/dist/commands/db/apply/helpers/plan-ordering.d.ts +6 -0
  65. package/dist/commands/db/apply/helpers/plan-statement-parser.d.ts +39 -0
  66. package/dist/commands/db/apply/helpers/plan-validator.d.ts +8 -40
  67. package/dist/commands/db/apply/helpers/retry-logic.d.ts +1 -10
  68. package/dist/commands/db/apply/helpers/temp-db-bootstrap.d.ts +18 -0
  69. package/dist/commands/db/apply/helpers/temp-db-dsn.d.ts +14 -0
  70. package/dist/commands/db/apply/machine.d.ts +56 -32
  71. package/dist/commands/db/commands/db-apply-error.d.ts +5 -0
  72. package/dist/commands/db/commands/db-apply.d.ts +2 -0
  73. package/dist/commands/db/commands/db-sync/directory-placement-check.d.ts +4 -0
  74. package/dist/commands/db/commands/db-sync/error-classifier.d.ts +1 -1
  75. package/dist/commands/db/commands/db-sync/plan-boundary-reconciliation.d.ts +3 -0
  76. package/dist/commands/db/commands/db-sync/precheck-helpers.d.ts +18 -0
  77. package/dist/commands/db/commands/db-sync/production-precheck.d.ts +15 -0
  78. package/dist/commands/db/commands/db-sync/risk-scan-collectors.d.ts +11 -0
  79. package/dist/commands/db/commands/db-sync.d.ts +11 -5
  80. package/dist/commands/db/sync/contract.d.ts +80 -0
  81. package/dist/commands/db/sync/machine.d.ts +60 -1
  82. package/dist/commands/db/types.d.ts +5 -0
  83. package/dist/commands/db/utils/boundary-policy/rule-compiler.d.ts +2 -1
  84. package/dist/commands/db/utils/boundary-policy/types.d.ts +23 -0
  85. package/dist/commands/db/utils/boundary-policy-runtime.d.ts +12 -3
  86. package/dist/commands/db/utils/boundary-policy.d.ts +1 -1
  87. package/dist/commands/db/utils/db-target.d.ts +5 -3
  88. package/dist/commands/db/utils/declarative-dependency-collectors.d.ts +6 -0
  89. package/dist/commands/db/utils/declarative-dependency-contract.d.ts +78 -0
  90. package/dist/commands/db/utils/declarative-dependency-sql-utils.d.ts +49 -0
  91. package/dist/commands/db/utils/declarative-dependency-warning-governance.d.ts +24 -0
  92. package/dist/commands/db/utils/preflight-check.d.ts +1 -1
  93. package/dist/commands/db/utils/preflight-checks/declarative-dependency-checks.d.ts +4 -0
  94. package/dist/commands/db/utils/preflight-checks/idempotent-risk-checks.d.ts +4 -0
  95. package/dist/commands/db/utils/preflight-checks/schema-boundary-checks.d.ts +4 -0
  96. package/dist/commands/db/utils/preflight-checks/schema-risk-policy.d.ts +4 -0
  97. package/dist/commands/db/utils/preflight-checks/supabase-checks.d.ts +12 -0
  98. package/dist/commands/db/utils/psql.d.ts +23 -0
  99. package/dist/commands/db/utils/sql-table-extractor.d.ts +42 -1
  100. package/dist/commands/env/commands/setup/types.d.ts +1 -0
  101. package/dist/commands/env/constants/local-supabase.d.ts +4 -1
  102. package/dist/commands/observability.d.ts +72 -0
  103. package/dist/commands/observability.helpers.d.ts +25 -0
  104. package/dist/commands/template-check/contract.d.ts +3 -3
  105. package/dist/commands/template-check/machine.d.ts +1 -1
  106. package/dist/commands/workflow/commands/deploy-production.d.ts +0 -1
  107. package/dist/constants/versions.d.ts +1 -1
  108. package/dist/{db-Q3GF7JWP.js → db-S4V4ETDR.js} +14629 -11270
  109. package/dist/{dev-5YXNPTCJ.js → dev-MLRKIP7F.js} +5 -5
  110. package/dist/{doctor-MZLOA53G.js → doctor-ROSWSMLH.js} +2 -2
  111. package/dist/{env-GMB3THRG.js → env-WNHJVLOT.js} +37 -20
  112. package/dist/{env-HMMRSYCI.js → env-XPPACZM4.js} +2 -2
  113. package/dist/{env-files-2UIUYLLR.js → env-files-HRNUGZ5O.js} +1 -1
  114. package/dist/{error-handler-HEXBRNVV.js → error-handler-YRQWRDEF.js} +17 -0
  115. package/dist/{hotfix-NDTPY2T4.js → hotfix-Z5EGVSMH.js} +4 -4
  116. package/dist/index.js +4 -4
  117. package/dist/{init-U4VCRHTD.js → init-35JLDFHI.js} +1 -1
  118. package/dist/{inject-test-attrs-P44BVTQS.js → inject-test-attrs-XN4I2AOR.js} +2 -2
  119. package/dist/internal/machines/index.d.ts +1 -1
  120. package/dist/internal/machines/snapshot-helpers.d.ts +6 -0
  121. package/dist/{manifest-TMFLESHW.js → manifest-EGCAZ4TK.js} +1 -1
  122. package/dist/observability-CJA5UFIC.js +721 -0
  123. package/dist/{risk-detector-4U6ZJ2G5.js → risk-detector-S7XQF4I2.js} +1 -1
  124. package/dist/{risk-detector-core-TK4OAI3N.js → risk-detector-core-TGFKWHRS.js} +61 -3
  125. package/dist/risk-detector-plpgsql-O32TUR34.js +736 -0
  126. package/dist/{template-check-FFJVDLBF.js → template-check-BDFMT6ZO.js} +1 -1
  127. package/dist/{upgrade-7TWORWBV.js → upgrade-7L4JIE4K.js} +1 -1
  128. package/dist/utils/db-url-utils.d.ts +81 -0
  129. package/dist/validators/risk-detector-plpgsql.d.ts +3 -1
  130. package/dist/{vuln-check-6CMNPSBR.js → vuln-check-D575VXIQ.js} +1 -1
  131. package/dist/{vuln-checker-EJJTNDNE.js → vuln-checker-QV6XODTJ.js} +1 -1
  132. package/dist/{watch-PNTKZYFB.js → watch-AL4LCBRM.js} +1 -1
  133. package/dist/{workflow-H75N4BXX.js → workflow-UZIZ2JUS.js} +2 -3
  134. package/package.json +3 -3
  135. package/dist/chunk-AKZAN4BC.js +0 -90
  136. package/dist/commands/ci/machine/actors/finalize/summary.d.ts +0 -32
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
- import { normalizeDatabaseUrlForDdl, parseBoolish, enhanceConnectionError, detectAppSchemas, formatSchemasForSql } from './chunk-XDCHRVE3.js';
3
+ import { normalizeDatabaseUrlForDdl, parseBoolish, enhanceConnectionError, detectAppSchemas, formatSchemasForSql } from './chunk-4XHZQRRK.js';
4
4
  import { isPathContained } from './chunk-DRSUEMAK.js';
5
5
  import './chunk-QDF7QXBL.js';
6
- import { getSnapshotStateName, isSnapshotComplete } from './chunk-EMB6IZFT.js';
7
- import { writeEnvLocal, startAppBackground, waitForAppReady, executePrSetupBase, createErrorOutput } from './chunk-HD74F6W2.js';
8
- import { parsePostgresUrl, buildPsqlArgs, buildPsqlEnv, psqlSyncQuery } from './chunk-7B5C6U2K.js';
6
+ import { getSnapshotStateName, getSnapshotStatePaths, isSnapshotComplete } from './chunk-XVNDDHAF.js';
7
+ import { writeEnvLocal, startAppBackground, waitForAppReady, executePrSetupBase, createErrorOutput } from './chunk-FWMGC5FP.js';
8
+ import { parsePostgresUrl, buildPsqlArgs, buildPsqlEnv, psqlSyncQuery } from './chunk-A6A7JIRD.js';
9
9
  import { ensureRunaTmpDir, runLogged } from './chunk-6FAU4IGR.js';
10
10
  import { createMachineStateChangeLogger } from './chunk-5FT3F36G.js';
11
11
  import { getSafeEnv, getFilteredEnv, redactSecrets } from './chunk-II7VYQEM.js';
12
- import { init_constants, detectSupabasePorts } from './chunk-VM3IWOT5.js';
12
+ import { init_constants, init_local_supabase, detectLocalSupabasePorts } from './chunk-QSEF4T3Y.js';
13
13
  import { emitJsonSuccess } from './chunk-KE6QJBZG.js';
14
14
  import './chunk-WJXC4MVY.js';
15
15
  import { setOutputFormat } from './chunk-HKUWEGUX.js';
@@ -19,9 +19,9 @@ import { Command } from 'commander';
19
19
  import { spawnSync, spawn, execFileSync } from 'child_process';
20
20
  import { mkdir, writeFile, appendFile, readFile } from 'fs/promises';
21
21
  import path4, { join } from 'path';
22
- import { createCLILogger, CLIError, syncFromProduction, formatDuration, GITHUB_API, getClassificationForProfile, getStatusIcon, detectDatabasePackage, DATABASE_PACKAGE_CANDIDATES } from '@runa-ai/runa';
22
+ import { CommandOutcomeSchema, createCLILogger, CLIError, syncFromProduction, formatDuration, GITHUB_API, getClassificationForProfile, isTimeoutLikeMessage, buildCommandOutcomeSummary, deriveCommandExitMode, getStatusIcon, detectDatabasePackage, DATABASE_PACKAGE_CANDIDATES } from '@runa-ai/runa';
23
23
  import { z } from 'zod';
24
- import { existsSync, readFileSync, readdirSync, promises, lstatSync, statSync } from 'fs';
24
+ import { existsSync, createWriteStream, readFileSync, readdirSync, statSync, promises, lstatSync } from 'fs';
25
25
  import { resolve4 } from 'dns/promises';
26
26
  import net, { isIP } from 'net';
27
27
  import { fromPromise, setup, assign, createActor } from 'xstate';
@@ -344,9 +344,51 @@ function logNextActions(items) {
344
344
 
345
345
  // src/commands/ci/utils/ci-summary.ts
346
346
  init_esm_shims();
347
+
348
+ // src/commands/ci/utils/ci-diagnostics.ts
349
+ init_esm_shims();
350
+ var ExecutionOwnerSchema = z.enum(["sdk", "external"]);
351
+ var SupabaseResolutionStrategySchema = z.enum([
352
+ "assume-ready",
353
+ "status-polling",
354
+ "fallback-default"
355
+ ]);
356
+ var SupabaseResolutionSourceSchema = z.enum([
357
+ "assumed-local-config",
358
+ "status-json",
359
+ "default-fallback"
360
+ ]);
361
+ var SupabaseResolutionDiagnosticsSchema = z.object({
362
+ strategy: SupabaseResolutionStrategySchema,
363
+ finalSource: SupabaseResolutionSourceSchema,
364
+ attempts: z.number().int().nonnegative(),
365
+ maxAttempts: z.number().int().positive(),
366
+ sleepSeconds: z.number().int().nonnegative(),
367
+ waitedMs: z.number().int().nonnegative(),
368
+ retriesExhausted: z.boolean().optional(),
369
+ lastError: z.string().optional()
370
+ }).strict();
371
+ var CiDiagnosticsSchema = z.object({
372
+ setup: z.object({
373
+ runtimeOwner: ExecutionOwnerSchema.optional(),
374
+ playwrightOwner: ExecutionOwnerSchema.optional(),
375
+ supabase: SupabaseResolutionDiagnosticsSchema.optional()
376
+ }).strict().optional()
377
+ }).strict();
378
+
379
+ // src/commands/ci/utils/ci-summary.ts
347
380
  var CiLayerStatusSchema = z.enum(["passed", "failed", "skipped", "killed", "timeout"]);
348
381
  var LayerBlockingLevelSchema = z.enum(["blocking", "warning", "reportOnly"]);
349
382
  var CiStepStatusSchema = z.enum(["passed", "failed", "skipped", "killed", "timeout"]);
383
+ var CiStepPhaseSchema = z.enum([
384
+ "setup",
385
+ "github",
386
+ "db",
387
+ "observability",
388
+ "build",
389
+ "test",
390
+ "finalize"
391
+ ]);
350
392
  var CiSummarySchema = z.object({
351
393
  version: z.string(),
352
394
  mode: z.enum(["github-actions", "local"]),
@@ -357,12 +399,22 @@ var CiSummarySchema = z.object({
357
399
  durationMs: z.number().int().nonnegative(),
358
400
  repoKind: z.enum(["monorepo", "pj-repo", "unknown"]),
359
401
  detected: z.record(z.string(), z.unknown()),
402
+ diagnostics: CiDiagnosticsSchema.default({}),
360
403
  steps: z.record(
361
404
  z.string(),
362
405
  z.object({
363
406
  status: CiStepStatusSchema,
364
407
  exitCode: z.number().int().optional(),
365
- logPath: z.string().optional()
408
+ logPath: z.string().optional(),
409
+ error: z.string().optional(),
410
+ reason: z.string().optional(),
411
+ title: z.string().optional(),
412
+ phase: CiStepPhaseSchema.optional(),
413
+ parentStep: z.string().optional(),
414
+ optional: z.boolean().optional(),
415
+ startedAt: z.string().optional(),
416
+ endedAt: z.string().optional(),
417
+ durationMs: z.number().int().nonnegative().optional()
366
418
  }).strict()
367
419
  ).default({}),
368
420
  layers: z.record(
@@ -389,6 +441,7 @@ var CiSummarySchema = z.object({
389
441
  details: z.string().optional()
390
442
  }).strict()
391
443
  ).default([]),
444
+ dbOutcome: CommandOutcomeSchema.optional(),
392
445
  /** Blocking policy metadata for DB deploy gating */
393
446
  blockingPolicy: z.object({
394
447
  /** CI profile (runa-strict or pj-stable) */
@@ -517,9 +570,8 @@ var IssueCommentSchema = z.object({
517
570
  id: z.number().int(),
518
571
  body: z.string().nullable()
519
572
  }).passthrough();
520
- async function upsertIssueComment(params) {
573
+ async function findIssueCommentByMarker(params) {
521
574
  let page = 1;
522
- let existingId = null;
523
575
  while (true) {
524
576
  const raw = await githubRequest({
525
577
  method: "GET",
@@ -530,28 +582,57 @@ async function upsertIssueComment(params) {
530
582
  for (const c of arr) {
531
583
  const body = c.body ?? "";
532
584
  if (body.includes(params.marker)) {
533
- existingId = c.id;
534
- break;
585
+ return { id: c.id, body };
535
586
  }
536
587
  }
537
- if (existingId) break;
538
588
  if (arr.length < 100) break;
539
589
  page += 1;
540
590
  if (page > 20) break;
541
591
  }
542
- if (existingId) {
592
+ return null;
593
+ }
594
+ var commentIdCache = /* @__PURE__ */ new Map();
595
+ var upsertChains = /* @__PURE__ */ new Map();
596
+ function commentCacheKey(params) {
597
+ return `${params.repo.owner}/${params.repo.repo}#${params.issueNumber}:${params.marker}`;
598
+ }
599
+ async function upsertIssueComment(params) {
600
+ const key = commentCacheKey(params);
601
+ const previous = upsertChains.get(key) ?? Promise.resolve();
602
+ const current = previous.catch(() => {
603
+ }).then(() => doUpsertIssueComment(params, key));
604
+ upsertChains.set(key, current);
605
+ return current;
606
+ }
607
+ async function doUpsertIssueComment(params, key) {
608
+ const cachedId = commentIdCache.get(key);
609
+ if (cachedId) {
543
610
  await githubRequest({
544
611
  method: "PATCH",
545
- path: `/repos/${params.repo.owner}/${params.repo.repo}/issues/comments/${existingId}`,
612
+ path: `/repos/${params.repo.owner}/${params.repo.repo}/issues/comments/${cachedId}`,
546
613
  body: { body: params.body }
547
614
  });
548
615
  return;
549
616
  }
550
- await githubRequest({
617
+ const existing = await findIssueCommentByMarker(params);
618
+ if (existing) {
619
+ commentIdCache.set(key, existing.id);
620
+ await githubRequest({
621
+ method: "PATCH",
622
+ path: `/repos/${params.repo.owner}/${params.repo.repo}/issues/comments/${existing.id}`,
623
+ body: { body: params.body }
624
+ });
625
+ return;
626
+ }
627
+ const result = await githubRequest({
551
628
  method: "POST",
552
629
  path: `/repos/${params.repo.owner}/${params.repo.repo}/issues/${params.issueNumber}/comments`,
553
630
  body: { body: params.body }
554
631
  });
632
+ const commentId = result?.id;
633
+ if (commentId) {
634
+ commentIdCache.set(key, commentId);
635
+ }
555
636
  }
556
637
  async function addIssueLabels(params) {
557
638
  await githubRequest({
@@ -762,7 +843,7 @@ async function showSchemaDiff(repoRoot, tmpDir) {
762
843
  });
763
844
  }
764
845
  }
765
- async function detectRisks(repoRoot, tmpDir) {
846
+ async function detectRisks(repoRoot, tmpDir, timeoutMs) {
766
847
  const logFile = path4.join(tmpDir, "db-risks.log");
767
848
  try {
768
849
  const env = getFilteredEnv();
@@ -772,13 +853,14 @@ async function detectRisks(repoRoot, tmpDir) {
772
853
  label: "db:risks",
773
854
  command: "pnpm",
774
855
  args: ["exec", "runa", "db", "risks"],
775
- logFile
856
+ logFile,
857
+ timeoutMs
776
858
  });
777
859
  } catch (error) {
778
860
  let logContent = "";
779
861
  try {
780
- const { readFileSync: readFileSync4 } = await import('fs');
781
- logContent = readFileSync4(logFile, "utf-8");
862
+ const { readFileSync: readFileSync5 } = await import('fs');
863
+ logContent = readFileSync5(logFile, "utf-8");
782
864
  } catch {
783
865
  }
784
866
  const isInitialDeployment = logContent.includes("No common ancestor") || logContent.includes("INITIAL DEPLOYMENT");
@@ -799,7 +881,7 @@ To bypass (if changes are intentional):
799
881
  throw enhancedError;
800
882
  }
801
883
  }
802
- async function snapshotCreate(repoRoot, tmpDir, productionDbUrlAdmin, commit) {
884
+ async function snapshotCreate(repoRoot, tmpDir, productionDbUrlAdmin, commit, timeoutMs) {
803
885
  await runLogged({
804
886
  cwd: repoRoot,
805
887
  env: {
@@ -809,10 +891,11 @@ async function snapshotCreate(repoRoot, tmpDir, productionDbUrlAdmin, commit) {
809
891
  label: "snapshot create production",
810
892
  command: "pnpm",
811
893
  args: ["exec", "runa", "db", "snapshot", "create", "production", "--commit", commit],
812
- logFile: path4.join(tmpDir, "snapshot-create.log")
894
+ logFile: path4.join(tmpDir, "snapshot-create.log"),
895
+ timeoutMs
813
896
  });
814
897
  }
815
- async function snapshotRestoreLatest(repoRoot, tmpDir, productionDbUrlAdmin) {
898
+ async function snapshotRestoreLatest(repoRoot, tmpDir, productionDbUrlAdmin, timeoutMs) {
816
899
  await runLogged({
817
900
  cwd: repoRoot,
818
901
  env: {
@@ -822,7 +905,8 @@ async function snapshotRestoreLatest(repoRoot, tmpDir, productionDbUrlAdmin) {
822
905
  label: "snapshot restore production (latest)",
823
906
  command: "pnpm",
824
907
  args: ["exec", "runa", "db", "snapshot", "restore", "production", "--latest", "--auto-approve"],
825
- logFile: path4.join(tmpDir, "snapshot-restore.log")
908
+ logFile: path4.join(tmpDir, "snapshot-restore.log"),
909
+ timeoutMs
826
910
  });
827
911
  }
828
912
  function parseApplyLog(logContent) {
@@ -901,13 +985,14 @@ async function applyProductionSchema(repoRoot, tmpDir, productionDbUrlAdmin, pro
901
985
  label: "db apply production",
902
986
  command: "pnpm",
903
987
  args,
904
- logFile: logPath
988
+ logFile: logPath,
989
+ timeoutMs: options?.timeoutMs
905
990
  });
906
991
  const totalMs = Date.now() - startTime;
907
992
  let logContent = "";
908
993
  try {
909
- const { readFileSync: readFileSync4 } = await import('fs');
910
- logContent = readFileSync4(logPath, "utf-8");
994
+ const { readFileSync: readFileSync5 } = await import('fs');
995
+ logContent = readFileSync5(logPath, "utf-8");
911
996
  } catch {
912
997
  }
913
998
  const parsed = parseApplyLog(logContent);
@@ -1145,6 +1230,7 @@ function createInitialSummary(params) {
1145
1230
  durationMs: 0,
1146
1231
  repoKind: resolveRepoKind(),
1147
1232
  detected: {},
1233
+ diagnostics: {},
1148
1234
  steps: {},
1149
1235
  layers: {},
1150
1236
  errors: []
@@ -1201,6 +1287,19 @@ function buildCiProdApplyStepSummaryMarkdown(params) {
1201
1287
  lines.push("");
1202
1288
  lines.push(`**Duration**: ${duration}`);
1203
1289
  lines.push("");
1290
+ if (summary.dbOutcome) {
1291
+ lines.push(`**Exit mode**: \`${summary.dbOutcome.exitMode}\``);
1292
+ const failedPhase = summary.dbOutcome.phases.find(
1293
+ (phase) => phase.status === "failed" || phase.status === "timeout"
1294
+ );
1295
+ if (failedPhase) {
1296
+ lines.push(`**Failed phase**: \`${failedPhase.id}\``);
1297
+ }
1298
+ if (summary.dbOutcome.summary.warnings > 0) {
1299
+ lines.push(`**Warnings**: ${summary.dbOutcome.summary.warnings}`);
1300
+ }
1301
+ lines.push("");
1302
+ }
1204
1303
  if (summary.errors.length > 0) {
1205
1304
  lines.push("### \u274C Errors");
1206
1305
  lines.push("");
@@ -1215,6 +1314,9 @@ function buildCiProdApplyStepSummaryMarkdown(params) {
1215
1314
  lines.push(`- Command: \`${summary.command}\``);
1216
1315
  lines.push(`- Mode: \`${summary.mode}\``);
1217
1316
  lines.push(`- Summary: \`${params.summaryPath}\``);
1317
+ if (summary.dbOutcome) {
1318
+ lines.push(`- DB exit mode: \`${summary.dbOutcome.exitMode}\``);
1319
+ }
1218
1320
  lines.push("");
1219
1321
  lines.push("</details>");
1220
1322
  lines.push("");
@@ -1471,7 +1573,7 @@ var CiProdApplyWorkflow = class {
1471
1573
  return null;
1472
1574
  },
1473
1575
  run: async (ctx) => {
1474
- await detectRisks(ctx.repoRoot, ctx.tmpDir);
1576
+ await detectRisks(ctx.repoRoot, ctx.tmpDir, ctx.options.riskTimeoutMs);
1475
1577
  }
1476
1578
  },
1477
1579
  {
@@ -1483,7 +1585,8 @@ var CiProdApplyWorkflow = class {
1483
1585
  ctx.repoRoot,
1484
1586
  ctx.tmpDir,
1485
1587
  ctx.inputs.productionDatabaseUrlAdmin,
1486
- ctx.inputs.githubSha
1588
+ ctx.inputs.githubSha,
1589
+ ctx.options.snapshotTimeoutMs
1487
1590
  );
1488
1591
  }
1489
1592
  },
@@ -1514,7 +1617,8 @@ var CiProdApplyWorkflow = class {
1514
1617
  {
1515
1618
  allowDataLoss: ctx.options.allowDataLoss === true,
1516
1619
  confirmAuthzUpdate: ctx.options.confirmAuthzUpdate === true,
1517
- maxLockWaitMs: typeof ctx.options.maxLockWaitMs === "number" ? ctx.options.maxLockWaitMs : void 0
1620
+ maxLockWaitMs: typeof ctx.options.maxLockWaitMs === "number" ? ctx.options.maxLockWaitMs : void 0,
1621
+ timeoutMs: ctx.options.applyTimeoutMs
1518
1622
  }
1519
1623
  );
1520
1624
  } catch (error) {
@@ -1522,7 +1626,8 @@ var CiProdApplyWorkflow = class {
1522
1626
  await snapshotRestoreLatest(
1523
1627
  ctx.repoRoot,
1524
1628
  ctx.tmpDir,
1525
- ctx.inputs.productionDatabaseUrlAdmin
1629
+ ctx.inputs.productionDatabaseUrlAdmin,
1630
+ ctx.options.snapshotTimeoutMs
1526
1631
  );
1527
1632
  throw error;
1528
1633
  }
@@ -1575,20 +1680,79 @@ var CiProdApplyWorkflow = class {
1575
1680
  async run() {
1576
1681
  logPlan(this.steps.map((step) => ({ id: step.id, description: step.description })));
1577
1682
  const total = this.steps.length;
1683
+ const startedAtMs = Date.now();
1684
+ const phases = [];
1578
1685
  for (let i = 0; i < total; i++) {
1579
1686
  const step = this.steps[i];
1580
1687
  const prefix = `[DEBUG] Step ${i + 1}/${total}: ${step.description}`;
1581
1688
  const skipResult = step.shouldSkip?.(this.ctx);
1582
1689
  if (skipResult) {
1583
1690
  console.log(`${prefix} SKIPPED (${skipResult.reason})`);
1691
+ phases.push({
1692
+ id: step.id,
1693
+ label: step.description,
1694
+ status: "skipped",
1695
+ warningCount: 0
1696
+ });
1584
1697
  continue;
1585
1698
  }
1586
1699
  console.log(`${prefix}...`);
1587
1700
  const startedAt = Date.now();
1588
- await step.run(this.ctx);
1589
- const duration = Date.now() - startedAt;
1590
- console.log(`${prefix} done (${duration}ms)`);
1701
+ try {
1702
+ await step.run(this.ctx);
1703
+ const duration = Date.now() - startedAt;
1704
+ phases.push({
1705
+ id: step.id,
1706
+ label: step.description,
1707
+ status: "passed",
1708
+ startedAt: new Date(startedAt).toISOString(),
1709
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1710
+ durationMs: duration
1711
+ });
1712
+ console.log(`${prefix} done (${duration}ms)`);
1713
+ } catch (error) {
1714
+ const message = error instanceof Error ? error.message : String(error);
1715
+ const timeoutLike = isTimeoutLikeMessage(message);
1716
+ phases.push({
1717
+ id: step.id,
1718
+ label: step.description,
1719
+ status: timeoutLike ? "timeout" : "failed",
1720
+ startedAt: new Date(startedAt).toISOString(),
1721
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1722
+ durationMs: Date.now() - startedAt,
1723
+ error: {
1724
+ code: timeoutLike ? "PHASE_TIMEOUT" : "CI_PROD_APPLY_FAILED",
1725
+ message,
1726
+ retryable: timeoutLike,
1727
+ phase: step.id
1728
+ }
1729
+ });
1730
+ this.ctx.summary.dbOutcome = {
1731
+ command: "runa ci prod-apply",
1732
+ exitMode: deriveCommandExitMode(phases),
1733
+ startedAt: new Date(startedAtMs).toISOString(),
1734
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1735
+ durationMs: Date.now() - startedAtMs,
1736
+ phases,
1737
+ warnings: [],
1738
+ errors: phases.map((phase) => phase.error).filter((value) => value != null),
1739
+ summary: buildCommandOutcomeSummary(phases)
1740
+ };
1741
+ throw error;
1742
+ }
1591
1743
  }
1744
+ const outcome = {
1745
+ command: "runa ci prod-apply",
1746
+ exitMode: deriveCommandExitMode(phases),
1747
+ startedAt: new Date(startedAtMs).toISOString(),
1748
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1749
+ durationMs: Date.now() - startedAtMs,
1750
+ phases,
1751
+ warnings: [],
1752
+ errors: [],
1753
+ summary: buildCommandOutcomeSummary(phases)
1754
+ };
1755
+ this.ctx.summary.dbOutcome = outcome;
1592
1756
  }
1593
1757
  };
1594
1758
 
@@ -1807,6 +1971,18 @@ var ciProdApplyCommand = new Command("prod-apply").description("Apply production
1807
1971
  "--max-lock-wait-ms <ms>",
1808
1972
  "Maximum time to wait for lock acquisition (default: 30000)",
1809
1973
  (value) => Number.parseInt(value, 10)
1974
+ ).option(
1975
+ "--risk-timeout-ms <ms>",
1976
+ "Maximum time for risk detection (ms)",
1977
+ (value) => Number.parseInt(value, 10)
1978
+ ).option(
1979
+ "--snapshot-timeout-ms <ms>",
1980
+ "Maximum time for snapshot create/restore (ms)",
1981
+ (value) => Number.parseInt(value, 10)
1982
+ ).option(
1983
+ "--apply-timeout-ms <ms>",
1984
+ "Maximum time for production schema apply (ms)",
1985
+ (value) => Number.parseInt(value, 10)
1810
1986
  ).option("--skip-notify", "Skip external notification (RUNA_APP_URL)", false).option("--skip-github-label", "Skip adding db-applied label (GitHub Actions only)", false).option(
1811
1987
  "--skip-risks",
1812
1988
  "Skip schema risk detection (use for trusted deployments or initial setup)",
@@ -2295,6 +2471,7 @@ async function stopApp(pid, cleanupStreams) {
2295
2471
  init_esm_shims();
2296
2472
  async function runAppBuild(params) {
2297
2473
  const { repoRoot, tmpDir, env } = params;
2474
+ const startedAtMs = Date.now();
2298
2475
  try {
2299
2476
  const hasTurbo = existsSync(path4.join(repoRoot, "turbo.json"));
2300
2477
  const hasApps = existsSync(path4.join(repoRoot, "apps"));
@@ -2308,33 +2485,61 @@ async function runAppBuild(params) {
2308
2485
  args,
2309
2486
  logFile: path4.join(tmpDir, "build.log")
2310
2487
  });
2311
- return { passed: true };
2488
+ return { passed: true, startedAtMs };
2312
2489
  } catch (error) {
2313
2490
  return {
2314
2491
  passed: false,
2492
+ startedAtMs,
2315
2493
  error: error instanceof Error ? error.message : String(error)
2316
2494
  };
2317
2495
  }
2318
2496
  }
2497
+ function wasManifestGeneratedDuringBuild(repoRoot, buildStartedAtMs) {
2498
+ const manifestPath = path4.join(repoRoot, ".runa", "manifests", "manifest.json");
2499
+ if (!existsSync(manifestPath)) {
2500
+ return false;
2501
+ }
2502
+ try {
2503
+ const manifestStat = statSync(manifestPath);
2504
+ return manifestStat.mtimeMs >= buildStartedAtMs;
2505
+ } catch {
2506
+ return false;
2507
+ }
2508
+ }
2319
2509
  async function runManifestGenerate(params) {
2320
- const { repoRoot, tmpDir, env } = params;
2510
+ const { repoRoot, tmpDir, env, buildStartedAtMs } = params;
2511
+ if (wasManifestGeneratedDuringBuild(repoRoot, buildStartedAtMs)) {
2512
+ console.log("\u25B6 manifest: reusing manifest generated during build");
2513
+ return { generated: true };
2514
+ }
2321
2515
  try {
2322
2516
  await runLogged({
2323
2517
  cwd: repoRoot,
2324
2518
  env,
2325
2519
  label: "manifest:generate",
2326
2520
  command: "pnpm",
2327
- args: ["manifest:generate"],
2521
+ args: ["exec", "runa", "manifest"],
2328
2522
  logFile: path4.join(tmpDir, "manifest-generate.log")
2329
2523
  });
2330
2524
  return { generated: true };
2331
2525
  } catch (error) {
2526
+ const message = error instanceof Error ? error.message : String(error);
2527
+ if (isOptionalManifestError(message)) {
2528
+ return {
2529
+ generated: null,
2530
+ error: message
2531
+ };
2532
+ }
2332
2533
  return {
2333
2534
  generated: false,
2334
- error: error instanceof Error ? error.message : String(error)
2535
+ error: message
2335
2536
  };
2336
2537
  }
2337
2538
  }
2539
+ function isOptionalManifestError(message) {
2540
+ const normalized = message.toLowerCase();
2541
+ return normalized.includes("enoent") && normalized.includes("docs/wip") || normalized.includes("no such file") && normalized.includes("docs/wip") || normalized.includes("strict_detect_manifest_required");
2542
+ }
2338
2543
  async function runPlaywrightInstall(params) {
2339
2544
  const { repoRoot, tmpDir, isCI } = params;
2340
2545
  try {
@@ -2362,7 +2567,12 @@ var buildAndPlaywrightActor = fromPromise(
2362
2567
  const buildResult = await runAppBuild({ repoRoot, tmpDir, env });
2363
2568
  let manifestResult = { generated: false };
2364
2569
  if (buildResult.passed) {
2365
- manifestResult = await runManifestGenerate({ repoRoot, tmpDir, env });
2570
+ manifestResult = await runManifestGenerate({
2571
+ repoRoot,
2572
+ tmpDir,
2573
+ env,
2574
+ buildStartedAtMs: buildResult.startedAtMs
2575
+ });
2366
2576
  }
2367
2577
  return {
2368
2578
  buildPassed: buildResult.passed,
@@ -2379,7 +2589,12 @@ var buildAndPlaywrightActor = fromPromise(
2379
2589
  if (!buildResult.passed) {
2380
2590
  return { buildResult, manifestResult: { generated: false, error: void 0 } };
2381
2591
  }
2382
- const manifestResult = await runManifestGenerate({ repoRoot, tmpDir, env });
2592
+ const manifestResult = await runManifestGenerate({
2593
+ repoRoot,
2594
+ tmpDir,
2595
+ env,
2596
+ buildStartedAtMs: buildResult.startedAtMs
2597
+ });
2383
2598
  return { buildResult, manifestResult };
2384
2599
  })(),
2385
2600
  runPlaywrightInstall({ repoRoot, tmpDir, isCI })
@@ -2399,7 +2614,7 @@ var buildAndPlaywrightActor = fromPromise(
2399
2614
  manifestGenerated,
2400
2615
  playwrightInstalled,
2401
2616
  buildError: buildPassed ? void 0 : buildError,
2402
- manifestError: manifestGenerated ? void 0 : manifestError,
2617
+ manifestError: manifestGenerated === true ? void 0 : manifestError,
2403
2618
  playwrightError: playwrightInstalled ? void 0 : playwrightError
2404
2619
  };
2405
2620
  }
@@ -2485,6 +2700,11 @@ init_esm_shims();
2485
2700
 
2486
2701
  // src/commands/ci/machine/actors/db/apply-seeds.ts
2487
2702
  init_esm_shims();
2703
+
2704
+ // src/commands/ci/utils/db-url-utils.ts
2705
+ init_esm_shims();
2706
+
2707
+ // src/commands/ci/machine/actors/db/apply-seeds.ts
2488
2708
  var applySeedsActor = fromPromise(
2489
2709
  async ({ input }) => {
2490
2710
  const { repoRoot, tmpDir, databaseUrl } = input;
@@ -2518,6 +2738,16 @@ init_esm_shims();
2518
2738
 
2519
2739
  // src/commands/ci/machine/actors/db/schema-canonical-diff.ts
2520
2740
  init_esm_shims();
2741
+ var SECURITY_METADATA_KINDS = /* @__PURE__ */ new Set([
2742
+ "schema_acl",
2743
+ "table_acl",
2744
+ "function_acl"
2745
+ ]);
2746
+ var DESCRIPTIVE_METADATA_KINDS = /* @__PURE__ */ new Set([
2747
+ "schema_comment",
2748
+ "table_comment",
2749
+ "function_comment"
2750
+ ]);
2521
2751
  function createUnavailableCanonicalSnapshot(error) {
2522
2752
  return {
2523
2753
  available: false,
@@ -2548,6 +2778,11 @@ var EXCLUDED_SCHEMAS = [
2548
2778
  function normalizeWhitespace(value) {
2549
2779
  return (value ?? "").replace(/\s+/g, " ").trim();
2550
2780
  }
2781
+ function normalizeAcl(value) {
2782
+ const trimmed = value?.trim();
2783
+ if (!trimmed) return "";
2784
+ return trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).sort((left, right) => left.localeCompare(right)).join(",");
2785
+ }
2551
2786
  function stableHash(payload) {
2552
2787
  return createHash("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 16);
2553
2788
  }
@@ -2787,6 +3022,128 @@ async function queryTriggers(sql, schemaList) {
2787
3022
  ORDER BY n.nspname, c.relname, t.tgname
2788
3023
  `;
2789
3024
  }
3025
+ async function querySchemaAclMetadata(sql, schemaList) {
3026
+ return await sql`
3027
+ SELECT
3028
+ n.nspname AS schema_name,
3029
+ n.nspname AS object_name,
3030
+ n.nspname AS object_key,
3031
+ format('%I schema ACL', n.nspname) AS object_label,
3032
+ COALESCE(
3033
+ (
3034
+ SELECT string_agg(acl::text, ',' ORDER BY acl::text)
3035
+ FROM unnest(COALESCE(n.nspacl, acldefault('n', n.nspowner))) acl
3036
+ ),
3037
+ ''
3038
+ ) AS value
3039
+ FROM pg_namespace n
3040
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
3041
+ ORDER BY n.nspname
3042
+ `;
3043
+ }
3044
+ async function querySchemaCommentMetadata(sql, schemaList) {
3045
+ return await sql`
3046
+ SELECT
3047
+ n.nspname AS schema_name,
3048
+ n.nspname AS object_name,
3049
+ n.nspname AS object_key,
3050
+ format('%I schema comment', n.nspname) AS object_label,
3051
+ COALESCE(obj_description(n.oid, 'pg_namespace'), '') AS value
3052
+ FROM pg_namespace n
3053
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
3054
+ ORDER BY n.nspname
3055
+ `;
3056
+ }
3057
+ async function queryTableAclMetadata(sql, schemaList) {
3058
+ return await sql`
3059
+ SELECT
3060
+ n.nspname AS schema_name,
3061
+ c.relname AS object_name,
3062
+ format('%I.%I', n.nspname, c.relname) AS object_key,
3063
+ format('%I.%I ACL', n.nspname, c.relname) AS object_label,
3064
+ COALESCE(
3065
+ (
3066
+ SELECT string_agg(acl::text, ',' ORDER BY acl::text)
3067
+ FROM unnest(COALESCE(c.relacl, acldefault('r', c.relowner))) acl
3068
+ ),
3069
+ ''
3070
+ ) AS value
3071
+ FROM pg_class c
3072
+ JOIN pg_namespace n ON n.oid = c.relnamespace
3073
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
3074
+ AND c.relkind IN ('r', 'p')
3075
+ ORDER BY n.nspname, c.relname
3076
+ `;
3077
+ }
3078
+ async function queryTableCommentMetadata(sql, schemaList) {
3079
+ return await sql`
3080
+ SELECT
3081
+ n.nspname AS schema_name,
3082
+ c.relname AS object_name,
3083
+ format('%I.%I', n.nspname, c.relname) AS object_key,
3084
+ format('%I.%I comment', n.nspname, c.relname) AS object_label,
3085
+ COALESCE(obj_description(c.oid, 'pg_class'), '') AS value
3086
+ FROM pg_class c
3087
+ JOIN pg_namespace n ON n.oid = c.relnamespace
3088
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
3089
+ AND c.relkind IN ('r', 'p')
3090
+ ORDER BY n.nspname, c.relname
3091
+ `;
3092
+ }
3093
+ async function queryFunctionAclMetadata(sql, schemaList) {
3094
+ return await sql`
3095
+ SELECT
3096
+ n.nspname AS schema_name,
3097
+ p.proname AS object_name,
3098
+ format('%I.%I(%s)', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)) AS object_key,
3099
+ format('%I.%I(%s) ACL', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)) AS object_label,
3100
+ COALESCE(
3101
+ (
3102
+ SELECT string_agg(acl::text, ',' ORDER BY acl::text)
3103
+ FROM unnest(COALESCE(p.proacl, acldefault('f', p.proowner))) acl
3104
+ ),
3105
+ ''
3106
+ ) AS value
3107
+ FROM pg_proc p
3108
+ JOIN pg_namespace n ON p.pronamespace = n.oid
3109
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
3110
+ AND p.prokind = 'f'
3111
+ AND NOT EXISTS (
3112
+ SELECT 1 FROM pg_depend d
3113
+ WHERE d.objid = p.oid
3114
+ AND d.deptype = 'e'
3115
+ )
3116
+ AND p.proname NOT LIKE 'supabase_%'
3117
+ AND p.proname NOT LIKE 'postgrest_%'
3118
+ AND p.proname NOT LIKE 'pgrst_%'
3119
+ AND p.proname NOT LIKE '_pgrst_%'
3120
+ ORDER BY n.nspname, p.proname
3121
+ `;
3122
+ }
3123
+ async function queryFunctionCommentMetadata(sql, schemaList) {
3124
+ return await sql`
3125
+ SELECT
3126
+ n.nspname AS schema_name,
3127
+ p.proname AS object_name,
3128
+ format('%I.%I(%s)', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)) AS object_key,
3129
+ format('%I.%I(%s) comment', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)) AS object_label,
3130
+ COALESCE(obj_description(p.oid, 'pg_proc'), '') AS value
3131
+ FROM pg_proc p
3132
+ JOIN pg_namespace n ON p.pronamespace = n.oid
3133
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
3134
+ AND p.prokind = 'f'
3135
+ AND NOT EXISTS (
3136
+ SELECT 1 FROM pg_depend d
3137
+ WHERE d.objid = p.oid
3138
+ AND d.deptype = 'e'
3139
+ )
3140
+ AND p.proname NOT LIKE 'supabase_%'
3141
+ AND p.proname NOT LIKE 'postgrest_%'
3142
+ AND p.proname NOT LIKE 'pgrst_%'
3143
+ AND p.proname NOT LIKE '_pgrst_%'
3144
+ ORDER BY n.nspname, p.proname
3145
+ `;
3146
+ }
2790
3147
  function rowsToObjects(kind, rows) {
2791
3148
  return rows.map(
2792
3149
  (row) => createCanonicalObject(
@@ -2799,6 +3156,17 @@ function rowsToObjects(kind, rows) {
2799
3156
  )
2800
3157
  );
2801
3158
  }
3159
+ function metadataRowsToObjects(kind, rows) {
3160
+ return rows.map(
3161
+ (row) => createCanonicalObject(
3162
+ kind,
3163
+ row.schema_name,
3164
+ row.object_key,
3165
+ row.object_label,
3166
+ kind.endsWith("_acl") ? normalizeAcl(row.value) : normalizeWhitespace(row.value)
3167
+ )
3168
+ );
3169
+ }
2802
3170
  async function getCanonicalSchemaSnapshot(databaseUrl) {
2803
3171
  let sql = null;
2804
3172
  try {
@@ -2814,17 +3182,35 @@ async function getCanonicalSchemaSnapshot(databaseUrl) {
2814
3182
  return { available: true, objects: [] };
2815
3183
  }
2816
3184
  const schemaList = buildSchemaListSql(userSchemas);
2817
- const [columns, flags, constraints, functions, policies, indexes, triggers] = await Promise.all(
2818
- [
2819
- queryTableColumns(sql, schemaList),
2820
- queryTableFlags(sql, schemaList),
2821
- queryTableConstraints(sql, schemaList),
2822
- queryFunctions(sql, schemaList),
2823
- queryPolicies(sql, schemaList),
2824
- queryIndexes(sql, schemaList),
2825
- queryTriggers(sql, schemaList)
2826
- ]
2827
- );
3185
+ const [
3186
+ columns,
3187
+ flags,
3188
+ constraints,
3189
+ functions,
3190
+ policies,
3191
+ indexes,
3192
+ triggers,
3193
+ schemaAcl,
3194
+ schemaComments,
3195
+ tableAcl,
3196
+ tableComments,
3197
+ functionAcl,
3198
+ functionComments
3199
+ ] = await Promise.all([
3200
+ queryTableColumns(sql, schemaList),
3201
+ queryTableFlags(sql, schemaList),
3202
+ queryTableConstraints(sql, schemaList),
3203
+ queryFunctions(sql, schemaList),
3204
+ queryPolicies(sql, schemaList),
3205
+ queryIndexes(sql, schemaList),
3206
+ queryTriggers(sql, schemaList),
3207
+ querySchemaAclMetadata(sql, schemaList),
3208
+ querySchemaCommentMetadata(sql, schemaList),
3209
+ queryTableAclMetadata(sql, schemaList),
3210
+ queryTableCommentMetadata(sql, schemaList),
3211
+ queryFunctionAclMetadata(sql, schemaList),
3212
+ queryFunctionCommentMetadata(sql, schemaList)
3213
+ ]);
2828
3214
  return {
2829
3215
  available: true,
2830
3216
  objects: [
@@ -2832,7 +3218,13 @@ async function getCanonicalSchemaSnapshot(databaseUrl) {
2832
3218
  ...rowsToObjects("function", functions),
2833
3219
  ...rowsToObjects("policy", policies),
2834
3220
  ...rowsToObjects("index", indexes),
2835
- ...rowsToObjects("trigger", triggers)
3221
+ ...rowsToObjects("trigger", triggers),
3222
+ ...metadataRowsToObjects("schema_acl", schemaAcl),
3223
+ ...metadataRowsToObjects("schema_comment", schemaComments),
3224
+ ...metadataRowsToObjects("table_acl", tableAcl),
3225
+ ...metadataRowsToObjects("table_comment", tableComments),
3226
+ ...metadataRowsToObjects("function_acl", functionAcl),
3227
+ ...metadataRowsToObjects("function_comment", functionComments)
2836
3228
  ].sort((a, b) => a.key.localeCompare(b.key))
2837
3229
  };
2838
3230
  } catch (error) {
@@ -2858,6 +3250,18 @@ function buildSignatureBuckets(objects) {
2858
3250
  }
2859
3251
  return buckets;
2860
3252
  }
3253
+ function findRenamedIndexTarget(referenceObject, targetSignatureBuckets, matchedTargetKeys) {
3254
+ if (referenceObject.kind !== "index") {
3255
+ return void 0;
3256
+ }
3257
+ const renameBucket = targetSignatureBuckets.get(
3258
+ `${referenceObject.kind}:${referenceObject.schema}:${referenceObject.signature}`
3259
+ ) ?? [];
3260
+ return renameBucket.find((candidate) => !matchedTargetKeys.has(candidate.key));
3261
+ }
3262
+ function isExtraTargetObject(targetObject, matchedTargetKeys, referenceKeys) {
3263
+ return !matchedTargetKeys.has(targetObject.key) && !referenceKeys.has(targetObject.key);
3264
+ }
2861
3265
  function compareCanonicalSnapshots(reference, target) {
2862
3266
  const missing = [];
2863
3267
  const extra = [];
@@ -2865,6 +3269,7 @@ function compareCanonicalSnapshots(reference, target) {
2865
3269
  const renamed = [];
2866
3270
  const targetByKey = buildObjectMap(target);
2867
3271
  const targetSignatureBuckets = buildSignatureBuckets(target.objects);
3272
+ const referenceKeys = new Set(reference.objects.map((object) => object.key));
2868
3273
  const matchedTargetKeys = /* @__PURE__ */ new Set();
2869
3274
  for (const referenceObject of reference.objects) {
2870
3275
  const matchedTarget = targetByKey.get(referenceObject.key);
@@ -2875,21 +3280,20 @@ function compareCanonicalSnapshots(reference, target) {
2875
3280
  }
2876
3281
  continue;
2877
3282
  }
2878
- if (referenceObject.kind === "index") {
2879
- const renameBucket = targetSignatureBuckets.get(
2880
- `${referenceObject.kind}:${referenceObject.schema}:${referenceObject.signature}`
2881
- ) ?? [];
2882
- const renamedTarget = renameBucket.find((candidate) => !matchedTargetKeys.has(candidate.key));
2883
- if (renamedTarget) {
2884
- matchedTargetKeys.add(renamedTarget.key);
2885
- renamed.push({ reference: referenceObject, target: renamedTarget });
2886
- continue;
2887
- }
3283
+ const renamedTarget = findRenamedIndexTarget(
3284
+ referenceObject,
3285
+ targetSignatureBuckets,
3286
+ matchedTargetKeys
3287
+ );
3288
+ if (renamedTarget) {
3289
+ matchedTargetKeys.add(renamedTarget.key);
3290
+ renamed.push({ reference: referenceObject, target: renamedTarget });
3291
+ continue;
2888
3292
  }
2889
3293
  missing.push(referenceObject);
2890
3294
  }
2891
3295
  for (const targetObject of target.objects) {
2892
- if (!matchedTargetKeys.has(targetObject.key) && !reference.objects.some((candidate) => candidate.key === targetObject.key)) {
3296
+ if (isExtraTargetObject(targetObject, matchedTargetKeys, referenceKeys)) {
2893
3297
  extra.push(targetObject);
2894
3298
  }
2895
3299
  }
@@ -2940,6 +3344,25 @@ function summarizeCanonicalDiffBySchema(diff) {
2940
3344
  function hasCanonicalChanges(diff) {
2941
3345
  return diff.missing.length > 0 || diff.extra.length > 0 || diff.changed.length > 0 || diff.renamed.length > 0;
2942
3346
  }
3347
+ function classifyCanonicalDiff(diff) {
3348
+ const kinds = /* @__PURE__ */ new Set();
3349
+ for (const object of diff.missing) kinds.add(object.kind);
3350
+ for (const object of diff.extra) kinds.add(object.kind);
3351
+ for (const pair of diff.changed) kinds.add(pair.reference.kind);
3352
+ for (const pair of diff.renamed) kinds.add(pair.reference.kind);
3353
+ const orderedKinds = Array.from(kinds).sort();
3354
+ const securityMetadata = orderedKinds.some((kind) => SECURITY_METADATA_KINDS.has(kind));
3355
+ const descriptiveMetadata = orderedKinds.some((kind) => DESCRIPTIVE_METADATA_KINDS.has(kind));
3356
+ const structural = orderedKinds.some(
3357
+ (kind) => !SECURITY_METADATA_KINDS.has(kind) && !DESCRIPTIVE_METADATA_KINDS.has(kind)
3358
+ );
3359
+ return {
3360
+ kinds: orderedKinds,
3361
+ structural,
3362
+ securityMetadata,
3363
+ descriptiveMetadata
3364
+ };
3365
+ }
2943
3366
 
2944
3367
  // src/commands/ci/machine/actors/db/schema-stats.ts
2945
3368
  init_esm_shims();
@@ -3158,6 +3581,9 @@ function formatTotalStatsCompact(stats) {
3158
3581
  parts.push(`${stats.triggers}Tr`);
3159
3582
  return parts.join(" ");
3160
3583
  }
3584
+ function hasStatsDiff(a, b) {
3585
+ return a.tables !== b.tables || a.functions !== b.functions || a.policies !== b.policies || a.indexes !== b.indexes || a.triggers !== b.triggers;
3586
+ }
3161
3587
  function hasDisplayedStatsDiff(a, b) {
3162
3588
  return a.tables !== b.tables || a.functions !== b.functions || a.policies !== b.policies || a.indexes !== b.indexes;
3163
3589
  }
@@ -3283,6 +3709,9 @@ function unavailableStats(error) {
3283
3709
  canonicalSnapshot: createUnavailableCanonicalSnapshot(error)
3284
3710
  };
3285
3711
  }
3712
+ function emptySchemaStats() {
3713
+ return { tables: 0, functions: 0, policies: 0, indexes: 0, triggers: 0 };
3714
+ }
3286
3715
  function cloneEnvironmentStats(stats) {
3287
3716
  return {
3288
3717
  ...stats,
@@ -3308,11 +3737,85 @@ async function collectEnvironmentStats(dbUrl, label, includeSchemas = false) {
3308
3737
  return null;
3309
3738
  }
3310
3739
  const stats = await getDbSchemaStats(dbUrl);
3311
- stats.canonicalSnapshot = await getCanonicalSchemaSnapshot(dbUrl);
3312
3740
  stats.available = true;
3313
3741
  logSchemaStats(label, stats, includeSchemas);
3314
3742
  return stats;
3315
3743
  }
3744
+ async function collectCanonicalSnapshot(stats, dbUrl, label) {
3745
+ if (!stats || !dbUrl || stats.available === false) {
3746
+ return;
3747
+ }
3748
+ console.log(`[schema-stats] Collecting ${label} semantic snapshot...`);
3749
+ stats.canonicalSnapshot = await getCanonicalSchemaSnapshot(dbUrl);
3750
+ }
3751
+ function getSchemaNames(reference, target) {
3752
+ return Array.from(
3753
+ /* @__PURE__ */ new Set([...Object.keys(reference.schemas), ...Object.keys(target.schemas)])
3754
+ ).sort();
3755
+ }
3756
+ function getSchemaOrEmpty(stats, schemaName) {
3757
+ return stats.schemas[schemaName] ?? emptySchemaStats();
3758
+ }
3759
+ function hasAnySchemaStatsDiff(reference, target) {
3760
+ for (const schemaName of getSchemaNames(reference, target)) {
3761
+ if (hasStatsDiff(getSchemaOrEmpty(reference, schemaName), getSchemaOrEmpty(target, schemaName))) {
3762
+ return true;
3763
+ }
3764
+ }
3765
+ return false;
3766
+ }
3767
+ function hasUnexpectedCountDrift(reference, target, expectedDrift) {
3768
+ const fields = [
3769
+ "tables",
3770
+ "functions",
3771
+ "policies",
3772
+ "indexes",
3773
+ "triggers"
3774
+ ];
3775
+ for (const schemaName of getSchemaNames(reference, target)) {
3776
+ const referenceStats = getSchemaOrEmpty(reference, schemaName);
3777
+ const targetStats = getSchemaOrEmpty(target, schemaName);
3778
+ for (const field of fields) {
3779
+ if (referenceStats[field] === targetStats[field]) continue;
3780
+ const expected = expectedDrift.find(
3781
+ (entry) => entry.field === field && (entry.schemas === void 0 || entry.schemas.includes(schemaName))
3782
+ );
3783
+ if (!expected) {
3784
+ return true;
3785
+ }
3786
+ }
3787
+ }
3788
+ return false;
3789
+ }
3790
+ function hasUnexpectedIndexDrift(reference, target, expectedDrift) {
3791
+ const diff = compareIndexLists(reference, target);
3792
+ if (!hasIndexDiff(diff)) return false;
3793
+ const touchedSchemas = /* @__PURE__ */ new Set([
3794
+ ...diff.missing.map((index) => index.schema),
3795
+ ...diff.extra.map((index) => index.schema)
3796
+ ]);
3797
+ for (const schemaName of touchedSchemas) {
3798
+ const expected = findExpectedDrift(schemaName, "I", expectedDrift);
3799
+ if (!expected) {
3800
+ return true;
3801
+ }
3802
+ }
3803
+ return false;
3804
+ }
3805
+ function shouldCollectProductionSemanticSnapshot(params) {
3806
+ const { reference, production, expectedDrift = [] } = params;
3807
+ if (!production) return false;
3808
+ if (reference.available === false || production.available === false) return false;
3809
+ const hasUnexpectedBasicDrift = hasUnexpectedCountDrift(reference, production, expectedDrift) || hasUnexpectedIndexDrift(reference, production, expectedDrift);
3810
+ if (hasUnexpectedBasicDrift) {
3811
+ return false;
3812
+ }
3813
+ return true;
3814
+ }
3815
+ function shouldCollectCiSemanticSnapshot(reference, ci) {
3816
+ if (reference.available === false || ci.available === false) return false;
3817
+ return !hasAnySchemaStatsDiff(reference, ci) && !hasIndexDiff(compareIndexLists(reference, ci));
3818
+ }
3316
3819
  function resolveSettledStats(result, failureLabel) {
3317
3820
  if (result.status === "fulfilled" && result.value) {
3318
3821
  return result.value;
@@ -3355,7 +3858,11 @@ function createReferenceDb(sourceDbUrl) {
3355
3858
  }
3356
3859
  return { dsn: buildShadowDsn(sourceDbUrl, dbName), dbName };
3357
3860
  }
3861
+ var REFERENCE_DB_NAME_PATTERN = /^ci_schema_ref_[0-9a-f]{12}$/;
3358
3862
  function dropReferenceDb(sourceDbUrl, dbName) {
3863
+ if (!REFERENCE_DB_NAME_PATTERN.test(dbName)) {
3864
+ throw new Error(`Invalid reference DB name: ${dbName}`);
3865
+ }
3359
3866
  const adminDsn = buildAdminDsn(sourceDbUrl);
3360
3867
  psqlSyncQuery({
3361
3868
  databaseUrl: adminDsn,
@@ -3368,14 +3875,17 @@ function dropReferenceDb(sourceDbUrl, dbName) {
3368
3875
  });
3369
3876
  psqlSyncQuery({
3370
3877
  databaseUrl: adminDsn,
3371
- sql: `DROP DATABASE IF EXISTS ${dbName}`,
3878
+ sql: `DROP DATABASE IF EXISTS "${dbName}"`,
3372
3879
  timeout: 3e4
3373
3880
  });
3374
3881
  }
3375
- async function buildReferenceStats(repoRoot, tmpDir, sourceDbUrl) {
3882
+ async function buildReferenceStatsSession(repoRoot, tmpDir, sourceDbUrl) {
3376
3883
  if (!sourceDbUrl) return null;
3377
3884
  const referenceDb = createReferenceDb(sourceDbUrl);
3378
3885
  const logFile = path4.join(tmpDir, "ci-reference-db-apply.log");
3886
+ const cleanup = () => {
3887
+ dropReferenceDb(sourceDbUrl, referenceDb.dbName);
3888
+ };
3379
3889
  try {
3380
3890
  const result = await execa(
3381
3891
  "pnpm",
@@ -3396,27 +3906,56 @@ ${result.stderr || ""}`;
3396
3906
  throw new Error(`Failed to build reference DB: ${fullOutput.substring(0, 1e3)}`);
3397
3907
  }
3398
3908
  await (await import('fs/promises')).writeFile(logFile, fullOutput);
3399
- return collectEnvironmentStats(referenceDb.dsn, "Reference", true);
3400
- } finally {
3401
- dropReferenceDb(sourceDbUrl, referenceDb.dbName);
3909
+ const stats = await collectEnvironmentStats(referenceDb.dsn, "Reference", true);
3910
+ if (!stats) {
3911
+ cleanup();
3912
+ return null;
3913
+ }
3914
+ return {
3915
+ stats,
3916
+ dsn: referenceDb.dsn,
3917
+ cleanup
3918
+ };
3919
+ } catch (error) {
3920
+ cleanup();
3921
+ throw error;
3402
3922
  }
3403
3923
  }
3404
3924
  async function resolveReferenceAndCiStats(params) {
3405
- const { repoRoot, tmpDir, referenceDbUrl, ciDbUrl, referenceStrategy } = params;
3925
+ const { repoRoot, tmpDir, referenceDbUrl, ciDbUrl, referenceStrategy} = params;
3406
3926
  if (referenceStrategy === "reuse-ci" && ciDbUrl) {
3407
3927
  console.log(
3408
3928
  "[schema-stats] Reusing synced CI database as reference schema (skip temporary rebuild)"
3409
3929
  );
3410
3930
  const [ciResult2] = await Promise.allSettled([collectEnvironmentStats(ciDbUrl, "CI")]);
3411
- const ci = resolveSettledStats(ciResult2, "CI");
3412
- const reference = ci.available === false ? unavailableStats(ci.error ?? "CI database is unavailable for reference schema reuse") : cloneEnvironmentStats(ci);
3413
- return [reference, ci];
3931
+ const ci2 = resolveSettledStats(ciResult2, "CI");
3932
+ const reference2 = ci2.available === false ? unavailableStats(ci2.error ?? "CI database is unavailable for reference schema reuse") : cloneEnvironmentStats(ci2);
3933
+ return {
3934
+ reference: reference2,
3935
+ ci: ci2,
3936
+ referenceSession: null
3937
+ };
3414
3938
  }
3415
- const [referenceResult, ciResult] = await Promise.allSettled([
3416
- buildReferenceStats(repoRoot, tmpDir, referenceDbUrl),
3939
+ const [referenceSessionResult, ciResult] = await Promise.allSettled([
3940
+ buildReferenceStatsSession(repoRoot, tmpDir, referenceDbUrl),
3417
3941
  collectEnvironmentStats(ciDbUrl, "CI")
3418
3942
  ]);
3419
- return [resolveSettledStats(referenceResult, "reference"), resolveSettledStats(ciResult, "CI")];
3943
+ const referenceSession = referenceSessionResult.status === "fulfilled" ? referenceSessionResult.value : null;
3944
+ let reference;
3945
+ if (referenceSessionResult.status === "fulfilled") {
3946
+ reference = referenceSession?.stats ?? unavailableStats("reference database URL is not available for schema comparison");
3947
+ } else {
3948
+ console.warn(
3949
+ `[schema-stats] Failed to query reference database: ${referenceSessionResult.reason}`
3950
+ );
3951
+ reference = unavailableStats(String(referenceSessionResult.reason));
3952
+ }
3953
+ const ci = resolveSettledStats(ciResult, "CI");
3954
+ return {
3955
+ reference,
3956
+ ci,
3957
+ referenceSession
3958
+ };
3420
3959
  }
3421
3960
  var collectSchemaStatsActor = fromPromise(
3422
3961
  async ({ input }) => {
@@ -3426,43 +3965,80 @@ var collectSchemaStatsActor = fromPromise(
3426
3965
  ciDbUrl,
3427
3966
  referenceStrategy = "rebuild",
3428
3967
  queryProduction,
3968
+ includeCiSemantic = false,
3969
+ expectedDrift = [],
3429
3970
  tmpDir
3430
3971
  } = input;
3431
3972
  console.log(
3432
3973
  "[schema-stats] Collecting schema statistics (Reference/CI/Production, parallel)..."
3433
3974
  );
3434
3975
  const productionUrl = process.env.GH_DATABASE_URL_ADMIN || process.env.GH_DATABASE_URL;
3435
- const [referenceResult, productionResult] = await Promise.allSettled([
3436
- resolveReferenceAndCiStats({
3437
- repoRoot,
3438
- tmpDir,
3439
- referenceDbUrl,
3440
- ciDbUrl,
3441
- referenceStrategy
3442
- }),
3443
- collectEnvironmentStats(queryProduction ? productionUrl ?? null : null, "Production")
3444
- ]);
3445
- let local;
3446
- let ci;
3447
- if (referenceResult.status === "fulfilled") {
3448
- local = referenceResult.value[0];
3449
- ci = referenceResult.value[1];
3450
- } else {
3451
- console.warn(
3452
- `[schema-stats] Failed to collect reference/CI schema statistics: ${referenceResult.reason}`
3453
- );
3454
- const reason = String(referenceResult.reason);
3455
- local = unavailableStats(reason);
3456
- ci = unavailableStats(reason);
3457
- }
3458
- const production = resolveSettledProductionStats(productionResult);
3459
- return {
3460
- schemaStats: {
3461
- local,
3462
- ci,
3463
- production
3976
+ const includeProductionSemantic = queryProduction;
3977
+ let referenceSession = null;
3978
+ try {
3979
+ const [referenceResult, productionResult] = await Promise.allSettled([
3980
+ resolveReferenceAndCiStats({
3981
+ repoRoot,
3982
+ tmpDir,
3983
+ referenceDbUrl,
3984
+ ciDbUrl,
3985
+ referenceStrategy,
3986
+ includeCiCanonical: false
3987
+ }),
3988
+ collectEnvironmentStats(queryProduction ? productionUrl ?? null : null, "Production")
3989
+ ]);
3990
+ let local;
3991
+ let ci;
3992
+ if (referenceResult.status === "fulfilled") {
3993
+ local = referenceResult.value.reference;
3994
+ ci = referenceResult.value.ci;
3995
+ referenceSession = referenceResult.value.referenceSession;
3996
+ } else {
3997
+ console.warn(
3998
+ `[schema-stats] Failed to collect reference/CI schema statistics: ${referenceResult.reason}`
3999
+ );
4000
+ const reason = String(referenceResult.reason);
4001
+ local = unavailableStats(reason);
4002
+ ci = unavailableStats(reason);
3464
4003
  }
3465
- };
4004
+ const production = resolveSettledProductionStats(productionResult);
4005
+ const shouldCollectProductionSemantic = includeProductionSemantic && shouldCollectProductionSemanticSnapshot({
4006
+ reference: local,
4007
+ production,
4008
+ expectedDrift
4009
+ });
4010
+ const shouldCollectCiSemantic = includeCiSemantic && shouldCollectCiSemanticSnapshot(local, ci);
4011
+ if (shouldCollectProductionSemantic || shouldCollectCiSemantic) {
4012
+ console.log(
4013
+ `[schema-stats] Deferred semantic diff collection: production=${shouldCollectProductionSemantic ? "on" : "off"} ci=${shouldCollectCiSemantic ? "on" : "off"}`
4014
+ );
4015
+ if (referenceStrategy === "reuse-ci") {
4016
+ await collectCanonicalSnapshot(ci, ciDbUrl, "CI");
4017
+ local = ci.available === false ? unavailableStats(ci.error ?? "CI database is unavailable") : cloneEnvironmentStats(ci);
4018
+ } else if (referenceSession) {
4019
+ await collectCanonicalSnapshot(local, referenceSession.dsn, "Reference");
4020
+ }
4021
+ if (shouldCollectCiSemantic && referenceStrategy !== "reuse-ci") {
4022
+ await collectCanonicalSnapshot(ci, ciDbUrl, "CI");
4023
+ }
4024
+ if (shouldCollectProductionSemantic) {
4025
+ await collectCanonicalSnapshot(production, productionUrl ?? null, "Production");
4026
+ }
4027
+ } else {
4028
+ console.log(
4029
+ "[schema-stats] Skipping semantic diff collection (basic drift already decisive)"
4030
+ );
4031
+ }
4032
+ return {
4033
+ schemaStats: {
4034
+ local,
4035
+ ci,
4036
+ production
4037
+ }
4038
+ };
4039
+ } finally {
4040
+ referenceSession?.cleanup();
4041
+ }
3466
4042
  }
3467
4043
  );
3468
4044
 
@@ -3587,6 +4163,9 @@ var installPgTapActor = fromPromise(
3587
4163
 
3588
4164
  // src/commands/ci/machine/actors/db/production-preview.ts
3589
4165
  init_esm_shims();
4166
+ var ANSI_ESCAPE_CHAR = String.fromCharCode(27);
4167
+ var ANSI_ESCAPE_PATTERN = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*[a-zA-Z]`, "g");
4168
+ var DEFAULT_PRODUCTION_PREVIEW_TIMEOUT_MS = 10 * 6e4;
3590
4169
  function buildSkipResult(hasUrl) {
3591
4170
  return {
3592
4171
  preview: {
@@ -3594,12 +4173,16 @@ function buildSkipResult(hasUrl) {
3594
4173
  planSql: null,
3595
4174
  hazards: [],
3596
4175
  hasChanges: false,
3597
- error: !hasUrl ? "GH_DATABASE_URL_ADMIN / GH_DATABASE_URL not set" : null
4176
+ error: !hasUrl ? "GH_DATABASE_URL_ADMIN not set" : null
3598
4177
  }
3599
4178
  };
3600
4179
  }
3601
4180
  function isLogMessage(line) {
3602
4181
  const trimmed = line.trim();
4182
+ const statusPrefixes = ["\u2713", "\u2717", "\u26A0", "\u2139", "\u{1F4E6}", "\u2501"];
4183
+ if (statusPrefixes.some((prefix) => trimmed.startsWith(prefix))) {
4184
+ return true;
4185
+ }
3603
4186
  const logPrefixes = [
3604
4187
  /^\[(?:DEBUG|INFO|WARN|ERROR|TRACE)\]/i,
3605
4188
  /^\d{4}-\d{2}-\d{2}/,
@@ -3607,8 +4190,6 @@ function isLogMessage(line) {
3607
4190
  /^(?:DEBUG|INFO|WARN|ERROR):?\s/i,
3608
4191
  /^▶/,
3609
4192
  // Our progress indicator
3610
- /^(?:✓|✗|⚠|ℹ️|📦|━)/,
3611
- // Status indicators and emojis (alternation, not character class)
3612
4193
  /completed in \d+/i,
3613
4194
  // Duration messages
3614
4195
  /^\s*#/,
@@ -3660,11 +4241,13 @@ function extractSqlFromLines(lines) {
3660
4241
  return null;
3661
4242
  }
3662
4243
  function extractSqlFromSchemaChanges(fullOutput) {
3663
- const schemaChangesMatch = fullOutput.match(
3664
- /(?:Schema changes?|Changes to apply|Plan):?\s*([\s\S]*?)(?:(?:✅|❌|Summary|$))/i
3665
- );
3666
- if (schemaChangesMatch?.[1]?.trim()) {
3667
- const content = schemaChangesMatch[1].trim();
4244
+ const sectionMatch = fullOutput.match(/(?:Schema changes?|Changes to apply|Plan):?\s*/i);
4245
+ if (sectionMatch && typeof sectionMatch.index === "number") {
4246
+ const contentStart = sectionMatch.index + sectionMatch[0].length;
4247
+ const sectionBody = fullOutput.slice(contentStart);
4248
+ const boundaryMarkers = ["\n\u2705", "\n\u274C", "\nSummary"];
4249
+ const boundaryIndexes = boundaryMarkers.map((marker) => sectionBody.indexOf(marker)).filter((index) => index >= 0);
4250
+ const content = (boundaryIndexes.length > 0 ? sectionBody.slice(0, Math.min(...boundaryIndexes)) : sectionBody).trim();
3668
4251
  const lines = content.split("\n");
3669
4252
  const sqlLines = lines.filter(isValidSqlStatement);
3670
4253
  if (sqlLines.length > 0) {
@@ -3763,9 +4346,14 @@ function collectCommentHazards(lines, idempotentRoles) {
3763
4346
  return hazards;
3764
4347
  }
3765
4348
  function appendEmojiHazards(fullOutput, idempotentRoles, hazards) {
3766
- const emojiMatches = fullOutput.matchAll(/(?:🔴|🟠|🟡|⚠️)\s*(\w+):\s*(.+)/gu);
3767
- for (const match of emojiMatches) {
3768
- if (!match[1] || !match[2]) continue;
4349
+ const hazardPrefixes = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26A0"];
4350
+ for (const line of fullOutput.split("\n")) {
4351
+ const trimmed = line.trim();
4352
+ const matchedPrefix = hazardPrefixes.find((prefix) => trimmed.startsWith(prefix));
4353
+ if (!matchedPrefix) continue;
4354
+ const payload = trimmed.slice(matchedPrefix.length).trim();
4355
+ const match = payload.match(/^(\w+):\s*(.+)$/u);
4356
+ if (!match?.[1] || !match[2]) continue;
3769
4357
  const type = match[1].toUpperCase();
3770
4358
  const message = match[2].trim();
3771
4359
  if (isIdempotentRoleHazard(type, message, void 0, idempotentRoles)) {
@@ -3786,8 +4374,76 @@ function extractHazardsWithContext(fullOutput, repoRoot) {
3786
4374
  appendEmojiHazards(fullOutput, idempotentRoles, hazards);
3787
4375
  return hazards;
3788
4376
  }
4377
+ function stripAnsi(text) {
4378
+ return text.replace(ANSI_ESCAPE_PATTERN, "");
4379
+ }
4380
+ function hasDisplayablePlanSql(planSql) {
4381
+ const normalized = planSql?.trim();
4382
+ if (!normalized) return false;
4383
+ return normalized !== "-- No changes after filtering";
4384
+ }
4385
+ var PreviewOutputSchema = z.object({
4386
+ success: z.boolean(),
4387
+ planSql: z.string().optional(),
4388
+ filteredPlanSql: z.string().optional(),
4389
+ hazards: z.array(z.string()),
4390
+ checkOnly: z.boolean().optional(),
4391
+ metrics: z.object({
4392
+ totalMs: z.number(),
4393
+ idempotentMs: z.number().optional(),
4394
+ applyMs: z.number().optional()
4395
+ }).optional()
4396
+ });
4397
+ function tryParseStructuredOutput(stdout) {
4398
+ try {
4399
+ const trimmed = stdout.trim();
4400
+ if (!trimmed.startsWith("{")) return null;
4401
+ const envelope = JSON.parse(trimmed);
4402
+ if (!envelope.ok || !envelope.data) return null;
4403
+ const parsed = PreviewOutputSchema.safeParse(envelope.data);
4404
+ if (!parsed.success) return null;
4405
+ const data = parsed.data;
4406
+ const effectivePlanSql = data.filteredPlanSql || data.planSql || null;
4407
+ const hasChanges = hasDisplayablePlanSql(effectivePlanSql) || data.hazards.length > 0;
4408
+ return {
4409
+ planSql: effectivePlanSql,
4410
+ filteredPlanSql: data.filteredPlanSql || null,
4411
+ hazards: data.hazards,
4412
+ hasChanges,
4413
+ checkOnly: data.checkOnly ?? false,
4414
+ metrics: data.metrics
4415
+ };
4416
+ } catch {
4417
+ return null;
4418
+ }
4419
+ }
4420
+ function formatDurationMs(durationMs) {
4421
+ if (durationMs < 1e3) return `${durationMs}ms`;
4422
+ return `${Math.round(durationMs / 100) / 10}s`;
4423
+ }
4424
+ function resolveProductionPreviewTimeoutMs() {
4425
+ const raw = process.env.RUNA_PRODUCTION_PREVIEW_TIMEOUT_MS?.trim();
4426
+ if (!raw) return DEFAULT_PRODUCTION_PREVIEW_TIMEOUT_MS;
4427
+ const parsed = Number.parseInt(raw, 10);
4428
+ if (!Number.isFinite(parsed) || parsed <= 0) {
4429
+ return DEFAULT_PRODUCTION_PREVIEW_TIMEOUT_MS;
4430
+ }
4431
+ return parsed;
4432
+ }
4433
+ function startHeartbeat(label, intervalMs = 3e4) {
4434
+ const startedAt = Date.now();
4435
+ const timer = setInterval(() => {
4436
+ const elapsedMs = Date.now() - startedAt;
4437
+ console.log(`\u25B6 ${label}: still running (${formatDurationMs(elapsedMs)} elapsed)`);
4438
+ }, intervalMs);
4439
+ timer.unref?.();
4440
+ return {
4441
+ startedAt,
4442
+ stop: () => clearInterval(timer)
4443
+ };
4444
+ }
3789
4445
  function detectSchemaChangeState(fullOutput) {
3790
- const lowerOutput = fullOutput.toLowerCase();
4446
+ const lowerOutput = stripAnsi(fullOutput).toLowerCase();
3791
4447
  const changesIndicators = [
3792
4448
  "check mode: schema changes detected (not applied)",
3793
4449
  "schema changes: detected"
@@ -3818,6 +4474,21 @@ async function writeLogFile(logFile, content) {
3818
4474
  } catch {
3819
4475
  }
3820
4476
  }
4477
+ async function closeLogStream(stream) {
4478
+ await new Promise((resolve) => {
4479
+ stream.end(() => resolve());
4480
+ });
4481
+ }
4482
+ function normalizeStreamChunk(chunk) {
4483
+ if (typeof chunk === "string") return chunk;
4484
+ return Buffer.from(chunk).toString("utf8");
4485
+ }
4486
+ function normalizePreviewError(error, timeoutMs) {
4487
+ if (error && typeof error === "object" && "timedOut" in error && error.timedOut === true) {
4488
+ return `Production preview timed out after ${formatDurationMs(timeoutMs)}`;
4489
+ }
4490
+ return error instanceof Error ? error.message : String(error);
4491
+ }
3821
4492
  function isNoiseLine(line) {
3822
4493
  const trimmed = line.trim();
3823
4494
  if (!trimmed) return true;
@@ -3862,17 +4533,28 @@ function buildErrorResult(error, databaseUrl) {
3862
4533
  }
3863
4534
  };
3864
4535
  }
3865
- function buildPreviewFromOutput(fullOutput, repoRoot, productionUrl) {
3866
- const lines = fullOutput.split("\n");
3867
- const planSql = extractSqlFromLines(lines) || extractSqlFromSchemaChanges(fullOutput);
3868
- const hazardDetails = extractHazardsWithContext(fullOutput, repoRoot);
4536
+ function buildPreviewFromOutput(stdout, _stderr, fullOutput, repoRoot, productionUrl) {
4537
+ const structured = tryParseStructuredOutput(stdout);
4538
+ if (structured) {
4539
+ return {
4540
+ planSql: structured.planSql?.substring(0, 5e3) || null,
4541
+ hazards: structured.hazards,
4542
+ hazardDetails: void 0,
4543
+ hasChanges: structured.hasChanges,
4544
+ error: null
4545
+ };
4546
+ }
4547
+ const cleanOutput = stripAnsi(fullOutput);
4548
+ const lines = cleanOutput.split("\n");
4549
+ const planSql = extractSqlFromLines(lines) || extractSqlFromSchemaChanges(cleanOutput);
4550
+ const hazardDetails = extractHazardsWithContext(cleanOutput, repoRoot);
3869
4551
  const hazards = hazardDetails.map((h) => `${h.type}: ${h.message}`);
3870
- const schemaChangeState = detectSchemaChangeState(fullOutput);
4552
+ const schemaChangeState = detectSchemaChangeState(cleanOutput);
3871
4553
  const hasSqlPlan = planSql !== null && planSql.trim().length > 0;
3872
4554
  const hasHazards = hazards.length > 0;
3873
4555
  const hasChanges = schemaChangeState === "changes" ? true : schemaChangeState === "no-changes" ? false : hasSqlPlan || hasHazards;
3874
4556
  const effectivePlanSql = hasChanges ? planSql?.substring(0, 5e3) || null : null;
3875
- const rawError = extractErrorMessage(fullOutput);
4557
+ const rawError = extractErrorMessage(cleanOutput);
3876
4558
  return {
3877
4559
  planSql: effectivePlanSql,
3878
4560
  hazards,
@@ -3881,9 +4563,9 @@ function buildPreviewFromOutput(fullOutput, repoRoot, productionUrl) {
3881
4563
  error: enhanceConnectionError(rawError, productionUrl)
3882
4564
  };
3883
4565
  }
3884
- function buildSuccessPreviewResult(exitCode, fullOutput, repoRoot, productionUrl) {
4566
+ function buildSuccessPreviewResult(exitCode, stdout, stderr, fullOutput, repoRoot, productionUrl) {
3885
4567
  const isSuccess = isCommandSuccess(exitCode, fullOutput);
3886
- const preview = buildPreviewFromOutput(fullOutput, repoRoot, productionUrl);
4568
+ const preview = buildPreviewFromOutput(stdout, stderr, fullOutput, repoRoot, productionUrl);
3887
4569
  return {
3888
4570
  preview: {
3889
4571
  executed: true,
@@ -3898,30 +4580,85 @@ function buildSuccessPreviewResult(exitCode, fullOutput, repoRoot, productionUrl
3898
4580
  var productionPreviewActor = fromPromise(
3899
4581
  async ({ input }) => {
3900
4582
  const { repoRoot, tmpDir, shouldExecute } = input;
3901
- const productionUrl = process.env.GH_DATABASE_URL_ADMIN || process.env.GH_DATABASE_URL;
4583
+ const productionUrl = process.env.GH_DATABASE_URL_ADMIN;
3902
4584
  if (!shouldExecute || !productionUrl) {
3903
4585
  return buildSkipResult(!!productionUrl);
3904
4586
  }
3905
4587
  const logFile = path4.join(tmpDir, "ci-production-preview.log");
3906
- console.log("\u25B6 production preview (dry-run): runa db apply production --check");
4588
+ const timeoutMs = resolveProductionPreviewTimeoutMs();
4589
+ console.log("\u25B6 production preview (dry-run): runa db apply production --check --compare-only");
4590
+ console.log(
4591
+ "\u25B6 production preview details: pg-schema-diff plan + hazard extraction (compare-only)"
4592
+ );
4593
+ console.log(`\u25B6 production preview timeout: ${formatDurationMs(timeoutMs)}`);
4594
+ const heartbeat = startHeartbeat("production preview");
4595
+ const logStream = createWriteStream(logFile, { flags: "a" });
4596
+ const stdoutChunks = [];
4597
+ const stderrChunks = [];
3907
4598
  try {
3908
- const result = await execa(
4599
+ const child = execa(
3909
4600
  "pnpm",
3910
- ["exec", "runa", "db", "apply", "production", "--check", "--verbose"],
4601
+ ["exec", "runa", "db", "apply", "production", "--check", "--compare-only"],
3911
4602
  {
3912
4603
  cwd: repoRoot,
3913
- env: { ...process.env, GH_DATABASE_URL_ADMIN: productionUrl },
4604
+ env: {
4605
+ ...process.env,
4606
+ GH_DATABASE_URL_ADMIN: productionUrl,
4607
+ RUNA_OUTPUT_FORMAT: "json"
4608
+ },
4609
+ shell: false,
4610
+ stdio: ["ignore", "pipe", "pipe"],
4611
+ timeout: timeoutMs,
3914
4612
  reject: false
3915
4613
  }
3916
4614
  );
3917
- const stdout = result.stdout || "";
3918
- const stderr = result.stderr || "";
4615
+ child.stdout?.on("data", (chunk) => {
4616
+ const text = normalizeStreamChunk(chunk);
4617
+ stdoutChunks.push(text);
4618
+ logStream.write(text);
4619
+ });
4620
+ child.stderr?.on("data", (chunk) => {
4621
+ const text = normalizeStreamChunk(chunk);
4622
+ stderrChunks.push(text);
4623
+ process.stderr.write(text);
4624
+ logStream.write(text);
4625
+ });
4626
+ const result = await child;
4627
+ const stdout = stdoutChunks.join("");
4628
+ const stderr = stderrChunks.join("");
3919
4629
  const fullOutput = `${stdout}
3920
4630
  ${stderr}`;
3921
- await writeLogFile(logFile, fullOutput);
3922
- return buildSuccessPreviewResult(result.exitCode, fullOutput, repoRoot, productionUrl);
4631
+ const durationMs = Date.now() - heartbeat.startedAt;
4632
+ heartbeat.stop();
4633
+ await closeLogStream(logStream);
4634
+ const structured = tryParseStructuredOutput(stdout);
4635
+ if (structured?.metrics) {
4636
+ console.log(
4637
+ `\u25B6 production preview complete in ${formatDurationMs(durationMs)} (total=${formatDurationMs(structured.metrics.totalMs ?? durationMs)}, idempotent=${formatDurationMs(structured.metrics.idempotentMs ?? 0)}, diff=${formatDurationMs(structured.metrics.applyMs ?? 0)})`
4638
+ );
4639
+ } else {
4640
+ console.log(`\u25B6 production preview complete in ${formatDurationMs(durationMs)}`);
4641
+ }
4642
+ console.log(`\u25B6 production preview log: ${logFile}`);
4643
+ return buildSuccessPreviewResult(
4644
+ result.exitCode,
4645
+ stdout,
4646
+ stderr,
4647
+ fullOutput,
4648
+ repoRoot,
4649
+ productionUrl
4650
+ );
3923
4651
  } catch (error) {
3924
- return buildErrorResult(error, productionUrl);
4652
+ heartbeat.stop();
4653
+ const stdout = stdoutChunks.join("");
4654
+ const stderr = stderrChunks.join("");
4655
+ const fullOutput = `${stdout}
4656
+ ${stderr}`.trim();
4657
+ await closeLogStream(logStream);
4658
+ if (fullOutput) {
4659
+ await writeLogFile(logFile, fullOutput);
4660
+ }
4661
+ return buildErrorResult(normalizePreviewError(error, timeoutMs), productionUrl);
3925
4662
  }
3926
4663
  }
3927
4664
  );
@@ -4246,56 +4983,84 @@ function analyzePostCheckResult(params) {
4246
4983
  commandFailed: !checkSucceeded
4247
4984
  };
4248
4985
  }
4986
+ function createSyncCommandArgs(params) {
4987
+ if (params.useDbApply) {
4988
+ return [
4989
+ "exec",
4990
+ "runa",
4991
+ "db",
4992
+ "apply",
4993
+ params.envArg,
4994
+ "--auto-approve",
4995
+ "--no-seed",
4996
+ "--verbose"
4997
+ ];
4998
+ }
4999
+ return [
5000
+ "exec",
5001
+ "runa",
5002
+ "db",
5003
+ "sync",
5004
+ params.envArg,
5005
+ "--auto-approve",
5006
+ "--verbose",
5007
+ ...params.skipCodegen ? ["--skip-codegen"] : []
5008
+ ];
5009
+ }
5010
+ function createCheckCommandArgs(params) {
5011
+ if (params.useDbApply) {
5012
+ return ["exec", "runa", "db", "apply", params.envArg, "--check", "--no-seed", "--verbose"];
5013
+ }
5014
+ return [
5015
+ "exec",
5016
+ "runa",
5017
+ "db",
5018
+ "sync",
5019
+ params.envArg,
5020
+ "--check",
5021
+ "--verbose",
5022
+ ...params.skipCodegen ? ["--skip-codegen"] : []
5023
+ ];
5024
+ }
5025
+ function readOptionalLogContent(filePath) {
5026
+ try {
5027
+ return readFileSync(filePath, "utf-8");
5028
+ } catch {
5029
+ return "";
5030
+ }
5031
+ }
5032
+ function createSchemaDriftSnapshot(params) {
5033
+ return {
5034
+ beforeSql: params.logContent.substring(0, 5e3),
5035
+ afterSql: params.postCheckHasDrift ? params.afterCheckLogContent.substring(0, 5e3) : "",
5036
+ checkExecuted: params.checkExecuted,
5037
+ hasDrift: params.postCheckHasDrift,
5038
+ changeStats: params.changeStats,
5039
+ gitDiff: params.gitDiff,
5040
+ logs: {
5041
+ beforeCheckLogPath: params.logFile,
5042
+ applyLogPath: params.logFile,
5043
+ afterCheckLogPath: params.afterCheckLogFile
5044
+ }
5045
+ };
5046
+ }
4249
5047
  var syncSchemaActor = fromPromise(
4250
5048
  async ({ input }) => {
4251
- const { repoRoot, tmpDir, databaseUrl, mode, skipCodegen } = input;
5049
+ const { repoRoot, tmpDir, databaseUrl, mode, skipCodegen, skipPostCheck = false } = input;
4252
5050
  const envArg = mode === "ci-local" ? "local" : "preview";
4253
5051
  const useDbApply = mode !== "ci-local";
4254
5052
  const gitDiff = getSchemaGitDiff(repoRoot);
5053
+ const syncArgs = createSyncCommandArgs({ useDbApply, envArg, skipCodegen });
5054
+ const checkArgs = createCheckCommandArgs({ useDbApply, envArg, skipCodegen });
5055
+ const logFile = path4.join(tmpDir, `ci-db-${useDbApply ? "apply" : "sync"}-${envArg}.log`);
5056
+ const afterCheckLogFile = path4.join(tmpDir, `ci-db-check-${envArg}.log`);
4255
5057
  try {
4256
5058
  const databaseUrlForRuntime = normalizeDatabaseUrlForDdl(databaseUrl);
4257
5059
  const baseEnv = {
4258
5060
  ...process.env,
4259
- // Runtime (app/tests): session pooler + IPv4 where needed
4260
5061
  DATABASE_URL: databaseUrlForRuntime,
4261
- // Schema ops (DDL): keep raw/admin URL if provided
4262
5062
  DATABASE_URL_ADMIN: databaseUrl
4263
5063
  };
4264
- const syncArgs = useDbApply ? [
4265
- "exec",
4266
- "runa",
4267
- "db",
4268
- "apply",
4269
- envArg,
4270
- "--auto-approve",
4271
- // Allow DELETES_DATA hazards in preview
4272
- "--no-seed",
4273
- // Seeds applied separately by applySeedsActor
4274
- "--verbose"
4275
- // Always verbose for full traceability
4276
- ] : [
4277
- "exec",
4278
- "runa",
4279
- "db",
4280
- "sync",
4281
- envArg,
4282
- "--auto-approve",
4283
- "--verbose",
4284
- // Always verbose for full traceability
4285
- ...skipCodegen ? ["--skip-codegen"] : []
4286
- ];
4287
- const checkArgs = useDbApply ? ["exec", "runa", "db", "apply", envArg, "--check", "--no-seed", "--verbose"] : [
4288
- "exec",
4289
- "runa",
4290
- "db",
4291
- "sync",
4292
- envArg,
4293
- "--check",
4294
- "--verbose",
4295
- ...skipCodegen ? ["--skip-codegen"] : []
4296
- ];
4297
- const logFile = path4.join(tmpDir, `ci-db-${useDbApply ? "apply" : "sync"}-${envArg}.log`);
4298
- const afterCheckLogFile = path4.join(tmpDir, `ci-db-check-${envArg}.log`);
4299
5064
  await runLogged({
4300
5065
  cwd: repoRoot,
4301
5066
  env: baseEnv,
@@ -4304,6 +5069,21 @@ var syncSchemaActor = fromPromise(
4304
5069
  args: syncArgs,
4305
5070
  logFile
4306
5071
  });
5072
+ const logContent = readOptionalLogContent(logFile);
5073
+ const changeStats = parseSchemaChangeStats(logContent, null, repoRoot);
5074
+ if (skipPostCheck) {
5075
+ const schemaDrift2 = createSchemaDriftSnapshot({
5076
+ logContent,
5077
+ afterCheckLogContent: "",
5078
+ postCheckHasDrift: false,
5079
+ checkExecuted: false,
5080
+ changeStats,
5081
+ gitDiff,
5082
+ logFile,
5083
+ afterCheckLogFile
5084
+ });
5085
+ return { applied: true, schemaDrift: schemaDrift2 };
5086
+ }
4307
5087
  const postCheckResult = await runLogged({
4308
5088
  cwd: repoRoot,
4309
5089
  env: baseEnv,
@@ -4313,17 +5093,7 @@ var syncSchemaActor = fromPromise(
4313
5093
  logFile: afterCheckLogFile,
4314
5094
  reject: false
4315
5095
  });
4316
- let logContent = "";
4317
- let afterCheckLogContent = "";
4318
- try {
4319
- logContent = readFileSync(logFile, "utf-8");
4320
- } catch {
4321
- }
4322
- try {
4323
- afterCheckLogContent = readFileSync(afterCheckLogFile, "utf-8");
4324
- } catch {
4325
- }
4326
- const changeStats = parseSchemaChangeStats(logContent, null, repoRoot);
5096
+ const afterCheckLogContent = readOptionalLogContent(afterCheckLogFile);
4327
5097
  const residualStats = parseSchemaChangeStats(afterCheckLogContent, null, repoRoot);
4328
5098
  const fullPostCheckOutput = `${postCheckResult.stdout || ""}
4329
5099
  ${postCheckResult.stderr || ""}`;
@@ -4340,20 +5110,16 @@ ${postCheckResult.stderr || ""}`;
4340
5110
  )}`
4341
5111
  );
4342
5112
  }
4343
- const schemaDrift = {
4344
- beforeSql: logContent.substring(0, 5e3),
4345
- // Truncate for comment
4346
- afterSql: postCheckAnalysis.hasDrift ? afterCheckLogContent.substring(0, 5e3) : "",
5113
+ const schemaDrift = createSchemaDriftSnapshot({
5114
+ logContent,
5115
+ afterCheckLogContent,
5116
+ postCheckHasDrift: postCheckAnalysis.hasDrift,
4347
5117
  checkExecuted: true,
4348
- hasDrift: postCheckAnalysis.hasDrift,
4349
5118
  changeStats,
4350
5119
  gitDiff,
4351
- logs: {
4352
- beforeCheckLogPath: logFile,
4353
- applyLogPath: logFile,
4354
- afterCheckLogPath: afterCheckLogFile
4355
- }
4356
- };
5120
+ logFile,
5121
+ afterCheckLogFile
5122
+ });
4357
5123
  return { applied: true, schemaDrift };
4358
5124
  } catch (error) {
4359
5125
  const schemaDrift = {
@@ -4433,23 +5199,6 @@ fromPromise(
4433
5199
  }
4434
5200
  );
4435
5201
 
4436
- // src/commands/ci/machine/actors/finalize/summary.ts
4437
- init_esm_shims();
4438
- var writeSummaryActor = fromPromise(
4439
- async ({ input }) => {
4440
- const { cwd, summary } = input;
4441
- try {
4442
- const filePath = await writeCiSummary({ cwd, summary });
4443
- return { filePath };
4444
- } catch (error) {
4445
- return {
4446
- filePath: "",
4447
- error: error instanceof Error ? error.message : String(error)
4448
- };
4449
- }
4450
- }
4451
- );
4452
-
4453
5202
  // src/commands/ci/machine/actors/setup/index.ts
4454
5203
  init_esm_shims();
4455
5204
 
@@ -4459,6 +5208,7 @@ init_esm_shims();
4459
5208
  // src/commands/ci/commands/ci-supabase-local.ts
4460
5209
  init_esm_shims();
4461
5210
  init_constants();
5211
+ init_local_supabase();
4462
5212
  function isPortAvailable(port) {
4463
5213
  return new Promise((resolve) => {
4464
5214
  const server = net.createServer();
@@ -4485,9 +5235,9 @@ function detectSupabaseContainers() {
4485
5235
  async function checkSupabasePortConflicts(repoRoot) {
4486
5236
  let dbPort = 54322;
4487
5237
  try {
4488
- const { readFileSync: readFileSync4 } = await import('fs');
5238
+ const { readFileSync: readFileSync5 } = await import('fs');
4489
5239
  const configPath = path4.join(repoRoot, "supabase", "config.toml");
4490
- const content = readFileSync4(configPath, "utf-8");
5240
+ const content = readFileSync5(configPath, "utf-8");
4491
5241
  const match = /\[db\][^[]*?port\s*=\s*(\d+)/s.exec(content);
4492
5242
  if (match?.[1]) dbPort = Number.parseInt(match[1], 10);
4493
5243
  } catch {
@@ -4519,6 +5269,10 @@ async function checkSupabasePortConflicts(repoRoot) {
4519
5269
  console.warn("");
4520
5270
  }
4521
5271
  async function startSupabaseLocal(params) {
5272
+ if (params.skipStart) {
5273
+ console.log("\u2139\uFE0F Skipping local Supabase start (assumed ready by caller)");
5274
+ return;
5275
+ }
4522
5276
  await checkSupabasePortConflicts(params.repoRoot);
4523
5277
  const exclude = process.env.RUNA_CI_SUPABASE_EXCLUDE ?? "studio,edge-runtime,storage-api,realtime,imgproxy,mailpit,logflare,vector,supavisor";
4524
5278
  await runLogged({
@@ -4531,13 +5285,17 @@ async function startSupabaseLocal(params) {
4531
5285
  });
4532
5286
  }
4533
5287
  function getDefaultLocalSupabaseUrl(repoRoot) {
4534
- const ports = detectSupabasePorts(repoRoot);
4535
- return `http://127.0.0.1:${ports.api}`;
5288
+ const ports = detectLocalSupabasePorts(repoRoot);
5289
+ return `http://${ports.host}:${ports.api}`;
5290
+ }
5291
+ function getConfiguredLocalSupabaseUrl(repoRoot) {
5292
+ const ports = detectLocalSupabasePorts(repoRoot, { skipStatusDetection: true });
5293
+ return `http://${ports.host}:${ports.api}`;
4536
5294
  }
4537
5295
  function getDefaultLocalDatabaseUrl(repoRoot) {
4538
5296
  try {
4539
- const ports = detectSupabasePorts(repoRoot);
4540
- return `postgresql://postgres:postgres@127.0.0.1:${ports.db}/postgres`;
5297
+ const ports = detectLocalSupabasePorts(repoRoot);
5298
+ return `postgresql://postgres:postgres@${ports.host}:${ports.db}/postgres`;
4541
5299
  } catch {
4542
5300
  console.warn("\u26A0\uFE0F Could not detect Supabase ports, using default port 54322");
4543
5301
  return "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
@@ -4545,9 +5303,32 @@ function getDefaultLocalDatabaseUrl(repoRoot) {
4545
5303
  }
4546
5304
  var DEFAULT_LOCAL_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0";
4547
5305
  async function resolveLocalSupabaseEnv(params) {
4548
- const maxRetries = parseIntOr(process.env.RUNA_CI_SUPABASE_STATUS_RETRIES, 20);
4549
- const sleepSeconds = parseIntOr(process.env.RUNA_CI_SUPABASE_STATUS_SLEEP_SECONDS, 3);
5306
+ const maxRetries = Math.max(1, parseIntOr(process.env.RUNA_CI_SUPABASE_STATUS_RETRIES, 20));
5307
+ const sleepSeconds = Math.max(
5308
+ 0,
5309
+ parseIntOr(process.env.RUNA_CI_SUPABASE_STATUS_SLEEP_SECONDS, 3)
5310
+ );
5311
+ if (params.assumeReady) {
5312
+ console.log("\u2139\uFE0F Reusing pre-started local Supabase connection (assume-ready)");
5313
+ return {
5314
+ supabaseUrl: getConfiguredLocalSupabaseUrl(params.repoRoot),
5315
+ anonKey: DEFAULT_LOCAL_ANON_KEY,
5316
+ diagnostics: {
5317
+ strategy: "assume-ready",
5318
+ finalSource: "assumed-local-config",
5319
+ attempts: 0,
5320
+ maxAttempts: maxRetries,
5321
+ sleepSeconds,
5322
+ waitedMs: 0
5323
+ }
5324
+ };
5325
+ }
5326
+ console.log(
5327
+ `\u2139\uFE0F Resolving local Supabase connection via status polling (maxRetries=${maxRetries}, interval=${sleepSeconds}s)`
5328
+ );
5329
+ let lastError;
4550
5330
  for (let i = 0; i < maxRetries; i++) {
5331
+ const attempt = i + 1;
4551
5332
  try {
4552
5333
  const res = await runLogged({
4553
5334
  cwd: params.repoRoot,
@@ -4557,7 +5338,9 @@ async function resolveLocalSupabaseEnv(params) {
4557
5338
  args: ["status", "--output", "json"],
4558
5339
  logFile: path4.join(params.tmpDir, `supabase-status-${i + 1}.log`)
4559
5340
  });
4560
- const parsed = JSON.parse(String(res.stdout ?? "{}"));
5341
+ const rawStdout = String(res.stdout ?? "{}");
5342
+ const jsonMatch = rawStdout.match(/\{[\s\S]*\}/);
5343
+ const parsed = JSON.parse(jsonMatch?.[0] ?? "{}");
4561
5344
  const out = z.object({
4562
5345
  API_URL: z.string().optional(),
4563
5346
  PUBLISHABLE_KEY: z.string().min(1).optional(),
@@ -4565,17 +5348,51 @@ async function resolveLocalSupabaseEnv(params) {
4565
5348
  }).passthrough().parse(parsed);
4566
5349
  const supabaseUrl = out.API_URL || getDefaultLocalSupabaseUrl(params.repoRoot);
4567
5350
  const anonKey = out.PUBLISHABLE_KEY ?? out.ANON_KEY ?? DEFAULT_LOCAL_ANON_KEY;
4568
- return { supabaseUrl, anonKey };
4569
- } catch {
5351
+ const waitedMs = i * sleepSeconds * 1e3;
5352
+ console.log(
5353
+ `\u2139\uFE0F Supabase status resolved after ${attempt} attempt${attempt === 1 ? "" : "s"} (waited ${Math.round(waitedMs / 1e3)}s)`
5354
+ );
5355
+ return {
5356
+ supabaseUrl,
5357
+ anonKey,
5358
+ diagnostics: {
5359
+ strategy: "status-polling",
5360
+ finalSource: "status-json",
5361
+ attempts: attempt,
5362
+ maxAttempts: maxRetries,
5363
+ sleepSeconds,
5364
+ waitedMs,
5365
+ lastError
5366
+ }
5367
+ };
5368
+ } catch (error) {
5369
+ lastError = error instanceof Error ? error.message : String(error);
5370
+ if (attempt < maxRetries) {
5371
+ console.log(
5372
+ `\u2139\uFE0F Supabase status not ready (attempt ${attempt}/${maxRetries}); waiting ${sleepSeconds}s before retry`
5373
+ );
5374
+ }
5375
+ }
5376
+ if (attempt < maxRetries) {
5377
+ await new Promise((r) => setTimeout(r, sleepSeconds * 1e3));
4570
5378
  }
4571
- await new Promise((r) => setTimeout(r, sleepSeconds * 1e3));
4572
5379
  }
4573
5380
  console.warn(
4574
5381
  "\u26A0\uFE0F Failed to resolve Supabase status after retries, using hardcoded local values. CI uses direct PostgreSQL, so this is non-blocking."
4575
5382
  );
4576
5383
  return {
4577
- supabaseUrl: "http://127.0.0.1:54321",
4578
- anonKey: DEFAULT_LOCAL_ANON_KEY
5384
+ supabaseUrl: getConfiguredLocalSupabaseUrl(params.repoRoot),
5385
+ anonKey: DEFAULT_LOCAL_ANON_KEY,
5386
+ diagnostics: {
5387
+ strategy: "fallback-default",
5388
+ finalSource: "default-fallback",
5389
+ attempts: maxRetries,
5390
+ maxAttempts: maxRetries,
5391
+ sleepSeconds,
5392
+ waitedMs: Math.max(0, maxRetries - 1) * sleepSeconds * 1e3,
5393
+ retriesExhausted: true,
5394
+ lastError
5395
+ }
4579
5396
  };
4580
5397
  }
4581
5398
 
@@ -4606,7 +5423,12 @@ var localSetupActor = fromPromise(
4606
5423
  productionUrl,
4607
5424
  supabaseUrl: supabaseInfo.supabaseUrl,
4608
5425
  anonKey: supabaseInfo.anonKey,
4609
- layers
5426
+ layers,
5427
+ diagnostics: {
5428
+ setup: {
5429
+ supabase: supabaseInfo.diagnostics
5430
+ }
5431
+ }
4610
5432
  };
4611
5433
  } catch (error) {
4612
5434
  return {
@@ -4617,6 +5439,7 @@ var localSetupActor = fromPromise(
4617
5439
  supabaseUrl: "",
4618
5440
  anonKey: "",
4619
5441
  layers: [],
5442
+ diagnostics: {},
4620
5443
  error: error instanceof Error ? error.message : String(error)
4621
5444
  };
4622
5445
  }
@@ -4625,25 +5448,160 @@ var localSetupActor = fromPromise(
4625
5448
 
4626
5449
  // src/commands/ci/machine/actors/setup/pr-local.ts
4627
5450
  init_esm_shims();
5451
+
5452
+ // src/commands/ci/machine/helpers.ts
5453
+ init_esm_shims();
5454
+ var CORE_LAYERS = [1, 2, 3];
5455
+ var E2E_LAYER = 4;
5456
+ function getLayersForCorePhase(selectedLayers, mode) {
5457
+ if (mode === "ci-local") {
5458
+ return selectedLayers;
5459
+ }
5460
+ return selectedLayers.filter((l) => CORE_LAYERS.includes(l));
5461
+ }
5462
+ function hasE2ELayer(selectedLayers) {
5463
+ return selectedLayers.includes(E2E_LAYER);
5464
+ }
5465
+ function getRuntimeOwner(context) {
5466
+ return context.input.skipLocalDbStart === true || context.input.assumeSupabaseReady === true || context.diagnostics.setup?.runtimeOwner === "external" || context.diagnostics.setup?.supabase?.strategy === "assume-ready" ? "external" : "sdk";
5467
+ }
5468
+ function shouldReusePreparedRuntime(context) {
5469
+ return getRuntimeOwner(context) === "external";
5470
+ }
5471
+ function getPlaywrightOwner(context) {
5472
+ return context.input.skipPlaywrightInstall === true || context.diagnostics.setup?.playwrightOwner === "external" ? "external" : "sdk";
5473
+ }
5474
+ function shouldReusePreparedPlaywright(context) {
5475
+ return getPlaywrightOwner(context) === "external";
5476
+ }
5477
+ function hasSchemaGitChanges(context) {
5478
+ return (context.schemaDrift?.gitDiff?.filesChanged.length ?? 0) > 0;
5479
+ }
5480
+ function shouldSkipSchemaPostCheck(context) {
5481
+ return context.mode === "ci-pr-local" && context.executionEnv === "github-actions";
5482
+ }
5483
+ function shouldReuseCiReferenceStats(context) {
5484
+ return context.mode !== "ci-local" && context.schemaApplied && context.schemaDrift !== null && context.schemaDrift?.hasDrift === false;
5485
+ }
5486
+ function mergeLayerResults(coreResults, e2eResults) {
5487
+ return { ...coreResults, ...e2eResults };
5488
+ }
5489
+ function convertLayerResults(results) {
5490
+ const layerResults = {};
5491
+ for (const r of results) {
5492
+ let status;
5493
+ if (r.killed) {
5494
+ status = "killed";
5495
+ } else if (r.skipped) {
5496
+ status = "skipped";
5497
+ } else {
5498
+ status = r.success ? "passed" : "failed";
5499
+ }
5500
+ layerResults[r.layer] = {
5501
+ status,
5502
+ exitCode: r.exitCode,
5503
+ // Preserve test count information
5504
+ passed: r.passedTests,
5505
+ total: r.totalTests,
5506
+ failed: r.failedTests,
5507
+ flaky: r.flakyTests
5508
+ };
5509
+ }
5510
+ return layerResults;
5511
+ }
5512
+ function getDatabaseUrlForRuntime(context) {
5513
+ const raw = context.supabase?.appDatabaseUrl ?? context.supabase?.databaseUrlRaw ?? "";
5514
+ return normalizeDatabaseUrlForDdl(raw);
5515
+ }
5516
+ function getSupabaseUrlWithFallback(context) {
5517
+ return context.supabase?.supabaseUrl || getDefaultLocalSupabaseUrl(context.repoRoot ?? void 0);
5518
+ }
5519
+ function getSupabaseAnonKeyWithFallback(context) {
5520
+ return context.supabase?.anonKey || DEFAULT_LOCAL_ANON_KEY;
5521
+ }
5522
+ function buildRuntimeEnv(context, options = {}) {
5523
+ const env = {
5524
+ ...context.input.runtimeEnv ?? {},
5525
+ DATABASE_URL: getDatabaseUrlForRuntime(context),
5526
+ NEXT_PUBLIC_SUPABASE_URL: getSupabaseUrlWithFallback(context),
5527
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: getSupabaseAnonKeyWithFallback(context)
5528
+ };
5529
+ if (options.enablePublicE2EFlag) {
5530
+ env.NEXT_PUBLIC_E2E_TEST = "true";
5531
+ }
5532
+ if (options.enableServerE2EFlag) {
5533
+ env.E2E_TEST = "true";
5534
+ }
5535
+ if (options.baseUrl) {
5536
+ env.BASE_URL = options.baseUrl;
5537
+ }
5538
+ return env;
5539
+ }
5540
+ function computeExitCodeFromLayerResults(layerResults) {
5541
+ const classification = getClassificationForProfile("runa-strict");
5542
+ const classificationMap = new Map(classification.map((c) => [c.layer, c.level]));
5543
+ let hasBlockingFailure = false;
5544
+ let hasWarningFailure = false;
5545
+ for (const [layerStr, result] of Object.entries(layerResults)) {
5546
+ const layer = Number.parseInt(layerStr, 10);
5547
+ if (Number.isNaN(layer)) continue;
5548
+ const isFailed = result.status !== "passed" && result.status !== "skipped";
5549
+ if (!isFailed) continue;
5550
+ const level = classificationMap.get(layer) ?? "blocking";
5551
+ if (level === "blocking") {
5552
+ hasBlockingFailure = true;
5553
+ } else {
5554
+ hasWarningFailure = true;
5555
+ }
5556
+ }
5557
+ if (hasBlockingFailure) return 1;
5558
+ if (hasWarningFailure) return 2;
5559
+ return 0;
5560
+ }
5561
+
5562
+ // src/commands/ci/machine/actors/setup/pr-local.ts
4628
5563
  var prLocalSetupActor = fromPromise(
4629
5564
  async ({ input }) => {
4630
5565
  const repoRoot = input.targetDir || process.cwd();
4631
5566
  try {
4632
5567
  const base = await executePrSetupBase(input, ensureRunaTmpDir);
5568
+ const ownershipContext = {
5569
+ input: {
5570
+ command: "pr",
5571
+ ...input,
5572
+ ...base,
5573
+ skipLocalDbStart: input.skipLocalDbStart,
5574
+ assumeSupabaseReady: input.assumeSupabaseReady,
5575
+ skipPlaywrightInstall: input.skipPlaywrightInstall
5576
+ },
5577
+ diagnostics: {}
5578
+ };
4633
5579
  try {
4634
- await startSupabaseLocal({ repoRoot: base.repoRoot, tmpDir: base.tmpDir });
5580
+ await startSupabaseLocal({
5581
+ repoRoot: base.repoRoot,
5582
+ tmpDir: base.tmpDir,
5583
+ skipStart: input.skipLocalDbStart || input.assumeSupabaseReady
5584
+ });
4635
5585
  } catch {
4636
5586
  }
4637
5587
  const supabaseInfo = await resolveLocalSupabaseEnv({
4638
5588
  repoRoot: base.repoRoot,
4639
- tmpDir: base.tmpDir
5589
+ tmpDir: base.tmpDir,
5590
+ assumeReady: input.assumeSupabaseReady
4640
5591
  });
4641
5592
  const databaseUrl = input.databaseUrl || getDefaultLocalDatabaseUrl(base.repoRoot);
4642
5593
  return {
4643
5594
  ...base,
4644
5595
  databaseUrl,
4645
5596
  supabaseUrl: supabaseInfo.supabaseUrl,
4646
- anonKey: supabaseInfo.anonKey
5597
+ anonKey: supabaseInfo.anonKey,
5598
+ diagnostics: {
5599
+ setup: {
5600
+ runtimeOwner: getRuntimeOwner(ownershipContext),
5601
+ playwrightOwner: getPlaywrightOwner(ownershipContext),
5602
+ supabase: supabaseInfo.diagnostics
5603
+ }
5604
+ }
4647
5605
  };
4648
5606
  } catch (error) {
4649
5607
  return createErrorOutput(repoRoot, error);
@@ -5605,6 +6563,15 @@ async function waitUntilAllFinished(params) {
5605
6563
  const timeoutId = setTimeout(() => {
5606
6564
  abortController.abort();
5607
6565
  }, maxWaitMs);
6566
+ const progressIntervalMs = Math.max(1, params.progressIntervalSeconds) * 1e3;
6567
+ const progressStartedAt = Date.now();
6568
+ const progressTimer = setInterval(() => {
6569
+ if (remaining.size === 0) return;
6570
+ const runningLayers = Array.from(remaining).map((p) => `L${p.layer}`).sort().join(", ");
6571
+ const elapsedSeconds = Math.round((Date.now() - progressStartedAt) / 1e3);
6572
+ console.log(`[Progress] Tests still running after ${elapsedSeconds}s: ${runningLayers}`);
6573
+ }, progressIntervalMs);
6574
+ progressTimer.unref?.();
5608
6575
  try {
5609
6576
  while (remaining.size > 0) {
5610
6577
  if (abortController.signal.aborted) {
@@ -5650,6 +6617,7 @@ async function waitUntilAllFinished(params) {
5650
6617
  }
5651
6618
  } finally {
5652
6619
  clearTimeout(timeoutId);
6620
+ clearInterval(progressTimer);
5653
6621
  }
5654
6622
  return { successMap };
5655
6623
  }
@@ -5803,7 +6771,7 @@ var SKIP_CONFIGS = {
5803
6771
  staticChecks: { skipInCiLocal: true, inputFlag: "skipStaticChecks" },
5804
6772
  build: { skipInCiLocal: true, inputFlag: "skipBuild" },
5805
6773
  appStart: { skipInCiLocal: true, requiresLayer: 4 },
5806
- playwrightInstall: { skipInCiLocal: true, requiresLayer: 4 }
6774
+ playwrightInstall: { skipInCiLocal: true, inputFlag: "skipPlaywrightInstall", requiresLayer: 4 }
5807
6775
  };
5808
6776
  function shouldSkipStep(context, key) {
5809
6777
  const config = SKIP_CONFIGS[key];
@@ -5844,6 +6812,9 @@ function shouldPostGitHubComment(context) {
5844
6812
  if (context.input.skipGithubComment === true) return false;
5845
6813
  return true;
5846
6814
  }
6815
+ function isTestPhase(context) {
6816
+ return isCiPrMode(context) && context.input.phase === "test";
6817
+ }
5847
6818
  function hasError(context) {
5848
6819
  return context.error !== null;
5849
6820
  }
@@ -5858,127 +6829,45 @@ function isDryRun(context) {
5858
6829
  return context.input.check === true;
5859
6830
  }
5860
6831
 
5861
- // src/commands/ci/machine/helpers.ts
5862
- init_esm_shims();
5863
- var CORE_LAYERS = [1, 2, 3];
5864
- var E2E_LAYER = 4;
5865
- function getLayersForCorePhase(selectedLayers, mode) {
5866
- if (mode === "ci-local") {
5867
- return selectedLayers;
5868
- }
5869
- return selectedLayers.filter((l) => CORE_LAYERS.includes(l));
5870
- }
5871
- function hasE2ELayer(selectedLayers) {
5872
- return selectedLayers.includes(E2E_LAYER);
5873
- }
5874
- function shouldReuseCiReferenceStats(context) {
5875
- return context.mode !== "ci-local" && context.schemaApplied && context.schemaDrift !== null && context.schemaDrift?.hasDrift === false;
5876
- }
5877
- function mergeLayerResults(coreResults, e2eResults) {
5878
- return { ...coreResults, ...e2eResults };
5879
- }
5880
- function convertLayerResults(results) {
5881
- const layerResults = {};
5882
- for (const r of results) {
5883
- let status;
5884
- if (r.killed) {
5885
- status = "killed";
5886
- } else if (r.skipped) {
5887
- status = "skipped";
5888
- } else {
5889
- status = r.success ? "passed" : "failed";
5890
- }
5891
- layerResults[r.layer] = {
5892
- status,
5893
- exitCode: r.exitCode,
5894
- // Preserve test count information
5895
- passed: r.passedTests,
5896
- total: r.totalTests,
5897
- failed: r.failedTests,
5898
- flaky: r.flakyTests
5899
- };
5900
- }
5901
- return layerResults;
5902
- }
5903
- function getDatabaseUrlForRuntime(context) {
5904
- const raw = context.supabase?.appDatabaseUrl ?? context.supabase?.databaseUrlRaw ?? "";
5905
- return normalizeDatabaseUrlForDdl(raw);
5906
- }
5907
- function getSupabaseUrlWithFallback(context) {
5908
- return context.supabase?.supabaseUrl || getDefaultLocalSupabaseUrl(context.repoRoot ?? void 0);
5909
- }
5910
- function getSupabaseAnonKeyWithFallback(context) {
5911
- return context.supabase?.anonKey || DEFAULT_LOCAL_ANON_KEY;
5912
- }
5913
- function buildRuntimeEnv(context, options = {}) {
5914
- const env = {
5915
- ...context.input.runtimeEnv ?? {},
5916
- DATABASE_URL: getDatabaseUrlForRuntime(context),
5917
- NEXT_PUBLIC_SUPABASE_URL: getSupabaseUrlWithFallback(context),
5918
- NEXT_PUBLIC_SUPABASE_ANON_KEY: getSupabaseAnonKeyWithFallback(context)
5919
- };
5920
- if (options.enablePublicE2EFlag) {
5921
- env.NEXT_PUBLIC_E2E_TEST = "true";
5922
- }
5923
- if (options.enableServerE2EFlag) {
5924
- env.E2E_TEST = "true";
5925
- }
5926
- if (options.baseUrl) {
5927
- env.BASE_URL = options.baseUrl;
5928
- }
5929
- return env;
5930
- }
5931
- function computeExitCodeFromLayerResults(layerResults) {
5932
- const classification = getClassificationForProfile("runa-strict");
5933
- const classificationMap = new Map(classification.map((c) => [c.layer, c.level]));
5934
- let hasBlockingFailure = false;
5935
- let hasWarningFailure = false;
5936
- for (const [layerStr, result] of Object.entries(layerResults)) {
5937
- const layer = Number.parseInt(layerStr, 10);
5938
- if (Number.isNaN(layer)) continue;
5939
- const isFailed = result.status !== "passed" && result.status !== "skipped";
5940
- if (!isFailed) continue;
5941
- const level = classificationMap.get(layer) ?? "blocking";
5942
- if (level === "blocking") {
5943
- hasBlockingFailure = true;
5944
- } else {
5945
- hasWarningFailure = true;
5946
- }
5947
- }
5948
- if (hasBlockingFailure) return 1;
5949
- if (hasWarningFailure) return 2;
5950
- return 0;
5951
- }
5952
-
5953
6832
  // src/commands/ci/machine/machine-state-helpers.ts
5954
6833
  init_esm_shims();
5955
6834
 
5956
6835
  // src/commands/ci/machine/formatters/github-comment.ts
5957
6836
  init_esm_shims();
5958
6837
 
5959
- // src/commands/ci/machine/formatters/sections/index.ts
5960
- init_esm_shims();
5961
-
5962
- // src/commands/ci/machine/formatters/sections/final-comment.ts
5963
- init_esm_shims();
5964
-
5965
- // src/commands/ci/machine/formatters/sections/format-helpers.ts
5966
- init_esm_shims();
5967
-
5968
6838
  // src/commands/ci/machine/formatters/github-comment-types.ts
5969
6839
  init_esm_shims();
5970
6840
  var CI_STEPS = [
5971
6841
  { step: "setup", label: "\u74B0\u5883\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7", detail: "\u30EA\u30DD\u30B8\u30C8\u30EA\u691C\u51FA\u3001Supabase\u8D77\u52D5" },
5972
6842
  { step: "syncSchema", label: "\u30B9\u30AD\u30FC\u30DE\u540C\u671F", detail: "pg-schema-diff \u2192 CI DB" },
5973
6843
  { step: "applySeeds", label: "\u30B7\u30FC\u30C9\u9069\u7528", detail: "ci.sql + prerequisite seeds" },
6844
+ {
6845
+ step: "postSeedChecks",
6846
+ label: "\u30ED\u30FC\u30EB\u6E96\u5099",
6847
+ detail: "db roles setup"
6848
+ },
6849
+ {
6850
+ step: "observability",
6851
+ label: "\u672C\u756A\u6BD4\u8F03\u30FBschema stats",
6852
+ detail: "production preview \u2225 schema stats"
6853
+ },
5974
6854
  { step: "staticChecks", label: "\u9759\u7684\u30C1\u30A7\u30C3\u30AF", detail: "\u578B\u30C1\u30A7\u30C3\u30AF \u2225 lint" },
5975
6855
  { step: "build", label: "\u30D3\u30EB\u30C9", detail: "build \u2225 playwright install" },
5976
6856
  { step: "runTests", label: "\u30C6\u30B9\u30C8\u5B9F\u884C", detail: "L0-L3 (\u30D6\u30ED\u30C3\u30AD\u30F3\u30B0) \u2192 L4 (E2E)" },
5977
6857
  { step: "finalize", label: "\u5B8C\u4E86\u51E6\u7406", detail: "\u30B5\u30DE\u30EA\u30FC + PR\u30B3\u30E1\u30F3\u30C8" }
5978
6858
  ];
5979
6859
  var BLOCKING_LAYERS = [0, 1, 2, 3];
6860
+ var CI_OBSERVABILITY_SECTION_START_MARKER = "<!-- runa-ci-observability:start -->";
6861
+ var CI_OBSERVABILITY_SECTION_END_MARKER = "<!-- runa-ci-observability:end -->";
6862
+
6863
+ // src/commands/ci/machine/formatters/sections/index.ts
6864
+ init_esm_shims();
6865
+
6866
+ // src/commands/ci/machine/formatters/sections/final-comment.ts
6867
+ init_esm_shims();
5980
6868
 
5981
6869
  // src/commands/ci/machine/formatters/sections/format-helpers.ts
6870
+ init_esm_shims();
5982
6871
  function formatDuration4(ms) {
5983
6872
  if (ms < 1e3) return `${ms}ms`;
5984
6873
  const seconds = Math.floor(ms / 1e3);
@@ -6441,7 +7330,20 @@ function describeObject(object) {
6441
7330
  return `function \`${object.label}\``;
6442
7331
  case "table":
6443
7332
  return `table \`${object.label}\``;
6444
- }
7333
+ case "schema_acl":
7334
+ return `schema ACL \`${object.label}\``;
7335
+ case "schema_comment":
7336
+ return `schema comment \`${object.label}\``;
7337
+ case "table_acl":
7338
+ return `table ACL \`${object.label}\``;
7339
+ case "table_comment":
7340
+ return `table comment \`${object.label}\``;
7341
+ case "function_acl":
7342
+ return `function ACL \`${object.label}\``;
7343
+ case "function_comment":
7344
+ return `function comment \`${object.label}\``;
7345
+ }
7346
+ return `object \`${object.label}\``;
6445
7347
  }
6446
7348
  function appendObjectLines(lines, prefix, objects, label) {
6447
7349
  if (objects.length === 0) return;
@@ -6740,7 +7642,7 @@ function generatePreviewChangesDetectedSection(prodPreview, previewOnlyChanges)
6740
7642
  if (previewOnlyChanges) {
6741
7643
  lines.push(
6742
7644
  "> \u88DC\u8DB3: `Prod semantic diff` / count diff / index diff \u3067\u306F\u5DEE\u5206\u304C\u306A\u304F\u3001`production --check` \u306E\u307F\u304C\u5909\u66F4\u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002",
6743
- "> \u3053\u308C\u306F grants / ACL / ownership \u306A\u3069 canonical semantic diff \u306E\u5BFE\u8C61\u5916\u5DEE\u5206\u3067\u3042\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002",
7645
+ "> \u3053\u308C\u306F ownership / session config / planner-only rewrite \u306A\u3069\u3001canonical diff \u3067\u306F\u8FFD\u308F\u306A\u3044\u5DEE\u5206\u3067\u3042\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002",
6744
7646
  ""
6745
7647
  );
6746
7648
  }
@@ -6754,8 +7656,13 @@ function buildComparisonMismatchReasons(schemaStats, expectedDrift) {
6754
7656
  const reasons = [];
6755
7657
  if (signals.hasSemanticChanges && signals.semanticDiff) {
6756
7658
  const diff = signals.semanticDiff;
7659
+ const classification = classifyCanonicalDiff(diff);
7660
+ const families = [];
7661
+ if (classification.structural) families.push("structural");
7662
+ if (classification.securityMetadata) families.push("security-metadata");
7663
+ if (classification.descriptiveMetadata) families.push("descriptive-metadata");
6757
7664
  reasons.push(
6758
- `semantic diff: changed ${diff.changed.length}, missing ${diff.missing.length}, extra ${diff.extra.length}, renamed ${diff.renamed.length}`
7665
+ `semantic diff (${families.join(", ")}): changed ${diff.changed.length}, missing ${diff.missing.length}, extra ${diff.extra.length}, renamed ${diff.renamed.length}`
6759
7666
  );
6760
7667
  }
6761
7668
  if (signals.hasIndexChanges && signals.indexDiff) {
@@ -6804,7 +7711,7 @@ function generateKnownDriftOnlySection(schemaStats, expectedDrift) {
6804
7711
  lines.push("");
6805
7712
  return lines;
6806
7713
  }
6807
- function generateProductionPreviewSection(prodPreview, schemaDrift, schemaStats, expectedDrift) {
7714
+ function generateProductionPreviewSection(phase, prodPreview, schemaDrift, schemaStats, expectedDrift) {
6808
7715
  const signals = getProductionSchemaSignals(prodPreview, schemaStats, expectedDrift);
6809
7716
  if (!prodPreview?.executed) {
6810
7717
  if (schemaDrift?.gitDiff?.filesChanged?.length) {
@@ -6835,7 +7742,7 @@ function shouldShowDeployButton(productionPreview, schemaDrift, hasSchemaChanges
6835
7742
  if (!productionPreview?.executed && schemaDrift?.gitDiff?.filesChanged?.length) return true;
6836
7743
  return false;
6837
7744
  }
6838
- function generateDeploySection(exitCode, layerResults, gitBranchName, productionPreview, schemaDrift, schemaStats, expectedDrift, env) {
7745
+ function generateDeploySection(exitCode, layerResults, phase, gitBranchName, productionPreview, schemaDrift, schemaStats, expectedDrift, env) {
6839
7746
  if (!checkIfDeployable(exitCode, layerResults)) return [];
6840
7747
  const deployWorkflowUrl = env.repository ? `${env.serverUrl}/${env.repository}/actions/workflows/deploy-db.yml` : null;
6841
7748
  const signals = getProductionSchemaSignals(productionPreview, schemaStats, expectedDrift);
@@ -6846,7 +7753,13 @@ function generateDeploySection(exitCode, layerResults, gitBranchName, production
6846
7753
  if (exitCode !== 0)
6847
7754
  lines.push("> **\u6CE8**: Layer 4 (E2E) \u306F\u5931\u6557\u3057\u307E\u3057\u305F\u304C\u3001\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u306F\u30C7\u30D7\u30ED\u30A4\u53EF\u80FD\u3067\u3059\u3002", "");
6848
7755
  lines.push(
6849
- ...generateProductionPreviewSection(productionPreview, schemaDrift, schemaStats, expectedDrift)
7756
+ ...generateProductionPreviewSection(
7757
+ phase,
7758
+ productionPreview,
7759
+ schemaDrift,
7760
+ schemaStats,
7761
+ expectedDrift
7762
+ )
6850
7763
  );
6851
7764
  const showButton = shouldShowDeployButton(productionPreview, schemaDrift, hasSchemaChanges2);
6852
7765
  if (deployWorkflowUrl && showButton) {
@@ -6862,6 +7775,37 @@ function generateDeploySection(exitCode, layerResults, gitBranchName, production
6862
7775
  }
6863
7776
  return lines;
6864
7777
  }
7778
+ function generateObservabilitySectionBody(input) {
7779
+ const env = getGitHubEnv();
7780
+ const gitBranchName = input.prContext?.headBranch ?? input.branchName ?? "unknown";
7781
+ const lines = [
7782
+ ...formatSchemaMatrix(
7783
+ input.schemaStats,
7784
+ input.schemaDrift?.gitDiff ?? null,
7785
+ input.expectedDrift ?? []
7786
+ ),
7787
+ ...generateDeploySection(
7788
+ input.exitCode,
7789
+ input.layerResults,
7790
+ input.phase,
7791
+ gitBranchName,
7792
+ input.productionPreview,
7793
+ input.schemaDrift,
7794
+ input.schemaStats ?? null,
7795
+ input.expectedDrift ?? [],
7796
+ env
7797
+ )
7798
+ ];
7799
+ return lines.join("\n").trim();
7800
+ }
7801
+ function wrapObservabilitySection(sectionBody) {
7802
+ return [
7803
+ CI_OBSERVABILITY_SECTION_START_MARKER,
7804
+ sectionBody,
7805
+ CI_OBSERVABILITY_SECTION_END_MARKER,
7806
+ ""
7807
+ ];
7808
+ }
6865
7809
  function generateIntermediateCommentBody(input) {
6866
7810
  const { branchName, layerResults, intermediateMessage, productionPreview, schemaDrift } = input;
6867
7811
  const env = getGitHubEnv();
@@ -6883,6 +7827,7 @@ function generateIntermediateCommentBody(input) {
6883
7827
  ...generateDeploySection(
6884
7828
  intermediateExitCode,
6885
7829
  layerResults,
7830
+ input.phase,
6886
7831
  gitBranchName,
6887
7832
  productionPreview,
6888
7833
  schemaDrift,
@@ -6903,29 +7848,14 @@ function generateCommentBody(input) {
6903
7848
  const runUrl = env.repository && env.runId ? `${env.serverUrl}/${env.repository}/actions/runs/${env.runId}` : null;
6904
7849
  const effectiveBranchName = branchName ?? input.prContext?.headBranch ?? "unknown";
6905
7850
  const supabaseUrl = supabase?.supabaseUrl ?? "";
6906
- const gitBranchName = input.prContext?.headBranch ?? branchName ?? "unknown";
6907
7851
  const layerLines = formatLayerResults(layerResults);
6908
7852
  const skippedLayersLines = input.layerSkipReasons && input.originalSelectedLayers ? formatSkippedLayers(input.layerSkipReasons, input.originalSelectedLayers) : [];
6909
7853
  const lines = [
6910
7854
  ...generateFinalHeader(exitCode, effectiveBranchName, supabaseUrl, schemaDrift, runUrl),
6911
7855
  ...layerLines.length > 0 ? ["### \u30C6\u30B9\u30C8\u7D50\u679C", ...layerLines, ""] : [],
6912
7856
  ...skippedLayersLines,
6913
- ...formatSchemaMatrix(
6914
- input.schemaStats,
6915
- schemaDrift?.gitDiff ?? null,
6916
- input.expectedDrift ?? []
6917
- ),
6918
- ...generateSchemaDetailsSection(schemaDrift),
6919
- ...generateDeploySection(
6920
- exitCode,
6921
- layerResults,
6922
- gitBranchName,
6923
- input.productionPreview,
6924
- schemaDrift,
6925
- input.schemaStats ?? null,
6926
- input.expectedDrift ?? [],
6927
- env
6928
- ),
7857
+ ...generateSchemaDetailsSection(schemaDrift),
7858
+ ...wrapObservabilitySection(generateObservabilitySectionBody(input)),
6929
7859
  "<sub>\u{1F916} RUNA CI \u751F\u6210</sub>"
6930
7860
  ];
6931
7861
  return lines.join("\n");
@@ -6937,18 +7867,20 @@ function calculateTotalElapsed(stepTimings2) {
6937
7867
  if (!stepTimings2) return 0;
6938
7868
  return Object.values(stepTimings2).reduce((sum, t) => sum + (t ?? 0), 0);
6939
7869
  }
6940
- function formatProgressBar(completedSteps, failedStep) {
7870
+ function formatProgressBar(currentStep, completedSteps, failedStep, skippedSteps) {
6941
7871
  const total = CI_STEPS.length;
6942
- const completed = failedStep ? completedSteps.length + 1 : completedSteps.length;
7872
+ const completedBase = completedSteps.length + skippedSteps.length;
7873
+ const completed = failedStep ? completedBase : completedBase + 1;
6943
7874
  const percentage = Math.round(completed / total * 100);
6944
7875
  const filled = Math.round(percentage / 10);
6945
7876
  const empty = 10 - filled;
6946
7877
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
6947
7878
  return `\`${bar}\` ${percentage}%`;
6948
7879
  }
6949
- function getStepStatus(step, currentStep, completedSteps, failedStep) {
7880
+ function getStepStatus(step, currentStep, completedSteps, failedStep, skippedSteps) {
6950
7881
  if (failedStep === step) return "failed";
6951
7882
  if (completedSteps.includes(step)) return "passed";
7883
+ if (skippedSteps.includes(step)) return "skipped";
6952
7884
  if (currentStep === step) return "running";
6953
7885
  return "pending";
6954
7886
  }
@@ -7014,6 +7946,8 @@ function formatRunTestsDetail(layerResults) {
7014
7946
  var stepDetailHandlers = {
7015
7947
  syncSchema: (_status, schemaDrift) => formatSyncSchemaDetail(schemaDrift),
7016
7948
  applySeeds: () => "ci.sql + prerequisite seeds",
7949
+ postSeedChecks: (status) => status === "passed" ? "db roles \u5B8C\u4E86" : "db roles \u3067\u505C\u6B62",
7950
+ observability: (status) => status === "passed" ? "\u672C\u756A\u6BD4\u8F03\u30FBschema stats \u5B8C\u4E86" : "\u672C\u756A\u6BD4\u8F03\u307E\u305F\u306Fschema stats \u3067\u505C\u6B62",
7017
7951
  staticChecks: (status) => status === "passed" ? "\u578B\u30C1\u30A7\u30C3\u30AF \u2713 lint \u2713" : "\u578B\u30C1\u30A7\u30C3\u30AF \u2717 \u307E\u305F\u306F lint \u2717",
7018
7952
  build: (status) => status === "passed" ? "\u30A2\u30D7\u30EA\u30D3\u30EB\u30C9\u5B8C\u4E86, Playwright\u6E96\u5099\u5B8C\u4E86" : "\u30D3\u30EB\u30C9\u5931\u6557",
7019
7953
  runTests: (_status, _schemaDrift, layerResults) => formatRunTestsDetail(layerResults)
@@ -7028,6 +7962,8 @@ function getStepDescription(step) {
7028
7962
  setup: "\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7",
7029
7963
  syncSchema: "\u30B9\u30AD\u30FC\u30DE\u540C\u671F",
7030
7964
  applySeeds: "Seed\u9069\u7528",
7965
+ postSeedChecks: "\u30ED\u30FC\u30EB\u6E96\u5099",
7966
+ observability: "\u672C\u756A\u6BD4\u8F03\u30FBschema stats",
7031
7967
  staticChecks: "\u9759\u7684\u30C1\u30A7\u30C3\u30AF",
7032
7968
  build: "\u30D3\u30EB\u30C9",
7033
7969
  runTests: "\u30C6\u30B9\u30C8\u5B9F\u884C",
@@ -7035,7 +7971,25 @@ function getStepDescription(step) {
7035
7971
  };
7036
7972
  return descriptions[step] || step;
7037
7973
  }
7038
- function generateProgressHeader(failedStep, completedSteps, totalElapsed) {
7974
+ function getRunningStepDetail(step, detail, productionPreview) {
7975
+ if (step !== "observability") {
7976
+ return detail;
7977
+ }
7978
+ if (!productionPreview) {
7979
+ return "production preview \u5B9F\u884C\u4E2D";
7980
+ }
7981
+ if (productionPreview.error) {
7982
+ return "production preview \u5931\u6557, schema stats \u306F\u88DC\u52A9\u60C5\u5831\u306E\u307F";
7983
+ }
7984
+ if (productionPreview.executed && productionPreview.hasChanges) {
7985
+ return "\u672C\u756A\u5DEE\u5206\u3092\u691C\u51FA, schema stats \u306F\u7701\u7565\u4E88\u5B9A";
7986
+ }
7987
+ if (productionPreview.executed) {
7988
+ return "production preview \u5B8C\u4E86, schema stats \u53CE\u96C6\u4E2D";
7989
+ }
7990
+ return detail;
7991
+ }
7992
+ function generateProgressHeader(currentStep, failedStep, completedSteps, skippedSteps, totalElapsed) {
7039
7993
  const lines = ["## RUNA CI", ""];
7040
7994
  if (failedStep) {
7041
7995
  const stepDesc = getStepDescription(failedStep);
@@ -7045,7 +7999,9 @@ function generateProgressHeader(failedStep, completedSteps, totalElapsed) {
7045
7999
  `> \`${failedStep}\` \u3067\u51E6\u7406\u304C\u505C\u6B62\u3057\u307E\u3057\u305F\u3002\u4E0B\u8A18\u306E\u30A8\u30E9\u30FC\u8A73\u7D30\u3068\u4FEE\u6B63\u65B9\u6CD5\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
7046
8000
  );
7047
8001
  } else {
7048
- lines.push(`**\u{1F504} \u5B9F\u884C\u4E2D...** ${formatProgressBar(completedSteps, failedStep)}`);
8002
+ lines.push(
8003
+ `**\u{1F504} \u5B9F\u884C\u4E2D...** ${formatProgressBar(currentStep, completedSteps, failedStep, skippedSteps)}`
8004
+ );
7049
8005
  }
7050
8006
  if (totalElapsed > 0) {
7051
8007
  lines.push(`<sub>\u23F1\uFE0F \u7D4C\u904E\u6642\u9593: ${formatDuration4(totalElapsed)}</sub>`);
@@ -7062,16 +8018,17 @@ function generateInfoLine(branchName, supabaseUrl, runUrl) {
7062
8018
  if (runUrl) items.push(`[\u{1F517} \u30EF\u30FC\u30AF\u30D5\u30ED\u30FC](${runUrl})`);
7063
8019
  return [items.join(" \xB7 "), ""];
7064
8020
  }
7065
- function generateProgressSteps(currentStep, completedSteps, failedStep, stepTimings2, schemaDrift, layerResults) {
8021
+ function generateProgressSteps(currentStep, completedSteps, failedStep, skippedSteps, stepTimings2, schemaDrift, layerResults, productionPreview) {
7066
8022
  const lines = ["### \u9032\u6357", ""];
7067
8023
  for (const { step, label, detail } of CI_STEPS) {
7068
- const status = getStepStatus(step, currentStep, completedSteps, failedStep);
8024
+ const status = getStepStatus(step, currentStep, completedSteps, failedStep, skippedSteps);
7069
8025
  const icon = getStepIcon(status);
7070
8026
  const timing = stepTimings2?.[step];
7071
8027
  const timingStr = timing !== void 0 ? ` \`${formatDuration4(timing)}\`` : "";
7072
8028
  const stepDetail = getStepDetail(step, status, schemaDrift, layerResults);
7073
- if (status === "running" && detail) {
7074
- lines.push(`${icon} **${label}**${timingStr} \u2014 _${detail}_`);
8029
+ const runningDetail = getRunningStepDetail(step, detail, productionPreview);
8030
+ if (status === "running" && runningDetail) {
8031
+ lines.push(`${icon} **${label}**${timingStr} \u2014 _${runningDetail}_`);
7075
8032
  } else if (status === "passed" && stepDetail) {
7076
8033
  lines.push(`${icon} ${label}${timingStr} \u2014 ${stepDetail}`);
7077
8034
  } else {
@@ -7095,6 +8052,15 @@ function getFixSuggestions(failedStep) {
7095
8052
  "\u30B9\u30AD\u30FC\u30DE\u3068seed\u306E\u4E0D\u4E00\u81F4\u3092\u78BA\u8A8D: runa db sync \u3092\u5148\u306B\u5B9F\u884C",
7096
8053
  "ci.sql\u304C\u5B58\u5728\u3059\u308B\u304B\u78BA\u8A8D: ls supabase/seeds/"
7097
8054
  ],
8055
+ postSeedChecks: [
8056
+ "db:setup-roles \u306E\u30B9\u30AF\u30EA\u30D7\u30C8\u6709\u7121\u3068\u5B9F\u884C\u7D50\u679C\u3092\u78BA\u8A8D",
8057
+ "workflow \u5074\u306E local DB / DATABASE_URL \u6E96\u5099\u72B6\u614B\u3092\u78BA\u8A8D"
8058
+ ],
8059
+ observability: [
8060
+ "production preview / schema stats \u306E\u30ED\u30B0\u3092\u78BA\u8A8D",
8061
+ "\u672C\u756A DB \u63A5\u7D9A\u60C5\u5831\u3068 compare-only dry-run \u306E\u51FA\u529B\u3092\u78BA\u8A8D",
8062
+ "schema stats \u304C\u5FC5\u8981\u306A\u30B1\u30FC\u30B9\u304B expected drift \u8A2D\u5B9A\u3092\u78BA\u8A8D"
8063
+ ],
7098
8064
  staticChecks: [
7099
8065
  "\u30ED\u30FC\u30AB\u30EB\u3067\u578B\u30C1\u30A7\u30C3\u30AF: pnpm type-check",
7100
8066
  "\u30ED\u30FC\u30AB\u30EB\u3067lint: pnpm lint",
@@ -7114,12 +8080,13 @@ function generateErrorSection(error, failedStep) {
7114
8080
  if (!error && !failedStep) return [];
7115
8081
  const lines = [];
7116
8082
  if (error) {
8083
+ const sanitized = error.substring(0, 1e3).replace(/`/g, "'");
7117
8084
  lines.push(
7118
8085
  "<details open>",
7119
8086
  "<summary>\u274C \u30A8\u30E9\u30FC\u8A73\u7D30</summary>",
7120
8087
  "",
7121
8088
  "```",
7122
- error.substring(0, 1e3),
8089
+ sanitized,
7123
8090
  error.length > 1e3 ? "... (\u7701\u7565)" : "",
7124
8091
  "```"
7125
8092
  );
@@ -7156,7 +8123,7 @@ function generateTestResultsSection(layerResults) {
7156
8123
  ""
7157
8124
  ];
7158
8125
  }
7159
- function generateProductionSchemaSection(productionPreview, deployStatus, env) {
8126
+ function generateProductionSchemaSection(productionPreview, phase, deployStatus, env) {
7160
8127
  const lines = ["### \u{1F4CB} \u672C\u756A\u30B9\u30AD\u30FC\u30DE\u72B6\u6CC1", ""];
7161
8128
  const state = resolveProductionSchemaState(productionPreview, deployStatus);
7162
8129
  switch (state) {
@@ -7171,6 +8138,7 @@ function generateProductionSchemaSection(productionPreview, deployStatus, env) {
7171
8138
  break;
7172
8139
  case "error":
7173
8140
  lines.push(`\u274C **\u30A8\u30E9\u30FC**: ${productionPreview?.error ?? ""}`, "");
8141
+ appendDeployLink(lines, env);
7174
8142
  break;
7175
8143
  case "in-sync":
7176
8144
  lines.push("\u2705 **\u672C\u756A\u3068\u540C\u671F\u6E08\u307F** \u2014 \u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u306A\u3057", "");
@@ -7271,19 +8239,27 @@ function generateProgressCommentBody(input) {
7271
8239
  const totalElapsed = calculateTotalElapsed(stepTimings2);
7272
8240
  const gitBranchName = input.prContext?.headBranch ?? branchName ?? "unknown";
7273
8241
  const lines = [
7274
- ...generateProgressHeader(failedStep, completedSteps, totalElapsed),
8242
+ ...generateProgressHeader(
8243
+ currentStep,
8244
+ failedStep,
8245
+ completedSteps,
8246
+ input.skippedSteps ?? [],
8247
+ totalElapsed
8248
+ ),
7275
8249
  ...generateInfoLine(effectiveBranchName, supabaseUrl, runUrl),
7276
8250
  ...generateProgressSteps(
7277
8251
  currentStep,
7278
8252
  completedSteps,
7279
8253
  failedStep,
8254
+ input.skippedSteps ?? [],
7280
8255
  stepTimings2,
7281
8256
  schemaDrift,
7282
- input.layerResults
8257
+ input.layerResults,
8258
+ productionPreview
7283
8259
  ),
7284
8260
  ...generateErrorSection(error ?? void 0, failedStep),
7285
8261
  ...generateTestResultsSection(input.layerResults),
7286
- ...generateProductionSchemaSection(productionPreview, deployStatus, env),
8262
+ ...generateProductionSchemaSection(productionPreview, input.phase, deployStatus, env),
7287
8263
  ...generateDbDeploySection(input.layerResults, gitBranchName, productionPreview, env)
7288
8264
  ];
7289
8265
  const jst = new Date(Date.now() + 9 * 60 * 60 * 1e3);
@@ -7299,6 +8275,7 @@ function createCommentInput(context, options) {
7299
8275
  .../* @__PURE__ */ new Set([...skippedLayerNumbers, ...context.selectedLayers])
7300
8276
  ].sort((a, b) => a - b);
7301
8277
  return {
8278
+ phase: context.input.phase ?? "all",
7302
8279
  exitCode: context.exitCode,
7303
8280
  branchName: context.branchName,
7304
8281
  prContext: context.prContext,
@@ -7315,10 +8292,14 @@ function createCommentInput(context, options) {
7315
8292
  };
7316
8293
  }
7317
8294
  function createProgressCommentInput(context, currentStep, completedSteps, failedStep = null, stepTimings2) {
8295
+ const phase = context.input.phase ?? "all";
8296
+ const skippedSteps = phase === "test" ? ["syncSchema", "applySeeds", "observability"] : [];
7318
8297
  return {
8298
+ phase,
7319
8299
  currentStep,
7320
8300
  completedSteps,
7321
8301
  failedStep,
8302
+ skippedSteps,
7322
8303
  branchName: context.branchName,
7323
8304
  prContext: context.prContext,
7324
8305
  supabase: context.supabase,
@@ -7392,6 +8373,18 @@ function computeExitCode(summary, classification) {
7392
8373
 
7393
8374
  // src/commands/ci/machine/types.ts
7394
8375
  init_esm_shims();
8376
+ function resolveInitialPrContext(input) {
8377
+ if (input.command !== "pr") return null;
8378
+ const prNumberMatch = input.githubRef?.match(/refs\/pull\/(\d+)/);
8379
+ const prNumber = prNumberMatch?.[1] ? Number.parseInt(prNumberMatch[1], 10) : null;
8380
+ return {
8381
+ prNumber,
8382
+ action: input.githubEventAction ?? null,
8383
+ sha: input.githubSha ?? null,
8384
+ baseBranch: input.githubBaseRef ?? null,
8385
+ headBranch: input.githubHeadRef ?? null
8386
+ };
8387
+ }
7395
8388
  function createInitialContext(input) {
7396
8389
  return {
7397
8390
  input,
@@ -7401,10 +8394,10 @@ function createInitialContext(input) {
7401
8394
  tmpDir: null,
7402
8395
  // Resolve executionEnv from input (captured at CLI entry point)
7403
8396
  executionEnv: input.isGitHubActions ? "github-actions" : "local",
7404
- phase: "all",
8397
+ phase: input.phase ?? "all",
7405
8398
  repoKind: "unknown",
7406
- branchName: null,
7407
- prContext: null,
8399
+ branchName: input.branchName ?? input.githubHeadRef ?? null,
8400
+ prContext: resolveInitialPrContext(input),
7408
8401
  policy: null,
7409
8402
  app: null,
7410
8403
  appPid: null,
@@ -7426,7 +8419,9 @@ function createInitialContext(input) {
7426
8419
  layerResults: {},
7427
8420
  testsRun: false,
7428
8421
  layerSkipReasons: {},
8422
+ stepOverrides: {},
7429
8423
  summary: null,
8424
+ diagnostics: {},
7430
8425
  summaryPath: null,
7431
8426
  schemaDrift: null,
7432
8427
  productionPreview: null,
@@ -7503,6 +8498,7 @@ function createSummaryInput(context) {
7503
8498
  durationMs: Date.now() - context.startTime,
7504
8499
  repoKind: context.repoKind,
7505
8500
  detected: {},
8501
+ diagnostics: context.diagnostics,
7506
8502
  steps: {},
7507
8503
  layers: formatLayerSummary(context.layerResults),
7508
8504
  errors: formatErrors(context.error)
@@ -7646,7 +8642,7 @@ function createBuildAndPlaywrightInput(context) {
7646
8642
  enablePublicE2EFlag: true
7647
8643
  }),
7648
8644
  isCI: context.input.isCI ?? false,
7649
- skipPlaywright: !context.selectedLayers.includes(4)
8645
+ skipPlaywright: !context.selectedLayers.includes(4) || shouldReusePreparedPlaywright(context)
7650
8646
  };
7651
8647
  }
7652
8648
  function createAppStartInput(context) {
@@ -7717,7 +8713,6 @@ var ciMachine = setup({
7717
8713
  capabilities: capabilitiesActor,
7718
8714
  runLayers: runLayersActor,
7719
8715
  // Finalize
7720
- writeSummary: writeSummaryActor,
7721
8716
  upsertComment: upsertCommentActor
7722
8717
  },
7723
8718
  guards: {
@@ -7797,6 +8792,7 @@ var ciMachine = setup({
7797
8792
  databaseUrlRaw: event.output.databaseUrl,
7798
8793
  appDatabaseUrl: void 0
7799
8794
  }),
8795
+ diagnostics: ({ event }) => event.output.diagnostics ?? {},
7800
8796
  selectedLayers: ({ event }) => event.output.layers,
7801
8797
  error: ({ event }) => event.output.error ?? null
7802
8798
  })
@@ -7818,6 +8814,9 @@ var ciMachine = setup({
7818
8814
  verbose: context.input.verbose,
7819
8815
  layers: context.input.layers,
7820
8816
  databaseUrl: context.input.databaseUrl,
8817
+ skipLocalDbStart: context.input.skipLocalDbStart,
8818
+ assumeSupabaseReady: context.input.assumeSupabaseReady,
8819
+ skipPlaywrightInstall: context.input.skipPlaywrightInstall,
7821
8820
  // GitHub context (captured at entry point)
7822
8821
  githubRef: context.input.githubRef,
7823
8822
  githubEventAction: context.input.githubEventAction,
@@ -7841,6 +8840,7 @@ var ciMachine = setup({
7841
8840
  databaseUrlRaw: event.output.databaseUrl,
7842
8841
  appDatabaseUrl: void 0
7843
8842
  }),
8843
+ diagnostics: ({ event }) => event.output.diagnostics ?? {},
7844
8844
  selectedLayers: ({ event }) => event.output.layers,
7845
8845
  expectedDrift: ({ event }) => event.output.expectedDrift ?? [],
7846
8846
  error: ({ event }) => event.output.error ?? null
@@ -7874,6 +8874,10 @@ var ciMachine = setup({
7874
8874
  // Skip if ci-local mode (no GitHub comment)
7875
8875
  always: [
7876
8876
  { guard: "isCiLocalMode", target: "dbReset" },
8877
+ {
8878
+ guard: ({ context }) => isTestPhase(context),
8879
+ target: "setupRoles"
8880
+ },
7877
8881
  {
7878
8882
  guard: ({ context }) => !shouldPostGitHubComment(context) || !context.prContext?.prNumber,
7879
8883
  target: "syncSchema"
@@ -7882,8 +8886,20 @@ var ciMachine = setup({
7882
8886
  invoke: {
7883
8887
  src: "upsertComment",
7884
8888
  input: ({ context }) => createInitialCommentRequest(context),
7885
- onDone: { target: "syncSchema" },
7886
- onError: { target: "syncSchema" }
8889
+ onDone: [
8890
+ {
8891
+ guard: ({ context }) => isTestPhase(context),
8892
+ target: "setupRoles"
8893
+ },
8894
+ { target: "syncSchema" }
8895
+ ],
8896
+ onError: [
8897
+ {
8898
+ guard: ({ context }) => isTestPhase(context),
8899
+ target: "setupRoles"
8900
+ },
8901
+ { target: "syncSchema" }
8902
+ ]
7887
8903
  // Non-critical, continue even if comment fails
7888
8904
  }
7889
8905
  },
@@ -7964,7 +8980,8 @@ var ciMachine = setup({
7964
8980
  repoRoot: assertRepoRoot(context),
7965
8981
  tmpDir: assertTmpDir(context),
7966
8982
  databaseUrl: context.supabase?.databaseUrlRaw ?? "",
7967
- mode: context.mode
8983
+ mode: context.mode,
8984
+ skipPostCheck: shouldSkipSchemaPostCheck(context)
7968
8985
  }),
7969
8986
  onDone: [
7970
8987
  {
@@ -8054,7 +9071,16 @@ var ciMachine = setup({
8054
9071
  onDone: {
8055
9072
  target: "staticChecks",
8056
9073
  actions: assign({
8057
- rolesSetup: true,
9074
+ rolesSetup: ({ event }) => event.output.skipped !== true,
9075
+ stepOverrides: ({ context, event }) => ({
9076
+ ...context.stepOverrides,
9077
+ ...event.output.skipped ? {
9078
+ "postSeedPr.execution.setupRoles": {
9079
+ status: "skipped",
9080
+ reason: event.output.skipReason ?? "Skipped because db:setup-roles is unavailable"
9081
+ }
9082
+ } : {}
9083
+ }),
8058
9084
  supabase: ({ context, event }) => mergeSetupRolesSupabase(context, event.output.appDatabaseUrl)
8059
9085
  })
8060
9086
  },
@@ -8334,6 +9360,12 @@ var ciMachine = setup({
8334
9360
  initial: "productionPreview",
8335
9361
  states: {
8336
9362
  productionPreview: {
9363
+ always: [
9364
+ {
9365
+ guard: ({ context }) => !hasSchemaGitChanges(context),
9366
+ target: "done"
9367
+ }
9368
+ ],
8337
9369
  invoke: {
8338
9370
  src: "productionPreview",
8339
9371
  input: ({ context }) => ({
@@ -8362,6 +9394,16 @@ var ciMachine = setup({
8362
9394
  }
8363
9395
  },
8364
9396
  collectSchemaStats: {
9397
+ always: [
9398
+ {
9399
+ guard: ({ context }) => !hasSchemaGitChanges(context),
9400
+ target: "done"
9401
+ },
9402
+ {
9403
+ guard: ({ context }) => context.productionPreview?.hasChanges === true,
9404
+ target: "done"
9405
+ }
9406
+ ],
8365
9407
  invoke: {
8366
9408
  src: "collectSchemaStats",
8367
9409
  input: ({ context }) => ({
@@ -8370,6 +9412,8 @@ var ciMachine = setup({
8370
9412
  ciDbUrl: context.supabase?.databaseUrlRaw ?? null,
8371
9413
  referenceStrategy: shouldReuseCiReferenceStats(context) ? "reuse-ci" : "rebuild",
8372
9414
  queryProduction: true,
9415
+ includeCiSemantic: false,
9416
+ expectedDrift: context.expectedDrift,
8373
9417
  tmpDir: context.tmpDir ?? process.cwd()
8374
9418
  }),
8375
9419
  onDone: {
@@ -8467,7 +9511,6 @@ var ciMachine = setup({
8467
9511
  })
8468
9512
  },
8469
9513
  onError: {
8470
- // Non-critical, continue without schema stats
8471
9514
  target: "decidePath",
8472
9515
  actions: assign({
8473
9516
  schemaStats: () => null
@@ -8865,19 +9908,9 @@ var ciMachine = setup({
8865
9908
  initial: "writeSummary",
8866
9909
  states: {
8867
9910
  writeSummary: {
8868
- invoke: {
8869
- src: "writeSummary",
8870
- input: ({ context }) => createWriteSummaryRequest(context),
8871
- onDone: {
8872
- target: "postComment",
8873
- actions: assign({ summaryPath: ({ event }) => event.output.filePath })
8874
- },
8875
- onError: {
8876
- target: "postComment"
8877
- }
8878
- }
8879
- },
8880
- postComment: {
9911
+ entry: assign({
9912
+ summary: ({ context }) => createWriteSummaryRequest(context).summary
9913
+ }),
8881
9914
  always: [
8882
9915
  {
8883
9916
  guard: ({ context }) => !shouldPostGitHubComment(context),
@@ -8886,8 +9919,11 @@ var ciMachine = setup({
8886
9919
  {
8887
9920
  guard: ({ context }) => !context.prContext?.prNumber,
8888
9921
  target: "complete"
8889
- }
8890
- ],
9922
+ },
9923
+ { target: "postComment" }
9924
+ ]
9925
+ },
9926
+ postComment: {
8891
9927
  invoke: {
8892
9928
  src: "upsertComment",
8893
9929
  input: ({ context }) => createFinalCommentRequest(context),
@@ -8970,16 +10006,492 @@ function isComplete(snapshot) {
8970
10006
 
8971
10007
  // src/commands/ci/machine/commands/machine-runner.ts
8972
10008
  init_esm_shims();
10009
+
10010
+ // src/commands/ci/machine/commands/step-telemetry.ts
10011
+ init_esm_shims();
10012
+ var STEP_METADATA = {
10013
+ "setup.local": {
10014
+ order: 10,
10015
+ title: "Setup local environment",
10016
+ phase: "setup"
10017
+ },
10018
+ "setup.prLocal": {
10019
+ order: 10,
10020
+ title: "Setup PR local environment",
10021
+ phase: "setup"
10022
+ },
10023
+ initialComment: {
10024
+ order: 20,
10025
+ title: "Post initial progress comment",
10026
+ phase: "github",
10027
+ optional: true
10028
+ },
10029
+ syncSchema: {
10030
+ order: 30,
10031
+ title: "Apply preview schema",
10032
+ phase: "db"
10033
+ },
10034
+ applySeeds: {
10035
+ order: 40,
10036
+ title: "Apply CI seeds",
10037
+ phase: "db"
10038
+ },
10039
+ productionPreview: {
10040
+ order: 50,
10041
+ title: "Run production preview",
10042
+ phase: "observability",
10043
+ optional: true,
10044
+ parentStep: "applySeeds"
10045
+ },
10046
+ collectSchemaStats: {
10047
+ order: 60,
10048
+ title: "Collect schema statistics",
10049
+ phase: "observability",
10050
+ optional: true,
10051
+ parentStep: "applySeeds"
10052
+ },
10053
+ "postSeedPr.observability.productionPreview": {
10054
+ order: 50,
10055
+ title: "Run production preview",
10056
+ phase: "observability",
10057
+ optional: true,
10058
+ parentStep: "applySeeds"
10059
+ },
10060
+ "postSeedPr.observability.collectSchemaStats": {
10061
+ order: 60,
10062
+ title: "Collect schema statistics",
10063
+ phase: "observability",
10064
+ optional: true,
10065
+ parentStep: "applySeeds"
10066
+ },
10067
+ "postSeedPr.execution.setupRoles": {
10068
+ order: 70,
10069
+ title: "Setup database roles",
10070
+ phase: "db",
10071
+ optional: true,
10072
+ parentStep: "applySeeds"
10073
+ },
10074
+ "postSeedPr.execution.staticChecks": {
10075
+ order: 80,
10076
+ title: "Run static checks",
10077
+ phase: "build"
10078
+ },
10079
+ "postSeedPr.execution.buildAndPlaywright": {
10080
+ order: 90,
10081
+ title: "Build app and prepare Playwright",
10082
+ phase: "build"
10083
+ },
10084
+ "postSeedPr.execution.buildAndPlaywright.build": {
10085
+ order: 91,
10086
+ title: "Build application",
10087
+ phase: "build",
10088
+ parentStep: "postSeedPr.execution.buildAndPlaywright"
10089
+ },
10090
+ "postSeedPr.execution.buildAndPlaywright.manifestGenerate": {
10091
+ order: 92,
10092
+ title: "Generate manifest",
10093
+ phase: "build",
10094
+ optional: true,
10095
+ parentStep: "postSeedPr.execution.buildAndPlaywright"
10096
+ },
10097
+ "postSeedPr.execution.buildAndPlaywright.playwrightInstall": {
10098
+ order: 93,
10099
+ title: "Install Playwright browsers",
10100
+ phase: "build",
10101
+ optional: true,
10102
+ parentStep: "postSeedPr.execution.buildAndPlaywright"
10103
+ },
10104
+ "postSeedPr.execution.appStart": {
10105
+ order: 100,
10106
+ title: "Start application",
10107
+ phase: "build"
10108
+ },
10109
+ "postSeedPr.execution.capabilities": {
10110
+ order: 110,
10111
+ title: "Detect test capabilities",
10112
+ phase: "test",
10113
+ optional: true
10114
+ },
10115
+ "postSeedPr.execution.runCoreTests": {
10116
+ order: 120,
10117
+ title: "Run core test layers",
10118
+ phase: "test"
10119
+ },
10120
+ "postSeedPr.execution.e2ePhase": {
10121
+ order: 130,
10122
+ title: "Run E2E phase",
10123
+ phase: "test",
10124
+ optional: true
10125
+ },
10126
+ "finalize.writeSummary": {
10127
+ order: 140,
10128
+ title: "Write CI summary",
10129
+ phase: "finalize"
10130
+ },
10131
+ "finalize.postComment": {
10132
+ order: 150,
10133
+ title: "Post final GitHub comment",
10134
+ phase: "finalize",
10135
+ optional: true
10136
+ }
10137
+ };
10138
+ var CANONICAL_STEP_IDS = Object.keys(STEP_METADATA).sort((a, b) => b.length - a.length);
10139
+ function toIsoString(timestampMs) {
10140
+ return new Date(timestampMs).toISOString();
10141
+ }
10142
+ function getMetadata(stepId) {
10143
+ return STEP_METADATA[stepId];
10144
+ }
10145
+ function getCanonicalStepId(statePath) {
10146
+ for (const stepId of CANONICAL_STEP_IDS) {
10147
+ if (statePath === stepId || statePath.startsWith(`${stepId}.`)) {
10148
+ return stepId;
10149
+ }
10150
+ }
10151
+ return null;
10152
+ }
10153
+ function getStepOrderFromStatePath(statePath) {
10154
+ const canonical = getCanonicalStepId(statePath);
10155
+ if (!canonical) return -1;
10156
+ return STEP_METADATA[canonical]?.order ?? -1;
10157
+ }
10158
+ function pickPrimaryStatePath(statePaths, previousStatePath) {
10159
+ if (statePaths.length === 0) return previousStatePath;
10160
+ if (statePaths.length === 1 && (statePaths[0] === "done" || statePaths[0] === "failed")) {
10161
+ return statePaths[0];
10162
+ }
10163
+ const sorted = [...statePaths].sort(
10164
+ (a, b) => getStepOrderFromStatePath(b) - getStepOrderFromStatePath(a)
10165
+ );
10166
+ const candidate = sorted[0];
10167
+ if (!previousStatePath) return candidate;
10168
+ return getStepOrderFromStatePath(candidate) >= getStepOrderFromStatePath(previousStatePath) ? candidate : previousStatePath;
10169
+ }
10170
+ function createSkippedSummary(stepId, reason) {
10171
+ return createSyntheticSummary(stepId, "skipped", { reason });
10172
+ }
10173
+ function createSyntheticSummary(stepId, status, params = {}) {
10174
+ const metadata = getMetadata(stepId);
10175
+ if (!metadata) return null;
10176
+ const durationMs = params.startedAtMs !== void 0 && params.endedAtMs !== void 0 ? Math.max(0, params.endedAtMs - params.startedAtMs) : void 0;
10177
+ return [
10178
+ stepId,
10179
+ {
10180
+ status,
10181
+ title: metadata.title,
10182
+ phase: metadata.phase,
10183
+ parentStep: metadata.parentStep,
10184
+ optional: metadata.optional,
10185
+ reason: params.reason,
10186
+ startedAt: params.startedAtMs !== void 0 ? toIsoString(params.startedAtMs) : void 0,
10187
+ endedAt: params.endedAtMs !== void 0 ? toIsoString(params.endedAtMs) : void 0,
10188
+ durationMs
10189
+ }
10190
+ ];
10191
+ }
10192
+ function getBuildSkipReason(context) {
10193
+ return isCiLocalMode(context) ? "Skipped in ci-local mode" : context.input.skipBuild === true ? "Skipped by --skip-build" : "Skipped by step guard";
10194
+ }
10195
+ function getPlaywrightSkipReason(context) {
10196
+ if (isCiLocalMode(context)) return "Skipped in ci-local mode";
10197
+ if (!context.selectedLayers.includes(4)) return "Skipped because Layer 4 is not selected";
10198
+ if (shouldReusePreparedPlaywright(context)) {
10199
+ return "Reused workflow-prepared Playwright browsers";
10200
+ }
10201
+ if (context.input.skipPlaywrightInstall === true) {
10202
+ return "Skipped by --skip-playwright-install";
10203
+ }
10204
+ return "Skipped by step guard";
10205
+ }
10206
+ function getSyntheticBuildEntries(context, trackedStepIds, _stepStates, _nowMs) {
10207
+ const entries = [];
10208
+ const pushEntry = (entry) => {
10209
+ if (!entry) return;
10210
+ if (trackedStepIds.has(entry[0])) return;
10211
+ entries.push(entry);
10212
+ };
10213
+ const syntheticTimingParams = {};
10214
+ if (shouldSkipBuild(context)) {
10215
+ const reason = getBuildSkipReason(context);
10216
+ pushEntry(createSkippedSummary("postSeedPr.execution.buildAndPlaywright.build", reason));
10217
+ pushEntry(
10218
+ createSkippedSummary("postSeedPr.execution.buildAndPlaywright.manifestGenerate", reason)
10219
+ );
10220
+ pushEntry(
10221
+ createSkippedSummary("postSeedPr.execution.buildAndPlaywright.playwrightInstall", reason)
10222
+ );
10223
+ return entries;
10224
+ }
10225
+ if (context.appBuildPassed === true) {
10226
+ pushEntry(
10227
+ createSyntheticSummary(
10228
+ "postSeedPr.execution.buildAndPlaywright.build",
10229
+ "passed",
10230
+ syntheticTimingParams
10231
+ )
10232
+ );
10233
+ } else if (context.appBuildPassed === false) {
10234
+ pushEntry(
10235
+ createSyntheticSummary("postSeedPr.execution.buildAndPlaywright.build", "failed", {
10236
+ ...syntheticTimingParams,
10237
+ reason: context.error ?? "App build failed"
10238
+ })
10239
+ );
10240
+ }
10241
+ if (context.appBuildPassed === true) {
10242
+ if (context.manifestGenerated === true) {
10243
+ pushEntry(
10244
+ createSyntheticSummary(
10245
+ "postSeedPr.execution.buildAndPlaywright.manifestGenerate",
10246
+ "passed",
10247
+ syntheticTimingParams
10248
+ )
10249
+ );
10250
+ } else if (context.manifestGenerated === null) {
10251
+ pushEntry(
10252
+ createSyntheticSummary(
10253
+ "postSeedPr.execution.buildAndPlaywright.manifestGenerate",
10254
+ "skipped",
10255
+ {
10256
+ ...syntheticTimingParams,
10257
+ reason: "Skipped because manifest generation is optional for this repository"
10258
+ }
10259
+ )
10260
+ );
10261
+ } else if (context.manifestGenerated === false) {
10262
+ pushEntry(
10263
+ createSyntheticSummary(
10264
+ "postSeedPr.execution.buildAndPlaywright.manifestGenerate",
10265
+ "failed",
10266
+ {
10267
+ ...syntheticTimingParams,
10268
+ reason: "Manifest generation failed but CI continued because it is optional"
10269
+ }
10270
+ )
10271
+ );
10272
+ }
10273
+ } else if (context.appBuildPassed === false) {
10274
+ pushEntry(
10275
+ createSyntheticSummary(
10276
+ "postSeedPr.execution.buildAndPlaywright.manifestGenerate",
10277
+ "skipped",
10278
+ {
10279
+ ...syntheticTimingParams,
10280
+ reason: "Skipped because app build failed before manifest generation"
10281
+ }
10282
+ )
10283
+ );
10284
+ }
10285
+ if (shouldReusePreparedPlaywright(context)) {
10286
+ pushEntry(
10287
+ createSyntheticSummary(
10288
+ "postSeedPr.execution.buildAndPlaywright.playwrightInstall",
10289
+ "skipped",
10290
+ {
10291
+ ...syntheticTimingParams,
10292
+ reason: getPlaywrightSkipReason(context)
10293
+ }
10294
+ )
10295
+ );
10296
+ } else if (shouldSkipPlaywrightInstall(context)) {
10297
+ pushEntry(
10298
+ createSyntheticSummary(
10299
+ "postSeedPr.execution.buildAndPlaywright.playwrightInstall",
10300
+ "skipped",
10301
+ {
10302
+ ...syntheticTimingParams,
10303
+ reason: getPlaywrightSkipReason(context)
10304
+ }
10305
+ )
10306
+ );
10307
+ } else if (context.playwrightInstalled === true) {
10308
+ pushEntry(
10309
+ createSyntheticSummary(
10310
+ "postSeedPr.execution.buildAndPlaywright.playwrightInstall",
10311
+ "passed",
10312
+ syntheticTimingParams
10313
+ )
10314
+ );
10315
+ } else if (context.playwrightInstalled === false) {
10316
+ pushEntry(
10317
+ createSyntheticSummary(
10318
+ "postSeedPr.execution.buildAndPlaywright.playwrightInstall",
10319
+ "failed",
10320
+ {
10321
+ ...syntheticTimingParams,
10322
+ reason: "Playwright browser install failed"
10323
+ }
10324
+ )
10325
+ );
10326
+ }
10327
+ return entries;
10328
+ }
10329
+ function getSkippedStepEntries(context, trackedStepIds) {
10330
+ const skipped = [];
10331
+ const pushSkipped = (stepId, reason) => {
10332
+ if (trackedStepIds.has(stepId)) return;
10333
+ const entry = createSkippedSummary(stepId, reason);
10334
+ if (entry) skipped.push(entry);
10335
+ };
10336
+ if (isTestPhase(context)) {
10337
+ pushSkipped("syncSchema", "Skipped in phase=test; reusing prepared database");
10338
+ pushSkipped("applySeeds", "Skipped in phase=test; reusing prepared database");
10339
+ pushSkipped(
10340
+ "postSeedPr.observability.productionPreview",
10341
+ "Skipped in phase=test; observability DB checks are disabled"
10342
+ );
10343
+ pushSkipped(
10344
+ "postSeedPr.observability.collectSchemaStats",
10345
+ "Skipped in phase=test; observability DB checks are disabled"
10346
+ );
10347
+ }
10348
+ if (!shouldPostGitHubComment(context) || context.prContext?.prNumber === null) {
10349
+ const commentReason = context.input.skipGithubComment === true ? "Skipped by --skip-github-comment" : context.executionEnv !== "github-actions" ? "Skipped outside GitHub Actions" : "Skipped because PR context is unavailable";
10350
+ pushSkipped("initialComment", commentReason);
10351
+ pushSkipped("finalize.postComment", commentReason);
10352
+ }
10353
+ if (shouldSkipStaticChecks(context)) {
10354
+ const reason = isCiLocalMode(context) ? "Skipped in ci-local mode" : context.input.skipStaticChecks === true ? "Skipped by --skip-static-checks" : "Skipped by step guard";
10355
+ pushSkipped("postSeedPr.execution.staticChecks", reason);
10356
+ }
10357
+ if (shouldSkipBuild(context)) {
10358
+ pushSkipped("postSeedPr.execution.buildAndPlaywright", getBuildSkipReason(context));
10359
+ }
10360
+ if (shouldSkipAppStart(context)) {
10361
+ const reason = isCiLocalMode(context) ? "Skipped in ci-local mode" : !context.selectedLayers.includes(4) ? "Skipped because Layer 4 is not selected" : "Skipped by step guard";
10362
+ pushSkipped("postSeedPr.execution.appStart", reason);
10363
+ }
10364
+ if (!context.selectedLayers.includes(4)) {
10365
+ pushSkipped("postSeedPr.execution.e2ePhase", "Skipped because Layer 4 is not selected");
10366
+ } else if (Object.values(context.layerResults).some((result) => result.status === "failed")) {
10367
+ pushSkipped("postSeedPr.execution.e2ePhase", "Skipped because blocking core tests failed");
10368
+ }
10369
+ return skipped;
10370
+ }
10371
+ function createStepTelemetryTracker() {
10372
+ const stepStates = /* @__PURE__ */ new Map();
10373
+ let activeStepIds = /* @__PURE__ */ new Set();
10374
+ const startStep = (stepId, nowMs) => {
10375
+ const metadata = getMetadata(stepId);
10376
+ if (!metadata) return;
10377
+ if (stepStates.has(stepId)) return;
10378
+ stepStates.set(stepId, {
10379
+ metadata,
10380
+ startedAtMs: nowMs,
10381
+ status: "running"
10382
+ });
10383
+ };
10384
+ const finishStep = (stepId, status, nowMs) => {
10385
+ const existing = stepStates.get(stepId);
10386
+ if (!existing || existing.status !== "running") return;
10387
+ existing.status = status;
10388
+ existing.endedAtMs = nowMs;
10389
+ };
10390
+ return {
10391
+ transition(statePaths, nowMs = Date.now()) {
10392
+ const nextStepIds = new Set(
10393
+ statePaths.map((statePath) => getCanonicalStepId(statePath)).filter((value) => value !== null)
10394
+ );
10395
+ for (const stepId of activeStepIds) {
10396
+ if (!nextStepIds.has(stepId)) {
10397
+ finishStep(stepId, "passed", nowMs);
10398
+ }
10399
+ }
10400
+ for (const stepId of nextStepIds) {
10401
+ if (!activeStepIds.has(stepId)) {
10402
+ startStep(stepId, nowMs);
10403
+ }
10404
+ }
10405
+ activeStepIds = nextStepIds;
10406
+ },
10407
+ finalize({ context, succeeded, failedStatePath, nowMs = Date.now() }) {
10408
+ const failedStepId = failedStatePath ? getCanonicalStepId(failedStatePath) : null;
10409
+ for (const stepId of activeStepIds) {
10410
+ if (stepId === failedStepId) {
10411
+ finishStep(stepId, "failed", nowMs);
10412
+ continue;
10413
+ }
10414
+ const metadata = getMetadata(stepId);
10415
+ finishStep(stepId, succeeded ? "passed" : metadata?.optional ? "killed" : "killed", nowMs);
10416
+ }
10417
+ activeStepIds = /* @__PURE__ */ new Set();
10418
+ const trackedEntries = [...stepStates.entries()].sort((a, b) => a[1].metadata.order - b[1].metadata.order).map(([stepId, state]) => {
10419
+ const endedAtMs = state.endedAtMs ?? nowMs;
10420
+ const durationMs = Math.max(0, endedAtMs - state.startedAtMs);
10421
+ const defaultStatus = state.status === "running" ? "killed" : state.status;
10422
+ const override = context.stepOverrides[stepId];
10423
+ const status = override?.status ?? defaultStatus;
10424
+ return [
10425
+ stepId,
10426
+ {
10427
+ status,
10428
+ title: state.metadata.title,
10429
+ phase: state.metadata.phase,
10430
+ parentStep: state.metadata.parentStep,
10431
+ optional: state.metadata.optional,
10432
+ reason: override?.reason ?? (status === "killed" && failedStepId !== null ? `Interrupted after ${failedStepId} failed` : void 0),
10433
+ startedAt: toIsoString(state.startedAtMs),
10434
+ endedAt: toIsoString(endedAtMs),
10435
+ durationMs
10436
+ }
10437
+ ];
10438
+ });
10439
+ const skippedEntries = getSkippedStepEntries(
10440
+ context,
10441
+ new Set(trackedEntries.map(([stepId]) => stepId))
10442
+ );
10443
+ const syntheticBuildEntries = getSyntheticBuildEntries(
10444
+ context,
10445
+ new Set([...trackedEntries, ...skippedEntries].map(([stepId]) => stepId)));
10446
+ return Object.fromEntries(
10447
+ [...trackedEntries, ...skippedEntries, ...syntheticBuildEntries].sort(
10448
+ ([leftStepId], [rightStepId]) => (getMetadata(leftStepId)?.order ?? Number.MAX_SAFE_INTEGER) - (getMetadata(rightStepId)?.order ?? Number.MAX_SAFE_INTEGER)
10449
+ )
10450
+ );
10451
+ }
10452
+ };
10453
+ }
10454
+
10455
+ // src/commands/ci/machine/commands/machine-runner.ts
8973
10456
  var FLUSH_DELAY_MS = 100;
8974
- var lastProgressUpdateTime = 0;
8975
- var PROGRESS_UPDATE_DEBOUNCE_MS = 2e3;
10457
+ var progressUpdateSequence = 0;
8976
10458
  var pendingProgressUpdate = null;
8977
- var progressUpdateCount = 0;
10459
+ var progressHeartbeatTimer = null;
10460
+ var latestSnapshot = null;
10461
+ var heartbeatStep = null;
10462
+ var heartbeatFailedStep = null;
8978
10463
  var progressUpdateSuccessCount = 0;
8979
- var progressUpdateFailCount = 0;
8980
10464
  var stepStartTime = Date.now();
8981
10465
  var currentTrackedStep = null;
8982
10466
  var stepTimings = {};
10467
+ var previousActiveSteps = /* @__PURE__ */ new Set();
10468
+ var DEFAULT_PROGRESS_HEARTBEAT_MS = 3e4;
10469
+ var HEARTBEAT_ELIGIBLE_STEPS = /* @__PURE__ */ new Set([
10470
+ "syncSchema",
10471
+ "applySeeds",
10472
+ "postSeedChecks",
10473
+ "observability",
10474
+ "build",
10475
+ "runTests"
10476
+ ]);
10477
+ function resetProgressTracking() {
10478
+ progressUpdateSequence = 0;
10479
+ pendingProgressUpdate = null;
10480
+ if (progressHeartbeatTimer) {
10481
+ clearInterval(progressHeartbeatTimer);
10482
+ progressHeartbeatTimer = null;
10483
+ }
10484
+ latestSnapshot = null;
10485
+ heartbeatStep = null;
10486
+ heartbeatFailedStep = null;
10487
+ progressUpdateSuccessCount = 0;
10488
+ previousActiveSteps = /* @__PURE__ */ new Set();
10489
+ stepStartTime = Date.now();
10490
+ currentTrackedStep = null;
10491
+ for (const key of Object.keys(stepTimings)) {
10492
+ delete stepTimings[key];
10493
+ }
10494
+ }
8983
10495
  var STATE_TO_STEP = {
8984
10496
  // Setup phase
8985
10497
  setup: "setup",
@@ -8992,8 +10504,14 @@ var STATE_TO_STEP = {
8992
10504
  pullProduction: "syncSchema",
8993
10505
  syncSchema: "syncSchema",
8994
10506
  applySeeds: "applySeeds",
8995
- postSeedPr: "applySeeds",
8996
- "postSeedPr.execution.setupRoles": "applySeeds",
10507
+ productionPreview: "observability",
10508
+ collectSchemaStats: "observability",
10509
+ postSeedPr: "postSeedChecks",
10510
+ "postSeedPr.execution": "postSeedChecks",
10511
+ "postSeedPr.observability": "observability",
10512
+ "postSeedPr.execution.setupRoles": "postSeedChecks",
10513
+ "postSeedPr.observability.productionPreview": "observability",
10514
+ "postSeedPr.observability.collectSchemaStats": "observability",
8997
10515
  "postSeedPr.execution.staticChecks": "staticChecks",
8998
10516
  "postSeedPr.execution.buildAndPlaywright": "build",
8999
10517
  "postSeedPr.execution.appStart": "build",
@@ -9003,9 +10521,7 @@ var STATE_TO_STEP = {
9003
10521
  "postSeedPr.execution.coreTestsFailed": "runTests",
9004
10522
  "postSeedPr.execution.e2ePhase": "runTests",
9005
10523
  "postSeedPr.execution.done": "runTests",
9006
- "postSeedPr.observability.productionPreview": "applySeeds",
9007
- "postSeedPr.observability.collectSchemaStats": "applySeeds",
9008
- decidePath: "applySeeds",
10524
+ decidePath: "postSeedChecks",
9009
10525
  installPgTap: "applySeeds",
9010
10526
  setupRoles: "applySeeds",
9011
10527
  // Build phase
@@ -9037,6 +10553,8 @@ var STEP_ORDER = [
9037
10553
  "setup",
9038
10554
  "syncSchema",
9039
10555
  "applySeeds",
10556
+ "postSeedChecks",
10557
+ "observability",
9040
10558
  "staticChecks",
9041
10559
  "build",
9042
10560
  "runTests",
@@ -9053,10 +10571,12 @@ function resolveStepForState(state) {
9053
10571
  }
9054
10572
  return void 0;
9055
10573
  }
9056
- function getCompletedSteps(currentStep) {
9057
- const currentIndex = STEP_ORDER.indexOf(currentStep);
9058
- if (currentIndex <= 0) return [];
9059
- return STEP_ORDER.slice(0, currentIndex);
10574
+ function getCompletedSteps(currentStep, stepTimings2 = {}) {
10575
+ const completed = new Set(
10576
+ Object.keys(stepTimings2).filter((step) => STEP_ORDER.includes(step))
10577
+ );
10578
+ completed.delete(currentStep);
10579
+ return STEP_ORDER.filter((step) => completed.has(step));
9060
10580
  }
9061
10581
  function recordStepTiming(newStep) {
9062
10582
  const now = Date.now();
@@ -9066,39 +10586,43 @@ function recordStepTiming(newStep) {
9066
10586
  currentTrackedStep = newStep;
9067
10587
  stepStartTime = now;
9068
10588
  }
10589
+ function parseProgressHeartbeatMs(context) {
10590
+ const rawValue = context.input.progressIntervalSeconds?.trim();
10591
+ if (!rawValue) return DEFAULT_PROGRESS_HEARTBEAT_MS;
10592
+ const parsedSeconds = Number.parseInt(rawValue, 10);
10593
+ if (!Number.isFinite(parsedSeconds) || parsedSeconds <= 0) {
10594
+ return DEFAULT_PROGRESS_HEARTBEAT_MS;
10595
+ }
10596
+ return parsedSeconds * 1e3;
10597
+ }
10598
+ function stopProgressHeartbeat() {
10599
+ if (progressHeartbeatTimer) {
10600
+ clearInterval(progressHeartbeatTimer);
10601
+ progressHeartbeatTimer = null;
10602
+ }
10603
+ }
10604
+ function refreshProgressHeartbeat(currentStep, failedStep = null) {
10605
+ heartbeatStep = currentStep;
10606
+ heartbeatFailedStep = failedStep;
10607
+ stopProgressHeartbeat();
10608
+ if (!HEARTBEAT_ELIGIBLE_STEPS.has(currentStep)) {
10609
+ return;
10610
+ }
10611
+ const intervalMs = latestSnapshot ? parseProgressHeartbeatMs(latestSnapshot.context) : DEFAULT_PROGRESS_HEARTBEAT_MS;
10612
+ progressHeartbeatTimer = setInterval(() => {
10613
+ if (!latestSnapshot || !heartbeatStep) return;
10614
+ updateProgressComment(latestSnapshot.context, heartbeatStep, heartbeatFailedStep);
10615
+ }, intervalMs);
10616
+ progressHeartbeatTimer.unref?.();
10617
+ }
9069
10618
  function updateProgressComment(context, currentStep, failedStep = null) {
9070
- if (context.executionEnv !== "github-actions") return;
10619
+ if (!shouldPostGitHubComment(context)) return;
9071
10620
  if (!context.prContext?.prNumber) return;
9072
- if (context.input.skipGithubComment) return;
9073
10621
  const token = process.env.GITHUB_TOKEN?.trim();
9074
10622
  if (!token) return;
9075
10623
  const repository = process.env.GITHUB_REPOSITORY?.trim();
9076
10624
  if (!repository || !repository.includes("/")) return;
9077
- const now = Date.now();
9078
- const timeSinceLastUpdate = now - lastProgressUpdateTime;
9079
- if (timeSinceLastUpdate < PROGRESS_UPDATE_DEBOUNCE_MS) {
9080
- console.log(
9081
- `[DEBUG] Progress update debounced: step=${currentStep}, waited=${timeSinceLastUpdate}ms < ${PROGRESS_UPDATE_DEBOUNCE_MS}ms`
9082
- );
9083
- return;
9084
- }
9085
- lastProgressUpdateTime = now;
9086
- progressUpdateCount++;
9087
- const elapsed = Object.values(stepTimings).reduce((sum, t) => sum + (t ?? 0), 0);
9088
- console.log(
9089
- `[DEBUG] Progress update #${progressUpdateCount}: step=${currentStep}, elapsed=${Math.round(elapsed / 1e3)}s, failed=${failedStep ?? "none"}`
9090
- );
9091
- const completedSteps = getCompletedSteps(currentStep);
9092
- const progressInput = createProgressCommentInput(
9093
- context,
9094
- currentStep,
9095
- completedSteps,
9096
- failedStep,
9097
- { ...stepTimings }
9098
- // Pass a copy of current timings
9099
- );
9100
- const body = `<!-- runa-ci-report -->
9101
- ${generateProgressCommentBody(progressInput)}`;
10625
+ const mySeq = ++progressUpdateSequence;
9102
10626
  const doUpdate = async () => {
9103
10627
  if (pendingProgressUpdate) {
9104
10628
  try {
@@ -9106,25 +10630,29 @@ ${generateProgressCommentBody(progressInput)}`;
9106
10630
  } catch {
9107
10631
  }
9108
10632
  }
9109
- const startTime = Date.now();
10633
+ if (mySeq < progressUpdateSequence) {
10634
+ return;
10635
+ }
10636
+ const freshContext = latestSnapshot?.context ?? context;
10637
+ const completedSteps = getCompletedSteps(currentStep, stepTimings);
10638
+ const progressInput = createProgressCommentInput(
10639
+ freshContext,
10640
+ currentStep,
10641
+ completedSteps,
10642
+ failedStep,
10643
+ { ...stepTimings }
10644
+ );
10645
+ const body = `<!-- runa-ci-report -->
10646
+ ${generateProgressCommentBody(progressInput)}`;
9110
10647
  try {
9111
10648
  await upsertIssueComment({
9112
10649
  repo: resolveRepoContextFromEnv(),
9113
- issueNumber: assertPrNumber(context),
10650
+ issueNumber: assertPrNumber(freshContext),
9114
10651
  marker: "<!-- runa-ci-report -->",
9115
10652
  body
9116
10653
  });
9117
10654
  progressUpdateSuccessCount++;
9118
- const duration = Date.now() - startTime;
9119
- console.log(
9120
- `[DEBUG] Progress update #${progressUpdateCount} succeeded: ${duration}ms (total: ${progressUpdateSuccessCount}/${progressUpdateCount})`
9121
- );
9122
10655
  } catch (error) {
9123
- progressUpdateFailCount++;
9124
- const duration = Date.now() - startTime;
9125
- console.error(
9126
- `[DEBUG] Progress update #${progressUpdateCount} failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)} (fails: ${progressUpdateFailCount}/${progressUpdateCount})`
9127
- );
9128
10656
  }
9129
10657
  };
9130
10658
  pendingProgressUpdate = doUpdate();
@@ -9138,46 +10666,108 @@ function determineFailedStep(context) {
9138
10666
  if (!context.repoRoot) return "setup";
9139
10667
  return "setup";
9140
10668
  }
9141
- function handleProgressCommentUpdate(snapshot, prevState) {
9142
- const currentState = getStateName(snapshot);
10669
+ function setsEqual(a, b) {
10670
+ if (a.size !== b.size) return false;
10671
+ for (const item of a) {
10672
+ if (!b.has(item)) return false;
10673
+ }
10674
+ return true;
10675
+ }
10676
+ function handleProgressCommentUpdate(snapshot, prevState, currentState, activeStatePaths) {
9143
10677
  const context = snapshot.context;
9144
10678
  const currentStep = resolveStepForState(currentState);
9145
10679
  const prevStep = resolveStepForState(prevState);
9146
- if (!currentStep || currentStep === prevStep) return;
9147
- recordStepTiming(currentStep);
10680
+ const shouldRefreshWithinStep = currentStep !== void 0 && currentStep === prevStep && currentStep === "observability" && currentState !== prevState;
10681
+ const currentStepSet = new Set(
10682
+ activeStatePaths.map((p) => resolveStepForState(p)).filter((s) => s !== void 0)
10683
+ );
10684
+ const parallelStepChanged = !setsEqual(previousActiveSteps, currentStepSet);
10685
+ previousActiveSteps = currentStepSet;
10686
+ if (!currentStep || !parallelStepChanged && currentStep === prevStep && !shouldRefreshWithinStep)
10687
+ return;
10688
+ if (!shouldRefreshWithinStep) {
10689
+ recordStepTiming(currentStep);
10690
+ }
9148
10691
  if (currentStep === "finalize") {
9149
- console.log(
9150
- "[DEBUG] Skipping progress update for finalize step (final comment will be posted by actor)"
9151
- );
9152
10692
  return;
9153
10693
  }
9154
10694
  const hasFailure = currentState === "failed" || currentState === "testsFailed" || Object.values(context.layerResults).some((r) => r.status === "failed") || context.staticChecksPassed === false || context.appBuildPassed === false;
9155
10695
  const failedStep = hasFailure ? determineFailedStep(context) : null;
9156
10696
  const effectiveStep = currentState === "failed" ? failedStep ?? currentStep : currentStep;
10697
+ if (effectiveStep) {
10698
+ refreshProgressHeartbeat(effectiveStep, failedStep);
10699
+ } else {
10700
+ stopProgressHeartbeat();
10701
+ }
9157
10702
  updateProgressComment(context, effectiveStep, failedStep);
9158
10703
  }
10704
+ async function persistSummary(params) {
10705
+ if (params.context.repoRoot === null) return void 0;
10706
+ const summaryInput = params.context.summary !== null ? {
10707
+ cwd: params.context.repoRoot,
10708
+ summary: params.context.summary
10709
+ } : createWriteSummaryRequest(params.context);
10710
+ const nextSummary = {
10711
+ ...summaryInput.summary,
10712
+ steps: params.steps
10713
+ };
10714
+ return await writeCiSummary({
10715
+ cwd: summaryInput.cwd,
10716
+ summary: nextSummary
10717
+ });
10718
+ }
9159
10719
  async function runCiMachine(input, logger, onStateChange) {
9160
10720
  return new Promise((resolve, reject) => {
10721
+ resetProgressTracking();
9161
10722
  const actor = createActor(ciMachine, { input });
9162
10723
  let previousState = "";
10724
+ let lastActionableState = null;
10725
+ let settled = false;
10726
+ const stepTelemetry = createStepTelemetryTracker();
9163
10727
  actor.subscribe((snapshot) => {
9164
- const currentState = getStateName(snapshot);
9165
- const status = snapshot.status;
10728
+ latestSnapshot = snapshot;
10729
+ const activeStatePaths = getSnapshotStatePaths(snapshot);
10730
+ stepTelemetry.transition(activeStatePaths);
10731
+ const currentState = pickPrimaryStatePath(activeStatePaths, previousState) ?? activeStatePaths[0] ?? previousState ?? "unknown";
10732
+ snapshot.status;
10733
+ if (currentState && currentState !== "done" && currentState !== "failed" && !currentState.startsWith("finalize")) {
10734
+ lastActionableState = currentState;
10735
+ }
9166
10736
  if (currentState !== previousState) {
9167
- console.log(
9168
- `[DEBUG] State change: ${previousState} -> ${currentState} (status: ${status})`
9169
- );
9170
- handleProgressCommentUpdate(snapshot, previousState);
10737
+ handleProgressCommentUpdate(snapshot, previousState, currentState, activeStatePaths);
9171
10738
  onStateChange?.(snapshot, previousState, logger);
9172
10739
  previousState = currentState;
9173
10740
  }
9174
- if (isComplete(snapshot)) {
10741
+ if (isComplete(snapshot) && !settled) {
10742
+ settled = true;
10743
+ stopProgressHeartbeat();
9175
10744
  const output = snapshot.output;
9176
- console.log(
9177
- `[DEBUG] Machine completed: state=${currentState}, status=${status}, exitCode=${output?.exitCode}`
9178
- );
9179
10745
  if (output) {
9180
- resolve(output);
10746
+ void (async () => {
10747
+ const steps = stepTelemetry.finalize({
10748
+ context: snapshot.context,
10749
+ succeeded: output.exitCode === 0,
10750
+ failedStatePath: output.exitCode === 0 ? null : lastActionableState
10751
+ });
10752
+ const nextOutput = {
10753
+ ...output,
10754
+ steps
10755
+ };
10756
+ try {
10757
+ const summaryPath = await persistSummary({
10758
+ context: snapshot.context,
10759
+ steps
10760
+ });
10761
+ if (summaryPath) {
10762
+ nextOutput.summaryPath = summaryPath;
10763
+ }
10764
+ } catch (error) {
10765
+ console.error(
10766
+ `[DEBUG] Failed to persist CI summary: ${error instanceof Error ? error.message : String(error)}`
10767
+ );
10768
+ }
10769
+ resolve(nextOutput);
10770
+ })().catch(reject);
9181
10771
  }
9182
10772
  }
9183
10773
  });
@@ -9295,7 +10885,7 @@ var stateLogHandlers = {
9295
10885
  if (ctx.input.productionDatabaseUrl) {
9296
10886
  logger.info("Running: pg_dump (production) \u2192 psql (local)");
9297
10887
  } else {
9298
- logger.info("GH_DATABASE_URL_ADMIN / GH_DATABASE_URL not set, skipping");
10888
+ logger.info("GH_DATABASE_URL_ADMIN not set, skipping");
9299
10889
  }
9300
10890
  },
9301
10891
  syncSchema: (ctx, logger) => {
@@ -9367,7 +10957,7 @@ function buildMachineInput(options) {
9367
10957
  executionMode: void 0,
9368
10958
  dbMode: void 0,
9369
10959
  // PRD: GH_DATABASE_URL_ADMIN = postgres role (DDL capable, for pg_dump)
9370
- productionDatabaseUrl: process.env.GH_DATABASE_URL_ADMIN || process.env.GH_DATABASE_URL,
10960
+ productionDatabaseUrl: process.env.GH_DATABASE_URL_ADMIN,
9371
10961
  databaseUrl: process.env.DATABASE_URL,
9372
10962
  runtimeEnv: captureRuntimeEnv()
9373
10963
  };
@@ -9419,7 +11009,7 @@ var CiExecutionEnvSchema = z.enum(["github-actions", "local"]);
9419
11009
  var CiPhaseSchema = z.enum(["all", "test"]);
9420
11010
  var CiDbModeSchema = z.enum(["auto", "local"]);
9421
11011
  var RepoKindSchema = z.enum(["monorepo", "pj-repo", "unknown"]);
9422
- var StepStatusSchema = z.enum(["passed", "failed", "skipped", "timeout"]);
11012
+ var StepStatusSchema = z.enum(["passed", "failed", "skipped", "killed", "timeout"]);
9423
11013
  var LayerStatusSchema = z.enum(["passed", "failed", "skipped", "timeout", "killed"]);
9424
11014
  z.object({
9425
11015
  // === Command ===
@@ -9487,6 +11077,12 @@ z.object({
9487
11077
  skipStaticChecks: z.boolean().optional(),
9488
11078
  /** Skip build */
9489
11079
  skipBuild: z.boolean().optional(),
11080
+ /** Skip local Supabase start inside ci pr setup */
11081
+ skipLocalDbStart: z.boolean().optional(),
11082
+ /** Assume local Supabase is already ready and skip status polling */
11083
+ assumeSupabaseReady: z.boolean().optional(),
11084
+ /** Skip Playwright browser installation even when Layer 4 is selected */
11085
+ skipPlaywrightInstall: z.boolean().optional(),
9490
11086
  /** CI optimization: skip DB codegen (TypeScript + Zod) during db sync */
9491
11087
  skipDbCodegen: z.boolean().optional(),
9492
11088
  /** Fail fast */
@@ -9526,6 +11122,8 @@ z.object({
9526
11122
  });
9527
11123
  z.object({
9528
11124
  passed: z.boolean(),
11125
+ skipped: z.boolean().optional(),
11126
+ skipReason: z.string().optional(),
9529
11127
  appDatabaseUrl: z.string().optional(),
9530
11128
  error: z.string().optional()
9531
11129
  });
@@ -9579,7 +11177,15 @@ var StepSummarySchema = z.object({
9579
11177
  status: StepStatusSchema,
9580
11178
  exitCode: z.number().int().optional(),
9581
11179
  logPath: z.string().optional(),
9582
- error: z.string().optional()
11180
+ error: z.string().optional(),
11181
+ reason: z.string().optional(),
11182
+ title: z.string().optional(),
11183
+ phase: z.enum(["setup", "github", "db", "observability", "build", "test", "finalize"]).optional(),
11184
+ parentStep: z.string().optional(),
11185
+ optional: z.boolean().optional(),
11186
+ startedAt: z.string().optional(),
11187
+ endedAt: z.string().optional(),
11188
+ durationMs: z.number().int().nonnegative().optional()
9583
11189
  });
9584
11190
  var LayerSummarySchema = z.object({
9585
11191
  status: LayerStatusSchema,
@@ -9682,13 +11288,28 @@ function logPlan2(steps) {
9682
11288
  console.log("");
9683
11289
  }
9684
11290
  }
11291
+ function logSetupRolesStatus(ctx, logger) {
11292
+ const setupRolesStep = ctx.stepOverrides["postSeedPr.execution.setupRoles"];
11293
+ if (setupRolesStep?.status === "skipped") {
11294
+ logger.info(`Database roles setup skipped: ${setupRolesStep.reason ?? "optional step"}`);
11295
+ return;
11296
+ }
11297
+ if (ctx.rolesSetup) {
11298
+ logger.success("Database roles configured");
11299
+ }
11300
+ }
9685
11301
  var CI_PR_UNKNOWN_STATE_LOG_SKIP = /* @__PURE__ */ new Set(["decidePath", "done", "failed"]);
9686
11302
  var stateLogHandlers2 = {
9687
11303
  // ─── Setup Phase ───────────────────────────────────────────
9688
11304
  setup: (_ctx, _logger) => {
9689
11305
  logSection3("Setup: Resolving Environment");
9690
11306
  },
9691
- "setup.prLocal": (_ctx, logger) => {
11307
+ "setup.prLocal": (ctx, logger) => {
11308
+ if (shouldReusePreparedRuntime(ctx)) {
11309
+ logSection3("Setup: PR + Prepared Local Runtime");
11310
+ logger.info("Reusing workflow-prepared local Supabase instance...");
11311
+ return;
11312
+ }
9692
11313
  logSection3("Setup: PR + Local Supabase");
9693
11314
  logger.info("Starting local Supabase instance...");
9694
11315
  },
@@ -9721,7 +11342,7 @@ var stateLogHandlers2 = {
9721
11342
  logger.success("Seeds applied successfully");
9722
11343
  }
9723
11344
  logSection3("Database: Production Preview (dry-run)");
9724
- logger.info("Running: pnpm exec runa db apply production --check");
11345
+ logger.info("Running: pnpm exec runa db apply production --check --compare-only");
9725
11346
  },
9726
11347
  "postSeedPr.observability.collectSchemaStats": (ctx, logger) => {
9727
11348
  if (ctx.productionPreview?.executed) {
@@ -9735,9 +11356,7 @@ var stateLogHandlers2 = {
9735
11356
  logger.info("Comparing Local/CI/Production schemas...");
9736
11357
  },
9737
11358
  "postSeedPr.execution.staticChecks": (ctx, logger) => {
9738
- if (ctx.rolesSetup) {
9739
- logger.success("Database roles configured");
9740
- }
11359
+ logSetupRolesStatus(ctx, logger);
9741
11360
  logSection3("Static Checks: type-check + lint (parallel)");
9742
11361
  logger.info("Running: pnpm type-check || pnpm lint");
9743
11362
  },
@@ -9745,6 +11364,11 @@ var stateLogHandlers2 = {
9745
11364
  if (ctx.staticChecksPassed) {
9746
11365
  logger.success("Static checks passed");
9747
11366
  }
11367
+ if (shouldReusePreparedPlaywright(ctx)) {
11368
+ logSection3("Build: App Build + Reuse Prepared Playwright");
11369
+ logger.info("Running: pnpm build (Playwright install owned by outer workflow)");
11370
+ return;
11371
+ }
9748
11372
  logSection3("Build: App Build + Playwright Install (parallel)");
9749
11373
  logger.info("Running: pnpm build || pnpm exec playwright install chromium");
9750
11374
  },
@@ -9754,6 +11378,8 @@ var stateLogHandlers2 = {
9754
11378
  }
9755
11379
  if (ctx.manifestGenerated) {
9756
11380
  logger.success("Manifest generated (Layer 3 ready)");
11381
+ } else if (ctx.manifestGenerated === null) {
11382
+ logger.info("Manifest generation skipped (optional)");
9757
11383
  } else if (ctx.manifestGenerated === false) {
9758
11384
  logger.warn("Manifest generation failed (Layer 3 may be skipped)");
9759
11385
  }
@@ -9785,7 +11411,7 @@ var stateLogHandlers2 = {
9785
11411
  logger.info("Seeds skipped (no seed files or already seeded)");
9786
11412
  }
9787
11413
  logSection3("Database: Production Preview (dry-run)");
9788
- logger.info("Running: pnpm exec runa db apply production --check");
11414
+ logger.info("Running: pnpm exec runa db apply production --check --compare-only");
9789
11415
  },
9790
11416
  collectSchemaStats: (ctx, logger) => {
9791
11417
  if (ctx.productionPreview?.executed) {
@@ -9807,9 +11433,7 @@ var stateLogHandlers2 = {
9807
11433
  },
9808
11434
  // ─── Static Analysis Phase ─────────────────────────────────
9809
11435
  staticChecks: (ctx, logger) => {
9810
- if (ctx.rolesSetup) {
9811
- logger.success("Database roles configured");
9812
- }
11436
+ logSetupRolesStatus(ctx, logger);
9813
11437
  logSection3("Static Checks: type-check + lint (parallel)");
9814
11438
  logger.info("Running: pnpm type-check || pnpm lint");
9815
11439
  },
@@ -9818,6 +11442,11 @@ var stateLogHandlers2 = {
9818
11442
  if (ctx.staticChecksPassed) {
9819
11443
  logger.success("Static checks passed");
9820
11444
  }
11445
+ if (shouldReusePreparedPlaywright(ctx)) {
11446
+ logSection3("Build: App Build + Reuse Prepared Playwright");
11447
+ logger.info("Running: pnpm build (Playwright install owned by outer workflow)");
11448
+ return;
11449
+ }
9821
11450
  logSection3("Build: App Build + Playwright Install (parallel)");
9822
11451
  logger.info("Running: pnpm build || pnpm exec playwright install chromium");
9823
11452
  },
@@ -9827,6 +11456,8 @@ var stateLogHandlers2 = {
9827
11456
  }
9828
11457
  if (ctx.manifestGenerated) {
9829
11458
  logger.success("Manifest generated (Layer 3 ready)");
11459
+ } else if (ctx.manifestGenerated === null) {
11460
+ logger.info("Manifest generation skipped (optional)");
9830
11461
  } else if (ctx.manifestGenerated === false) {
9831
11462
  logger.warn("Manifest generation failed (Layer 3 may be skipped)");
9832
11463
  }
@@ -9928,8 +11559,7 @@ function getGitHubEventAction() {
9928
11559
  const eventPath = process.env.GITHUB_EVENT_PATH;
9929
11560
  if (!eventPath) return void 0;
9930
11561
  try {
9931
- const fs2 = __require("fs");
9932
- const eventPayload = JSON.parse(fs2.readFileSync(eventPath, "utf-8"));
11562
+ const eventPayload = JSON.parse(readFileSync(eventPath, "utf-8"));
9933
11563
  return eventPayload.action;
9934
11564
  } catch {
9935
11565
  return void 0;
@@ -9945,6 +11575,9 @@ function optionsToMachineInput(options) {
9945
11575
  phase: options.phase,
9946
11576
  skipStaticChecks: options.skipStaticChecks,
9947
11577
  skipBuild: options.skipBuild,
11578
+ skipLocalDbStart: options.skipLocalDbStart,
11579
+ assumeSupabaseReady: options.assumeSupabaseReady,
11580
+ skipPlaywrightInstall: options.skipPlaywrightInstall,
9948
11581
  skipDbCodegen: options.skipDbCodegen,
9949
11582
  failFast: options.failFast,
9950
11583
  maxWaitSeconds: options.maxWaitSeconds,
@@ -9955,9 +11588,9 @@ function optionsToMachineInput(options) {
9955
11588
  targetDir: process.cwd(),
9956
11589
  layers: [1, 2, 3, 4],
9957
11590
  // Environment capture (Environment Capture Pattern: capture at entry point, not in machine/guards)
9958
- databaseUrl: process.env.DATABASE_URL,
11591
+ databaseUrl: options.databaseUrl ?? process.env.DATABASE_URL,
9959
11592
  // PRD: GH_DATABASE_URL_ADMIN = postgres role (DDL capable, for pg-schema-diff dry-run)
9960
- productionDatabaseUrl: process.env.GH_DATABASE_URL_ADMIN || process.env.GH_DATABASE_URL,
11593
+ productionDatabaseUrl: process.env.GH_DATABASE_URL_ADMIN,
9961
11594
  runtimeEnv: captureRuntimeEnv(),
9962
11595
  githubRef: process.env.GITHUB_REF,
9963
11596
  // FIX: Read action from GITHUB_EVENT_PATH JSON, not non-existent GITHUB_EVENT_ACTION env var
@@ -9980,16 +11613,33 @@ async function runCiPrCommand(options) {
9980
11613
  const logger = createCLILogger("ci:pr");
9981
11614
  const skipStaticChecks = options.skipStaticChecks === true;
9982
11615
  const skipBuild = options.skipBuild === true;
11616
+ const phase = options.phase ?? "all";
11617
+ const testOnlyPhase = phase === "test";
11618
+ const preparedRuntime = options.skipLocalDbStart === true || options.assumeSupabaseReady === true;
11619
+ const preparedPlaywright = options.skipPlaywrightInstall === true;
9983
11620
  const planSteps = [
9984
- { id: "setup", description: "Start local Supabase instance" },
9985
11621
  {
9986
- id: "db",
9987
- description: "db apply \u2192 db seed \u2192 (production preview \u2225 schema stats \u2225 db:setup-roles)"
11622
+ id: "setup",
11623
+ description: testOnlyPhase ? "Resolve repo/app context (optionally reuse pre-started local Supabase)" : preparedRuntime ? "Reuse workflow-prepared local Supabase runtime" : "Start local Supabase instance"
9988
11624
  },
11625
+ ...testOnlyPhase ? [{ id: "db", description: "Skip schema apply/seed and reuse the prepared database" }] : [
11626
+ {
11627
+ id: "db",
11628
+ description: "db apply \u2192 db seed \u2192 (production preview \u2225 schema stats \u2225 db:setup-roles)"
11629
+ }
11630
+ ],
9989
11631
  ...skipStaticChecks ? [] : [{ id: "static", description: "pnpm type-check + pnpm lint (parallel)" }],
9990
- ...skipBuild ? [] : [{ id: "build", description: "pnpm build \u2192 manifest:generate + playwright install" }],
11632
+ ...skipBuild ? [] : [
11633
+ {
11634
+ id: "build",
11635
+ description: preparedPlaywright ? "pnpm build \u2192 manifest:generate (reuse preinstalled Playwright)" : "pnpm build \u2192 manifest:generate + playwright install"
11636
+ }
11637
+ ],
9991
11638
  { id: "test", description: "Start app \u2192 detect capabilities \u2192 run layers 1-4" },
9992
- { id: "finalize", description: "Write ci-summary.json + post GitHub comment" }
11639
+ {
11640
+ id: "finalize",
11641
+ description: "Write ci-summary.json + post GitHub comment"
11642
+ }
9993
11643
  ];
9994
11644
  logPlan2(planSteps);
9995
11645
  const machineInput = optionsToMachineInput(options);
@@ -10010,7 +11660,19 @@ async function runCiPrCommand(options) {
10010
11660
  await flushAndExit(1);
10011
11661
  }
10012
11662
  }
10013
- var ciPrUnifiedCommand = new Command("pr").description("Run PR CI end-to-end (uses local Docker Supabase)").option("--mode <mode>", "Mode: github-actions | local").option("--output <output>", "Output: human | json").option("--config <path>", "Config path (defaults to .runa/ci.config.json if present)").option("--phase <phase>", "Phase: all | test", "all").option("--skip-static-checks", "Skip type-check and lint (assumes already executed)", false).option("--skip-build", "Skip build (advanced; only if app start does not require build)", false).option(
11663
+ var ciPrUnifiedCommand = new Command("pr").description("Run PR CI end-to-end (uses local Docker Supabase or workflow-prepared runtime)").option("--mode <mode>", "Mode: github-actions | local").option("--output <output>", "Output: human | json").option("--config <path>", "Config path (defaults to .runa/ci.config.json if present)").option("--database-url <url>", "Override local database URL (defaults to DATABASE_URL)").option("--phase <phase>", "Phase: all | blocking | observability | test", "all").option("--skip-static-checks", "Skip type-check and lint (assumes already executed)", false).option("--skip-build", "Skip build (advanced; only if app start does not require build)", false).option(
11664
+ "--skip-local-db-start",
11665
+ "Skip local Supabase start inside ci pr setup (for workflow-prepared environments)",
11666
+ false
11667
+ ).option(
11668
+ "--assume-supabase-ready",
11669
+ "Skip Supabase status polling and reuse configured local URLs/keys",
11670
+ false
11671
+ ).option(
11672
+ "--skip-playwright-install",
11673
+ "Skip Playwright browser installation even when Layer 4 is selected",
11674
+ false
11675
+ ).option(
10014
11676
  "--skip-db-codegen",
10015
11677
  "Skip DB codegen (drizzle introspect + zod) during db sync (recommended for CI preview)",
10016
11678
  false