@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.
- package/dist/{build-HUDIP6KU.js → build-HQMSVN6N.js} +3 -3
- package/dist/{check-LOMVIRHX.js → check-PCSQPYDM.js} +2 -2
- package/dist/{chunk-QM53IQHM.js → chunk-2QX7T24B.js} +1 -1
- package/dist/{chunk-CCW3PLQY.js → chunk-3JO6YP3T.js} +1 -1
- package/dist/{chunk-XDCHRVE3.js → chunk-4XHZQRRK.js} +2 -2
- package/dist/{chunk-7B5C6U2K.js → chunk-A6A7JIRD.js} +35 -2
- package/dist/{chunk-AFY3TX4I.js → chunk-AO554K3G.js} +1 -1
- package/dist/{chunk-Z4Z5DNW4.js → chunk-B3POLMII.js} +12 -0
- package/dist/chunk-CKRLVEIO.js +119 -0
- package/dist/{chunk-HD74F6W2.js → chunk-FWMGC5FP.js} +1 -0
- package/dist/{chunk-FHG3ILE4.js → chunk-OBYZDT2E.js} +38 -8
- package/dist/{chunk-H2AHNI75.js → chunk-PAWNJA3N.js} +1 -1
- package/dist/{chunk-VM3IWOT5.js → chunk-QSEF4T3Y.js} +13 -5
- package/dist/{chunk-NPSRD26F.js → chunk-UHDAYPHH.js} +1 -1
- package/dist/{chunk-2APB25TT.js → chunk-VSH3IXDQ.js} +7 -3
- package/dist/{chunk-644FVGIQ.js → chunk-WPMR7RQ4.js} +9 -2
- package/dist/{chunk-EMB6IZFT.js → chunk-XVNDDHAF.js} +20 -1
- package/dist/{risk-detector-plpgsql-HWKS4OLR.js → chunk-Y5ANTCKE.js} +3 -412
- package/dist/{ci-XY6IKEDC.js → ci-Z4525QW6.js} +2150 -488
- package/dist/{cli-UZA4RBNQ.js → cli-SVXOSMW6.js} +72 -54
- package/dist/commands/ci/commands/ci-prod-db-operations.d.ts +6 -4
- package/dist/commands/ci/commands/ci-prod-types.d.ts +3 -0
- package/dist/commands/ci/commands/ci-prod-workflow.d.ts +1 -1
- package/dist/commands/ci/commands/ci-resolvers.d.ts +1 -1
- package/dist/commands/ci/commands/ci-supabase-local.d.ts +4 -0
- package/dist/commands/ci/machine/actors/build/build-and-playwright.d.ts +1 -1
- package/dist/commands/ci/machine/actors/db/collect-schema-stats.d.ts +11 -1
- package/dist/commands/ci/machine/actors/db/production-preview.d.ts +22 -4
- package/dist/commands/ci/machine/actors/db/schema-canonical-diff.d.ts +8 -1
- package/dist/commands/ci/machine/actors/db/sync-schema.d.ts +1 -0
- package/dist/commands/ci/machine/actors/finalize/index.d.ts +0 -1
- package/dist/commands/ci/machine/actors/index.d.ts +1 -1
- package/dist/commands/ci/machine/actors/setup/local.d.ts +2 -0
- package/dist/commands/ci/machine/actors/setup/pr-common.d.ts +3 -0
- package/dist/commands/ci/machine/actors/setup/pr-local.d.ts +2 -0
- package/dist/commands/ci/machine/commands/machine-runner.d.ts +5 -1
- package/dist/commands/ci/machine/commands/step-telemetry.d.ts +16 -0
- package/dist/commands/ci/machine/contract.d.ts +40 -0
- package/dist/commands/ci/machine/formatters/github-comment-types.d.ts +7 -2
- package/dist/commands/ci/machine/formatters/github-comment.d.ts +2 -1
- package/dist/commands/ci/machine/formatters/sections/final-comment.d.ts +2 -1
- package/dist/commands/ci/machine/formatters/sections/index.d.ts +1 -1
- package/dist/commands/ci/machine/formatters/summary.d.ts +4 -4
- package/dist/commands/ci/machine/guards.d.ts +4 -0
- package/dist/commands/ci/machine/helpers.d.ts +25 -0
- package/dist/commands/ci/machine/machine-state-helpers.d.ts +1 -1
- package/dist/commands/ci/machine/machine.d.ts +15 -8
- package/dist/commands/ci/machine/types.d.ts +9 -0
- package/dist/commands/ci/utils/ci-diagnostics.d.ts +67 -0
- package/dist/commands/ci/utils/ci-summary.d.ts +118 -0
- package/dist/commands/ci/utils/db-url-utils.d.ts +4 -77
- package/dist/commands/ci/utils/github-api.d.ts +14 -0
- package/dist/commands/db/apply/contract.d.ts +73 -0
- package/dist/commands/db/apply/helpers/alter-statement-parsers.d.ts +95 -0
- package/dist/commands/db/apply/helpers/data-compatibility-checker.d.ts +0 -61
- package/dist/commands/db/apply/helpers/function-plan-false-positive-filter.d.ts +36 -0
- package/dist/commands/db/apply/helpers/hazard-handler.d.ts +4 -4
- package/dist/commands/db/apply/helpers/index.d.ts +14 -5
- package/dist/commands/db/apply/helpers/partition-acl-cleaner.d.ts +3 -1
- package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts +69 -6
- package/dist/commands/db/apply/helpers/plan-ast.d.ts +56 -0
- package/dist/commands/db/apply/helpers/plan-check-filter.d.ts +26 -0
- package/dist/commands/db/apply/helpers/plan-drop-protection.d.ts +43 -0
- package/dist/commands/db/apply/helpers/plan-ordering.d.ts +6 -0
- package/dist/commands/db/apply/helpers/plan-statement-parser.d.ts +39 -0
- package/dist/commands/db/apply/helpers/plan-validator.d.ts +8 -40
- package/dist/commands/db/apply/helpers/retry-logic.d.ts +1 -10
- package/dist/commands/db/apply/helpers/temp-db-bootstrap.d.ts +18 -0
- package/dist/commands/db/apply/helpers/temp-db-dsn.d.ts +14 -0
- package/dist/commands/db/apply/machine.d.ts +56 -32
- package/dist/commands/db/commands/db-apply-error.d.ts +5 -0
- package/dist/commands/db/commands/db-apply.d.ts +2 -0
- package/dist/commands/db/commands/db-sync/directory-placement-check.d.ts +4 -0
- package/dist/commands/db/commands/db-sync/error-classifier.d.ts +1 -1
- package/dist/commands/db/commands/db-sync/plan-boundary-reconciliation.d.ts +3 -0
- package/dist/commands/db/commands/db-sync/precheck-helpers.d.ts +18 -0
- package/dist/commands/db/commands/db-sync/production-precheck.d.ts +15 -0
- package/dist/commands/db/commands/db-sync/risk-scan-collectors.d.ts +11 -0
- package/dist/commands/db/commands/db-sync.d.ts +11 -5
- package/dist/commands/db/sync/contract.d.ts +80 -0
- package/dist/commands/db/sync/machine.d.ts +60 -1
- package/dist/commands/db/types.d.ts +5 -0
- package/dist/commands/db/utils/boundary-policy/rule-compiler.d.ts +2 -1
- package/dist/commands/db/utils/boundary-policy/types.d.ts +23 -0
- package/dist/commands/db/utils/boundary-policy-runtime.d.ts +12 -3
- package/dist/commands/db/utils/boundary-policy.d.ts +1 -1
- package/dist/commands/db/utils/db-target.d.ts +5 -3
- package/dist/commands/db/utils/declarative-dependency-collectors.d.ts +6 -0
- package/dist/commands/db/utils/declarative-dependency-contract.d.ts +78 -0
- package/dist/commands/db/utils/declarative-dependency-sql-utils.d.ts +49 -0
- package/dist/commands/db/utils/declarative-dependency-warning-governance.d.ts +24 -0
- package/dist/commands/db/utils/preflight-check.d.ts +1 -1
- package/dist/commands/db/utils/preflight-checks/declarative-dependency-checks.d.ts +4 -0
- package/dist/commands/db/utils/preflight-checks/idempotent-risk-checks.d.ts +4 -0
- package/dist/commands/db/utils/preflight-checks/schema-boundary-checks.d.ts +4 -0
- package/dist/commands/db/utils/preflight-checks/schema-risk-policy.d.ts +4 -0
- package/dist/commands/db/utils/preflight-checks/supabase-checks.d.ts +12 -0
- package/dist/commands/db/utils/psql.d.ts +23 -0
- package/dist/commands/db/utils/sql-table-extractor.d.ts +42 -1
- package/dist/commands/env/commands/setup/types.d.ts +1 -0
- package/dist/commands/env/constants/local-supabase.d.ts +4 -1
- package/dist/commands/observability.d.ts +72 -0
- package/dist/commands/observability.helpers.d.ts +25 -0
- package/dist/commands/template-check/contract.d.ts +3 -3
- package/dist/commands/template-check/machine.d.ts +1 -1
- package/dist/commands/workflow/commands/deploy-production.d.ts +0 -1
- package/dist/constants/versions.d.ts +1 -1
- package/dist/{db-Q3GF7JWP.js → db-S4V4ETDR.js} +14629 -11270
- package/dist/{dev-5YXNPTCJ.js → dev-MLRKIP7F.js} +5 -5
- package/dist/{doctor-MZLOA53G.js → doctor-ROSWSMLH.js} +2 -2
- package/dist/{env-GMB3THRG.js → env-WNHJVLOT.js} +37 -20
- package/dist/{env-HMMRSYCI.js → env-XPPACZM4.js} +2 -2
- package/dist/{env-files-2UIUYLLR.js → env-files-HRNUGZ5O.js} +1 -1
- package/dist/{error-handler-HEXBRNVV.js → error-handler-YRQWRDEF.js} +17 -0
- package/dist/{hotfix-NDTPY2T4.js → hotfix-Z5EGVSMH.js} +4 -4
- package/dist/index.js +4 -4
- package/dist/{init-U4VCRHTD.js → init-35JLDFHI.js} +1 -1
- package/dist/{inject-test-attrs-P44BVTQS.js → inject-test-attrs-XN4I2AOR.js} +2 -2
- package/dist/internal/machines/index.d.ts +1 -1
- package/dist/internal/machines/snapshot-helpers.d.ts +6 -0
- package/dist/{manifest-TMFLESHW.js → manifest-EGCAZ4TK.js} +1 -1
- package/dist/observability-CJA5UFIC.js +721 -0
- package/dist/{risk-detector-4U6ZJ2G5.js → risk-detector-S7XQF4I2.js} +1 -1
- package/dist/{risk-detector-core-TK4OAI3N.js → risk-detector-core-TGFKWHRS.js} +61 -3
- package/dist/risk-detector-plpgsql-O32TUR34.js +736 -0
- package/dist/{template-check-FFJVDLBF.js → template-check-BDFMT6ZO.js} +1 -1
- package/dist/{upgrade-7TWORWBV.js → upgrade-7L4JIE4K.js} +1 -1
- package/dist/utils/db-url-utils.d.ts +81 -0
- package/dist/validators/risk-detector-plpgsql.d.ts +3 -1
- package/dist/{vuln-check-6CMNPSBR.js → vuln-check-D575VXIQ.js} +1 -1
- package/dist/{vuln-checker-EJJTNDNE.js → vuln-checker-QV6XODTJ.js} +1 -1
- package/dist/{watch-PNTKZYFB.js → watch-AL4LCBRM.js} +1 -1
- package/dist/{workflow-H75N4BXX.js → workflow-UZIZ2JUS.js} +2 -3
- package/package.json +3 -3
- package/dist/chunk-AKZAN4BC.js +0 -90
- 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-
|
|
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-
|
|
7
|
-
import { writeEnvLocal, startAppBackground, waitForAppReady, executePrSetupBase, createErrorOutput } from './chunk-
|
|
8
|
-
import { parsePostgresUrl, buildPsqlArgs, buildPsqlEnv, psqlSyncQuery } from './chunk-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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/${
|
|
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
|
|
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:
|
|
781
|
-
logContent =
|
|
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:
|
|
910
|
-
logContent =
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
|
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:
|
|
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({
|
|
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({
|
|
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 [
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
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
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
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
|
|
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
|
|
3412
|
-
const
|
|
3413
|
-
return
|
|
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 [
|
|
3416
|
-
|
|
3939
|
+
const [referenceSessionResult, ciResult] = await Promise.allSettled([
|
|
3940
|
+
buildReferenceStatsSession(repoRoot, tmpDir, referenceDbUrl),
|
|
3417
3941
|
collectEnvironmentStats(ciDbUrl, "CI")
|
|
3418
3942
|
]);
|
|
3419
|
-
|
|
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
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
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
|
|
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
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
const
|
|
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
|
|
3767
|
-
for (const
|
|
3768
|
-
|
|
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
|
|
3867
|
-
|
|
3868
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
4599
|
+
const child = execa(
|
|
3909
4600
|
"pnpm",
|
|
3910
|
-
["exec", "runa", "db", "apply", "production", "--check", "--
|
|
4601
|
+
["exec", "runa", "db", "apply", "production", "--check", "--compare-only"],
|
|
3911
4602
|
{
|
|
3912
4603
|
cwd: repoRoot,
|
|
3913
|
-
env: {
|
|
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
|
-
|
|
3918
|
-
|
|
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
|
-
|
|
3922
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
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
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
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:
|
|
5238
|
+
const { readFileSync: readFileSync5 } = await import('fs');
|
|
4489
5239
|
const configPath = path4.join(repoRoot, "supabase", "config.toml");
|
|
4490
|
-
const content =
|
|
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 =
|
|
4535
|
-
return `http
|
|
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 =
|
|
4540
|
-
return `postgresql://postgres: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 =
|
|
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
|
|
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
|
-
|
|
4569
|
-
|
|
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:
|
|
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({
|
|
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
|
|
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(
|
|
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
|
-
...
|
|
6914
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
7074
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
7886
|
-
|
|
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
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
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
|
|
8975
|
-
var PROGRESS_UPDATE_DEBOUNCE_MS = 2e3;
|
|
10457
|
+
var progressUpdateSequence = 0;
|
|
8976
10458
|
var pendingProgressUpdate = null;
|
|
8977
|
-
var
|
|
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
|
-
|
|
8996
|
-
|
|
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
|
-
|
|
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
|
|
9058
|
-
|
|
9059
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
9142
|
-
|
|
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
|
-
|
|
9147
|
-
|
|
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
|
-
|
|
9165
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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": (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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: "
|
|
9987
|
-
description: "
|
|
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 ? [] : [
|
|
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
|
-
{
|
|
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
|