@treeseed/sdk 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/capacity.d.ts +33 -0
  2. package/dist/fixture-support.d.ts +1 -1
  3. package/dist/fixture-support.js +5 -5
  4. package/dist/managed-dependencies.js +132 -10
  5. package/dist/operations/services/bootstrap-runner.js +7 -1
  6. package/dist/operations/services/config-runtime.js +13 -4
  7. package/dist/operations/services/github-actions-verification.d.ts +3 -0
  8. package/dist/operations/services/github-actions-verification.js +3 -0
  9. package/dist/operations/services/github-api.d.ts +4 -1
  10. package/dist/operations/services/github-api.js +26 -8
  11. package/dist/operations/services/github-automation.d.ts +14 -5
  12. package/dist/operations/services/github-automation.js +45 -11
  13. package/dist/operations/services/hub-provider-launch.js +9 -8
  14. package/dist/operations/services/project-platform.d.ts +93 -210
  15. package/dist/operations/services/project-platform.js +74 -34
  16. package/dist/operations/services/railway-deploy.d.ts +25 -2
  17. package/dist/operations/services/railway-deploy.js +312 -20
  18. package/dist/operations/services/repository-save-orchestrator.d.ts +8 -0
  19. package/dist/operations/services/repository-save-orchestrator.js +40 -3
  20. package/dist/operations/services/runtime-paths.d.ts +1 -0
  21. package/dist/operations/services/runtime-paths.js +3 -1
  22. package/dist/operations/services/runtime-tools.d.ts +1 -0
  23. package/dist/operations/services/runtime-tools.js +2 -0
  24. package/dist/operations/services/template-registry.js +3 -0
  25. package/dist/platform/contracts.d.ts +9 -0
  26. package/dist/platform/deploy-config.js +28 -0
  27. package/dist/platform/env.yaml +1 -745
  28. package/dist/platform/environment.js +69 -9
  29. package/dist/reconcile/builtin-adapters.js +7 -2
  30. package/dist/scripts/install-managed-dependencies.js +12 -0
  31. package/dist/scripts/tenant-workflow-action.js +11 -9
  32. package/dist/scripts/test-scaffold.js +3 -1
  33. package/dist/scripts/workflow-commands.test.js +10 -6
  34. package/dist/scripts/workspace-command-e2e.js +1 -1
  35. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +1 -0
  36. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +1 -1
  37. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +7 -6
  38. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +6 -0
  39. package/dist/workflow/operations.d.ts +41 -8
  40. package/dist/workflow/operations.js +119 -24
  41. package/dist/workflow/runs.js +31 -0
  42. package/package.json +1 -1
  43. package/templates/github/deploy-processing.workflow.yml +120 -0
  44. package/templates/github/deploy-web.workflow.yml +116 -0
  45. package/templates/github/hosted-project.workflow.yml +4 -4
  46. package/templates/github/deploy.managed.workflow.yml +0 -208
  47. package/templates/github/deploy.workflow.yml +0 -746
@@ -44,6 +44,8 @@ import { CloudflareQueuePullClient, CloudflareQueuePushClient } from "../../remo
44
44
  import { runPrefixedCommand, runTreeseedBootstrapDag, sleep, writeTreeseedBootstrapLine } from "./bootstrap-runner.js";
45
45
  import { runTenantDeployPreflight } from "./save-deploy-preflight.js";
46
46
  const PROJECT_PLATFORM_BOOTSTRAP_SYSTEMS = ["data", "web", "api", "agents"];
47
+ const WEB_PLATFORM_BOOTSTRAP_SYSTEMS = ["data", "web"];
48
+ const PROCESSING_PLATFORM_BOOTSTRAP_SYSTEMS = ["api", "agents"];
47
49
  function stableHash(value) {
48
50
  return createHash("sha256").update(value).digest("hex");
49
51
  }
@@ -448,24 +450,39 @@ function stableArtifactAliasKey(teamId, artifact) {
448
450
  const fileName = typeof artifact.metadata?.fileName === "string" && artifact.metadata.fileName ? artifact.metadata.fileName : `${artifact.itemId}${extname(artifact.content.objectKey) || ".bin"}`;
449
451
  return `teams/${teamId}/published/artifacts/${artifact.kind}/${artifact.itemId}/${fileName}`;
450
452
  }
