@runa-ai/runa-cli 0.7.2 → 0.8.0

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 (41) hide show
  1. package/dist/{chunk-Z7A4BEWF.js → chunk-3JO6YP3T.js} +1 -1
  2. package/dist/chunk-6E2DRXIL.js +452 -0
  3. package/dist/{chunk-PMXE5XOJ.js → chunk-GHQH6UC5.js} +1 -1
  4. package/dist/{chunk-LCK2LGVR.js → chunk-PAWNJA3N.js} +1 -1
  5. package/dist/{chunk-FWMGC5FP.js → chunk-RB2ZUS76.js} +249 -12
  6. package/dist/{chunk-CKRLVEIO.js → chunk-ZYT7OQJB.js} +16 -11
  7. package/dist/{ci-Z4525QW6.js → ci-ZK3LKYFX.js} +305 -429
  8. package/dist/{cli-Q2XIQDRS.js → cli-ZY5VRIJA.js} +13 -13
  9. package/dist/commands/ci/commands/ci-resolvers.d.ts +1 -2
  10. package/dist/commands/ci/machine/actors/setup/pr-common.d.ts +1 -1
  11. package/dist/commands/ci/machine/contract.d.ts +6 -1
  12. package/dist/commands/ci/machine/guards.d.ts +16 -0
  13. package/dist/commands/ci/machine/machine.d.ts +11 -3
  14. package/dist/commands/db/apply/actors/seed-actors.d.ts +1 -0
  15. package/dist/commands/db/apply/contract.d.ts +23 -0
  16. package/dist/commands/db/apply/helpers/fresh-db-handler.d.ts +2 -1
  17. package/dist/commands/db/apply/helpers/hazard-handler.d.ts +19 -8
  18. package/dist/commands/db/apply/helpers/index.d.ts +2 -1
  19. package/dist/commands/db/apply/helpers/no-change-plan.d.ts +2 -0
  20. package/dist/commands/db/apply/helpers/plan-check-filter.d.ts +11 -0
  21. package/dist/commands/db/apply/machine.d.ts +52 -1
  22. package/dist/commands/db/utils/boundary-policy/types.d.ts +2 -0
  23. package/dist/commands/db/utils/duplicate-function-ownership.d.ts +35 -0
  24. package/dist/commands/db/utils/plan-size-guard.d.ts +16 -0
  25. package/dist/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.d.ts +4 -0
  26. package/dist/constants/versions.d.ts +1 -1
  27. package/dist/{db-BPQ2TEQM.js → db-EPI2DQYN.js} +1203 -410
  28. package/dist/{dev-MLRKIP7F.js → dev-GB5ERUVR.js} +1 -1
  29. package/dist/{env-WNHJVLOT.js → env-WP74UUMO.js} +1 -1
  30. package/dist/{hotfix-Z5EGVSMH.js → hotfix-TOSGTVCW.js} +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/{init-S2ATHLJ6.js → init-35JLDFHI.js} +1 -1
  33. package/dist/{risk-detector-VO5HJR4R.js → risk-detector-S7XQF4I2.js} +1 -1
  34. package/dist/{risk-detector-core-7WZJZ5ZI.js → risk-detector-core-TGFKWHRS.js} +1 -1
  35. package/dist/{risk-detector-plpgsql-ULV7NLDB.js → risk-detector-plpgsql-O32TUR34.js} +103 -5
  36. package/dist/{upgrade-BDUWBRT5.js → upgrade-7L4JIE4K.js} +1 -1
  37. package/dist/{vuln-check-66RXX3TO.js → vuln-check-G6I4YYDC.js} +1 -1
  38. package/dist/{vuln-checker-FFOGOJPT.js → vuln-checker-CT2AYPIS.js} +1 -1
  39. package/dist/{watch-ITYW57SL.js → watch-AL4LCBRM.js} +1 -1
  40. package/package.json +3 -3
  41. package/dist/chunk-4XHZQRRK.js +0 -215
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
- import { normalizeDatabaseUrlForDdl, parseBoolish, enhanceConnectionError, detectAppSchemas, formatSchemasForSql } from './chunk-4XHZQRRK.js';
4
- import { isPathContained } from './chunk-DRSUEMAK.js';
3
+ import { normalizeDatabaseUrlForDdl, parseBoolish, enhanceConnectionError, isIdempotentRoleHazard, detectAppSchemas, formatSchemasForSql } from './chunk-6E2DRXIL.js';
5
4
  import './chunk-QDF7QXBL.js';
6
5
  import { getSnapshotStateName, getSnapshotStatePaths, isSnapshotComplete } from './chunk-XVNDDHAF.js';
7
- import { writeEnvLocal, startAppBackground, waitForAppReady, executePrSetupBase, createErrorOutput } from './chunk-FWMGC5FP.js';
6
+ import { createInitialSummary, resolveMode, appendGithubStepSummary, buildCiProdApplyStepSummaryMarkdown, setSummaryErrorFromUnknown, writeEnvLocal, startAppBackground, waitForAppReady, executePrSetupBase, createErrorOutput, requireCiAutoApprove, resolveProdApplyInputs, parseIntOr, addGithubMask } from './chunk-RB2ZUS76.js';
8
7
  import { parsePostgresUrl, buildPsqlArgs, buildPsqlEnv, psqlSyncQuery } from './chunk-A6A7JIRD.js';
9
8
  import { ensureRunaTmpDir, runLogged } from './chunk-6FAU4IGR.js';
10
9
  import { createMachineStateChangeLogger } from './chunk-5FT3F36G.js';
@@ -13,13 +12,13 @@ import { init_constants, init_local_supabase, detectLocalSupabasePorts } from '.
13
12
  import { emitJsonSuccess } from './chunk-KE6QJBZG.js';
14
13
  import './chunk-WJXC4MVY.js';
15
14
  import { setOutputFormat } from './chunk-HKUWEGUX.js';
16
- import { detectEnvironment } from './chunk-JMJP4A47.js';
17
- import { init_esm_shims, __require } from './chunk-VRXHCR5K.js';
15
+ import './chunk-JMJP4A47.js';
16
+ import { init_esm_shims } from './chunk-VRXHCR5K.js';
18
17
  import { Command } from 'commander';
19
18
  import { spawnSync, spawn, execFileSync } from 'child_process';
20
- import { mkdir, writeFile, appendFile, readFile } from 'fs/promises';
19
+ import { mkdir, writeFile, readFile } from 'fs/promises';
21
20
  import path4, { join } from 'path';
22
- import { CommandOutcomeSchema, createCLILogger, CLIError, syncFromProduction, formatDuration, GITHUB_API, getClassificationForProfile, isTimeoutLikeMessage, buildCommandOutcomeSummary, deriveCommandExitMode, getStatusIcon, detectDatabasePackage, DATABASE_PACKAGE_CANDIDATES } from '@runa-ai/runa';
21
+ import { CommandOutcomeSchema, createCLILogger, CLIError, syncFromProduction, GITHUB_API, getClassificationForProfile, formatDuration, isTimeoutLikeMessage, buildCommandOutcomeSummary, deriveCommandExitMode, getStatusIcon, detectDatabasePackage, DATABASE_PACKAGE_CANDIDATES } from '@runa-ai/runa';
23
22
  import { z } from 'zod';
24
23
  import { existsSync, createWriteStream, readFileSync, readdirSync, statSync, promises, lstatSync } from 'fs';
25
24
  import { resolve4 } from 'dns/promises';
@@ -37,28 +36,6 @@ init_esm_shims();
37
36
 
38
37
  // src/commands/ci/commands/ci-checks.ts
39
38
  init_esm_shims();
