@treeseed/sdk 0.10.22 → 0.10.24
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/db/market-schema.js +3 -2
- package/dist/market-client.d.ts +4 -0
- package/dist/market-client.js +6 -0
- package/dist/operations/providers/default.js +26 -4
- package/dist/operations/repository-operations.js +6 -2
- package/dist/operations/services/bootstrap-runner.d.ts +5 -1
- package/dist/operations/services/bootstrap-runner.js +34 -5
- package/dist/operations/services/config-runtime.d.ts +2 -1
- package/dist/operations/services/deploy.d.ts +18 -1
- package/dist/operations/services/deploy.js +176 -24
- package/dist/operations/services/github-automation.d.ts +10 -1
- package/dist/operations/services/github-automation.js +18 -4
- package/dist/operations/services/hosting-audit.d.ts +2 -1
- package/dist/operations/services/hosting-audit.js +12 -1
- package/dist/operations/services/hub-launch.d.ts +1 -0
- package/dist/operations/services/hub-launch.js +1 -0
- package/dist/operations/services/hub-provider-launch.d.ts +9 -0
- package/dist/operations/services/hub-provider-launch.js +140 -40
- package/dist/operations/services/managed-host-security.d.ts +1 -1
- package/dist/operations/services/managed-host-security.js +4 -1
- package/dist/operations/services/project-platform.d.ts +25 -0
- package/dist/operations/services/project-platform.js +91 -23
- package/dist/operations/services/railway-api.js +2 -1
- package/dist/operations/services/railway-deploy.d.ts +32 -2
- package/dist/operations/services/railway-deploy.js +94 -27
- package/dist/operations/services/template-registry.js +33 -3
- package/dist/platform/contracts.d.ts +1 -0
- package/dist/platform/deploy-config.js +8 -1
- package/dist/platform/deploy-runtime.js +1 -0
- package/dist/platform/environment.d.ts +1 -1
- package/dist/platform/environment.js +1 -1
- package/dist/reconcile/builtin-adapters.js +155 -25
- package/dist/reconcile/contracts.d.ts +1 -1
- package/dist/reconcile/desired-state.js +17 -1
- package/dist/reconcile/engine.d.ts +2 -0
- package/dist/reconcile/engine.js +58 -3
- package/dist/reconcile/units.js +1 -0
- package/dist/sdk-types.d.ts +1 -1
- package/dist/sdk-types.js +2 -0
- package/dist/timing.d.ts +20 -0
- package/dist/timing.js +73 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +150 -0
- package/dist/workflow/operations.d.ts +2 -0
- package/drizzle/market/0000_market_control_plane.sql +3 -3
- package/drizzle/market/0003_project_team_slug_unique.sql +4 -0
- package/package.json +1 -1
- package/templates/github/deploy-web.workflow.yml +4 -0
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { basename, extname, join, resolve } from "node:path";
|
|
6
|
+
import { elapsedMs, formatTimingMarkdown, formatTimingSummary } from "../../timing.js";
|
|
6
7
|
import {
|
|
7
8
|
createControlPlaneReporter
|
|
8
9
|
} from "../../control-plane.js";
|
|
@@ -52,6 +53,39 @@ const PROCESSING_PLATFORM_BOOTSTRAP_SYSTEMS = ["api", "agents"];
|
|
|
52
53
|
function stableHash(value) {
|
|
53
54
|
return createHash("sha256").update(value).digest("hex");
|
|
54
55
|
}
|
|
56
|
+
function recordTiming(timings, name, startMs, status = "success", metadata) {
|
|
57
|
+
const entry = {
|
|
58
|
+
name,
|
|
59
|
+
durationMs: elapsedMs(startMs),
|
|
60
|
+
status,
|
|
61
|
+
...metadata ? { metadata } : {}
|
|
62
|
+
};
|
|
63
|
+
timings.push(entry);
|
|
64
|
+
return entry;
|
|
65
|
+
}
|
|
66
|
+
async function timedPhase(timings, name, run, metadata) {
|
|
67
|
+
const startMs = performance.now();
|
|
68
|
+
try {
|
|
69
|
+
const result = await Promise.resolve(run());
|
|
70
|
+
recordTiming(timings, name, startMs, "success", metadata);
|
|
71
|
+
return result;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
recordTiming(timings, name, startMs, "failed", {
|
|
74
|
+
...metadata ?? {},
|
|
75
|
+
error: error instanceof Error ? error.message : String(error)
|
|
76
|
+
});
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function writeProviderTimingSummary(options, timings) {
|
|
81
|
+
const text = formatTimingSummary(timings);
|
|
82
|
+
options.write?.(text);
|
|
83
|
+
const summaryPath = String(options.env?.TREESEED_PROVIDER_TIMING_SUMMARY_PATH ?? process.env.TREESEED_PROVIDER_TIMING_SUMMARY_PATH ?? "").trim();
|
|
84
|
+
if (!summaryPath) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
writeFileSync(summaryPath, formatTimingMarkdown(timings), { flag: "a" });
|
|
88
|
+
}
|
|
55
89
|
function inferEnvironmentFromBranch(tenantRoot) {
|
|
56
90
|
const branch = currentManagedBranch(tenantRoot);
|
|
57
91
|
if (branch === STAGING_BRANCH) {
|
|
@@ -1024,25 +1058,30 @@ async function publishContent(options, reporter, publishOptions = {}) {
|
|
|
1024
1058
|
}
|
|
1025
1059
|
}
|
|
1026
1060
|
async function provisionProjectPlatform(options) {
|
|
1061
|
+
const timings = [];
|
|
1027
1062
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
1028
1063
|
const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
|
|
1029
1064
|
const siteConfig = loadCliDeployConfig(options.tenantRoot);
|
|
1030
1065
|
const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options, siteConfig);
|
|
1031
1066
|
const selectedSystems = new Set(bootstrapSystems);
|
|
1032
1067
|
const env = { ...process.env, ...options.env ?? {} };
|
|
1033
|
-
const summary = await reconcileTreeseedTarget({
|
|
1068
|
+
const summary = await timedPhase(timings, "provision:reconcile", () => reconcileTreeseedTarget({
|
|
1034
1069
|
tenantRoot: options.tenantRoot,
|
|
1035
1070
|
target,
|
|
1036
1071
|
env,
|
|
1037
|
-
systems: bootstrapSystems
|
|
1038
|
-
|
|
1039
|
-
|
|
1072
|
+
systems: bootstrapSystems,
|
|
1073
|
+
write: options.write
|
|
1074
|
+
}));
|
|
1075
|
+
timings.push(...summary.timings ?? []);
|
|
1076
|
+
const verification = await timedPhase(timings, "provision:collect-reconcile-status", () => collectTreeseedReconcileStatus({
|
|
1040
1077
|
tenantRoot: options.tenantRoot,
|
|
1041
1078
|
target,
|
|
1042
1079
|
env,
|
|
1043
1080
|
systems: bootstrapSystems
|
|
1081
|
+
}));
|
|
1082
|
+
await timedPhase(timings, "provision:ensure-wrangler-config", () => {
|
|
1083
|
+
ensureGeneratedWranglerConfig(options.tenantRoot, { target });
|
|
1044
1084
|
});
|
|
1045
|
-
ensureGeneratedWranglerConfig(options.tenantRoot, { target });
|
|
1046
1085
|
const shouldValidateRailway = selectedSystems.has("api") || selectedSystems.has("agents");
|
|
1047
1086
|
const railwayValidation = shouldValidateRailway ? options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env }) : { services: [] };
|
|
1048
1087
|
const railwaySchedules = [];
|
|
@@ -1087,6 +1126,22 @@ async function provisionProjectPlatform(options) {
|
|
|
1087
1126
|
locator: state.lastDeployedUrl ?? null,
|
|
1088
1127
|
metadata: { workerName: state.workerName }
|
|
1089
1128
|
},
|
|
1129
|
+
{
|
|
1130
|
+
environment: options.scope,
|
|
1131
|
+
provider: "cloudflare",
|
|
1132
|
+
resourceKind: "kv",
|
|
1133
|
+
logicalName: state.kvNamespaces?.FORM_GUARD_KV?.name ?? "form-guard",
|
|
1134
|
+
locator: state.kvNamespaces?.FORM_GUARD_KV?.id ?? null,
|
|
1135
|
+
metadata: state.kvNamespaces?.FORM_GUARD_KV ?? {}
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
environment: options.scope,
|
|
1139
|
+
provider: "cloudflare",
|
|
1140
|
+
resourceKind: "turnstile-widget",
|
|
1141
|
+
logicalName: state.turnstileWidgets?.formGuard?.name ?? "form-guard-turnstile",
|
|
1142
|
+
locator: state.turnstileWidgets?.formGuard?.sitekey ?? null,
|
|
1143
|
+
metadata: state.turnstileWidgets?.formGuard ?? {}
|
|
1144
|
+
},
|
|
1090
1145
|
{
|
|
1091
1146
|
environment: options.scope,
|
|
1092
1147
|
provider: "cloudflare",
|
|
@@ -1157,6 +1212,7 @@ async function provisionProjectPlatform(options) {
|
|
|
1157
1212
|
target: deployTargetLabel(target),
|
|
1158
1213
|
summary,
|
|
1159
1214
|
verification,
|
|
1215
|
+
timings,
|
|
1160
1216
|
reconcileActions: summary.results.map((result) => ({
|
|
1161
1217
|
unitId: result.unit.unitId,
|
|
1162
1218
|
action: result.action,
|
|
@@ -1174,6 +1230,7 @@ async function provisionProjectPlatform(options) {
|
|
|
1174
1230
|
target: deployTargetLabel(target),
|
|
1175
1231
|
summary,
|
|
1176
1232
|
verification,
|
|
1233
|
+
timings,
|
|
1177
1234
|
railway: {
|
|
1178
1235
|
services: railwayValidation.services.map((service) => service.key),
|
|
1179
1236
|
schedules: railwaySchedules,
|
|
@@ -1182,6 +1239,8 @@ async function provisionProjectPlatform(options) {
|
|
|
1182
1239
|
};
|
|
1183
1240
|
}
|
|
1184
1241
|
async function deployProjectPlatform(options) {
|
|
1242
|
+
const timings = [];
|
|
1243
|
+
const deployStartMs = performance.now();
|
|
1185
1244
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
1186
1245
|
const commitSha = currentCommit(options.tenantRoot);
|
|
1187
1246
|
const branchName = currentRef(options.tenantRoot);
|
|
@@ -1200,7 +1259,8 @@ async function deployProjectPlatform(options) {
|
|
|
1200
1259
|
metadata: { scope: options.scope }
|
|
1201
1260
|
});
|
|
1202
1261
|
if (!options.skipProvision) {
|
|
1203
|
-
await provisionProjectPlatform({ ...options, reporter, bootstrapSystems });
|
|
1262
|
+
const provision = await timedPhase(timings, "deploy:provision", () => provisionProjectPlatform({ ...options, reporter, bootstrapSystems }));
|
|
1263
|
+
timings.push(...provision.timings ?? []);
|
|
1204
1264
|
}
|
|
1205
1265
|
const nodes = [];
|
|
1206
1266
|
let cloudflareContext = null;
|
|
@@ -1319,7 +1379,7 @@ async function deployProjectPlatform(options) {
|
|
|
1319
1379
|
}
|
|
1320
1380
|
});
|
|
1321
1381
|
}
|
|
1322
|
-
await runTreeseedBootstrapDag({ nodes, execution });
|
|
1382
|
+
await runTreeseedBootstrapDag({ nodes, execution, write, timings });
|
|
1323
1383
|
const serviceResults = selectedRailwayServiceKeys.map((serviceKey) => serviceResultsByKey.get(serviceKey)).filter(Boolean);
|
|
1324
1384
|
if (options.scope !== "local" && !options.dryRun && (selectedSystems.has("web") || serviceResults.length > 0)) {
|
|
1325
1385
|
finalizeDeploymentState(options.tenantRoot, {
|
|
@@ -1342,8 +1402,11 @@ async function deployProjectPlatform(options) {
|
|
|
1342
1402
|
scheduleVerification: railwayScheduleVerification
|
|
1343
1403
|
});
|
|
1344
1404
|
}
|
|
1345
|
-
const monitor = await monitorProjectPlatform({ ...options, reporter, bootstrapSystems });
|
|
1346
|
-
|
|
1405
|
+
const monitor = await timedPhase(timings, "deploy:monitor", () => monitorProjectPlatform({ ...options, reporter, bootstrapSystems }));
|
|
1406
|
+
timings.push(...monitor.timings ?? []);
|
|
1407
|
+
const hostingRepair = await timedPhase(timings, "deploy:hosting-repair", () => repairHostingAfterSuccessfulDeploy(options, bootstrapSystems));
|
|
1408
|
+
recordTiming(timings, "deploy:total", deployStartMs);
|
|
1409
|
+
writeProviderTimingSummary(options, timings);
|
|
1347
1410
|
await reportDeployment(reporter, {
|
|
1348
1411
|
environment: options.scope,
|
|
1349
1412
|
deploymentKind: "code",
|
|
@@ -1355,7 +1418,8 @@ async function deployProjectPlatform(options) {
|
|
|
1355
1418
|
scope: options.scope,
|
|
1356
1419
|
railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key).filter((serviceKey) => serviceKey === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")),
|
|
1357
1420
|
monitor,
|
|
1358
|
-
hostingRepair
|
|
1421
|
+
hostingRepair,
|
|
1422
|
+
timings
|
|
1359
1423
|
},
|
|
1360
1424
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1361
1425
|
});
|
|
@@ -1364,7 +1428,8 @@ async function deployProjectPlatform(options) {
|
|
|
1364
1428
|
scope: options.scope,
|
|
1365
1429
|
monitor,
|
|
1366
1430
|
hostingRepair,
|
|
1367
|
-
serviceResults
|
|
1431
|
+
serviceResults,
|
|
1432
|
+
timings
|
|
1368
1433
|
};
|
|
1369
1434
|
}
|
|
1370
1435
|
function resolveRailwayServiceDeployDependencies({
|
|
@@ -1381,6 +1446,7 @@ async function publishProjectContent(options) {
|
|
|
1381
1446
|
return publishContent(options, reporter);
|
|
1382
1447
|
}
|
|
1383
1448
|
async function monitorProjectPlatform(options) {
|
|
1449
|
+
const timings = [];
|
|
1384
1450
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
1385
1451
|
const env = { ...process.env, ...options.env ?? {} };
|
|
1386
1452
|
const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
|
|
@@ -1392,23 +1458,23 @@ async function monitorProjectPlatform(options) {
|
|
|
1392
1458
|
const webProbeUrl = resolveImmediatePagesProbeUrl(siteConfig, state, target);
|
|
1393
1459
|
const apiBaseUrl = resolveImmediateApiProbeUrl(siteConfig, state, target);
|
|
1394
1460
|
const apiMonitorEndpoints = resolveApiMonitorEndpoints(siteConfig, apiBaseUrl);
|
|
1395
|
-
const railwayResources = options.scope === "local" || !apiSelected && !agentsSelected ? { ok: true, skipped: true, reason: options.scope === "local" ? "local_scope" : "railway_not_selected" } : await verifyRailwayManagedResources(options.tenantRoot, options.scope, {
|
|
1461
|
+
const railwayResources = options.scope === "local" || !apiSelected && !agentsSelected ? { ok: true, skipped: true, reason: options.scope === "local" ? "local_scope" : "railway_not_selected" } : await timedPhase(timings, "monitor:railway-resources", () => verifyRailwayManagedResources(options.tenantRoot, options.scope, {
|
|
1396
1462
|
env,
|
|
1397
1463
|
settleDeployments: true,
|
|
1398
1464
|
onProgress: options.write
|
|
1399
|
-
});
|
|
1465
|
+
}));
|
|
1400
1466
|
const skippedApiCheck = apiSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "api_not_selected" };
|
|
1401
1467
|
const skippedAgentCheck = agentsSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "agents_not_selected" };
|
|
1402
1468
|
const skippedD1Check = apiMonitorEndpoints.processingAgentApi ? { ok: true, skipped: true, reason: "processing_agent_api" } : skippedApiCheck;
|
|
1403
1469
|
const checks = {
|
|
1404
|
-
pages: await probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 }),
|
|
1405
|
-
apiHealth: apiSelected && apiMonitorEndpoints.apiHealth ? await probeHttp(apiMonitorEndpoints.apiHealth, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
|
|
1406
|
-
apiReady: apiSelected && apiMonitorEndpoints.apiReady ? await probeHttp(apiMonitorEndpoints.apiReady, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
|
|
1407
|
-
d1Health: apiSelected && apiMonitorEndpoints.d1Health ? await probeHttp(apiMonitorEndpoints.d1Health, { attempts: 8, delayMs: 1e4 }) : skippedD1Check,
|
|
1408
|
-
agentHealth: agentsSelected && apiMonitorEndpoints.agentHealth ? await probeHttp(apiMonitorEndpoints.agentHealth, { attempts: 8, delayMs: 1e4 }) : skippedAgentCheck,
|
|
1409
|
-
r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : probeR2(options.tenantRoot, siteConfig, state, target),
|
|
1410
|
-
queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : probeQueue(siteConfig, state),
|
|
1411
|
-
scaleProbe: probeScaleConfiguration(siteConfig, state),
|
|
1470
|
+
pages: await timedPhase(timings, "monitor:probe-pages", () => probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 })),
|
|
1471
|
+
apiHealth: apiSelected && apiMonitorEndpoints.apiHealth ? await timedPhase(timings, "monitor:probe-api-health", () => probeHttp(apiMonitorEndpoints.apiHealth, { attempts: 8, delayMs: 1e4 })) : skippedApiCheck,
|
|
1472
|
+
apiReady: apiSelected && apiMonitorEndpoints.apiReady ? await timedPhase(timings, "monitor:probe-api-ready", () => probeHttp(apiMonitorEndpoints.apiReady, { attempts: 8, delayMs: 1e4 })) : skippedApiCheck,
|
|
1473
|
+
d1Health: apiSelected && apiMonitorEndpoints.d1Health ? await timedPhase(timings, "monitor:probe-d1-health", () => probeHttp(apiMonitorEndpoints.d1Health, { attempts: 8, delayMs: 1e4 })) : skippedD1Check,
|
|
1474
|
+
agentHealth: agentsSelected && apiMonitorEndpoints.agentHealth ? await timedPhase(timings, "monitor:probe-agent-health", () => probeHttp(apiMonitorEndpoints.agentHealth, { attempts: 8, delayMs: 1e4 })) : skippedAgentCheck,
|
|
1475
|
+
r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : timedPhase(timings, "monitor:probe-r2", () => probeR2(options.tenantRoot, siteConfig, state, target)),
|
|
1476
|
+
queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : timedPhase(timings, "monitor:probe-queue", () => probeQueue(siteConfig, state)),
|
|
1477
|
+
scaleProbe: await timedPhase(timings, "monitor:probe-scale", () => probeScaleConfiguration(siteConfig, state)),
|
|
1412
1478
|
railwayResources,
|
|
1413
1479
|
readiness: state.readiness,
|
|
1414
1480
|
apiMonitor: {
|
|
@@ -1448,14 +1514,16 @@ ${failedChecks.join("\n")}`);
|
|
|
1448
1514
|
metadata: {
|
|
1449
1515
|
mode: "monitor",
|
|
1450
1516
|
target: deployTargetLabel(target),
|
|
1451
|
-
checks: resolvedChecks
|
|
1517
|
+
checks: resolvedChecks,
|
|
1518
|
+
timings
|
|
1452
1519
|
},
|
|
1453
1520
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1454
1521
|
});
|
|
1455
1522
|
return {
|
|
1456
1523
|
ok,
|
|
1457
1524
|
target: deployTargetLabel(target),
|
|
1458
|
-
checks: resolvedChecks
|
|
1525
|
+
checks: resolvedChecks,
|
|
1526
|
+
timings
|
|
1459
1527
|
};
|
|
1460
1528
|
}
|
|
1461
1529
|
async function syncControlPlaneState(options) {
|
|
@@ -1295,7 +1295,8 @@ async function ensureRailwayServiceVolume({
|
|
|
1295
1295
|
if (!volume) {
|
|
1296
1296
|
volume = await createReplacementVolume();
|
|
1297
1297
|
}
|
|
1298
|
-
|
|
1298
|
+
const desiredNameInUse = volumes.some((candidate) => candidate.id !== volume?.id && candidate.name === name);
|
|
1299
|
+
if (volume.name && volume.name !== name && !desiredNameInUse) {
|
|
1299
1300
|
try {
|
|
1300
1301
|
volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
|
|
1301
1302
|
} catch (error) {
|
|
@@ -6,9 +6,10 @@ export declare function deriveRailwayMarketOperationsRunnerVolumeName(serviceNam
|
|
|
6
6
|
export declare function railwayServiceRuntimeStartCommand(service: any): any;
|
|
7
7
|
export declare function parseRailwayJsonOutput(output: any): any;
|
|
8
8
|
export declare function collectRailwayDeploymentStatusChecks(statusPayload: any, scope: any, services: any): any;
|
|
9
|
-
export declare function listRailwayServiceVolumesWithCli({ cwd, serviceId, environmentId, name, mountPath, env, }: {
|
|
9
|
+
export declare function listRailwayServiceVolumesWithCli({ cwd, serviceId, serviceName, environmentId, name, mountPath, env, }: {
|
|
10
10
|
cwd: any;
|
|
11
11
|
serviceId: any;
|
|
12
|
+
serviceName: any;
|
|
12
13
|
environmentId: any;
|
|
13
14
|
name: any;
|
|
14
15
|
mountPath: any;
|
|
@@ -30,18 +31,47 @@ export declare function runRailway(args: any, { cwd, capture, allowFailure, inpu
|
|
|
30
31
|
retryAttempts?: number | undefined;
|
|
31
32
|
retryDelayMs?: number | undefined;
|
|
32
33
|
}): import("child_process").SpawnSyncReturns<string> | null;
|
|
33
|
-
export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, onProgress, }?: {
|
|
34
|
+
export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, fetchImpl, onProgress, }?: {
|
|
34
35
|
services?: unknown[] | undefined;
|
|
35
36
|
env?: NodeJS.ProcessEnv | undefined;
|
|
36
37
|
timeoutMs?: number | undefined;
|
|
37
38
|
pollMs?: number | undefined;
|
|
39
|
+
fetchImpl?: typeof fetch | undefined;
|
|
38
40
|
}): Promise<{
|
|
41
|
+
ok: boolean;
|
|
42
|
+
checks: {
|
|
43
|
+
type: string;
|
|
44
|
+
service: any;
|
|
45
|
+
serviceName: any;
|
|
46
|
+
environment: string;
|
|
47
|
+
ok: boolean;
|
|
48
|
+
status: string;
|
|
49
|
+
settle: {
|
|
50
|
+
durationMs: number;
|
|
51
|
+
pollCount: number;
|
|
52
|
+
finalStatus: string;
|
|
53
|
+
};
|
|
54
|
+
message: string;
|
|
55
|
+
}[];
|
|
56
|
+
settle?: undefined;
|
|
57
|
+
message?: undefined;
|
|
58
|
+
} | {
|
|
39
59
|
ok: boolean;
|
|
40
60
|
checks: any;
|
|
61
|
+
settle: {
|
|
62
|
+
durationMs: number;
|
|
63
|
+
pollCount: number;
|
|
64
|
+
status: string;
|
|
65
|
+
};
|
|
41
66
|
message?: undefined;
|
|
42
67
|
} | {
|
|
43
68
|
ok: boolean;
|
|
44
69
|
checks: any;
|
|
70
|
+
settle: {
|
|
71
|
+
durationMs: number;
|
|
72
|
+
pollCount: number;
|
|
73
|
+
status: string;
|
|
74
|
+
};
|
|
45
75
|
message: string;
|
|
46
76
|
}>;
|
|
47
77
|
export declare function setRailwaySecretVariable({ cwd, service, environment, key, value, env, capture, allowFailure }: {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveRailwayWorkspaceContext,
|
|
25
25
|
upsertRailwayVariables
|
|
26
26
|
} from "./railway-api.js";
|
|
27
|
+
import { elapsedMs, formatDurationMs } from "../../timing.js";
|
|
27
28
|
function normalizeScope(scope) {
|
|
28
29
|
return scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
|
|
29
30
|
}
|
|
@@ -171,6 +172,26 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
|
|
|
171
172
|
};
|
|
172
173
|
}
|
|
173
174
|
const deployment = instance.latestDeployment ?? null;
|
|
175
|
+
if (!deployment) {
|
|
176
|
+
return {
|
|
177
|
+
type: "deployment-status",
|
|
178
|
+
service: service.key,
|
|
179
|
+
serviceName: service.serviceName,
|
|
180
|
+
environment: normalizeRailwayEnvironmentName(environment.name),
|
|
181
|
+
ok: true,
|
|
182
|
+
skipped: true,
|
|
183
|
+
status: "no_active_deployment",
|
|
184
|
+
observed: {
|
|
185
|
+
status: null,
|
|
186
|
+
deploymentId: null,
|
|
187
|
+
deploymentCreatedAt: null,
|
|
188
|
+
deploymentStopped: null,
|
|
189
|
+
instanceStatuses: [],
|
|
190
|
+
volumeMounts: []
|
|
191
|
+
},
|
|
192
|
+
message: `Railway service ${service.serviceName} has no active deployment to wait for.`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
174
195
|
const status = String(deployment?.status ?? "").trim().toUpperCase();
|
|
175
196
|
const instanceStatuses = Array.isArray(deployment?.instances) ? deployment.instances.map((entry) => String(entry?.status ?? "").trim()).filter(Boolean) : [];
|
|
176
197
|
const ok = railwayStatusDeploymentSettled(status);
|
|
@@ -183,6 +204,8 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
|
|
|
183
204
|
status: status || "missing_deployment",
|
|
184
205
|
observed: {
|
|
185
206
|
status: status || null,
|
|
207
|
+
deploymentId: deployment?.id ?? null,
|
|
208
|
+
deploymentCreatedAt: deployment?.createdAt ?? null,
|
|
186
209
|
deploymentStopped: deployment?.deploymentStopped ?? null,
|
|
187
210
|
instanceStatuses,
|
|
188
211
|
volumeMounts: Array.isArray(deployment?.meta?.volumeMounts) ? deployment.meta.volumeMounts : []
|
|
@@ -191,7 +214,7 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
|
|
|
191
214
|
};
|
|
192
215
|
});
|
|
193
216
|
}
|
|
194
|
-
function normalizeRailwayCliVolume(value, { serviceId, environmentId, fallbackName, fallbackMountPath }) {
|
|
217
|
+
function normalizeRailwayCliVolume(value, { serviceId, serviceName, environmentId, fallbackName, fallbackMountPath }) {
|
|
195
218
|
if (!value || typeof value !== "object") {
|
|
196
219
|
return null;
|
|
197
220
|
}
|
|
@@ -200,6 +223,10 @@ function normalizeRailwayCliVolume(value, { serviceId, environmentId, fallbackNa
|
|
|
200
223
|
if (!id) {
|
|
201
224
|
return null;
|
|
202
225
|
}
|
|
226
|
+
const listedServiceName = typeof record.serviceName === "string" && record.serviceName.trim() ? record.serviceName.trim() : "";
|
|
227
|
+
if (listedServiceName && serviceName && listedServiceName !== serviceName) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
203
230
|
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : fallbackName;
|
|
204
231
|
const mountPath = typeof record.mountPath === "string" && record.mountPath.trim() ? record.mountPath.trim() : fallbackMountPath;
|
|
205
232
|
const sizeMb = typeof record.sizeMB === "number" ? record.sizeMB : null;
|
|
@@ -228,6 +255,7 @@ function normalizeRailwayCliVolumeList(value, options) {
|
|
|
228
255
|
function listRailwayServiceVolumesWithCli({
|
|
229
256
|
cwd,
|
|
230
257
|
serviceId,
|
|
258
|
+
serviceName,
|
|
231
259
|
environmentId,
|
|
232
260
|
name,
|
|
233
261
|
mountPath,
|
|
@@ -244,6 +272,7 @@ function listRailwayServiceVolumesWithCli({
|
|
|
244
272
|
}
|
|
245
273
|
return normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
|
|
246
274
|
serviceId,
|
|
275
|
+
serviceName,
|
|
247
276
|
environmentId,
|
|
248
277
|
fallbackName: name,
|
|
249
278
|
fallbackMountPath: mountPath
|
|
@@ -468,8 +497,10 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
468
497
|
env = process.env,
|
|
469
498
|
timeoutMs = 6e5,
|
|
470
499
|
pollMs = 15e3,
|
|
500
|
+
fetchImpl = fetch,
|
|
471
501
|
onProgress
|
|
472
502
|
} = {}) {
|
|
503
|
+
const startMs = performance.now();
|
|
473
504
|
const deadline = Date.now() + timeoutMs;
|
|
474
505
|
const projectId = services.find((service) => typeof service.projectId === "string" && service.projectId.trim())?.projectId ?? null;
|
|
475
506
|
if (!projectId) {
|
|
@@ -482,6 +513,11 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
482
513
|
environment: resolveRailwayEnvironmentForScope(scope),
|
|
483
514
|
ok: false,
|
|
484
515
|
status: "missing_project",
|
|
516
|
+
settle: {
|
|
517
|
+
durationMs: elapsedMs(startMs),
|
|
518
|
+
pollCount: 0,
|
|
519
|
+
finalStatus: "missing_project"
|
|
520
|
+
},
|
|
485
521
|
message: `Railway deployment status for ${service.serviceName} cannot be checked without a project id.`
|
|
486
522
|
}))
|
|
487
523
|
};
|
|
@@ -489,12 +525,15 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
489
525
|
let checks = [];
|
|
490
526
|
let lastError = null;
|
|
491
527
|
let lastSummary = "";
|
|
528
|
+
let pollCount = 0;
|
|
492
529
|
for (; ; ) {
|
|
493
530
|
lastError = null;
|
|
531
|
+
pollCount += 1;
|
|
494
532
|
try {
|
|
495
533
|
const statusPayload = await fetchRailwayProjectDeploymentStatus({
|
|
496
534
|
projectId,
|
|
497
|
-
env
|
|
535
|
+
env,
|
|
536
|
+
fetchImpl
|
|
498
537
|
});
|
|
499
538
|
checks = collectRailwayDeploymentStatusChecks(statusPayload, scope, services);
|
|
500
539
|
} catch (error) {
|
|
@@ -506,28 +545,63 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
506
545
|
environment: resolveRailwayEnvironmentForScope(scope),
|
|
507
546
|
ok: false,
|
|
508
547
|
status: "status_error",
|
|
548
|
+
settle: {
|
|
549
|
+
durationMs: elapsedMs(startMs),
|
|
550
|
+
pollCount,
|
|
551
|
+
finalStatus: "status_error"
|
|
552
|
+
},
|
|
509
553
|
message: error instanceof Error ? error.message : String(error)
|
|
510
554
|
}));
|
|
511
555
|
}
|
|
512
556
|
const summary = formatRailwayDeploymentStatusSummary(scope, checks);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
557
|
+
const progress = `${summary} poll=${pollCount} elapsed=${formatDurationMs(elapsedMs(startMs))}`;
|
|
558
|
+
if (progress !== lastSummary || !checks.every((entry) => entry.ok === true || entry.skipped === true)) {
|
|
559
|
+
onProgress?.(progress, "stdout");
|
|
560
|
+
lastSummary = progress;
|
|
516
561
|
}
|
|
517
|
-
if (checks.every((entry) => entry.ok === true)) {
|
|
518
|
-
return {
|
|
562
|
+
if (checks.every((entry) => entry.ok === true || entry.skipped === true)) {
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
checks: checks.map((check) => ({
|
|
566
|
+
...check,
|
|
567
|
+
settle: {
|
|
568
|
+
durationMs: elapsedMs(startMs),
|
|
569
|
+
pollCount,
|
|
570
|
+
finalStatus: check.status,
|
|
571
|
+
fastSkipped: check.skipped === true
|
|
572
|
+
}
|
|
573
|
+
})),
|
|
574
|
+
settle: {
|
|
575
|
+
durationMs: elapsedMs(startMs),
|
|
576
|
+
pollCount,
|
|
577
|
+
status: checks.every((entry) => entry.skipped === true) ? "skipped" : "settled"
|
|
578
|
+
}
|
|
579
|
+
};
|
|
519
580
|
}
|
|
520
581
|
if (Date.now() >= deadline) {
|
|
521
582
|
return {
|
|
522
583
|
ok: false,
|
|
523
|
-
checks
|
|
584
|
+
checks: checks.map((check) => ({
|
|
585
|
+
...check,
|
|
586
|
+
settle: {
|
|
587
|
+
durationMs: elapsedMs(startMs),
|
|
588
|
+
pollCount,
|
|
589
|
+
finalStatus: check.status,
|
|
590
|
+
timeout: true
|
|
591
|
+
}
|
|
592
|
+
})),
|
|
593
|
+
settle: {
|
|
594
|
+
durationMs: elapsedMs(startMs),
|
|
595
|
+
pollCount,
|
|
596
|
+
status: "timeout"
|
|
597
|
+
},
|
|
524
598
|
message: lastError instanceof Error ? lastError.message : "Railway deployments did not settle before the monitor timeout."
|
|
525
599
|
};
|
|
526
600
|
}
|
|
527
601
|
await sleep(pollMs);
|
|
528
602
|
}
|
|
529
603
|
}
|
|
530
|
-
async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env }) {
|
|
604
|
+
async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env, fetchImpl = fetch }) {
|
|
531
605
|
const payload = await railwayGraphqlRequest({
|
|
532
606
|
query: `
|
|
533
607
|
query TreeseedRailwayDeploymentStatus($projectId: String!) {
|
|
@@ -565,7 +639,8 @@ query TreeseedRailwayDeploymentStatus($projectId: String!) {
|
|
|
565
639
|
}
|
|
566
640
|
`.trim(),
|
|
567
641
|
variables: { projectId },
|
|
568
|
-
env
|
|
642
|
+
env,
|
|
643
|
+
fetchImpl
|
|
569
644
|
});
|
|
570
645
|
return payload.data?.project ?? null;
|
|
571
646
|
}
|
|
@@ -1294,6 +1369,7 @@ async function verifyRailwayManagedResources(tenantRoot, scope, {
|
|
|
1294
1369
|
const settled = await waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
1295
1370
|
services: deploymentStatusServices.length > 0 ? deploymentStatusServices : services,
|
|
1296
1371
|
env: effectiveEnv,
|
|
1372
|
+
fetchImpl,
|
|
1297
1373
|
timeoutMs: settleTimeoutMs,
|
|
1298
1374
|
pollMs: settlePollMs,
|
|
1299
1375
|
onProgress
|
|
@@ -1776,6 +1852,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
|
|
|
1776
1852
|
const listResult = runRailway([...volumeArgs, "list", "--json"], cliOptions);
|
|
1777
1853
|
const existingVolumes = normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
|
|
1778
1854
|
serviceId,
|
|
1855
|
+
serviceName,
|
|
1779
1856
|
environmentId,
|
|
1780
1857
|
fallbackName: name,
|
|
1781
1858
|
fallbackMountPath: mountPath
|
|
@@ -1787,6 +1864,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
|
|
|
1787
1864
|
const createResult = runRailway([...volumeArgs, "add", "--mount-path", mountPath, "--json"], cliOptions);
|
|
1788
1865
|
volume = normalizeRailwayCliVolume(parseRailwayJsonOutput(createResult.stdout ?? ""), {
|
|
1789
1866
|
serviceId,
|
|
1867
|
+
serviceName,
|
|
1790
1868
|
environmentId,
|
|
1791
1869
|
fallbackName: name,
|
|
1792
1870
|
fallbackMountPath: mountPath
|
|
@@ -1810,6 +1888,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
|
|
|
1810
1888
|
}
|
|
1811
1889
|
const attachedVolume = (attachResult.status ?? 1) === 0 ? normalizeRailwayCliVolume(parseRailwayJsonOutput(attachResult.stdout ?? ""), {
|
|
1812
1890
|
serviceId,
|
|
1891
|
+
serviceName,
|
|
1813
1892
|
environmentId,
|
|
1814
1893
|
fallbackName: name,
|
|
1815
1894
|
fallbackMountPath: mountPath
|
|
@@ -1833,21 +1912,6 @@ async function ensureRailwayServiceVolumeWithCliFallback({
|
|
|
1833
1912
|
instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
|
|
1834
1913
|
updated = true;
|
|
1835
1914
|
}
|
|
1836
|
-
if (volume.name !== name || instance?.mountPath !== mountPath) {
|
|
1837
|
-
const updateResult = runRailway([...volumeArgs, "update", "--volume", volume.id, "--name", name, "--mount-path", mountPath, "--json"], cliOptions);
|
|
1838
|
-
const updatedVolume = normalizeRailwayCliVolume(parseRailwayJsonOutput(updateResult.stdout ?? ""), {
|
|
1839
|
-
serviceId,
|
|
1840
|
-
environmentId,
|
|
1841
|
-
fallbackName: name,
|
|
1842
|
-
fallbackMountPath: mountPath
|
|
1843
|
-
});
|
|
1844
|
-
volume = updatedVolume ?? {
|
|
1845
|
-
...volume,
|
|
1846
|
-
name,
|
|
1847
|
-
instances: volume.instances.map((entry) => ({ ...entry, mountPath }))
|
|
1848
|
-
};
|
|
1849
|
-
updated = true;
|
|
1850
|
-
}
|
|
1851
1915
|
const apiVolume = await waitForRailwayServiceVolumeMount({
|
|
1852
1916
|
projectId,
|
|
1853
1917
|
volumeId: volume.id,
|
|
@@ -1859,6 +1923,8 @@ async function ensureRailwayServiceVolumeWithCliFallback({
|
|
|
1859
1923
|
});
|
|
1860
1924
|
if (apiVolume) {
|
|
1861
1925
|
volume = apiVolume;
|
|
1926
|
+
} else {
|
|
1927
|
+
throw new Error(`Railway volume ${name} was not attached to ${serviceName} at ${mountPath}.`);
|
|
1862
1928
|
}
|
|
1863
1929
|
return {
|
|
1864
1930
|
volume,
|
|
@@ -1878,11 +1944,12 @@ async function waitForRailwayServiceVolumeMount({
|
|
|
1878
1944
|
}) {
|
|
1879
1945
|
for (let attempt = 0; attempt <= 24; attempt += 1) {
|
|
1880
1946
|
const volumes = await listRailwayVolumes({ projectId, env });
|
|
1881
|
-
const
|
|
1882
|
-
(entry) => entry.
|
|
1947
|
+
const mounted = volumes.find(
|
|
1948
|
+
(entry) => entry.instances.some(
|
|
1883
1949
|
(instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
|
|
1884
1950
|
)
|
|
1885
1951
|
) ?? null;
|
|
1952
|
+
const match = mounted ?? volumes.find((entry) => entry.id === volumeId) ?? volumes.find((entry) => entry.name === volumeName) ?? null;
|
|
1886
1953
|
if (match?.instances.some(
|
|
1887
1954
|
(instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
|
|
1888
1955
|
)) {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
cliPackageVersion,
|
|
12
12
|
agentPackageVersion,
|
|
13
13
|
corePackageVersion,
|
|
14
|
+
cliPackageRoot,
|
|
14
15
|
localTemplateArtifactsRoot,
|
|
15
16
|
sdkPackageVersion
|
|
16
17
|
} from "./runtime-paths.js";
|
|
@@ -37,10 +38,31 @@ function listFiles(root) {
|
|
|
37
38
|
return files;
|
|
38
39
|
}
|
|
39
40
|
function listTemplateArtifactIds() {
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const packagedIds = existsSync(localTemplateArtifactsRoot) ? readdirSync(localTemplateArtifactsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
|
|
42
|
+
const localStarterIds = listLocalStarterArtifacts().map((entry) => entry.id);
|
|
43
|
+
return [.../* @__PURE__ */ new Set([...packagedIds, ...localStarterIds])].sort((left, right) => left.localeCompare(right, void 0, { sensitivity: "base" }));
|
|
44
|
+
}
|
|
45
|
+
const LOCAL_STARTER_ID_TO_DIRECTORY = {
|
|
46
|
+
"starter-research": "research",
|
|
47
|
+
"starter-engineering": "engineering",
|
|
48
|
+
"starter-information-hub": "information-hub"
|
|
49
|
+
};
|
|
50
|
+
function localStartersRoot() {
|
|
51
|
+
return resolve(cliPackageRoot, "..", "..", "starters");
|
|
52
|
+
}
|
|
53
|
+
function resolveLocalStarterArtifactRoot(id) {
|
|
54
|
+
const directory = LOCAL_STARTER_ID_TO_DIRECTORY[id];
|
|
55
|
+
if (!directory) {
|
|
56
|
+
return null;
|
|
42
57
|
}
|
|
43
|
-
|
|
58
|
+
const artifactRoot = resolve(localStartersRoot(), directory);
|
|
59
|
+
return existsSync(resolve(artifactRoot, "template.config.json")) && existsSync(resolve(artifactRoot, "template")) ? artifactRoot : null;
|
|
60
|
+
}
|
|
61
|
+
function listLocalStarterArtifacts() {
|
|
62
|
+
return Object.keys(LOCAL_STARTER_ID_TO_DIRECTORY).map((id) => {
|
|
63
|
+
const artifactRoot = resolveLocalStarterArtifactRoot(id);
|
|
64
|
+
return artifactRoot ? { id, artifactRoot } : null;
|
|
65
|
+
}).filter((entry) => Boolean(entry));
|
|
44
66
|
}
|
|
45
67
|
function isTextFile(filePath) {
|
|
46
68
|
return !/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|eot|pdf|zip|gz)$/iu.test(filePath);
|
|
@@ -166,6 +188,14 @@ function materializeR2TemplateSource(product) {
|
|
|
166
188
|
);
|
|
167
189
|
}
|
|
168
190
|
function resolveTemplateDefinitionPaths(product, options) {
|
|
191
|
+
const localStarterArtifactRoot = resolveLocalStarterArtifactRoot(product.id);
|
|
192
|
+
if (localStarterArtifactRoot) {
|
|
193
|
+
return {
|
|
194
|
+
artifactRoot: localStarterArtifactRoot,
|
|
195
|
+
manifestPath: resolve(localStarterArtifactRoot, "template.config.json"),
|
|
196
|
+
templateRoot: resolve(localStarterArtifactRoot, "template")
|
|
197
|
+
};
|
|
198
|
+
}
|
|
169
199
|
if (existsSync(product.artifactManifestPath) && existsSync(product.templateRoot)) {
|
|
170
200
|
return {
|
|
171
201
|
artifactRoot: product.artifactRoot,
|