451
- async function probeHttp(url) {
452
- try {
453
- const response = await fetch(url, {
454
- headers: { accept: "application/json,text/html;q=0.9,*/*;q=0.8" }
455
- });
456
- return {
457
- ok: response.ok,
458
- status: response.status,
459
- url
460
- };
461
- } catch (error) {
462
- return {
463
- ok: false,
464
- status: null,
465
- url,
466
- error: error instanceof Error ? error.message : String(error)
467
- };
453
+ async function probeHttp(url, { attempts = 1, delayMs = 2e3 } = {}) {
454
+ let result = {
455
+ ok: false,
456
+ status: null,
457
+ url
458
+ };
459
+ const maxAttempts = Math.max(1, Math.floor(attempts));
460
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
461
+ try {
462
+ const response = await fetch(url, {
463
+ headers: { accept: "application/json,text/html;q=0.9,*/*;q=0.8" }
464
+ });
465
+ result = {
466
+ ok: response.ok,
467
+ status: response.status,
468
+ url,
469
+ attempts: attempt
470
+ };
471
+ } catch (error) {
472
+ result = {
473
+ ok: false,
474
+ status: null,
475
+ url,
476
+ error: error instanceof Error ? error.message : String(error),
477
+ attempts: attempt
478
+ };
479
+ }
480
+ if (result.ok || attempt >= maxAttempts) {
481
+ return result;
482
+ }
483
+ await sleep(delayMs);
468
484
  }
485
+ return result;
469
486
  }
470
487
  function queueClientConfig(siteConfig, state) {
471
488
  const accountId = String(process.env.CLOUDFLARE_ACCOUNT_ID ?? siteConfig.cloudflare.accountId ?? "").trim();
@@ -1219,29 +1236,29 @@ async function monitorProjectPlatform(options) {
1219
1236
  const state = loadDeployState(options.tenantRoot, siteConfig, { target });
1220
1237
  const webProbeUrl = resolveImmediatePagesProbeUrl(siteConfig, state, target);
1221
1238
  const apiBaseUrl = resolveImmediateApiProbeUrl(siteConfig, state, target);
1239
+ 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, {
1240
+ env,
1241
+ settleDeployments: true,
1242
+ onProgress: options.write
1243
+ });
1222
1244
  const skippedApiCheck = apiSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "api_not_selected" };
1223
1245
  const skippedAgentCheck = agentsSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "agents_not_selected" };
1224
1246
  const checks = {
1225
- pages: await probeHttp(webProbeUrl),
1226
- apiHealth: apiSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/healthz`) : skippedApiCheck,
1227
- apiReady: apiSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/readyz`) : skippedApiCheck,
1228
- d1Health: apiSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/healthz/deep`) : skippedApiCheck,
1229
- agentHealth: agentsSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/internal/core/agent/healthz`) : skippedAgentCheck,
1247
+ pages: await probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 }),
1248
+ apiHealth: apiSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/healthz`, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
1249
+ apiReady: apiSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/readyz`, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
1250
+ d1Health: apiSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/healthz/deep`, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
1251
+ agentHealth: agentsSelected && apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/internal/core/agent/healthz`, { attempts: 8, delayMs: 1e4 }) : skippedAgentCheck,
1230
1252
  r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : probeR2(options.tenantRoot, siteConfig, state, target),
1231
1253
  queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : probeQueue(siteConfig, state),
1232
1254
  scaleProbe: probeScaleConfiguration(siteConfig, state),
1233
- railwayResources: options.scope === "local" || !apiSelected && !agentsSelected ? Promise.resolve({ ok: true, skipped: true, reason: options.scope === "local" ? "local_scope" : "railway_not_selected" }) : verifyRailwayManagedResources(options.tenantRoot, options.scope, {
1234
- env,
1235
- settleDeployments: true,
1236
- onProgress: options.write
1237
- }),
1255
+ railwayResources,
1238
1256
  readiness: state.readiness
1239
1257
  };
1240
1258
  const resolvedChecks = {
1241
1259
  ...checks,
1242
1260
  r2: await checks.r2,
1243
- queue: await checks.queue,
1244
- railwayResources: await checks.railwayResources
1261
+ queue: await checks.queue
1245
1262
  };
1246
1263
  const ok = [
1247
1264
  resolvedChecks.pages,
@@ -1299,14 +1316,26 @@ async function syncControlPlaneState(options) {
1299
1316
  });
1300
1317
  }