40
-
41
- // src/commands/ci/utils/github.ts
42
- init_esm_shims();
43
- async function appendGithubStepSummary(markdown) {
44
- const file = process.env.GITHUB_STEP_SUMMARY;
45
- if (!file) return;
46
- await appendFile(file, `${markdown.trimEnd()}
47
-
48
- `, "utf-8");
49
- }
50
- function addGithubMask(value) {
51
- if (!value) return;
52
- console.log(`::add-mask::${value}`);
53
- }
54
- z.object({
55
- action: z.string().optional(),
56
- pull_request: z.object({
57
- number: z.number().int()
58
- })
59
- }).passthrough();
60
-
61
- // src/commands/ci/commands/ci-checks.ts
62
39
  async function runTool(params) {
63
40
  const startTime = Date.now();
64
41
  return new Promise((resolve) => {
@@ -859,8 +836,8 @@ async function detectRisks(repoRoot, tmpDir, timeoutMs) {
859
836
  } catch (error) {
860
837
  let logContent = "";
861
838
  try {
862
- const { readFileSync: readFileSync5 } = await import('fs');
863
- logContent = readFileSync5(logFile, "utf-8");
839
+ const { readFileSync: readFileSync4 } = await import('fs');
840
+ logContent = readFileSync4(logFile, "utf-8");
864
841
  } catch {
865
842
  }
866
843
  const isInitialDeployment = logContent.includes("No common ancestor") || logContent.includes("INITIAL DEPLOYMENT");
@@ -991,8 +968,8 @@ async function applyProductionSchema(repoRoot, tmpDir, productionDbUrlAdmin, pro
991
968
  const totalMs = Date.now() - startTime;
992
969
  let logContent = "";
993
970
  try {
994
- const { readFileSync: readFileSync5 } = await import('fs');
995
- logContent = readFileSync5(logPath, "utf-8");
971
+ const { readFileSync: readFileSync4 } = await import('fs');
972
+ logContent = readFileSync4(logPath, "utf-8");
996
973
  } catch {
997
974
  }
998
975
  const parsed = parseApplyLog(logContent);
@@ -1194,135 +1171,6 @@ async function maybePostFailureComment(params) {
1194
1171
  }
1195
1172
  }
1196
1173
 
1197
- // src/commands/ci/commands/ci-prod-utils.ts
1198
- init_esm_shims();
1199
- function requireEnv(name) {
1200
- const v = process.env[name];
1201
- if (v && v.trim().length > 0) return v.trim();
1202
- throw new CLIError(
1203
- `Missing required environment variable: ${name}`,
1204
- "CI_INPUT_MISSING",
1205
- [`Set ${name} in GitHub Actions secrets/env`, "If unsure, run: runa check"],
1206
- void 0,
1207
- 10
1208
- );
1209
- }
1210
- function resolveRepoKind() {
1211
- const env = detectEnvironment(process.cwd());
1212
- const result = env === "runa-repo" ? "monorepo" : env === "pj-repo" ? "pj-repo" : "unknown";
1213
- if (process.env.RUNA_DEBUG === "true") {
1214
- console.error("[DEBUG:resolveRepoKind]", {
1215
- cwd: process.cwd(),
1216
- detectedEnv: env,
1217
- result
1218
- });
1219
- }
1220
- return result;
1221
- }
1222
- function createInitialSummary(params) {
1223
- return {
1224
- version: "1.0",
1225
- mode: params.mode,
1226
- command: "ci prod-apply",
1227
- status: "failure",
1228
- startedAt: params.startedAt.toISOString(),
1229
- endedAt: params.startedAt.toISOString(),
1230
- durationMs: 0,
1231
- repoKind: resolveRepoKind(),
1232
- detected: {},
1233
- diagnostics: {},
1234
- steps: {},
1235
- layers: {},
1236
- errors: []
1237
- };
1238
- }
1239
- function requireCiAutoApprove(params) {
1240
- if (params.mode !== "github-actions") return;
1241
- if (params.autoApprove === true) return;
1242
- throw new CLIError(
1243
- "Missing required flag: --auto-approve (required in CI mode)",
1244
- "CI_AUTO_APPROVE_REQUIRED",
1245
- ["Re-run with: runa ci prod-apply --auto-approve", "Keep CI non-interactive and deterministic"],
1246
- void 0,
1247
- 10
1248
- );
1249
- }
1250
- function resolveProdApplyInputs() {
1251
- const productionDatabaseUrlAdmin = requireEnv("GH_DATABASE_URL_ADMIN");
1252
- const productionDatabaseUrl = requireEnv("GH_DATABASE_URL");
1253
- addGithubMask(productionDatabaseUrlAdmin);
1254
- addGithubMask(productionDatabaseUrl);
1255
- return {
1256
- productionDatabaseUrlAdmin,
1257
- productionDatabaseUrl,
1258
- githubSha: process.env.GITHUB_SHA ?? "unknown",
1259
- githubActor: process.env.GITHUB_ACTOR ?? "unknown",
1260
- githubRepository: process.env.GITHUB_REPOSITORY ?? "unknown"
1261
- };
1262
- }
1263
- function setSummaryErrorFromUnknown(summary, error) {
1264
- if (error instanceof CLIError) {
1265
- summary.errors.push({
1266
- code: error.code ?? "CI_ERROR",
1267
- message: error.message,
1268
- step: "ci prod-apply",
1269
- details: error.cause instanceof Error ? error.cause.message : void 0
1270
- });
1271
- return;
1272
- }
1273
- if (error instanceof Error) {
1274
- summary.errors.push({ code: "CI_ERROR", message: error.message, step: "ci prod-apply" });
1275
- return;
1276
- }
1277
- summary.errors.push({ code: "CI_ERROR", message: String(error), step: "ci prod-apply" });
1278
- }
1279
- function buildCiProdApplyStepSummaryMarkdown(params) {
1280
- const { summary } = params;
1281
- const lines = [];
1282
- const statusEmoji = summary.status === "success" ? "\u2705" : "\u274C";
1283
- const duration = formatDuration(summary.durationMs);
1284
- lines.push(
1285
- `## ${statusEmoji} Production Deploy ${summary.status === "success" ? "Completed" : "Failed"}`
1286
- );
1287
- lines.push("");
1288
- lines.push(`**Duration**: ${duration}`);
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
- }
1303
- if (summary.errors.length > 0) {
1304
- lines.push("### \u274C Errors");
1305
- lines.push("");
1306
- for (const e of summary.errors) {
1307
- lines.push(`- **${e.code}**: ${e.message}`);
1308
- }
1309
- lines.push("");
1310
- }
1311
- lines.push("<details>");
1312
- lines.push("<summary>\u{1F4CB} Technical Details</summary>");
1313
- lines.push("");
1314
- lines.push(`- Command: \`${summary.command}\``);
1315
- lines.push(`- Mode: \`${summary.mode}\``);
1316
- lines.push(`- Summary: \`${params.summaryPath}\``);
1317
- if (summary.dbOutcome) {
1318
- lines.push(`- DB exit mode: \`${summary.dbOutcome.exitMode}\``);
1319
- }
1320
- lines.push("");
1321
- lines.push("</details>");
1322
- lines.push("");
1323
- return lines.join("\n");
1324
- }
1325
-
1326
1174
  // src/commands/ci/commands/ci-prod-workflow.ts
1327
1175
  init_esm_shims();
1328
1176
 
@@ -1756,22 +1604,6 @@ var CiProdApplyWorkflow = class {
1756
1604
  }
1757
1605
  };
1758
1606
 
1759
- // src/commands/ci/commands/ci-resolvers.ts
1760
- init_esm_shims();
1761
-
1762
- // src/commands/ci/utils/config-readers.ts
1763
- init_esm_shims();
1764
-
1765
- // src/commands/ci/commands/ci-resolvers.ts
1766
- function resolveMode(modeRaw) {
1767
- if (modeRaw === "github-actions" || modeRaw === "local") return modeRaw;
1768
- return process.env.GITHUB_ACTIONS === "true" ? "github-actions" : "local";
1769
- }
1770
- function parseIntOr(value, fallback) {
1771
- const n = Number.parseInt(String(value ?? ""), 10);
1772
- return Number.isNaN(n) ? fallback : n;
1773
- }
1774
-
1775
1607
  // src/commands/ci/utils/workflow-idempotency.ts
1776
1608
  init_esm_shims();
1777
1609
  var PROD_DEPLOY_LOCK_ID = 88889;
@@ -4256,41 +4088,6 @@ function extractSqlFromSchemaChanges(fullOutput) {
4256
4088
  }
4257
4089
  return null;
4258
4090
  }
4259
- function getIdempotentRoleNames(repoRoot) {
4260
- const idempotentDir = path4.join(repoRoot, "supabase", "schemas", "idempotent");
4261
- const roles = [];
4262
- try {
4263
- if (!existsSync(idempotentDir)) return [];
4264
- const files = readdirSync(idempotentDir).filter((f) => f.endsWith(".sql"));
4265
- for (const file of files) {
4266
- const content = readFileSync(path4.join(idempotentDir, file), "utf-8");
4267
- const roleMatches = content.matchAll(/CREATE\s+ROLE\s+(\w+)\s+WITH/gi);
4268
- for (const match of roleMatches) {
4269
- if (match[1]) roles.push(match[1].toLowerCase());
4270
- }
4271
- const existsMatches = content.matchAll(/rolname\s*=\s*'(\w+)'/gi);
4272
- for (const match of existsMatches) {
4273
- if (match[1] && !roles.includes(match[1].toLowerCase())) {
4274
- roles.push(match[1].toLowerCase());
4275
- }
4276
- }
4277
- }
4278
- } catch {
4279
- }
4280
- return [...new Set(roles)];
4281
- }
4282
- function isIdempotentRoleHazard(hazardType, hazardMessage, causingSql, idempotentRoles) {
4283
- if (hazardType.toUpperCase() !== "AUTHZ_UPDATE") return false;
4284
- if (idempotentRoles.length === 0) return false;
4285
- const sql = (causingSql || "").trim().toLowerCase();
4286
- const isGrantRevoke = /^\s*(?:grant|revoke)\b/i.test(sql);
4287
- if (sql && !isGrantRevoke) return false;
4288
- const textToCheck = sql || hazardMessage.toLowerCase();
4289
- for (const role of idempotentRoles) {
4290
- if (textToCheck.includes(role)) return true;
4291
- }
4292
- return false;
4293
- }
4294
4091
  function getHazardSeverity(type) {
4295
4092
  switch (type.toUpperCase()) {
4296
4093
  case "DELETES_DATA":
@@ -4332,20 +4129,26 @@ function createHazardDetail(type, message, causingSql) {
4332
4129
  causingSql
4333
4130
  };
4334
4131
  }
4335
- function collectCommentHazards(lines, idempotentRoles) {
4132
+ function collectCommentHazards(lines, schemasDir) {
4336
4133
  const hazards = [];
4337
4134
  for (let i = 0; i < lines.length; i++) {
4338
4135
  const parsed = parseHazardLine(lines[i] ?? "");
4339
4136
  if (!parsed) continue;
4340
4137
  const causingSql = findCausingSqlLine(lines, i);
4341
- if (isIdempotentRoleHazard(parsed.type, parsed.message, causingSql, idempotentRoles)) {
4138
+ const registryHazard = {
4139
+ type: parsed.type,
4140
+ message: parsed.message,
4141
+ fullMatch: lines[i] ?? "",
4142
+ causingSql
4143
+ };
4144
+ if (isIdempotentRoleHazard(registryHazard, schemasDir)) {
4342
4145
  continue;
4343
4146
  }
4344
4147
  hazards.push(createHazardDetail(parsed.type, parsed.message, causingSql));
4345
4148
  }
4346
4149
  return hazards;
4347
4150
  }
4348
- function appendEmojiHazards(fullOutput, idempotentRoles, hazards) {
4151
+ function appendEmojiHazards(fullOutput, schemasDir, hazards) {
4349
4152
  const hazardPrefixes = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26A0"];
4350
4153
  for (const line of fullOutput.split("\n")) {
4351
4154
  const trimmed = line.trim();
@@ -4356,7 +4159,12 @@ function appendEmojiHazards(fullOutput, idempotentRoles, hazards) {
4356
4159
  if (!match?.[1] || !match[2]) continue;
4357
4160
  const type = match[1].toUpperCase();
4358
4161
  const message = match[2].trim();
4359
- if (isIdempotentRoleHazard(type, message, void 0, idempotentRoles)) {
4162
+ const registryHazard = {
4163
+ type,
4164
+ message,
4165
+ fullMatch: trimmed
4166
+ };
4167
+ if (isIdempotentRoleHazard(registryHazard, schemasDir)) {
4360
4168
  continue;
4361
4169
  }
4362
4170
  const isDuplicate = hazards.some(
@@ -4369,9 +4177,9 @@ function appendEmojiHazards(fullOutput, idempotentRoles, hazards) {
4369
4177
  }
4370
4178
  function extractHazardsWithContext(fullOutput, repoRoot) {
4371
4179
  const lines = fullOutput.split("\n");
4372
- const idempotentRoles = repoRoot ? getIdempotentRoleNames(repoRoot) : [];
4373
- const hazards = collectCommentHazards(lines, idempotentRoles);
4374
- appendEmojiHazards(fullOutput, idempotentRoles, hazards);
4180
+ const schemasDir = repoRoot ? path4.join(repoRoot, "supabase", "schemas", "declarative") : void 0;
4181
+ const hazards = collectCommentHazards(lines, schemasDir);
4182
+ appendEmojiHazards(fullOutput, schemasDir, hazards);
4375
4183
  return hazards;
4376
4184
  }
4377
4185
  function stripAnsi(text) {
@@ -4870,51 +4678,6 @@ function getSchemaGitDiff(repoRoot) {
4870
4678
  return null;
4871
4679
  }
4872
4680
  }
4873
- function extractRolesFromSql(content) {
4874
- const roles = [];
4875
- const roleMatches = content.matchAll(/CREATE\s+ROLE\s+(\w+)\s+WITH/gi);
4876
- for (const match of roleMatches) {
4877
- if (match[1]) roles.push(match[1].toLowerCase());
4878
- }
4879
- const existsMatches = content.matchAll(/rolname\s*=\s*'(\w+)'/gi);
4880
- for (const match of existsMatches) {
4881
- if (match[1] && !roles.includes(match[1].toLowerCase())) {
4882
- roles.push(match[1].toLowerCase());
4883
- }
4884
- }
4885
- return roles;
4886
- }
4887
- function getIdempotentRoleNames2(repoRoot) {
4888
- const idempotentDir = path4.join(repoRoot, "supabase", "schemas", "idempotent");
4889
- const roles = [];
4890
- try {
4891
- const fs2 = __require("fs");
4892
- if (!fs2.existsSync(idempotentDir)) return [];
4893
- const files = fs2.readdirSync(idempotentDir).filter((f) => f.endsWith(".sql"));
4894
- for (const file of files) {
4895
- const filePath = path4.join(idempotentDir, file);
4896
- if (!isPathContained(idempotentDir, filePath)) {
4897
- continue;
4898
- }
4899
- const content = fs2.readFileSync(filePath, "utf-8");
4900
- roles.push(...extractRolesFromSql(content));
4901
- }
4902
- } catch {
4903
- }
4904
- return [...new Set(roles)];
4905
- }
4906
- function isIdempotentRoleHazard2(hazardType, hazardMessage, idempotentRoles, causingSql) {
4907
- if (hazardType !== "AUTHZ_UPDATE") return false;
4908
- if (idempotentRoles.length === 0) return false;
4909
- const sql = ("").trim().toLowerCase();
4910
- const isGrantRevoke = /^\s*(?:grant|revoke)\b/i.test(sql);
4911
- if (sql && !isGrantRevoke) return false;
4912
- const textToCheck = sql || hazardMessage.toLowerCase();
4913
- for (const role of idempotentRoles) {
4914
- if (textToCheck.includes(role)) return true;
4915
- }
4916
- return false;
4917
- }
4918
4681
  function createEmptySchemaChangeStats() {
4919
4682
  return {
4920
4683
  creates: { tables: 0, indexes: 0, policies: 0, functions: 0, other: 0 },
@@ -4934,13 +4697,18 @@ function resolveSqlInput(sqlInput, logPath) {
4934
4697
  return null;
4935
4698
  }
4936
4699
  }
4937
- function parseHazards(sql, idempotentRoles) {
4700
+ function parseHazards(sql, schemasDir) {
4938
4701
  const hazards = [];
4939
4702
  const hazardMatches = sql.matchAll(/-- Hazard (\w+): (.+)/g);
4940
4703
  for (const match of hazardMatches) {
4941
4704
  const hazardType = match[1];
4942
4705
  const hazardMessage = match[2];
4943
- if (isIdempotentRoleHazard2(hazardType, hazardMessage, idempotentRoles)) {
4706
+ const registryHazard = {
4707
+ type: hazardType,
4708
+ message: hazardMessage,
4709
+ fullMatch: match[0]
4710
+ };
4711
+ if (isIdempotentRoleHazard(registryHazard, schemasDir)) {
4944
4712
  continue;
4945
4713
  }
4946
4714
  hazards.push(`${hazardType}: ${hazardMessage}`);
@@ -4960,8 +4728,8 @@ function parseSchemaChangeStats(sqlInput, logPath, repoRoot) {
4960
4728
  stats.creates.other = countMatches(sql, /CREATE (TRIGGER|TYPE|SEQUENCE|VIEW|SCHEMA)/gi);
4961
4729
  stats.alters = countMatches(sql, /ALTER (TABLE|COLUMN|INDEX|POLICY|FUNCTION)/gi);
4962
4730
  stats.drops = countMatches(sql, /DROP (TABLE|COLUMN|INDEX|POLICY|FUNCTION|TRIGGER)/gi);
4963
- const idempotentRoles = repoRoot ? getIdempotentRoleNames2(repoRoot) : [];
4964
- stats.hazards = parseHazards(sql, idempotentRoles);
4731
+ const schemasDir = repoRoot ? path4.join(repoRoot, "supabase", "schemas", "declarative") : void 0;
4732
+ stats.hazards = parseHazards(sql, schemasDir);
4965
4733
  return stats;
4966
4734
  }
4967
4735
  function isCheckSummaryOutput(output) {
@@ -5235,9 +5003,9 @@ function detectSupabaseContainers() {
5235
5003
  async function checkSupabasePortConflicts(repoRoot) {
5236
5004
  let dbPort = 54322;
5237
5005
  try {
5238
- const { readFileSync: readFileSync5 } = await import('fs');
5006
+ const { readFileSync: readFileSync4 } = await import('fs');
5239
5007
  const configPath = path4.join(repoRoot, "supabase", "config.toml");
5240
- const content = readFileSync5(configPath, "utf-8");
5008
+ const content = readFileSync4(configPath, "utf-8");
5241
5009
  const match = /\[db\][^[]*?port\s*=\s*(\d+)/s.exec(content);
5242
5010
  if (match?.[1]) dbPort = Number.parseInt(match[1], 10);
5243
5011
  } catch {
@@ -5654,116 +5422,96 @@ async function queryScalar(params) {
5654
5422
  async function detectDbCapabilities(params) {
5655
5423
  const caps = /* @__PURE__ */ new Set();
5656
5424
  const diagnostics = {};
5657
- const isPostgresRaw = await queryScalar({
5425
+ const batchedSql = `
5426
+ SELECT json_build_object(
5427
+ 'isPostgres', (SELECT version() LIKE 'PostgreSQL%'),
5428
+ 'tableCount', (
5429
+ SELECT COUNT(*)::int
5430
+ FROM information_schema.tables
5431
+ WHERE table_type = 'BASE TABLE'
5432
+ AND table_schema NOT LIKE 'pg_%'
5433
+ AND table_schema NOT IN ('information_schema')
5434
+ ),
5435
+ 'rlsCount', (
5436
+ SELECT COUNT(*)::int
5437
+ FROM pg_class c
5438
+ JOIN pg_namespace n ON n.oid = c.relnamespace
5439
+ WHERE c.relkind IN ('r', 'p')
5440
+ AND n.nspname NOT LIKE 'pg_%'
5441
+ AND n.nspname NOT IN ('information_schema')
5442
+ AND c.relrowsecurity
5443
+ ),
5444
+ 'canSelectAny', (
5445
+ SELECT COALESCE(bool_or(has_table_privilege(current_user, tbl, 'select')), false)
5446
+ FROM (
5447
+ SELECT quote_ident(table_schema) || '.' || quote_ident(table_name) AS tbl
5448
+ FROM information_schema.tables
5449
+ WHERE table_type = 'BASE TABLE'
5450
+ AND table_schema NOT LIKE 'pg_%'
5451
+ AND table_schema NOT IN ('information_schema')
5452
+ LIMIT 50 -- cap sampling to prevent slow scans on large databases
5453
+ ) candidates
5454
+ ),
5455
+ 'authenticatedExists', (
5456
+ SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated')
5457
+ ),
5458
+ 'isMember', (
5459
+ SELECT CASE
5460
+ WHEN EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated')
5461
+ THEN pg_has_role(current_user, 'authenticated', 'member')
5462
+ ELSE false
5463
+ END
5464
+ )
5465
+ )::text;
5466
+ `.trim();
5467
+ const raw = await queryScalar({
5658
5468
  repoRoot: params.repoRoot,
5659
5469
  tmpDir: params.tmpDir,
5660
5470
  databaseUrl: params.databaseUrlApp,
5661
- sql: "SELECT version() LIKE 'PostgreSQL%';",
5662
- logName: "cap-is-postgres"
5471
+ sql: batchedSql,
5472
+ logName: "cap-db-batch"
5663
5473
  });
5664
- const isPostgres = parseBoolish(isPostgresRaw);
5665
- if (isPostgres) {
5474
+ let parsed;
5475
+ try {
5476
+ parsed = JSON.parse(raw);
5477
+ } catch {
5478
+ diagnostics["db.postgres"] = `Failed to parse batched query result (raw=${raw.substring(0, 200)})`;
5479
+ return { capabilities: caps, diagnostics };
5480
+ }
5481
+ if (parsed.isPostgres) {
5666
5482
  caps.add("db.postgres");
5667
5483
  diagnostics["db.postgres"] = "PostgreSQL detected via SELECT version()";
5668
5484
  } else {
5669
- diagnostics["db.postgres"] = `Not PostgreSQL (raw=${isPostgresRaw})`;
5485
+ diagnostics["db.postgres"] = `Not PostgreSQL`;
5670
5486
  return { capabilities: caps, diagnostics };
5671
5487
  }
5672
- const tablesRaw = await queryScalar({
5673
- repoRoot: params.repoRoot,
5674
- tmpDir: params.tmpDir,
5675
- databaseUrl: params.databaseUrlApp,
5676
- sql: `
5677
- SELECT COUNT(*)::int
5678
- FROM information_schema.tables
5679
- WHERE table_type='BASE TABLE'
5680
- AND table_schema NOT LIKE 'pg_%'
5681
- AND table_schema NOT IN ('information_schema');
5682
- `.trim(),
5683
- logName: "cap-table-count"
5684
- });
5685
- const tableCount = Number.parseInt(tablesRaw, 10);
5686
- if (Number.isFinite(tableCount) && tableCount > 0) {
5488
+ if (Number.isFinite(parsed.tableCount) && parsed.tableCount > 0) {
5687
5489
  caps.add("db.schemaApplied");
5688
- diagnostics["db.schemaApplied"] = `Non-system base tables detected: ${tableCount}`;
5490
+ diagnostics["db.schemaApplied"] = `Non-system base tables detected: ${parsed.tableCount}`;
5689
5491
  } else {
5690
- diagnostics["db.schemaApplied"] = `No non-system base tables detected (raw=${tablesRaw})`;
5492
+ diagnostics["db.schemaApplied"] = `No non-system base tables detected (count=${parsed.tableCount})`;
5691
5493
  }
5692
- const rlsCountRaw = await queryScalar({
5693
- repoRoot: params.repoRoot,
5694
- tmpDir: params.tmpDir,
5695
- databaseUrl: params.databaseUrlApp,
5696
- sql: `
5697
- SELECT COUNT(*)::int
5698
- FROM pg_class c
5699
- JOIN pg_namespace n ON n.oid=c.relnamespace
5700
- WHERE c.relkind IN ('r','p')
5701
- AND n.nspname NOT LIKE 'pg_%'
5702
- AND n.nspname NOT IN ('information_schema')
5703
- AND c.relrowsecurity;
5704
- `.trim(),
5705
- logName: "cap-rls-count"
5706
- });
5707
- const rlsCount = Number.parseInt(rlsCountRaw, 10);
5708
- if (Number.isFinite(rlsCount) && rlsCount > 0) {
5494
+ if (Number.isFinite(parsed.rlsCount) && parsed.rlsCount > 0) {
5709
5495
  caps.add("db.rlsEnabled");
5710
- diagnostics["db.rlsEnabled"] = `RLS-enabled tables detected: ${rlsCount}`;
5496
+ diagnostics["db.rlsEnabled"] = `RLS-enabled tables detected: ${parsed.rlsCount}`;
5711
5497
  } else {
5712
- diagnostics["db.rlsEnabled"] = `No RLS-enabled tables detected (raw=${rlsCountRaw})`;
5498
+ diagnostics["db.rlsEnabled"] = `No RLS-enabled tables detected (count=${parsed.rlsCount})`;
5713
5499
  }
5714
- const canSelectAnyRaw = await queryScalar({
5715
- repoRoot: params.repoRoot,
5716
- tmpDir: params.tmpDir,
5717
- databaseUrl: params.databaseUrlApp,
5718
- sql: `
5719
- WITH candidates AS (
5720
- SELECT quote_ident(table_schema)||'.'||quote_ident(table_name) AS tbl
5721
- FROM information_schema.tables
5722
- WHERE table_type='BASE TABLE'
5723
- AND table_schema NOT LIKE 'pg_%'
5724
- AND table_schema NOT IN ('information_schema')
5725
- LIMIT 50
5726
- ),
5727
- checks AS (
5728
- SELECT bool_or(has_table_privilege(current_user, tbl, 'select')) AS ok
5729
- FROM candidates
5730
- )
5731
- SELECT COALESCE(ok,false) FROM checks;
5732
- `.trim(),
5733
- logName: "cap-select-any"
5734
- });
5735
- const canSelectAny = parseBoolish(canSelectAnyRaw);
5736
- if (canSelectAny) {
5500
+ if (parsed.canSelectAny) {
5737
5501
  caps.add("db.appCanSelectSomeTable");
5738
5502
  diagnostics["db.appCanSelectSomeTable"] = "has_table_privilege(select) true for at least one table";
5739
5503
  } else {
5740
- diagnostics["db.appCanSelectSomeTable"] = `App role cannot SELECT any sampled table (raw=${canSelectAnyRaw})`;
5504
+ diagnostics["db.appCanSelectSomeTable"] = "App role cannot SELECT any sampled table";
5741
5505
  }
5742
- const authenticatedExistsRaw = await queryScalar({
5743
- repoRoot: params.repoRoot,
5744
- tmpDir: params.tmpDir,
5745
- databaseUrl: params.databaseUrlApp,
5746
- sql: "SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='authenticated');",
5747
- logName: "cap-authenticated-role-exists"
5748
- });
5749
- const authenticatedExists = parseBoolish(authenticatedExistsRaw);
5750
- if (!authenticatedExists) {
5506
+ if (!parsed.authenticatedExists) {
5751
5507
  diagnostics["db.appIsAuthenticatedMember"] = "n/a (authenticated role does not exist)";
5752
5508
  return { capabilities: caps, diagnostics };
5753
5509
  }
5754
- const isMemberRaw = await queryScalar({
5755
- repoRoot: params.repoRoot,
5756
- tmpDir: params.tmpDir,
5757
- databaseUrl: params.databaseUrlApp,
5758
- sql: "SELECT pg_has_role(current_user, 'authenticated', 'member');",
5759
- logName: "cap-app-is-authenticated-member"
5760
- });
5761
- const isMember = parseBoolish(isMemberRaw);
5762
- if (isMember) {
5510
+ if (parsed.isMember) {
5763
5511
  caps.add("db.appIsAuthenticatedMember");
5764
5512
  diagnostics["db.appIsAuthenticatedMember"] = "pg_has_role(member, authenticated) is true";
5765
5513
  } else {
5766
- diagnostics["db.appIsAuthenticatedMember"] = `pg_has_role(member, authenticated)=false (raw=${isMemberRaw})`;
5514
+ diagnostics["db.appIsAuthenticatedMember"] = "pg_has_role(member, authenticated)=false";
5767
5515
  }
5768
5516
  return { capabilities: caps, diagnostics };
5769
5517
  }
@@ -5874,23 +5622,25 @@ async function detectUiContractReady(params) {
5874
5622
  async function detectCiCapabilities(params) {
5875
5623
  const caps = /* @__PURE__ */ new Set();
5876
5624
  const diagnostics = {};
5877
- const db = await detectDbCapabilities({
5878
- repoRoot: params.repoRoot,
5879
- tmpDir: params.tmpDir,
5880
- databaseUrlApp: params.databaseUrlApp
5881
- });
5882
- for (const c of db.capabilities) caps.add(c);
5883
- for (const [k, v] of Object.entries(db.diagnostics)) diagnostics[k] = String(v);
5884
- const e2e = await detectGeneratedE2eCapabilities({ repoRoot: params.repoRoot });
5885
- for (const c of e2e.capabilities) caps.add(c);
5886
- for (const [k, v] of Object.entries(e2e.diagnostics)) diagnostics[k] = String(v);
5625
+ const mergeDiagnostics = (result) => {
5626
+ for (const c of result.capabilities) caps.add(c);
5627
+ for (const [k, v] of Object.entries(result.diagnostics))
5628
+ diagnostics[k] = String(v);
5629
+ };
5630
+ const [db, e2e, ui] = await Promise.all([
5631
+ detectDbCapabilities({
5632
+ repoRoot: params.repoRoot,
5633
+ tmpDir: params.tmpDir,
5634
+ databaseUrlApp: params.databaseUrlApp
5635
+ }),
5636
+ detectGeneratedE2eCapabilities({ repoRoot: params.repoRoot }),
5637
+ detectUiContractReady({ baseUrl: params.baseUrl })
5638
+ ]);
5639
+ mergeDiagnostics(db);
5640
+ mergeDiagnostics(e2e);
5641
+ mergeDiagnostics(ui);
5887
5642
  const preview = await detectPreviewReachable({ baseUrl: params.baseUrl });
5888
- for (const c of preview.capabilities) caps.add(c);
5889
- for (const [k, v] of Object.entries(preview.diagnostics))
5890
- diagnostics[k] = String(v);
5891
- const ui = await detectUiContractReady({ baseUrl: params.baseUrl });
5892
- for (const c of ui.capabilities) caps.add(c);
5893
- for (const [k, v] of Object.entries(ui.diagnostics)) diagnostics[k] = String(v);
5643
+ mergeDiagnostics(preview);
5894
5644
  return { capabilities: Array.from(caps), diagnostics };
5895
5645
  }
5896
5646
  function resolveRequiredCapabilities(params) {
@@ -6711,9 +6461,6 @@ var runLayersActor = fromPromise(
6711
6461
  progressIntervalSeconds = 30
6712
6462
  } = input;
6713
6463
  try {
6714
- console.log(
6715
- `[DEBUG] runLayersActor: Starting with layers=${JSON.stringify(layers)}, failFast=${failFast}`
6716
- );
6717
6464
  const results = await runLayersInParallel({
6718
6465
  repoRoot,
6719
6466
  tmpDir,
@@ -6727,15 +6474,9 @@ var runLayersActor = fromPromise(
6727
6474
  });
6728
6475
  const failedLayers = results.filter((r) => !r.success).map((r) => r.layer);
6729
6476
  const allPassed = failedLayers.length === 0;
6730
- console.log(
6731
- `[DEBUG] runLayersActor: Complete. results=${results.length}, failedLayers=${JSON.stringify(failedLayers)}, allPassed=${allPassed}`
6732
- );
6733
6477
  return { results, allPassed, failedLayers };
6734
6478
  } catch (error) {
6735
6479
  const errorMessage = error instanceof Error ? error.message : String(error);
6736
- console.log(
6737
- `[DEBUG] runLayersActor: Caught error: ${errorMessage}. Returning failedLayers=${JSON.stringify(layers)}`
6738
- );
6739
6480
  return {
6740
6481
  results: [],
6741
6482
  allPassed: false,
@@ -6812,8 +6553,22 @@ function shouldPostGitHubComment(context) {
6812
6553
  if (context.input.skipGithubComment === true) return false;
6813
6554
  return true;
6814
6555
  }
6556
+ function isBlockingPhase(context) {
6557
+ return isCiPrMode(context) && context.phase === "blocking";
6558
+ }
6559
+ function isObservabilityPhase(context) {
6560
+ return isCiPrMode(context) && context.phase === "observability";
6561
+ }
6815
6562
  function isTestPhase(context) {
6816
- return isCiPrMode(context) && context.input.phase === "test";
6563
+ return isCiPrMode(context) && context.phase === "test";
6564
+ }
6565
+ function shouldRunPrExecutionPhase(context) {
6566
+ if (!isCiPrMode(context)) return true;
6567
+ return !isObservabilityPhase(context);
6568
+ }
6569
+ function shouldRunPrObservabilityPhase(context) {
6570
+ if (!isCiPrMode(context)) return true;
6571
+ return !isBlockingPhase(context) && !isTestPhase(context);
6817
6572
  }
6818
6573
  function hasError(context) {
6819
6574
  return context.error !== null;
@@ -6868,7 +6623,7 @@ init_esm_shims();
6868
6623
 
6869
6624
  // src/commands/ci/machine/formatters/sections/format-helpers.ts
6870
6625
  init_esm_shims();
6871
- function formatDuration4(ms) {
6626
+ function formatDuration3(ms) {
6872
6627
  if (ms < 1e3) return `${ms}ms`;
6873
6628
  const seconds = Math.floor(ms / 1e3);
6874
6629
  if (seconds < 60) return `${seconds}s`;
@@ -7713,6 +7468,10 @@ function generateKnownDriftOnlySection(schemaStats, expectedDrift) {
7713
7468
  }
7714
7469
  function generateProductionPreviewSection(phase, prodPreview, schemaDrift, schemaStats, expectedDrift) {
7715
7470
  const signals = getProductionSchemaSignals(prodPreview, schemaStats, expectedDrift);
7471
+ const deferredSection = getDeferredProductionPreviewSection(phase, prodPreview);
7472
+ if (deferredSection.length > 0) {
7473
+ return deferredSection;
7474
+ }
7716
7475
  if (!prodPreview?.executed) {
7717
7476
  if (schemaDrift?.gitDiff?.filesChanged?.length) {
7718
7477
  return [
@@ -7736,6 +7495,27 @@ function generateProductionPreviewSection(phase, prodPreview, schemaDrift, schem
7736
7495
  }
7737
7496
  return ["**\u672C\u756A\u30D7\u30EC\u30D3\u30E5\u30FC**: \u2705 \u672C\u756A\u306B\u9069\u7528\u3059\u308B\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u306F\u3042\u308A\u307E\u305B\u3093", ""];
7738
7497
  }
7498
+ function getDeferredProductionPreviewSection(phase, prodPreview) {
7499
+ if (prodPreview?.executed) return [];
7500
+ switch (phase) {
7501
+ case "blocking":
7502
+ return [
7503
+ "**\u672C\u756A\u30D7\u30EC\u30D3\u30E5\u30FC**: \u23ED\uFE0F blocking phase \u3067\u306F\u5B9F\u884C\u3057\u307E\u305B\u3093",
7504
+ "",
7505
+ "> \u672C\u756A\u6BD4\u8F03\u3068 schema stats \u306F blocking CI \u306E\u30AF\u30EA\u30C6\u30A3\u30AB\u30EB\u30D1\u30B9\u304B\u3089\u5207\u308A\u96E2\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u672C\u756A\u53CD\u6620\u524D\u306B\u306F `deploy-db.yml` \u304C compare-only dry-run \u3067\u518D\u691C\u8A3C\u3057\u307E\u3059\u3002",
7506
+ ""
7507
+ ];
7508
+ case "test":
7509
+ return [
7510
+ "**\u672C\u756A\u30D7\u30EC\u30D3\u30E5\u30FC**: \u23ED\uFE0F phase=test \u306E\u305F\u3081\u30B9\u30AD\u30C3\u30D7",
7511
+ "",
7512
+ "> workflow-prepared DB \u3092\u518D\u5229\u7528\u3059\u308B\u8EFD\u91CF\u30D5\u30A7\u30FC\u30BA\u306E\u305F\u3081\u3001\u672C\u756A\u6BD4\u8F03\u306F\u5B9F\u884C\u3057\u3066\u3044\u307E\u305B\u3093\u3002",
7513
+ ""
7514
+ ];
7515
+ default:
7516
+ return [];
7517
+ }
7518
+ }
7739
7519
  function shouldShowDeployButton(productionPreview, schemaDrift, hasSchemaChanges2) {
7740
7520
  if (hasSchemaChanges2) return true;
7741
7521
  if (productionPreview?.error) return true;
@@ -7744,6 +7524,7 @@ function shouldShowDeployButton(productionPreview, schemaDrift, hasSchemaChanges
7744
7524
  }
7745
7525
  function generateDeploySection(exitCode, layerResults, phase, gitBranchName, productionPreview, schemaDrift, schemaStats, expectedDrift, env) {
7746
7526
  if (!checkIfDeployable(exitCode, layerResults)) return [];
7527
+ if (phase === "blocking" || phase === "test") return [];
7747
7528
  const deployWorkflowUrl = env.repository ? `${env.serverUrl}/${env.repository}/actions/workflows/deploy-db.yml` : null;
7748
7529
  const signals = getProductionSchemaSignals(productionPreview, schemaStats, expectedDrift);
7749
7530
  const hasSchemaChanges2 = signals.requiresDeploy;
@@ -7778,12 +7559,20 @@ function generateDeploySection(exitCode, layerResults, phase, gitBranchName, pro
7778
7559
  function generateObservabilitySectionBody(input) {
7779
7560
  const env = getGitHubEnv();
7780
7561
  const gitBranchName = input.prContext?.headBranch ?? input.branchName ?? "unknown";
7562
+ const deferredPreviewLines = input.phase === "blocking" || input.phase === "test" ? generateProductionPreviewSection(
7563
+ input.phase,
7564
+ input.productionPreview,
7565
+ input.schemaDrift,
7566
+ input.schemaStats ?? null,
7567
+ input.expectedDrift ?? []
7568
+ ) : [];
7781
7569
  const lines = [
7782
7570
  ...formatSchemaMatrix(
7783
7571
  input.schemaStats,
7784
7572
  input.schemaDrift?.gitDiff ?? null,
7785
7573
  input.expectedDrift ?? []
7786
7574
  ),
7575
+ ...deferredPreviewLines,
7787
7576
  ...generateDeploySection(
7788
7577
  input.exitCode,
7789
7578
  input.layerResults,
@@ -7867,7 +7656,7 @@ function calculateTotalElapsed(stepTimings2) {
7867
7656
  if (!stepTimings2) return 0;
7868
7657
  return Object.values(stepTimings2).reduce((sum, t) => sum + (t ?? 0), 0);
7869
7658
  }
7870
- function formatProgressBar(currentStep, completedSteps, failedStep, skippedSteps) {
7659
+ function formatProgressBar(_currentStep, completedSteps, failedStep, skippedSteps) {
7871
7660
  const total = CI_STEPS.length;
7872
7661
  const completedBase = completedSteps.length + skippedSteps.length;
7873
7662
  const completed = failedStep ? completedBase : completedBase + 1;
@@ -8004,7 +7793,7 @@ function generateProgressHeader(currentStep, failedStep, completedSteps, skipped
8004
7793
  );
8005
7794
  }
8006
7795
  if (totalElapsed > 0) {
8007
- lines.push(`<sub>\u23F1\uFE0F \u7D4C\u904E\u6642\u9593: ${formatDuration4(totalElapsed)}</sub>`);
7796
+ lines.push(`<sub>\u23F1\uFE0F \u7D4C\u904E\u6642\u9593: ${formatDuration3(totalElapsed)}</sub>`);
8008
7797
  }
8009
7798
  lines.push("");
8010
7799
  return lines;
@@ -8024,7 +7813,7 @@ function generateProgressSteps(currentStep, completedSteps, failedStep, skippedS
8024
7813
  const status = getStepStatus(step, currentStep, completedSteps, failedStep, skippedSteps);
8025
7814
  const icon = getStepIcon(status);
8026
7815
  const timing = stepTimings2?.[step];
8027
- const timingStr = timing !== void 0 ? ` \`${formatDuration4(timing)}\`` : "";
7816
+ const timingStr = timing !== void 0 ? ` \`${formatDuration3(timing)}\`` : "";
8028
7817
  const stepDetail = getStepDetail(step, status, schemaDrift, layerResults);
8029
7818
  const runningDetail = getRunningStepDetail(step, detail, productionPreview);
8030
7819
  if (status === "running" && runningDetail) {
@@ -8125,6 +7914,11 @@ function generateTestResultsSection(layerResults) {
8125
7914
  }
8126
7915
  function generateProductionSchemaSection(productionPreview, phase, deployStatus, env) {
8127
7916
  const lines = ["### \u{1F4CB} \u672C\u756A\u30B9\u30AD\u30FC\u30DE\u72B6\u6CC1", ""];
7917
+ const deferredMessage = getDeferredProductionPreviewMessage(phase, productionPreview);
7918
+ if (!deployStatus?.deployed && deferredMessage) {
7919
+ lines.push(deferredMessage, "");
7920
+ return lines;
7921
+ }
8128
7922
  const state = resolveProductionSchemaState(productionPreview, deployStatus);
8129
7923
  switch (state) {
8130
7924
  case "deployed":
@@ -8149,6 +7943,17 @@ function generateProductionSchemaSection(productionPreview, phase, deployStatus,
8149
7943
  }
8150
7944
  return lines;
8151
7945
  }
7946
+ function getDeferredProductionPreviewMessage(phase, productionPreview) {
7947
+ if (productionPreview?.executed) return null;
7948
+ switch (phase) {
7949
+ case "blocking":
7950
+ return "\u23ED\uFE0F _blocking phase \u306E\u305F\u3081\u3001\u672C\u756A\u6BD4\u8F03\u3068 schema stats \u306F\u3053\u306E job \u3067\u306F\u5B9F\u884C\u3057\u307E\u305B\u3093_";
7951
+ case "test":
7952
+ return "\u23ED\uFE0F _phase=test \u306E\u305F\u3081\u3001\u672C\u756A\u6BD4\u8F03\u306F\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u3066\u3044\u307E\u3059_";
7953
+ default:
7954
+ return null;
7955
+ }
7956
+ }
8152
7957
  function resolveProductionSchemaState(productionPreview, deployStatus) {
8153
7958
  if (deployStatus?.deployed) return "deployed";
8154
7959
  if (!productionPreview) return "checking";
@@ -8197,7 +8002,8 @@ function appendChangesPendingSection(lines, productionPreview, env) {
8197
8002
  }
8198
8003
  appendDeployLink(lines, env);
8199
8004
  }
8200
- function generateDbDeploySection(layerResults, gitBranchName, productionPreview, env) {
8005
+ function generateDbDeploySection(phase, layerResults, gitBranchName, productionPreview, env) {
8006
+ if (phase === "blocking" || phase === "test") return [];
8201
8007
  if (!checkBlockingLayersPassed(layerResults)) return [];
8202
8008
  if (productionPreview?.hasChanges) return [];
8203
8009
  const deployWorkflowUrl = env.repository ? `${env.serverUrl}/${env.repository}/actions/workflows/deploy-db.yml` : null;
@@ -8260,7 +8066,13 @@ function generateProgressCommentBody(input) {
8260
8066
  ...generateErrorSection(error ?? void 0, failedStep),
8261
8067
  ...generateTestResultsSection(input.layerResults),
8262
8068
  ...generateProductionSchemaSection(productionPreview, input.phase, deployStatus, env),
8263
- ...generateDbDeploySection(input.layerResults, gitBranchName, productionPreview, env)
8069
+ ...generateDbDeploySection(
8070
+ input.phase,
8071
+ input.layerResults,
8072
+ gitBranchName,
8073
+ productionPreview,
8074
+ env
8075
+ )
8264
8076
  ];
8265
8077
  const jst = new Date(Date.now() + 9 * 60 * 60 * 1e3);
8266
8078
  const now = jst.toISOString().replace("T", " ").substring(0, 19);
@@ -8269,13 +8081,25 @@ function generateProgressCommentBody(input) {
8269
8081
  }
8270
8082
 
8271
8083
  // src/commands/ci/machine/formatters/github-comment.ts
8084
+ function getSkippedStepsForPhase(phase) {
8085
+ switch (phase) {
8086
+ case "test":
8087
+ return ["syncSchema", "applySeeds", "observability"];
8088
+ case "blocking":
8089
+ return ["observability"];
8090
+ case "observability":
8091
+ return ["postSeedChecks", "staticChecks", "build", "runTests"];
8092
+ default:
8093
+ return [];
8094
+ }
8095
+ }
8272
8096
  function createCommentInput(context, options) {
8273
8097
  const skippedLayerNumbers = Object.keys(context.layerSkipReasons || {}).map(Number);
8274
8098
  const originalSelectedLayers = [
8275
8099
  .../* @__PURE__ */ new Set([...skippedLayerNumbers, ...context.selectedLayers])
8276
8100
  ].sort((a, b) => a - b);
8277
8101
  return {
8278
- phase: context.input.phase ?? "all",
8102
+ phase: context.phase,
8279
8103
  exitCode: context.exitCode,
8280
8104
  branchName: context.branchName,
8281
8105
  prContext: context.prContext,
@@ -8292,8 +8116,8 @@ function createCommentInput(context, options) {
8292
8116
  };
8293
8117
  }
8294
8118
  function createProgressCommentInput(context, currentStep, completedSteps, failedStep = null, stepTimings2) {
8295
- const phase = context.input.phase ?? "all";
8296
- const skippedSteps = phase === "test" ? ["syncSchema", "applySeeds", "observability"] : [];
8119
+ const phase = context.phase;
8120
+ const skippedSteps = getSkippedStepsForPhase(phase);
8297
8121
  return {
8298
8122
  phase,
8299
8123
  currentStep,
@@ -8728,6 +8552,8 @@ var ciMachine = setup({
8728
8552
  shouldInstallPgTap: ({ context }) => shouldInstallPgTap(context),
8729
8553
  shouldSetupRoles: ({ context }) => shouldSetupRoles(context),
8730
8554
  shouldPostGitHubComment: ({ context }) => shouldPostGitHubComment(context),
8555
+ shouldRunPrExecutionPhase: ({ context }) => shouldRunPrExecutionPhase(context),
8556
+ shouldRunPrObservabilityPhase: ({ context }) => shouldRunPrObservabilityPhase(context),
8731
8557
  isDryRun: ({ context }) => isDryRun(context),
8732
8558
  hasError: ({ context }) => hasError(context),
8733
8559
  allTestsPassed: ({ context }) => allTestsPassed(context)
@@ -9062,8 +8888,14 @@ var ciMachine = setup({
9062
8888
  },
9063
8889
  states: {
9064
8890
  execution: {
9065
- initial: "setupRoles",
8891
+ initial: "gate",
9066
8892
  states: {
8893
+ gate: {
8894
+ always: [
8895
+ { guard: "shouldRunPrExecutionPhase", target: "setupRoles" },
8896
+ { target: "done" }
8897
+ ]
8898
+ },
9067
8899
  setupRoles: {
9068
8900
  invoke: {
9069
8901
  src: "setupRoles",
@@ -9357,8 +9189,14 @@ var ciMachine = setup({
9357
9189
  }
9358
9190
  },
9359
9191
  observability: {
9360
- initial: "productionPreview",
9192
+ initial: "gate",
9361
9193
  states: {
9194
+ gate: {
9195
+ always: [
9196
+ { guard: "shouldRunPrObservabilityPhase", target: "productionPreview" },
9197
+ { target: "done" }
9198
+ ]
9199
+ },
9362
9200
  productionPreview: {
9363
9201
  always: [
9364
9202
  {
@@ -10190,9 +10028,15 @@ function createSyntheticSummary(stepId, status, params = {}) {
10190
10028
  ];
10191
10029
  }
10192
10030
  function getBuildSkipReason(context) {
10031
+ if (!shouldRunPrExecutionPhase(context)) {
10032
+ return getExecutionSkipReason(context);
10033
+ }
10193
10034
  return isCiLocalMode(context) ? "Skipped in ci-local mode" : context.input.skipBuild === true ? "Skipped by --skip-build" : "Skipped by step guard";
10194
10035
  }
10195
10036
  function getPlaywrightSkipReason(context) {
10037
+ if (!shouldRunPrExecutionPhase(context)) {
10038
+ return getExecutionSkipReason(context);
10039
+ }
10196
10040
  if (isCiLocalMode(context)) return "Skipped in ci-local mode";
10197
10041
  if (!context.selectedLayers.includes(4)) return "Skipped because Layer 4 is not selected";
10198
10042
  if (shouldReusePreparedPlaywright(context)) {
@@ -10203,6 +10047,21 @@ function getPlaywrightSkipReason(context) {
10203
10047
  }
10204
10048
  return "Skipped by step guard";
10205
10049
  }
10050
+ function getObservabilitySkipReason(context) {
10051
+ if (isTestPhase(context)) {
10052
+ return "Skipped in phase=test; observability DB checks are disabled";
10053
+ }
10054
+ if (isBlockingPhase(context)) {
10055
+ return "Skipped in phase=blocking; observability runs outside the blocking path";
10056
+ }
10057
+ return "Skipped by phase selection";
10058
+ }
10059
+ function getExecutionSkipReason(context) {
10060
+ if (isObservabilityPhase(context)) {
10061
+ return "Skipped in phase=observability; execution work is disabled";
10062
+ }
10063
+ return "Skipped by phase selection";
10064
+ }
10206
10065
  function getSyntheticBuildEntries(context, trackedStepIds, _stepStates, _nowMs) {
10207
10066
  const entries = [];
10208
10067
  const pushEntry = (entry) => {
@@ -10211,6 +10070,17 @@ function getSyntheticBuildEntries(context, trackedStepIds, _stepStates, _nowMs)
10211
10070
  entries.push(entry);
10212
10071
  };
10213
10072
  const syntheticTimingParams = {};
10073
+ if (!shouldRunPrExecutionPhase(context)) {
10074
+ const reason = getExecutionSkipReason(context);
10075
+ pushEntry(createSkippedSummary("postSeedPr.execution.buildAndPlaywright.build", reason));
10076
+ pushEntry(
10077
+ createSkippedSummary("postSeedPr.execution.buildAndPlaywright.manifestGenerate", reason)
10078
+ );
10079
+ pushEntry(
10080
+ createSkippedSummary("postSeedPr.execution.buildAndPlaywright.playwrightInstall", reason)
10081
+ );
10082
+ return entries;
10083
+ }
10214
10084
  if (shouldSkipBuild(context)) {
10215
10085
  const reason = getBuildSkipReason(context);
10216
10086
  pushEntry(createSkippedSummary("postSeedPr.execution.buildAndPlaywright.build", reason));
@@ -10333,38 +10203,46 @@ function getSkippedStepEntries(context, trackedStepIds) {
10333
10203
  const entry = createSkippedSummary(stepId, reason);
10334
10204
  if (entry) skipped.push(entry);
10335
10205
  };
10206
+ if (isCiPrMode(context) && !shouldRunPrObservabilityPhase(context)) {
10207
+ const reason = getObservabilitySkipReason(context);
10208
+ pushSkipped("postSeedPr.observability.productionPreview", reason);
10209
+ pushSkipped("postSeedPr.observability.collectSchemaStats", reason);
10210
+ }
10336
10211
  if (isTestPhase(context)) {
10337
10212
  pushSkipped("syncSchema", "Skipped in phase=test; reusing prepared database");
10338
10213
  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
10214
  }
10348
10215
  if (!shouldPostGitHubComment(context) || context.prContext?.prNumber === null) {
10349
10216
  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
10217
  pushSkipped("initialComment", commentReason);
10351
10218
  pushSkipped("finalize.postComment", commentReason);
10352
10219
  }
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";
10220
+ if (isCiPrMode(context) && !shouldRunPrExecutionPhase(context)) {
10221
+ const reason = getExecutionSkipReason(context);
10222
+ pushSkipped("postSeedPr.execution.setupRoles", reason);
10355
10223
  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";
10224
+ pushSkipped("postSeedPr.execution.buildAndPlaywright", reason);
10362
10225
  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");
10226
+ pushSkipped("postSeedPr.execution.capabilities", reason);
10227
+ pushSkipped("postSeedPr.execution.runCoreTests", reason);
10228
+ pushSkipped("postSeedPr.execution.e2ePhase", reason);
10229
+ } else {
10230
+ if (shouldSkipStaticChecks(context)) {
10231
+ const reason = isCiLocalMode(context) ? "Skipped in ci-local mode" : context.input.skipStaticChecks === true ? "Skipped by --skip-static-checks" : "Skipped by step guard";
10232
+ pushSkipped("postSeedPr.execution.staticChecks", reason);
10233
+ }
10234
+ if (shouldSkipBuild(context)) {
10235
+ pushSkipped("postSeedPr.execution.buildAndPlaywright", getBuildSkipReason(context));
10236
+ }
10237
+ if (shouldSkipAppStart(context)) {
10238
+ const reason = isCiLocalMode(context) ? "Skipped in ci-local mode" : !context.selectedLayers.includes(4) ? "Skipped because Layer 4 is not selected" : "Skipped by step guard";
10239
+ pushSkipped("postSeedPr.execution.appStart", reason);
10240
+ }
10241
+ if (!context.selectedLayers.includes(4)) {
10242
+ pushSkipped("postSeedPr.execution.e2ePhase", "Skipped because Layer 4 is not selected");
10243
+ } else if (Object.values(context.layerResults).some((result) => result.status === "failed")) {
10244
+ pushSkipped("postSeedPr.execution.e2ePhase", "Skipped because blocking core tests failed");
10245
+ }
10368
10246
  }
10369
10247
  return skipped;
10370
10248
  }
@@ -10460,7 +10338,7 @@ var progressHeartbeatTimer = null;
10460
10338
  var latestSnapshot = null;
10461
10339
  var heartbeatStep = null;
10462
10340
  var heartbeatFailedStep = null;
10463
- var progressUpdateSuccessCount = 0;
10341
+ var _progressUpdateSuccessCount = 0;
10464
10342
  var stepStartTime = Date.now();
10465
10343
  var currentTrackedStep = null;
10466
10344
  var stepTimings = {};
@@ -10484,7 +10362,7 @@ function resetProgressTracking() {
10484
10362
  latestSnapshot = null;
10485
10363
  heartbeatStep = null;
10486
10364
  heartbeatFailedStep = null;
10487
- progressUpdateSuccessCount = 0;
10365
+ _progressUpdateSuccessCount = 0;
10488
10366
  previousActiveSteps = /* @__PURE__ */ new Set();
10489
10367
  stepStartTime = Date.now();
10490
10368
  currentTrackedStep = null;
@@ -10651,8 +10529,8 @@ ${generateProgressCommentBody(progressInput)}`;
10651
10529
  marker: "<!-- runa-ci-report -->",
10652
10530
  body
10653
10531
  });
10654
- progressUpdateSuccessCount++;
10655
- } catch (error) {
10532
+ _progressUpdateSuccessCount++;
10533
+ } catch (_error) {
10656
10534
  }
10657
10535
  };
10658
10536
  pendingProgressUpdate = doUpdate();
@@ -11006,7 +10884,7 @@ init_esm_shims();
11006
10884
  init_esm_shims();
11007
10885
  var CiModeSchema = z.enum(["ci-local", "ci-pr-local"]);
11008
10886
  var CiExecutionEnvSchema = z.enum(["github-actions", "local"]);
11009
- var CiPhaseSchema = z.enum(["all", "test"]);
10887
+ var CiPhaseSchema = z.enum(["all", "blocking", "observability", "test"]);
11010
10888
  var CiDbModeSchema = z.enum(["auto", "local"]);
11011
10889
  var RepoKindSchema = z.enum(["monorepo", "pj-repo", "unknown"]);
11012
10890
  var StepStatusSchema = z.enum(["passed", "failed", "skipped", "killed", "timeout"]);
@@ -11235,7 +11113,6 @@ z.object({
11235
11113
  });
11236
11114
  z.object({
11237
11115
  allowLocalFallback: z.boolean(),
11238
- allowPartialPhases: z.boolean(),
11239
11116
  source: z.enum(["config", "default"])
11240
11117
  });
11241
11118
  z.object({
@@ -11614,28 +11491,27 @@ async function runCiPrCommand(options) {
11614
11491
  const skipStaticChecks = options.skipStaticChecks === true;
11615
11492
  const skipBuild = options.skipBuild === true;
11616
11493
  const phase = options.phase ?? "all";
11494
+ const blockingPhase = phase === "blocking";
11495
+ const observabilityPhase = phase === "observability";
11617
11496
  const testOnlyPhase = phase === "test";
11497
+ const runExecutionPath = !observabilityPhase;
11618
11498
  const preparedRuntime = options.skipLocalDbStart === true || options.assumeSupabaseReady === true;
11619
11499
  const preparedPlaywright = options.skipPlaywrightInstall === true;
11500
+ const dbDescription = testOnlyPhase ? "Skip schema apply/seed and reuse the prepared database" : observabilityPhase ? "db apply \u2192 db seed \u2192 production preview \u2192 schema stats" : blockingPhase ? "db apply \u2192 db seed \u2192 db:setup-roles (observability deferred)" : "db apply \u2192 db seed \u2192 (production preview \u2225 schema stats \u2225 db:setup-roles)";
11620
11501
  const planSteps = [
11621
11502
  {
11622
11503
  id: "setup",
11623
11504
  description: testOnlyPhase ? "Resolve repo/app context (optionally reuse pre-started local Supabase)" : preparedRuntime ? "Reuse workflow-prepared local Supabase runtime" : "Start local Supabase instance"
11624
11505
  },
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
- ],
11631
- ...skipStaticChecks ? [] : [{ id: "static", description: "pnpm type-check + pnpm lint (parallel)" }],
11632
- ...skipBuild ? [] : [
11506
+ { id: "db", description: dbDescription },
11507
+ ...!runExecutionPath || skipStaticChecks ? [] : [{ id: "static", description: "pnpm type-check + pnpm lint (parallel)" }],
11508
+ ...!runExecutionPath || skipBuild ? [] : [
11633
11509
  {
11634
11510
  id: "build",
11635
11511
  description: preparedPlaywright ? "pnpm build \u2192 manifest:generate (reuse preinstalled Playwright)" : "pnpm build \u2192 manifest:generate + playwright install"
11636
11512
  }
11637
11513
  ],
11638
- { id: "test", description: "Start app \u2192 detect capabilities \u2192 run layers 1-4" },
11514
+ ...!runExecutionPath ? [] : [{ id: "test", description: "Start app \u2192 detect capabilities \u2192 run layers 1-4" }],
11639
11515
  {
11640
11516
  id: "finalize",
11641
11517
  description: "Write ci-summary.json + post GitHub comment"