1301
1318
  async function runProjectPlatformAction(action, options) {
1319
+ const previousWorkflowAction = process.env.TREESEED_WORKFLOW_ACTION;
1320
+ const previousWorkflowPlane = process.env.TREESEED_WORKFLOW_PLANE;
1321
+ process.env.TREESEED_WORKFLOW_ACTION = action;
1322
+ process.env.TREESEED_WORKFLOW_PLANE = previousWorkflowPlane ?? (action === "deploy_processing" ? "processing" : "web");
1302
1323
  applyTreeseedEnvironmentToProcess({ tenantRoot: options.tenantRoot, scope: options.scope, override: true });
1303
1324
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
1304
1325
  try {
1305
1326
  switch (action) {
1306
- case "provision":
1307
- return await provisionProjectPlatform({ ...options, reporter });
1308
- case "deploy_code":
1309
- return await deployProjectPlatform({ ...options, reporter });
1327
+ case "deploy_web":
1328
+ return await deployProjectPlatform({
1329
+ ...options,
1330
+ reporter,
1331
+ bootstrapSystems: options.bootstrapSystems ?? WEB_PLATFORM_BOOTSTRAP_SYSTEMS
1332
+ });
1333
+ case "deploy_processing":
1334
+ return await deployProjectPlatform({
1335
+ ...options,
1336
+ reporter,
1337
+ bootstrapSystems: options.bootstrapSystems ?? PROCESSING_PLATFORM_BOOTSTRAP_SYSTEMS
1338
+ });
1310
1339
  case "publish_content":
1311
1340
  return await publishProjectContent({ ...options, reporter });
1312
1341
  case "monitor":
@@ -1317,7 +1346,7 @@ async function runProjectPlatformAction(action, options) {
1317
1346
  } catch (error) {
1318
1347
  await reportDeployment(reporter, {
1319
1348
  environment: options.scope,
1320
- deploymentKind: action === "provision" ? "provision" : action === "publish_content" ? "content" : action === "deploy_code" ? "code" : "mixed",
1349
+ deploymentKind: action === "publish_content" ? "content" : action === "deploy_web" || action === "deploy_processing" ? "code" : "mixed",
1321
1350
  status: "failed",
1322
1351
  sourceRef: currentRef(options.tenantRoot),
1323
1352
  commitSha: currentCommit(options.tenantRoot),
@@ -1328,6 +1357,17 @@ async function runProjectPlatformAction(action, options) {
1328
1357
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1329
1358
  }).catch(() => void 0);
1330
1359
  throw error;
1360
+ } finally {
1361
+ if (previousWorkflowAction === void 0) {
1362
+ delete process.env.TREESEED_WORKFLOW_ACTION;
1363
+ } else {
1364
+ process.env.TREESEED_WORKFLOW_ACTION = previousWorkflowAction;
1365
+ }
1366
+ if (previousWorkflowPlane === void 0) {
1367
+ delete process.env.TREESEED_WORKFLOW_PLANE;
1368
+ } else {
1369
+ process.env.TREESEED_WORKFLOW_PLANE = previousWorkflowPlane;
1370
+ }
1331
1371
  }
1332
1372
  }
1333
1373
  export {
@@ -8,6 +8,9 @@ export declare function resolveRailwayAuthToken(env?: NodeJS.ProcessEnv): string
8
8
  export declare function buildRailwayCommandEnv(env?: NodeJS.ProcessEnv): {
9
9
  [key: string]: string | undefined;
10
10
  };
11
+ export declare function buildRailwayDeployCommandEnv(env?: NodeJS.ProcessEnv): {
12
+ [key: string]: string | undefined;
13
+ };
11
14
  export declare function isRailwayTransientFailure(result: any): boolean;
12
15
  export declare function runRailway(args: any, { cwd, capture, allowFailure, input, env }?: {
13
16
  capture?: boolean | undefined;
@@ -329,11 +332,31 @@ export declare function verifyRailwayManagedResources(tenantRoot: any, scope: an
329
332
  ok: boolean;
330
333
  checks: any[];
331
334
  }>;
332
- export declare function planRailwayServiceDeploy(service: any, { env }?: {
335
+ export declare function shouldRunRailwayPredeployBuild(env?: NodeJS.ProcessEnv): boolean;
336
+ export declare function planRailwayServiceDeploy(service: any, { env, projectTokenMode }?: {
337
+ env?: NodeJS.ProcessEnv | undefined;
338
+ projectTokenMode?: boolean | undefined;
339
+ }): {
340
+ command: string;
341
+ args: string[];
342
+ cwd: any;
343
+ };
344
+ export declare function buildRailwayLinkCommandEnv(env?: NodeJS.ProcessEnv, service?: {}): any;
345
+ export declare function writeRailwayCliProjectConfig(service: any, { env, cwd }?: {
346
+ env?: NodeJS.ProcessEnv | undefined;
347
+ cwd?: any;
348
+ }): {
349
+ configPath: string;
350
+ projectPath: string;
351
+ projectId: string;
352
+ environmentId: string;
353
+ serviceId: string;
354
+ } | null;
355
+ export declare function planRailwayServiceLink(service: any, { env }?: {
333
356
  env?: NodeJS.ProcessEnv | undefined;
334
357
  }): {
335
358
  command: string;
336
- args: any[];
359
+ args: string[];
337
360
  cwd: any;
338
361
  };
339
362
  export declare function deployRailwayService(tenantRoot: any, service: any, { dryRun, write, prefix, env, }?: {
@@ -1,5 +1,5 @@
1
- import { existsSync } from "node:fs";
2
- import { relative, resolve } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { loadCliDeployConfig } from "./runtime-tools.js";
5
5
  import { createPersistentDeployTarget, resolveTreeseedResourceIdentity } from "./deploy.js";
@@ -221,10 +221,19 @@ function resolveRailwayAuthToken(env = process.env) {
221
221
  function buildRailwayCommandEnv(env = process.env) {
222
222
  const merged = { ...env };
223
223
  const token = resolveRailwayAuthToken(merged);
224
+ const projectToken = configuredEnvValue(merged, "RAILWAY_TOKEN");
224
225
  if (token) {
225
226
  merged.RAILWAY_API_TOKEN = token;
226
227
  } else {
227
- delete merged.RAILWAY_API_TOKEN;
228
+ merged.RAILWAY_API_TOKEN = void 0;
229
+ }
230
+ merged.RAILWAY_TOKEN = projectToken || void 0;
231
+ return merged;
232
+ }
233
+ function buildRailwayDeployCommandEnv(env = process.env) {
234
+ const merged = buildRailwayCommandEnv(env);
235
+ if (shouldAttachRailwayDeployLogs(merged)) {
236
+ merged.CI = "true";
228
237
  }
229
238
  return merged;
230
239
  }
@@ -306,7 +315,11 @@ function isRailwayAlreadyExistsMessage(result) {
306
315
  return /already exists|already taken|duplicate|has already been taken/iu.test(railwayMessage(result));
307
316
  }
308
317
  function isRailwayTransientFailure(result) {
309
- return /timed out|failed to fetch|temporarily unavailable|econnreset|etimedout|failed to stream build logs|failed to retrieve build log/iu.test(railwayMessage(result));
318
+ const message = railwayMessage(result);
319
+ if (!message.trim() && result?.status === 1) {
320
+ return true;
321
+ }
322
+ return /timed out|failed to fetch|temporarily unavailable|econnreset|etimedout|failed to stream build logs|failed to retrieve build log/iu.test(message);
310
323
  }
311
324
  function sleepSync(milliseconds) {
312
325
  if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
@@ -1165,28 +1178,249 @@ async function verifyRailwayManagedResources(tenantRoot, scope, {
1165
1178
  };
1166
1179
  }
1167
1180
  function shouldAttachRailwayDeployLogs(env = process.env) {
1168
- return configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_ATTACH_LOGS") === "1";
1169
- }
1170
- function planRailwayServiceDeploy(service, { env = process.env } = {}) {
1171
- const args = [
1172
- "up",
1173
- "--service",
1174
- service.serviceName ?? service.serviceId,
1175
- shouldAttachRailwayDeployLogs(env) ? "--ci" : "--detach"
1176
- ];
1181
+ const configured = configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_ATTACH_LOGS");
1182
+ if (configured === "1" || configured === "true") {
1183
+ return true;
1184
+ }
1185
+ if (configured === "0" || configured === "false") {
1186
+ return false;
1187
+ }
1188
+ return false;
1189
+ }
1190
+ function shouldUseVerboseRailwayDeploy(env = process.env) {
1191
+ const configured = configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_VERBOSE");
1192
+ if (configured === "1" || configured === "true") {
1193
+ return true;
1194
+ }
1195
+ if (configured === "0" || configured === "false") {
1196
+ return false;
1197
+ }
1198
+ return shouldAttachRailwayDeployLogs(env);
1199
+ }
1200
+ function shouldIncludeRailwayIgnoredFiles(env = process.env) {
1201
+ const configured = configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_INCLUDE_IGNORED");
1202
+ return configured === "1" || configured === "true";
1203
+ }
1204
+ function shouldRunRailwayPredeployBuild(env = process.env) {
1205
+ const configured = configuredEnvValue(env, "TREESEED_RAILWAY_PREDEPLOY_BUILD");
1206
+ if (configured === "1" || configured === "true") {
1207
+ return true;
1208
+ }
1209
+ if (configured === "0" || configured === "false") {
1210
+ return false;
1211
+ }
1212
+ return configuredEnvValue(env, "CI") !== "true";
1213
+ }
1214
+ function planRailwayServiceDeploy(service, { env = process.env, projectTokenMode = false } = {}) {
1215
+ const serviceSelector = service.serviceName ?? service.serviceId;
1216
+ const args = ["up"];
1217
+ if (serviceSelector) {
1218
+ args.push("--service", serviceSelector);
1219
+ }
1220
+ if (shouldIncludeRailwayIgnoredFiles(env)) {
1221
+ args.push("--no-gitignore");
1222
+ }
1223
+ args.push(shouldAttachRailwayDeployLogs(env) ? "--ci" : "--detach");
1224
+ if (shouldUseVerboseRailwayDeploy(env)) {
1225
+ args.push("--verbose");
1226
+ }
1227
+ if (!projectTokenMode && service.projectId) {
1228
+ args.push("--project", service.projectId);
1229
+ }
1230
+ const environmentName = normalizeRailwayEnvironmentName(service.railwayEnvironment);
1231
+ if (!projectTokenMode && environmentName) {
1232
+ args.push("--environment", environmentName);
1233
+ }
1234
+ return {
1235
+ command: "railway",
1236
+ args,
1237
+ cwd: service.rootDir
1238
+ };
1239
+ }
1240
+ function planRailwayProjectEnvironmentLink(service) {
1241
+ const args = ["link"];
1242
+ if (service.projectId) {
1243
+ args.push("--project", service.projectId);
1244
+ }
1245
+ const environmentName = normalizeRailwayEnvironmentName(service.railwayEnvironment);
1246
+ if (environmentName) {
1247
+ args.push("--environment", environmentName);
1248
+ }
1249
+ args.push("--json");
1250
+ return {
1251
+ command: "railway",
1252
+ args,
1253
+ cwd: service.rootDir
1254
+ };
1255
+ }
1256
+ function buildRailwayCliContextEnv(env, service) {
1257
+ return {
1258
+ ...env,
1259
+ RAILWAY_PROJECT_ID: configuredEnvValue(service, "projectId") || configuredEnvValue(env, "RAILWAY_PROJECT_ID") || void 0,
1260
+ RAILWAY_ENVIRONMENT_ID: configuredEnvValue(service, "environmentId") || configuredEnvValue(env, "RAILWAY_ENVIRONMENT_ID") || void 0,
1261
+ RAILWAY_SERVICE_ID: configuredEnvValue(service, "serviceId") || configuredEnvValue(env, "RAILWAY_SERVICE_ID") || void 0
1262
+ };
1263
+ }
1264
+ function buildRailwayLinkCommandEnv(env = process.env, service = {}) {
1265
+ return buildRailwayCliContextEnv({
1266
+ ...buildRailwayDeployCommandEnv({ ...env, RAILWAY_TOKEN: void 0 }),
1267
+ RAILWAY_TOKEN: void 0
1268
+ }, service);
1269
+ }
1270
+ function railwayCliConfigPath(env = process.env) {
1271
+ const railwayHome = configuredEnvValue(env, "RAILWAY_HOME");
1272
+ if (railwayHome) {
1273
+ return resolve(railwayHome, "config.json");
1274
+ }
1275
+ const home = configuredEnvValue(env, "HOME");
1276
+ if (!home) {
1277
+ return "";
1278
+ }
1279
+ return resolve(home, ".railway", "config.json");
1280
+ }
1281
+ function readRailwayCliConfig(configPath) {
1282
+ if (!configPath || !existsSync(configPath)) {
1283
+ return {};
1284
+ }
1285
+ try {
1286
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
1287
+ return parsed && typeof parsed === "object" ? parsed : {};
1288
+ } catch {
1289
+ return {};
1290
+ }
1291
+ }
1292
+ function writeRailwayCliProjectConfig(service, { env = process.env, cwd = service.rootDir } = {}) {
1293
+ const projectId = configuredEnvValue(service, "projectId");
1294
+ const environmentId = configuredEnvValue(service, "environmentId");
1295
+ const serviceId = configuredEnvValue(service, "serviceId");
1296
+ const projectPath = cwd ? resolve(cwd) : "";
1297
+ const configPath = railwayCliConfigPath(env);
1298
+ if (!configPath || !projectPath || !projectId || !environmentId || !serviceId) {
1299
+ return null;
1300
+ }
1301
+ const existing = readRailwayCliConfig(configPath);
1302
+ const projects = existing.projects && typeof existing.projects === "object" ? existing.projects : {};
1303
+ const next = {
1304
+ ...existing,
1305
+ projects: {
1306
+ ...projects,
1307
+ [projectPath]: {
1308
+ projectPath,
1309
+ name: configuredEnvValue(service, "projectName") || configuredEnvValue(service, "serviceName") || projectId,
1310
+ project: projectId,
1311
+ environment: environmentId,
1312
+ environmentName: normalizeRailwayEnvironmentName(service.railwayEnvironment) || configuredEnvValue(service, "environmentName") || environmentId,
1313
+ service: serviceId
1314
+ }
1315
+ },
1316
+ user: existing.user && typeof existing.user === "object" ? existing.user : {
1317
+ token: null,
1318
+ accessToken: null,
1319
+ refreshToken: null,
1320
+ tokenExpiresAt: null
1321
+ },
1322
+ linkedFunctions: existing.linkedFunctions ?? null
1323
+ };
1324
+ mkdirSync(dirname(configPath), { recursive: true });
1325
+ writeFileSync(configPath, `${JSON.stringify(next, null, 2)}
1326
+ `, "utf8");
1327
+ return {
1328
+ configPath,
1329
+ projectPath,
1330
+ projectId,
1331
+ environmentId,
1332
+ serviceId
1333
+ };
1334
+ }
1335
+ function planRailwayServiceLink(service, { env = process.env } = {}) {
1336
+ const args = ["link"];
1177
1337
  if (service.projectId) {
1178
1338
  args.push("--project", service.projectId);
1179
1339
  }
1340
+ const workspace = resolveRailwayWorkspace(env);
1341
+ if (workspace) {
1342
+ args.push("--workspace", workspace);
1343
+ }
1180
1344
  const environmentName = normalizeRailwayEnvironmentName(service.railwayEnvironment);
1181
1345
  if (environmentName) {
1182
1346
  args.push("--environment", environmentName);
1183
1347
  }
1348
+ const serviceSelector = service.serviceName ?? service.serviceId;
1349
+ if (serviceSelector) {
1350
+ args.push("--service", serviceSelector);
1351
+ }
1352
+ args.push("--json");
1184
1353
  return {
1185
1354
  command: "railway",
1186
1355
  args,
1187
1356
  cwd: service.rootDir
1188
1357
  };
1189
1358
  }
1359
+ function railwayProjectTokenName(service) {
1360
+ const environment = normalizeRailwayEnvironmentName(service.railwayEnvironment) || "environment";
1361
+ const serviceName = String(service.serviceName ?? service.serviceId ?? service.key ?? "service").toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || "service";
1362
+ return `treeseed-ci-${environment}-${serviceName}`;
1363
+ }
1364
+ async function createRailwayCliProjectToken(service, { env = process.env } = {}) {
1365
+ const projectId = configuredEnvValue(service, "projectId");
1366
+ const environmentId = configuredEnvValue(service, "environmentId");
1367
+ if (!projectId || !environmentId) {
1368
+ return "";
1369
+ }
1370
+ const name = railwayProjectTokenName(service);
1371
+ try {
1372
+ const existingPayload = await railwayGraphqlRequest({
1373
+ query: `
1374
+ query TreeseedProjectTokens($projectId: String!) {
1375
+ projectTokens(projectId: $projectId, first: 100) {
1376
+ edges {
1377
+ node {
1378
+ id
1379
+ name
1380
+ projectId
1381
+ environmentId
1382
+ }
1383
+ }
1384
+ }
1385
+ }
1386
+ `.trim(),
1387
+ variables: { projectId },
1388
+ env
1389
+ });
1390
+ const existingTokens = railwayEdgeNodes(existingPayload.data?.projectTokens);
1391
+ for (const token2 of existingTokens) {
1392
+ if (token2?.name === name && token2?.projectId === projectId && token2?.environmentId === environmentId && token2?.id) {
1393
+ await railwayGraphqlRequest({
1394
+ query: `
1395
+ mutation TreeseedProjectTokenDelete($id: String!) {
1396
+ projectTokenDelete(id: $id)
1397
+ }
1398
+ `.trim(),
1399
+ variables: { id: token2.id },
1400
+ env
1401
+ });
1402
+ }
1403
+ }
1404
+ } catch {
1405
+ }
1406
+ const payload = await railwayGraphqlRequest({
1407
+ query: `
1408
+ mutation TreeseedProjectTokenCreate($input: ProjectTokenCreateInput!) {
1409
+ projectTokenCreate(input: $input)
1410
+ }
1411
+ `.trim(),
1412
+ variables: {
1413
+ input: {
1414
+ projectId,
1415
+ environmentId,
1416
+ name
1417
+ }
1418
+ },
1419
+ env
1420
+ });
1421
+ const token = payload.data?.projectTokenCreate;
1422
+ return typeof token === "string" && token.trim() ? token.trim() : "";
1423
+ }
1190
1424
  async function resolveRailwayDeployProjectContext(service, { env = process.env } = {}) {
1191
1425
  if (service.projectId) {
1192
1426
  return service;
@@ -1302,6 +1536,12 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
1302
1536
  });
1303
1537
  }
1304
1538
  return {
1539
+ projectId: project.id,
1540
+ projectName: project.name ?? service.projectName ?? null,
1541
+ environmentId: environment.id,
1542
+ environmentName: environment.name ?? environmentName,
1543
+ serviceId: railwayService.id,
1544
+ serviceName: railwayService.name ?? service.serviceName ?? null,
1305
1545
  instance: runtimeConfiguration?.instance ?? null,
1306
1546
  updated: Boolean(runtimeConfiguration?.updated || volumeConfiguration?.updated || volumeConfiguration?.created),
1307
1547
  volume: volumeConfiguration ? {
@@ -1408,8 +1648,12 @@ async function deployRailwayService(tenantRoot, service, {
1408
1648
  };
1409
1649
  }
1410
1650
  const deployService = await resolveRailwayDeployProjectContext(service, { env });
1411
- const plan = planRailwayServiceDeploy(deployService, { env });
1412
1651
  const commandEnv = buildRailwayCommandEnv({ ...process.env, ...env });
1652
+ let railwayDeployEnv = buildRailwayDeployCommandEnv(commandEnv);
1653
+ const railway = resolveTreeseedToolCommand("railway", { env: commandEnv });
1654
+ if (!railway) {
1655
+ throw new Error("Railway CLI is unavailable.");
1656
+ }
1413
1657
  const taskPrefix = prefix ?? {
1414
1658
  scope: normalizeScope(deployService.scope ?? deployService.railwayEnvironment ?? "railway"),
1415
1659
  system: deployService.key === "api" ? "api" : "agents",
@@ -1419,7 +1663,33 @@ async function deployRailwayService(tenantRoot, service, {
1419
1663
  const runtimeConfiguration = await syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, deployService, {
1420
1664
  env: commandEnv
1421
1665
  });
1422
- if (deployService.buildCommand) {
1666
+ const cliDeployService = {
1667
+ ...deployService,
1668
+ projectId: runtimeConfiguration?.projectId ?? deployService.projectId,
1669
+ projectName: runtimeConfiguration?.projectName ?? deployService.projectName,
1670
+ environmentId: runtimeConfiguration?.environmentId ?? deployService.environmentId,
1671
+ serviceId: runtimeConfiguration?.serviceId ?? deployService.serviceId,
1672
+ serviceName: runtimeConfiguration?.serviceName ?? deployService.serviceName,
1673
+ railwayEnvironment: runtimeConfiguration?.environmentName ?? runtimeConfiguration?.environmentId ?? deployService.railwayEnvironment
1674
+ };
1675
+ railwayDeployEnv = buildRailwayCliContextEnv(railwayDeployEnv, cliDeployService);
1676
+ const hasCommandApiToken = Boolean(configuredEnvValue(commandEnv, "RAILWAY_API_TOKEN"));
1677
+ let usesProjectToken = Boolean(configuredEnvValue(railwayDeployEnv, "RAILWAY_TOKEN"));
1678
+ if (usesProjectToken) {
1679
+ railwayDeployEnv = { ...railwayDeployEnv, RAILWAY_API_TOKEN: void 0 };
1680
+ }
1681
+ if (!usesProjectToken && !hasCommandApiToken) {
1682
+ const projectToken = await createRailwayCliProjectToken(cliDeployService, { env: commandEnv });
1683
+ if (projectToken) {
1684
+ railwayDeployEnv = buildRailwayCliContextEnv({ ...railwayDeployEnv, RAILWAY_API_TOKEN: void 0, RAILWAY_TOKEN: projectToken }, cliDeployService);
1685
+ usesProjectToken = true;
1686
+ } else if (configuredEnvValue(commandEnv, "CI") === "true") {
1687
+ throw new Error(`Railway CI deploy requires a project token for ${cliDeployService.serviceName ?? cliDeployService.key}. Automatic project token creation did not return a token.`);
1688
+ }
1689
+ }
1690
+ const linkPlan = planRailwayServiceLink(cliDeployService, { env: commandEnv });
1691
+ const plan = planRailwayServiceDeploy(cliDeployService, { env, projectTokenMode: usesProjectToken });
1692
+ if (deployService.buildCommand && shouldRunRailwayPredeployBuild(commandEnv)) {
1423
1693
  const buildResult = await runPrefixedCommand("bash", ["-lc", deployService.buildCommand], {
1424
1694
  cwd: deployService.rootDir,
1425
1695
  env: commandEnv,
@@ -1430,11 +1700,28 @@ async function deployRailwayService(tenantRoot, service, {
1430
1700
  throw new Error(`Railway ${deployService.key} build command failed.`);
1431
1701
  }
1432
1702
  }
1703
+ const hasRailwayApiToken = Boolean(configuredEnvValue(commandEnv, "RAILWAY_API_TOKEN"));
1704
+ const cliConfig = configuredEnvValue(commandEnv, "CI") === "true" ? writeRailwayCliProjectConfig(cliDeployService, { env: railwayDeployEnv, cwd: plan.cwd }) : null;
1705
+ const effectiveLinkPlan = hasRailwayApiToken ? linkPlan : usesProjectToken ? planRailwayProjectEnvironmentLink(cliDeployService) : linkPlan;
1706
+ const railwayLinkEnv = hasRailwayApiToken ? buildRailwayLinkCommandEnv(commandEnv, cliDeployService) : railwayDeployEnv;
1707
+ if (cliConfig) {
1708
+ write ? write(`[${taskPrefix.scope}][${taskPrefix.system}][${taskPrefix.task}][link] Wrote Railway CLI project context for ${cliConfig.projectPath}.`, "stdout") : null;
1709
+ } else {
1710
+ const linkResult = await runPrefixedCommand(railway.command, [...railway.argsPrefix, ...effectiveLinkPlan.args], {
1711
+ cwd: effectiveLinkPlan.cwd,
1712
+ env: railwayLinkEnv,
1713
+ write,
1714
+ prefix: { ...taskPrefix, stage: "link" }
1715
+ });
1716
+ if (linkResult.status !== 0) {
1717
+ throw new Error(linkResult.stderr?.trim() || linkResult.stdout?.trim() || `railway ${effectiveLinkPlan.args.join(" ")} failed with exit code ${linkResult.status ?? "unknown"} in ${effectiveLinkPlan.cwd}`);
1718
+ }
1719
+ }
1433
1720
  let lastFailure = null;
1434
1721
  for (let attempt = 1; attempt <= 5; attempt += 1) {
1435
- const result = await runPrefixedCommand(plan.command, plan.args, {
1436
- cwd: service.rootDir,
1437
- env: commandEnv,
1722
+ const result = await runPrefixedCommand(railway.command, [...railway.argsPrefix, ...plan.args], {
1723
+ cwd: plan.cwd,
1724
+ env: railwayDeployEnv,
1438
1725
  write,
1439
1726
  prefix: taskPrefix
1440
1727
  });
@@ -1444,7 +1731,7 @@ async function deployRailwayService(tenantRoot, service, {
1444
1731
  }
1445
1732
  lastFailure = result;
1446
1733
  if (!isRailwayTransientFailure(result) || attempt === 5) {
1447
- throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${plan.args.join(" ")} failed`);
1734
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${plan.args.join(" ")} failed with exit code ${result.status ?? "unknown"} in ${plan.cwd}`);
1448
1735
  }
1449
1736
  const backoffMs = 5e3 * attempt;
1450
1737
  const warning = `Railway deploy for ${deployService.serviceName ?? deployService.serviceId ?? deployService.key} hit a transient failure; retrying in ${Math.round(backoffMs / 1e3)}s...`;
@@ -1471,6 +1758,8 @@ async function deployRailwayService(tenantRoot, service, {
1471
1758
  }
1472
1759
  export {
1473
1760
  buildRailwayCommandEnv,
1761
+ buildRailwayDeployCommandEnv,
1762
+ buildRailwayLinkCommandEnv,
1474
1763
  collectRailwayDeploymentStatusChecks,
1475
1764
  configuredRailwayScheduledJobs,
1476
1765
  configuredRailwayServices,
@@ -1485,14 +1774,17 @@ export {
1485
1774
  isRailwayTransientFailure,
1486
1775
  isUsableRailwayToken,
1487
1776
  planRailwayServiceDeploy,
1777
+ planRailwayServiceLink,
1488
1778
  railwayServiceRuntimeStartCommand,
1489
1779
  resolveRailwayAuthToken,
1490
1780
  resolveRailwayDeploymentProfile,
1491
1781
  runRailway,
1492
1782
  setRailwaySecretVariable,
1783
+ shouldRunRailwayPredeployBuild,
1493
1784
  validateRailwayDeployPrerequisites,
1494
1785
  validateRailwayServiceConfiguration,
1495
1786
  verifyRailwayManagedResources,
1496
1787
  verifyRailwayScheduledJobs,
1497
- waitForRailwayManagedDeploymentsSettled
1788
+ waitForRailwayManagedDeploymentsSettled,
1789
+ writeRailwayCliProjectConfig
1498
1790
  };
@@ -85,6 +85,7 @@ export type RepositorySaveResult = {
85
85
  rootRepo: RepositorySaveReport;
86
86
  waves: string[][];
87
87
  plannedVersions: Record<string, string>;
88
+ workflowGates?: Array<Record<string, unknown>>;
88
89
  };
89
90
  export type RepositorySavePlanRepo = {
90
91
  id: string;
@@ -150,6 +151,13 @@ export type RepositorySaveOptions = {
150
151
  includeRoot?: boolean;
151
152
  stablePackageRelease?: boolean;
152
153
  onProgress?: (message: string, stream?: 'stdout' | 'stderr') => void;
154
+ onWaveSaved?: (wave: {
155
+ index: number;
156
+ nodes: RepositorySaveNode[];
157
+ reports: RepositorySaveReport[];
158
+ allReports: RepositorySaveReport[];
159
+ rootRepo: RepositorySaveReport | null;
160
+ }) => Promise<Array<Record<string, unknown>> | void> | Array<Record<string, unknown>> | void;
153
161
  };
154
162
  export declare function runStreamingCommand(node: Pick<RepositorySaveNode, 'name' | 'path'>, options: Pick<RepositorySaveOptions, 'onProgress'>, phase: string, command: string, args: string[], commandOptions?: {
155
163
  cwd?: string;