@treeseed/sdk 0.10.28 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +207 -6
  2. package/dist/capacity-provider.d.ts +3 -1
  3. package/dist/capacity-provider.js +25 -5
  4. package/dist/control-plane.d.ts +1 -0
  5. package/dist/control-plane.js +38 -13
  6. package/dist/db/market-schema.d.ts +8860 -6172
  7. package/dist/db/market-schema.js +108 -0
  8. package/dist/db/node-sqlite.js +7 -2
  9. package/dist/hosting/apps.d.ts +12 -0
  10. package/dist/hosting/apps.js +107 -0
  11. package/dist/hosting/builtins.d.ts +25 -0
  12. package/dist/hosting/builtins.js +791 -0
  13. package/dist/hosting/contracts.d.ts +207 -0
  14. package/dist/hosting/contracts.js +0 -0
  15. package/dist/hosting/graph.d.ts +192 -0
  16. package/dist/hosting/graph.js +1106 -0
  17. package/dist/hosting/index.d.ts +4 -0
  18. package/dist/hosting/index.js +4 -0
  19. package/dist/index.d.ts +10 -3
  20. package/dist/index.js +63 -6
  21. package/dist/managed-dependencies.js +1 -2
  22. package/dist/market-client.d.ts +63 -3
  23. package/dist/market-client.js +83 -11
  24. package/dist/operations/services/bootstrap-runner.d.ts +3 -1
  25. package/dist/operations/services/bootstrap-runner.js +22 -2
  26. package/dist/operations/services/config-runtime.d.ts +10 -5
  27. package/dist/operations/services/config-runtime.js +209 -66
  28. package/dist/operations/services/deploy.d.ts +70 -7
  29. package/dist/operations/services/deploy.js +579 -64
  30. package/dist/operations/services/deployment-readiness.d.ts +30 -0
  31. package/dist/operations/services/deployment-readiness.js +175 -0
  32. package/dist/operations/services/git-workflow.d.ts +2 -1
  33. package/dist/operations/services/git-workflow.js +9 -3
  34. package/dist/operations/services/github-actions-verification.d.ts +1 -0
  35. package/dist/operations/services/github-actions-verification.js +1 -0
  36. package/dist/operations/services/github-api.js +1 -1
  37. package/dist/operations/services/github-automation.d.ts +1 -1
  38. package/dist/operations/services/github-automation.js +4 -3
  39. package/dist/operations/services/github-credentials.d.ts +13 -0
  40. package/dist/operations/services/github-credentials.js +58 -0
  41. package/dist/operations/services/hosted-service-checks.d.ts +63 -0
  42. package/dist/operations/services/hosted-service-checks.js +327 -0
  43. package/dist/operations/services/hub-provider-launch.js +3 -3
  44. package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
  45. package/dist/operations/services/live-hosted-service-checks.js +350 -0
  46. package/dist/operations/services/managed-host-security.js +1 -1
  47. package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
  48. package/dist/operations/services/operations-runner-smoke.js +180 -0
  49. package/dist/operations/services/package-adapters.d.ts +95 -0
  50. package/dist/operations/services/package-adapters.js +288 -0
  51. package/dist/operations/services/package-reference-policy.d.ts +1 -0
  52. package/dist/operations/services/package-reference-policy.js +15 -2
  53. package/dist/operations/services/project-platform.d.ts +80 -22
  54. package/dist/operations/services/project-platform.js +49 -8
  55. package/dist/operations/services/project-web-monitor.js +26 -4
  56. package/dist/operations/services/railway-api.d.ts +88 -5
  57. package/dist/operations/services/railway-api.js +626 -35
  58. package/dist/operations/services/railway-deploy.d.ts +46 -40
  59. package/dist/operations/services/railway-deploy.js +261 -293
  60. package/dist/operations/services/release-candidate.d.ts +19 -0
  61. package/dist/operations/services/release-candidate.js +375 -38
  62. package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
  63. package/dist/operations/services/repository-save-orchestrator.js +279 -66
  64. package/dist/operations/services/runtime-tools.d.ts +1 -0
  65. package/dist/operations/services/runtime-tools.js +10 -9
  66. package/dist/operations/services/verification-cache.d.ts +25 -0
  67. package/dist/operations/services/verification-cache.js +71 -0
  68. package/dist/operations/services/workspace-dependency-mode.js +9 -1
  69. package/dist/operations/services/workspace-save.js +1 -1
  70. package/dist/operations/services/workspace-tools.js +2 -1
  71. package/dist/platform/contracts.d.ts +32 -1
  72. package/dist/platform/deploy-config.js +73 -8
  73. package/dist/platform/env.yaml +163 -35
  74. package/dist/platform/environment.d.ts +1 -0
  75. package/dist/platform/environment.js +74 -5
  76. package/dist/platform/plugin.d.ts +9 -0
  77. package/dist/platform-operation-store.js +2 -2
  78. package/dist/platform-operations.js +1 -1
  79. package/dist/reconcile/bootstrap-systems.js +2 -2
  80. package/dist/reconcile/builtin-adapters.js +372 -189
  81. package/dist/reconcile/contracts.d.ts +9 -5
  82. package/dist/reconcile/desired-state.d.ts +1 -0
  83. package/dist/reconcile/desired-state.js +5 -5
  84. package/dist/reconcile/engine.d.ts +5 -2
  85. package/dist/reconcile/engine.js +53 -32
  86. package/dist/reconcile/index.d.ts +2 -0
  87. package/dist/reconcile/index.js +2 -0
  88. package/dist/reconcile/live-acceptance.d.ts +79 -0
  89. package/dist/reconcile/live-acceptance.js +1615 -0
  90. package/dist/reconcile/platform.d.ts +104 -0
  91. package/dist/reconcile/platform.js +100 -0
  92. package/dist/reconcile/state.js +4 -4
  93. package/dist/reconcile/units.js +2 -2
  94. package/dist/scripts/deployment-readiness.js +20 -0
  95. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  96. package/dist/scripts/operations-runner-smoke.js +16 -0
  97. package/dist/scripts/release-verify.js +4 -1
  98. package/dist/scripts/tenant-workflow-action.js +10 -1
  99. package/dist/sdk-types.d.ts +169 -4
  100. package/dist/sdk-types.js +20 -2
  101. package/dist/sdk.d.ts +35 -24
  102. package/dist/sdk.js +186 -17
  103. package/dist/template-launch-requirements.js +9 -0
  104. package/dist/treedx/adapters.d.ts +6 -0
  105. package/dist/treedx/adapters.js +36 -0
  106. package/dist/treedx/client.d.ts +222 -0
  107. package/dist/treedx/client.js +871 -0
  108. package/dist/treedx/errors.d.ts +13 -0
  109. package/dist/treedx/errors.js +17 -0
  110. package/dist/treedx/federated-client.d.ts +27 -0
  111. package/dist/treedx/federated-client.js +158 -0
  112. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  113. package/dist/treedx/generated/openapi-types.js +0 -0
  114. package/dist/treedx/graph-adapter.d.ts +33 -0
  115. package/dist/treedx/graph-adapter.js +156 -0
  116. package/dist/treedx/index.d.ts +14 -0
  117. package/dist/treedx/index.js +48 -0
  118. package/dist/treedx/market-integration.d.ts +27 -0
  119. package/dist/treedx/market-integration.js +131 -0
  120. package/dist/treedx/ports.d.ts +166 -0
  121. package/dist/treedx/ports.js +231 -0
  122. package/dist/treedx/query-adapter.d.ts +19 -0
  123. package/dist/treedx/query-adapter.js +62 -0
  124. package/dist/treedx/registry-client.d.ts +11 -0
  125. package/dist/treedx/registry-client.js +19 -0
  126. package/dist/treedx/repository-adapter.d.ts +45 -0
  127. package/dist/treedx/repository-adapter.js +308 -0
  128. package/dist/treedx/sdk-integration.d.ts +27 -0
  129. package/dist/treedx/sdk-integration.js +63 -0
  130. package/dist/treedx/types.d.ts +1084 -0
  131. package/dist/treedx/types.js +8 -0
  132. package/dist/treedx/workspace-adapter.d.ts +27 -0
  133. package/dist/treedx/workspace-adapter.js +65 -0
  134. package/dist/treedx-backends.d.ts +218 -0
  135. package/dist/treedx-backends.js +632 -0
  136. package/dist/treedx-client.d.ts +86 -0
  137. package/dist/treedx-client.js +175 -0
  138. package/dist/treeseed/template-catalog/catalog.fixture.json +23 -23
  139. package/dist/workflow/operations.d.ts +119 -13
  140. package/dist/workflow/operations.js +309 -53
  141. package/dist/workflow-state.d.ts +13 -0
  142. package/dist/workflow-state.js +43 -26
  143. package/dist/workflow-support.d.ts +11 -3
  144. package/dist/workflow-support.js +67 -3
  145. package/dist/workflow.d.ts +5 -0
  146. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  147. package/package.json +34 -3
  148. package/templates/github/deploy-web.workflow.yml +39 -6
@@ -3,6 +3,7 @@ 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";
6
+ import { discoverTreeseedApplications } from "../../hosting/apps.js";
6
7
  import { runPrefixedCommand, sleep } from "./bootstrap-runner.js";
7
8
  import { resolveTreeseedToolCommand } from "../../managed-dependencies.js";
8
9
  import {
@@ -11,6 +12,7 @@ import {
11
12
  ensureRailwayService,
12
13
  ensureRailwayServiceInstanceConfiguration,
13
14
  ensureRailwayServiceVolume,
15
+ deployRailwayServiceInstance,
14
16
  getRailwayServiceInstance,
15
17
  listRailwayEnvironments,
16
18
  listRailwayProjects,
@@ -31,37 +33,49 @@ function normalizeScope(scope) {
31
33
  function resolveRailwayEnvironmentForScope(scope, configuredEnvironment) {
32
34
  return normalizeRailwayEnvironmentName(configuredEnvironment || normalizeScope(scope));
33
35
  }
34
- const RAILWAY_SERVICE_KEYS = ["api", "marketOperationsRunner"];
36
+ const RAILWAY_SERVICE_KEYS = ["api", "operationsRunner"];
35
37
  const HOSTED_PROJECT_SERVICE_KEYS = ["api"];
36
38
  const WORKER_RUNNER_BOOTSTRAP_INDEX = 1;
37
39
  const WORKER_RUNNER_VOLUME_MOUNT_PATH = "/data";
38
- const MARKET_OPERATIONS_RUNNER_BOOTSTRAP_COUNT = 2;
40
+ const OPERATIONS_RUNNER_BOOTSTRAP_COUNT = 2;
41
+ function isTreeseedOperationsRunnerResourceName(value) {
42
+ const normalized = String(value ?? "").trim().toLowerCase();
43
+ if (!normalized) {
44
+ return false;
45
+ }
46
+ if (normalized.startsWith("market-ops")) {
47
+ return true;
48
+ }
49
+ return normalized.includes("operations-runner");
50
+ }
51
+ function findStaleTreeseedOperationsRunnerResources(resources, desiredNames) {
52
+ const desired = new Set([...desiredNames].map((value) => String(value ?? "").trim()).filter(Boolean));
53
+ return resources.filter((resource) => {
54
+ const name = String(resource?.name ?? "").trim();
55
+ return name && isTreeseedOperationsRunnerResourceName(name) && !desired.has(name);
56
+ });
57
+ }
39
58
  function shouldManageRailwaySchedules(scope, phase = "deploy") {
40
59
  const environment = normalizeRailwayEnvironmentName(scope);
41
60
  return phase === "deploy" && (environment === "staging" || environment === "production");
42
61
  }
43
62
  function railwayServiceNameSuffix(serviceKey) {
44
- return serviceKey === "workdayManager" ? "workday-manager" : serviceKey === "workerRunner" ? "worker-runner" : serviceKey === "marketOperationsRunner" ? "market-operations-runner" : serviceKey;
63
+ return serviceKey === "workdayManager" ? "workday-manager" : serviceKey === "workerRunner" ? "worker-runner" : serviceKey === "operationsRunner" ? "operations-runner" : serviceKey;
45
64
  }
46
65
  function deriveRailwayWorkerRunnerServiceName(projectSlug, index = WORKER_RUNNER_BOOTSTRAP_INDEX) {
47
66
  const normalizedIndex = Math.max(1, Number.parseInt(String(index), 10) || WORKER_RUNNER_BOOTSTRAP_INDEX);
48
67
  return `${projectSlug}-worker-runner-${String(normalizedIndex).padStart(2, "0")}`;
49
68
  }
50
- function deriveRailwayMarketOperationsRunnerServiceName(baseServiceName, index = WORKER_RUNNER_BOOTSTRAP_INDEX) {
69
+ function deriveRailwayOperationsRunnerServiceName(baseServiceName, index = WORKER_RUNNER_BOOTSTRAP_INDEX) {
51
70
  const normalizedIndex = Math.max(1, Number.parseInt(String(index), 10) || WORKER_RUNNER_BOOTSTRAP_INDEX);
52
- const base = String(baseServiceName ?? "").trim().replace(/-\d+$/u, "") || "treeseed-market-operations-runner";
71
+ const base = String(baseServiceName ?? "").trim().replace(/-\d+$/u, "").replace(/-\d{2}$/u, "") || "treeseed-api-operations-runner";
53
72
  return `${base}-${String(normalizedIndex).padStart(2, "0")}`;
54
73
  }
55
74
  function deriveRailwayWorkerRunnerVolumeName(serviceName, environmentName = "") {
56
- const environment = normalizeRailwayEnvironmentName(environmentName);
57
- const environmentSuffix = environment === "production" ? "-prod" : environment ? `-${environment}` : "";
58
- return `${serviceName}${environmentSuffix}-data`;
75
+ return `${serviceName}-volume`;
59
76
  }
60
- function deriveRailwayMarketOperationsRunnerVolumeName(serviceName, environmentName = "") {
61
- const environment = normalizeRailwayEnvironmentName(environmentName);
62
- const environmentSuffix = environment === "production" ? "-prod" : environment ? `-${environment}` : "";
63
- const index = String(serviceName ?? "").match(/-(\d+)$/u)?.[1] ?? "01";
64
- return `market-ops-runner-${index}${environmentSuffix}-data`;
77
+ function deriveRailwayOperationsRunnerVolumeName(serviceName, environmentName = "") {
78
+ return `${serviceName}-volume`;
65
79
  }
66
80
  function railwayServiceRuntimeStartCommand(service) {
67
81
  return service.startCommand;
@@ -87,6 +101,10 @@ function configuredEnvValue(env, name) {
87
101
  const value = env?.[name];
88
102
  return typeof value === "string" && value.trim() ? value.trim() : "";
89
103
  }
104
+ function railwayDeployTransport(env) {
105
+ const configured = configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_TRANSPORT").toLowerCase();
106
+ return configured === "cli-fallback" ? "cli-fallback" : "api";
107
+ }
90
108
  async function timedRailwayPhase(timings, name, run, metadata) {
91
109
  const startMs = performance.now();
92
110
  try {
@@ -149,6 +167,10 @@ function railwayStatusDeploymentSettled(status) {
149
167
  const normalized = String(status ?? "").trim().toUpperCase();
150
168
  return normalized === "SUCCESS" || normalized === "SLEEPING";
151
169
  }
170
+ function railwayStatusDeploymentTerminalFailure(status) {
171
+ const normalized = String(status ?? "").trim().toUpperCase();
172
+ return ["FAILED", "CRASHED", "REMOVED"].includes(normalized);
173
+ }
152
174
  function formatRailwayDeploymentStatusSummary(scope, checks) {
153
175
  const aliases = {
154
176
  api: "api",
@@ -219,12 +241,14 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
219
241
  const status = String(deployment?.status ?? "").trim().toUpperCase();
220
242
  const instanceStatuses = Array.isArray(deployment?.instances) ? deployment.instances.map((entry) => String(entry?.status ?? "").trim()).filter(Boolean) : [];
221
243
  const ok = railwayStatusDeploymentSettled(status);
244
+ const terminalFailure = railwayStatusDeploymentTerminalFailure(status);
222
245
  return {
223
246
  type: "deployment-status",
224
247
  service: service.key,
225
248
  serviceName: service.serviceName,
226
249
  environment: normalizeRailwayEnvironmentName(environment.name),
227
250
  ok,
251
+ terminalFailure,
228
252
  status: status || "missing_deployment",
229
253
  observed: {
230
254
  status: status || null,
@@ -234,74 +258,10 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
234
258
  instanceStatuses,
235
259
  volumeMounts: Array.isArray(deployment?.meta?.volumeMounts) ? deployment.meta.volumeMounts : []
236
260
  },
237
- message: ok ? void 0 : `Railway deployment for ${service.serviceName} is not settled yet; observed ${status || "missing deployment status"}.`
261
+ message: ok ? void 0 : terminalFailure ? `Railway deployment for ${service.serviceName} failed with terminal status ${status}.` : `Railway deployment for ${service.serviceName} is not settled yet; observed ${status || "missing deployment status"}.`
238
262
  };
239
263
  });
240
264
  }
241
- function normalizeRailwayCliVolume(value, { serviceId, serviceName, environmentId, fallbackName, fallbackMountPath }) {
242
- if (!value || typeof value !== "object") {
243
- return null;
244
- }
245
- const record = value;
246
- const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : "";
247
- if (!id) {
248
- return null;
249
- }
250
- const listedServiceName = typeof record.serviceName === "string" && record.serviceName.trim() ? record.serviceName.trim() : "";
251
- if (listedServiceName && serviceName && listedServiceName !== serviceName) {
252
- return null;
253
- }
254
- const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : fallbackName;
255
- const mountPath = typeof record.mountPath === "string" && record.mountPath.trim() ? record.mountPath.trim() : fallbackMountPath;
256
- const sizeMb = typeof record.sizeMB === "number" ? record.sizeMB : null;
257
- const currentSizeMb = typeof record.currentSizeMB === "number" ? record.currentSizeMB : null;
258
- return {
259
- id,
260
- name,
261
- projectId: null,
262
- instances: [{
263
- id,
264
- serviceId,
265
- environmentId,
266
- mountPath,
267
- state: "READY",
268
- sizeGb: sizeMb === null ? null : sizeMb / 1e3,
269
- usedGb: currentSizeMb === null ? null : currentSizeMb / 1e3
270
- }]
271
- };
272
- }
273
- function normalizeRailwayCliVolumeList(value, options) {
274
- if (!value || typeof value !== "object" || !Array.isArray(value.volumes)) {
275
- return [];
276
- }
277
- return value.volumes.map((entry) => normalizeRailwayCliVolume(entry, options)).filter(Boolean);
278
- }
279
- function listRailwayServiceVolumesWithCli({
280
- cwd,
281
- serviceId,
282
- serviceName,
283
- environmentId,
284
- name,
285
- mountPath,
286
- env = process.env
287
- }) {
288
- const listResult = runRailway(["volume", "--service", serviceId, "--environment", environmentId, "list", "--json"], {
289
- cwd,
290
- capture: true,
291
- allowFailure: true,
292
- env
293
- });
294
- if ((listResult.status ?? 1) !== 0) {
295
- return [];
296
- }
297
- return normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
298
- serviceId,
299
- serviceName,
300
- environmentId,
301
- fallbackName: name,
302
- fallbackMountPath: mountPath
303
- });
304
- }
305
265
  function isUsableRailwayToken(value) {
306
266
  return typeof value === "string" && value.trim().length >= 8;
307
267
  }
@@ -324,6 +284,8 @@ function buildRailwayDeployCommandEnv(env = process.env) {
324
284
  const merged = buildRailwayCommandEnv(env);
325
285
  if (shouldAttachRailwayDeployLogs(merged)) {
326
286
  merged.CI = "true";
287
+ } else {
288
+ merged.CI = void 0;
327
289
  }
328
290
  return merged;
329
291
  }
@@ -489,7 +451,7 @@ mutation TreeseedScheduleUpdate($id: String!, $name: String!, $schedule: String!
489
451
  `.trim()
490
452
  };
491
453
  }
492
- function runRailway(args, { cwd, capture = false, allowFailure = false, input, env, retryTransient = true, retryAttempts = 3, retryDelayMs = 2e3 } = {}) {
454
+ function runRailway(args, { cwd, capture = false, allowFailure = false, input, env, retryTransient = true, retryAttempts = 3, retryDelayMs = 2e3, timeoutMs = 12e4 } = {}) {
493
455
  const effectiveEnv = buildRailwayCommandEnv({ ...process.env, ...env ?? {} });
494
456
  const railway = resolveTreeseedToolCommand("railway", { env: effectiveEnv });
495
457
  if (!railway) {
@@ -500,7 +462,8 @@ function runRailway(args, { cwd, capture = false, allowFailure = false, input, e
500
462
  stdio: input !== void 0 ? ["pipe", capture ? "pipe" : "inherit", capture ? "pipe" : "inherit"] : capture ? "pipe" : "inherit",
501
463
  encoding: "utf8",
502
464
  env: spawnEnv,
503
- input
465
+ input,
466
+ timeout: Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? Number(timeoutMs) : void 0
504
467
  });
505
468
  let result = null;
506
469
  const maxAttempts = retryTransient && !allowFailure ? Math.max(1, Number(retryAttempts) || 1) : 1;
@@ -511,6 +474,14 @@ function runRailway(args, { cwd, capture = false, allowFailure = false, input, e
511
474
  }
512
475
  sleepSync((Number(retryDelayMs) || 0) * attempt);
513
476
  }
477
+ if (result?.error) {
478
+ const errorMessage = result.error instanceof Error ? result.error.message : String(result.error);
479
+ const timeoutMessage = /timed out|ETIMEDOUT/iu.test(errorMessage) ? `railway ${args.join(" ")} timed out after ${Math.round(Number(timeoutMs) / 1e3)}s` : errorMessage;
480
+ if (!allowFailure) {
481
+ throw new Error(timeoutMessage);
482
+ }
483
+ result.stderr = result.stderr || timeoutMessage;
484
+ }
514
485
  if (result?.status !== 0 && !allowFailure) {
515
486
  throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${args.join(" ")} failed`);
516
487
  }
@@ -602,6 +573,26 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
602
573
  }
603
574
  };
604
575
  }
576
+ if (checks.some((entry) => entry.terminalFailure === true)) {
577
+ return {
578
+ ok: false,
579
+ checks: checks.map((check) => ({
580
+ ...check,
581
+ settle: {
582
+ durationMs: elapsedMs(startMs),
583
+ pollCount,
584
+ finalStatus: check.status,
585
+ terminalFailure: check.terminalFailure === true
586
+ }
587
+ })),
588
+ settle: {
589
+ durationMs: elapsedMs(startMs),
590
+ pollCount,
591
+ status: "failed"
592
+ },
593
+ message: "Railway deployment reached a terminal failed state."
594
+ };
595
+ }
605
596
  if (Date.now() >= deadline) {
606
597
  return {
607
598
  ok: false,
@@ -923,8 +914,7 @@ function ensureRailwayProjectContext(service, { env = process.env, allowFailure
923
914
  }
924
915
  return null;
925
916
  }
926
- function configuredRailwayServices(tenantRoot, scope) {
927
- const deployConfig = loadCliDeployConfig(tenantRoot);
917
+ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, application = null) {
928
918
  const normalizedScope = normalizeScope(scope);
929
919
  let identity;
930
920
  try {
@@ -944,7 +934,7 @@ function configuredRailwayServices(tenantRoot, scope) {
944
934
  if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
945
935
  return [];
946
936
  }
947
- const defaultRootDir = ["api", "marketOperationsRunner"].includes(serviceKey) ? "." : "packages/core";
937
+ const defaultRootDir = ["api", "operationsRunner"].includes(serviceKey) ? "." : "packages/core";
948
938
  const serviceRoot = resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir);
949
939
  const railwayEnvironment = resolveRailwayEnvironmentForScope(
950
940
  normalizedScope,
@@ -953,28 +943,29 @@ function configuredRailwayServices(tenantRoot, scope) {
953
943
  const publicBaseUrl = service.environments?.[normalizedScope]?.baseUrl ?? service.publicBaseUrl ?? null;
954
944
  const configuredServiceName = service.railway?.serviceName ?? (serviceKey === "workerRunner" ? deriveRailwayWorkerRunnerServiceName(identity.deploymentKey) : `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`);
955
945
  const configuredRunnerPool = service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
956
- const runnerPool = serviceKey === "marketOperationsRunner" ? {
957
- bootstrapCount: Math.max(1, Number.parseInt(String(configuredRunnerPool?.bootstrapCount ?? MARKET_OPERATIONS_RUNNER_BOOTSTRAP_COUNT), 10) || MARKET_OPERATIONS_RUNNER_BOOTSTRAP_COUNT),
958
- maxRunners: Math.max(1, Number.parseInt(String(configuredRunnerPool?.maxRunners ?? configuredRunnerPool?.bootstrapCount ?? MARKET_OPERATIONS_RUNNER_BOOTSTRAP_COUNT), 10) || MARKET_OPERATIONS_RUNNER_BOOTSTRAP_COUNT),
946
+ const runnerPool = serviceKey === "operationsRunner" ? {
947
+ bootstrapCount: Math.max(1, Number.parseInt(String(configuredRunnerPool?.bootstrapCount ?? OPERATIONS_RUNNER_BOOTSTRAP_COUNT), 10) || OPERATIONS_RUNNER_BOOTSTRAP_COUNT),
948
+ maxRunners: Math.max(1, Number.parseInt(String(configuredRunnerPool?.maxRunners ?? configuredRunnerPool?.bootstrapCount ?? OPERATIONS_RUNNER_BOOTSTRAP_COUNT), 10) || OPERATIONS_RUNNER_BOOTSTRAP_COUNT),
959
949
  volumeMountPath: service.railway?.volumeMountPath ?? configuredRunnerPool?.volumeMountPath ?? WORKER_RUNNER_VOLUME_MOUNT_PATH
960
950
  } : serviceKey === "workerRunner" ? {
961
951
  bootstrapIndex: WORKER_RUNNER_BOOTSTRAP_INDEX,
962
952
  volumeMountPath: WORKER_RUNNER_VOLUME_MOUNT_PATH
963
953
  } : null;
964
- const instanceCount = serviceKey === "marketOperationsRunner" ? runnerPool.bootstrapCount : 1;
954
+ const instanceCount = serviceKey === "operationsRunner" ? runnerPool.bootstrapCount : 1;
965
955
  return Array.from({ length: instanceCount }, (_, offset) => {
966
956
  const runnerIndex = offset + 1;
967
- const serviceName = serviceKey === "marketOperationsRunner" ? deriveRailwayMarketOperationsRunnerServiceName(configuredServiceName, runnerIndex) : configuredServiceName;
957
+ const serviceName = serviceKey === "operationsRunner" ? deriveRailwayOperationsRunnerServiceName(configuredServiceName, runnerIndex) : configuredServiceName;
968
958
  return {
969
959
  key: serviceKey,
970
- instanceKey: serviceKey === "marketOperationsRunner" ? `${serviceKey}:${runnerIndex}` : serviceKey,
971
- runnerIndex: serviceKey === "marketOperationsRunner" ? runnerIndex : null,
960
+ instanceKey: serviceKey === "operationsRunner" ? `${serviceKey}:${runnerIndex}` : serviceKey,
961
+ runnerIndex: serviceKey === "operationsRunner" ? runnerIndex : null,
962
+ serviceConfig: service,
972
963
  scope: normalizedScope,
973
964
  projectId: service.railway?.projectId ?? null,
974
965
  projectName: service.railway?.projectName ?? identity.deploymentKey,
975
966
  serviceId: service.railway?.serviceId ?? null,
976
967
  serviceName,
977
- runnerId: serviceKey === "marketOperationsRunner" ? serviceName : null,
968
+ runnerId: serviceKey === "operationsRunner" ? serviceName : null,
978
969
  rootDir: serviceRoot,
979
970
  publicBaseUrl,
980
971
  railwayEnvironment,
@@ -985,14 +976,30 @@ function configuredRailwayServices(tenantRoot, scope) {
985
976
  healthcheckIntervalSeconds: service.railway?.healthcheckIntervalSeconds ?? null,
986
977
  restartPolicy: service.railway?.restartPolicy ?? null,
987
978
  runtimeMode: service.railway?.runtimeMode ?? null,
988
- volumeMountPath: serviceKey === "marketOperationsRunner" ? runnerPool.volumeMountPath : service.railway?.volumeMountPath ?? null,
979
+ volumeMountPath: serviceKey === "operationsRunner" ? runnerPool.volumeMountPath : service.railway?.volumeMountPath ?? null,
989
980
  schedule: normalizeScheduleExpressions(service.railway?.schedule),
990
981
  hostingKind,
991
- runnerPool
982
+ runnerPool,
983
+ application
992
984
  };
993
985
  });
994
986
  }).filter(Boolean);
995
987
  }
988
+ function configuredRailwayServices(tenantRoot, scope) {
989
+ const deployConfig = loadCliDeployConfig(tenantRoot);
990
+ const direct = configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig);
991
+ const nested = discoverTreeseedApplications(tenantRoot).filter((application) => application.root !== resolve(tenantRoot)).flatMap((application) => configuredRailwayServicesForConfig(
992
+ application.root,
993
+ scope,
994
+ application.config,
995
+ {
996
+ id: application.id,
997
+ root: application.root,
998
+ relativeRoot: application.relativeRoot
999
+ }
1000
+ ));
1001
+ return [...direct, ...nested];
1002
+ }
996
1003
  function configuredRailwayScheduledJobs(tenantRoot, scope, { phase = "deploy" } = {}) {
997
1004
  if (!shouldManageRailwaySchedules(scope, phase)) {
998
1005
  return [];
@@ -1427,6 +1434,39 @@ function shouldUseVerboseRailwayDeploy(env = process.env) {
1427
1434
  }
1428
1435
  return shouldAttachRailwayDeployLogs(env);
1429
1436
  }
1437
+ function railwayDeployCommandTimeoutMs(env = process.env) {
1438
+ const configured = Number.parseInt(configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_COMMAND_TIMEOUT_MS"), 10);
1439
+ return Number.isFinite(configured) && configured > 0 ? configured : 3e5;
1440
+ }
1441
+ function railwayPhaseTimeoutMs(env = process.env, phase = "default") {
1442
+ const configured = Number.parseInt(configuredEnvValue(env, `TREESEED_RAILWAY_${String(phase).toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}_TIMEOUT_MS`), 10);
1443
+ if (Number.isFinite(configured) && configured > 0) {
1444
+ return configured;
1445
+ }
1446
+ const defaultConfigured = Number.parseInt(configuredEnvValue(env, "TREESEED_RAILWAY_PHASE_TIMEOUT_MS"), 10);
1447
+ if (Number.isFinite(defaultConfigured) && defaultConfigured > 0) {
1448
+ return defaultConfigured;
1449
+ }
1450
+ if (phase === "sync_runtime_config") {
1451
+ return 6e5;
1452
+ }
1453
+ return phase === "deploy" ? 3e5 : 18e4;
1454
+ }
1455
+ async function withRailwayPhaseTimeout(run, timeoutMs, message) {
1456
+ let timer = null;
1457
+ try {
1458
+ return await Promise.race([
1459
+ Promise.resolve().then(run),
1460
+ new Promise((_, reject) => {
1461
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
1462
+ })
1463
+ ]);
1464
+ } finally {
1465
+ if (timer) {
1466
+ clearTimeout(timer);
1467
+ }
1468
+ }
1469
+ }
1430
1470
  function shouldIncludeRailwayIgnoredFiles(env = process.env) {
1431
1471
  const configured = configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_INCLUDE_IGNORED");
1432
1472
  return configured === "1" || configured === "true";
@@ -1699,15 +1739,23 @@ async function resolveRailwayDeployProjectContext(service, { env = process.env }
1699
1739
  projectName: project.name ?? service.projectName
1700
1740
  };
1701
1741
  }
1702
- async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, service, { env = process.env } = {}) {
1742
+ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, service, { env = process.env, writePhase = null } = {}) {
1743
+ const writeSyncPhase = (stage, message) => {
1744
+ if (typeof writePhase === "function") {
1745
+ writePhase(`sync-runtime-config:${stage}`, message);
1746
+ }
1747
+ };
1703
1748
  const wantsInstanceConfig = service.buildCommand || service.startCommand || service.rootDir || service.healthcheckPath || service.healthcheckTimeoutSeconds !== null || service.healthcheckTimeoutSeconds !== void 0 || service.healthcheckIntervalSeconds !== null || service.healthcheckIntervalSeconds !== void 0 || service.restartPolicy || service.runtimeMode;
1704
1749
  const wantsRunnerVolume = service.key === "workerRunner" || Boolean(service.volumeMountPath);
1705
1750
  if (!wantsInstanceConfig && !wantsRunnerVolume) {
1751
+ writeSyncPhase("skip", "No runtime configuration changes requested.");
1706
1752
  return null;
1707
1753
  }
1754
+ writeSyncPhase("workspace", "Resolving Railway workspace.");
1708
1755
  const workspace = await resolveRailwayWorkspaceContext({ env });
1709
1756
  let project = null;
1710
1757
  if (service.projectId) {
1758
+ writeSyncPhase("project", `Resolving Railway project ${service.projectName ?? service.projectId}.`);
1711
1759
  project = await ensureRailwayProject({
1712
1760
  projectId: service.projectId,
1713
1761
  projectName: service.projectName,
@@ -1716,9 +1764,11 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
1716
1764
  workspace: workspace.id
1717
1765
  }).then((result) => result.project);
1718
1766
  } else {
1767
+ writeSyncPhase("project", `Looking up Railway project ${service.projectName}.`);
1719
1768
  const projects = await listRailwayProjects({ env, workspaceId: workspace.id });
1720
1769
  project = projects.find((entry) => entry.name === service.projectName) ?? null;
1721
1770
  if (!project) {
1771
+ writeSyncPhase("project", `Creating Railway project ${service.projectName}.`);
1722
1772
  project = await ensureRailwayProject({
1723
1773
  projectName: service.projectName,
1724
1774
  defaultEnvironmentName: service.railwayEnvironment,
@@ -1730,6 +1780,7 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
1730
1780
  const environmentName = normalizeRailwayEnvironmentName(service.railwayEnvironment);
1731
1781
  let environment = project.environments.find((entry) => entry.name === environmentName || entry.id === environmentName) ?? null;
1732
1782
  if (!environment) {
1783
+ writeSyncPhase("environment", `Creating Railway environment ${environmentName}.`);
1733
1784
  environment = await ensureRailwayEnvironment({
1734
1785
  projectId: project.id,
1735
1786
  environmentName,
@@ -1738,6 +1789,7 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
1738
1789
  }
1739
1790
  let railwayService = project.services.find((entry) => entry.id === service.serviceId || entry.name === service.serviceName) ?? null;
1740
1791
  if (!railwayService) {
1792
+ writeSyncPhase("service", `Creating Railway service ${service.serviceName ?? service.key}.`);
1741
1793
  railwayService = await ensureRailwayService({
1742
1794
  projectId: project.id,
1743
1795
  serviceId: service.serviceId,
@@ -1745,56 +1797,63 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
1745
1797
  env
1746
1798
  }).then((result) => result.service);
1747
1799
  }
1800
+ if (wantsInstanceConfig) {
1801
+ writeSyncPhase("instance", "Ensuring Railway service instance configuration.");
1802
+ }
1748
1803
  const runtimeConfiguration = wantsInstanceConfig ? await ensureRailwayServiceInstanceConfiguration({
1749
1804
  serviceId: railwayService.id,
1750
1805
  environmentId: environment.id,
1751
1806
  buildCommand: service.buildCommand,
1752
1807
  startCommand: railwayServiceRuntimeStartCommand(service),
1753
1808
  cronSchedule: service.schedule?.[0] ?? null,
1754
- rootDirectory: relativeRailwayRootDir(tenantRoot, service.rootDir),
1809
+ rootDirectory: ".",
1755
1810
  healthcheckPath: service.healthcheckPath,
1756
1811
  healthcheckTimeoutSeconds: service.healthcheckTimeoutSeconds,
1757
1812
  healthcheckIntervalSeconds: service.healthcheckIntervalSeconds,
1758
1813
  restartPolicy: service.restartPolicy,
1759
1814
  runtimeMode: service.runtimeMode,
1815
+ deploymentRegion: wantsRunnerVolume ? configuredEnvValue(env, "TREESEED_RAILWAY_STATEFUL_REGION") || "us-west2" : null,
1760
1816
  env
1761
1817
  }) : null;
1818
+ writeSyncPhase("variables", "Upserting Railway runtime variables.");
1762
1819
  await upsertRailwayVariables({
1763
1820
  projectId: project.id,
1764
1821
  environmentId: environment.id,
1765
1822
  serviceId: railwayService.id,
1766
1823
  variables: {
1767
1824
  TREESEED_SKIP_PACKAGE_PREPARE: "1",
1768
- ...service.key === "marketOperationsRunner" ? {
1825
+ ...service.key === "operationsRunner" ? {
1769
1826
  NIXPACKS_APT_PKGS: "git",
1770
1827
  NIXPACKS_PKGS: "git",
1771
1828
  TREESEED_PLATFORM_RUNNER_ID: service.runnerId ?? railwayService.name,
1772
1829
  TREESEED_PLATFORM_RUNNER_DATA_DIR: service.volumeMountPath ?? WORKER_RUNNER_VOLUME_MOUNT_PATH,
1773
1830
  TREESEED_PLATFORM_RUNNER_ENVIRONMENT: normalizeScope(service.scope) === "prod" ? "production" : normalizeScope(service.scope),
1774
- TREESEED_MARKET_ID: normalizeScope(service.scope),
1831
+ TREESEED_MANAGER_ID: normalizeScope(service.scope),
1832
+ ...configuredEnvValue(env, "RAILWAY_API_TOKEN") ? { RAILWAY_API_TOKEN: configuredEnvValue(env, "RAILWAY_API_TOKEN") } : {},
1833
+ ...configuredEnvValue(env, "TREESEED_RAILWAY_WORKSPACE") ? { TREESEED_RAILWAY_WORKSPACE: configuredEnvValue(env, "TREESEED_RAILWAY_WORKSPACE") } : {},
1775
1834
  ...configuredEnvValue(env, "TREESEED_PLATFORM_RUNNER_SECRET") ? { TREESEED_PLATFORM_RUNNER_SECRET: configuredEnvValue(env, "TREESEED_PLATFORM_RUNNER_SECRET") } : {},
1776
- ...configuredEnvValue(env, "TREESEED_MARKET_API_BASE_URL") || configuredEnvValue(env, "TREESEED_MARKET_URL") ? {
1777
- TREESEED_MARKET_API_BASE_URL: configuredEnvValue(env, "TREESEED_MARKET_API_BASE_URL") || configuredEnvValue(env, "TREESEED_MARKET_URL")
1835
+ ...configuredEnvValue(env, "TREESEED_API_BASE_URL") || configuredEnvValue(env, "TREESEED_URL") ? {
1836
+ TREESEED_API_BASE_URL: configuredEnvValue(env, "TREESEED_API_BASE_URL") || configuredEnvValue(env, "TREESEED_URL")
1778
1837
  } : {}
1779
1838
  } : {}
1780
1839
  },
1781
1840
  env
1782
1841
  });
1783
1842
  const volumeMountPath = service.volumeMountPath ?? service.runnerPool?.volumeMountPath ?? WORKER_RUNNER_VOLUME_MOUNT_PATH;
1784
- const volumeConfiguration = wantsRunnerVolume ? await ensureRailwayServiceVolumeWithCliFallback({
1785
- tenantRoot,
1843
+ if (wantsRunnerVolume) {
1844
+ writeSyncPhase("volume", `Ensuring Railway volume mounted at ${volumeMountPath}.`);
1845
+ }
1846
+ const volumeConfiguration = wantsRunnerVolume ? await ensureRailwayServiceVolume({
1786
1847
  projectId: project.id,
1787
1848
  environmentId: environment.id,
1788
- environmentName: environment.name,
1789
1849
  serviceId: railwayService.id,
1790
- serviceName: railwayService.name,
1791
- name: service.key === "marketOperationsRunner" ? deriveRailwayMarketOperationsRunnerVolumeName(railwayService.name, environment.name) : deriveRailwayWorkerRunnerVolumeName(railwayService.name, environment.name),
1850
+ name: service.key === "operationsRunner" ? deriveRailwayOperationsRunnerVolumeName(railwayService.name, environment.name) : deriveRailwayWorkerRunnerVolumeName(railwayService.name, environment.name),
1792
1851
  mountPath: volumeMountPath,
1793
- preferCli: service.key === "marketOperationsRunner",
1794
1852
  env
1795
1853
  }) : null;
1796
1854
  if (wantsRunnerVolume) {
1797
1855
  if (service.key === "workerRunner") {
1856
+ writeSyncPhase("volume-vars", "Upserting Railway worker volume variables.");
1798
1857
  await upsertRailwayVariables({
1799
1858
  projectId: project.id,
1800
1859
  environmentId: environment.id,
@@ -1810,6 +1869,7 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
1810
1869
  });
1811
1870
  }
1812
1871
  }
1872
+ writeSyncPhase("done", "Runtime configuration is synchronized.");
1813
1873
  return {
1814
1874
  projectId: project.id,
1815
1875
  projectName: project.name ?? service.projectName ?? null,
@@ -1828,163 +1888,6 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
1828
1888
  } : null
1829
1889
  };
1830
1890
  }
1831
- async function ensureRailwayServiceVolumeWithCliFallback({
1832
- tenantRoot,
1833
- projectId,
1834
- environmentId,
1835
- environmentName,
1836
- serviceId,
1837
- serviceName,
1838
- name,
1839
- mountPath,
1840
- preferCli = false,
1841
- env = process.env
1842
- }) {
1843
- if (!preferCli) {
1844
- try {
1845
- return await ensureRailwayServiceVolume({
1846
- projectId,
1847
- environmentId,
1848
- serviceId,
1849
- name,
1850
- mountPath,
1851
- env
1852
- });
1853
- } catch (error) {
1854
- const message = error instanceof Error ? error.message : String(error);
1855
- if (!message.includes("Problem processing request")) {
1856
- throw error;
1857
- }
1858
- }
1859
- }
1860
- const cliOptions = {
1861
- cwd: tenantRoot,
1862
- capture: true,
1863
- env
1864
- };
1865
- ensureRailwayProjectContext({
1866
- key: serviceName,
1867
- projectId,
1868
- serviceName,
1869
- rootDir: tenantRoot,
1870
- railwayEnvironment: environmentName
1871
- }, {
1872
- env,
1873
- capture: true
1874
- });
1875
- const volumeArgs = ["volume", "--service", serviceId, "--environment", environmentId];
1876
- const listResult = runRailway([...volumeArgs, "list", "--json"], cliOptions);
1877
- const existingVolumes = normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
1878
- serviceId,
1879
- serviceName,
1880
- environmentId,
1881
- fallbackName: name,
1882
- fallbackMountPath: mountPath
1883
- });
1884
- let volume = existingVolumes.find((entry) => entry.name === name) ?? existingVolumes.find((entry) => entry.instances.some((instance2) => instance2.mountPath === mountPath)) ?? existingVolumes[0] ?? null;
1885
- let created = false;
1886
- let updated = false;
1887
- if (!volume) {
1888
- const createResult = runRailway([...volumeArgs, "add", "--mount-path", mountPath, "--json"], cliOptions);
1889
- volume = normalizeRailwayCliVolume(parseRailwayJsonOutput(createResult.stdout ?? ""), {
1890
- serviceId,
1891
- serviceName,
1892
- environmentId,
1893
- fallbackName: name,
1894
- fallbackMountPath: mountPath
1895
- });
1896
- if (!volume) {
1897
- throw new Error(`Railway CLI volume add did not return a usable volume for ${serviceName} in ${environmentName}.`);
1898
- }
1899
- created = true;
1900
- }
1901
- let instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
1902
- if (!instance || instance.mountPath !== mountPath) {
1903
- const attachResult = runRailway([...volumeArgs, "attach", "--volume", volume.id, "--yes", "--json"], {
1904
- ...cliOptions,
1905
- allowFailure: true
1906
- });
1907
- if ((attachResult.status ?? 1) !== 0) {
1908
- const attachMessage = attachResult.stderr?.trim() || attachResult.stdout?.trim() || "";
1909
- if (!/already mounted/iu.test(attachMessage)) {
1910
- throw new Error(attachMessage || `Railway volume attach failed for ${serviceName} in ${environmentName}.`);
1911
- }
1912
- }
1913
- const attachedVolume = (attachResult.status ?? 1) === 0 ? normalizeRailwayCliVolume(parseRailwayJsonOutput(attachResult.stdout ?? ""), {
1914
- serviceId,
1915
- serviceName,
1916
- environmentId,
1917
- fallbackName: name,
1918
- fallbackMountPath: mountPath
1919
- }) : null;
1920
- volume = attachedVolume ?? {
1921
- ...volume,
1922
- instances: [{
1923
- ...instance ?? {
1924
- id: volume.id,
1925
- serviceId,
1926
- environmentId,
1927
- state: "READY",
1928
- sizeGb: null,
1929
- usedGb: null
1930
- },
1931
- serviceId,
1932
- environmentId,
1933
- mountPath
1934
- }]
1935
- };
1936
- instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
1937
- updated = true;
1938
- }
1939
- const apiVolume = await waitForRailwayServiceVolumeMount({
1940
- projectId,
1941
- volumeId: volume.id,
1942
- volumeName: name,
1943
- serviceId,
1944
- environmentId,
1945
- mountPath,
1946
- env
1947
- });
1948
- if (apiVolume) {
1949
- volume = apiVolume;
1950
- } else {
1951
- throw new Error(`Railway volume ${name} was not attached to ${serviceName} at ${mountPath}.`);
1952
- }
1953
- return {
1954
- volume,
1955
- instance: volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null,
1956
- created,
1957
- updated
1958
- };
1959
- }
1960
- async function waitForRailwayServiceVolumeMount({
1961
- projectId,
1962
- volumeId,
1963
- volumeName,
1964
- serviceId,
1965
- environmentId,
1966
- mountPath,
1967
- env
1968
- }) {
1969
- for (let attempt = 0; attempt <= 24; attempt += 1) {
1970
- const volumes = await listRailwayVolumes({ projectId, env });
1971
- const mounted = volumes.find(
1972
- (entry) => entry.instances.some(
1973
- (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1974
- )
1975
- ) ?? null;
1976
- const match = mounted ?? volumes.find((entry) => entry.id === volumeId) ?? volumes.find((entry) => entry.name === volumeName) ?? null;
1977
- if (match?.instances.some(
1978
- (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1979
- )) {
1980
- return match;
1981
- }
1982
- if (attempt < 24) {
1983
- await sleep(5e3);
1984
- }
1985
- }
1986
- return null;
1987
- }
1988
1891
  async function deployRailwayService(tenantRoot, service, {
1989
1892
  dryRun = false,
1990
1893
  write,
@@ -2000,7 +1903,13 @@ async function deployRailwayService(tenantRoot, service, {
2000
1903
  command: [plan2.command, ...plan2.args].join(" "),
2001
1904
  cwd: plan2.cwd,
2002
1905
  publicBaseUrl: service.publicBaseUrl,
2003
- timings
1906
+ timings,
1907
+ transport: {
1908
+ railway: {
1909
+ reconcile: "api",
1910
+ deploy: railwayDeployTransport(env)
1911
+ }
1912
+ }
2004
1913
  };
2005
1914
  }
2006
1915
  const deployService = await timedRailwayPhase(timings, "railway:resolve-context", () => resolveRailwayDeployProjectContext(service, { env }), {
@@ -2008,19 +1917,23 @@ async function deployRailwayService(tenantRoot, service, {
2008
1917
  });
2009
1918
  const commandEnv = buildRailwayCommandEnv({ ...process.env, ...env });
2010
1919
  let railwayDeployEnv = buildRailwayDeployCommandEnv(commandEnv);
2011
- const railway = resolveTreeseedToolCommand("railway", { env: commandEnv });
2012
- if (!railway) {
2013
- throw new Error("Railway CLI is unavailable.");
2014
- }
1920
+ const deployTransport = railwayDeployTransport(commandEnv);
2015
1921
  const taskPrefix = prefix ?? {
2016
1922
  scope: normalizeScope(deployService.scope ?? deployService.railwayEnvironment ?? "railway"),
2017
1923
  system: deployService.key === "api" ? "api" : "agents",
2018
1924
  task: `${deployService.key}-railway-deploy`,
2019
1925
  stage: "deploy"
2020
1926
  };
2021
- const runtimeConfiguration = await timedRailwayPhase(timings, "railway:sync-runtime-config", () => syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, deployService, {
2022
- env: commandEnv
2023
- }), { service: deployService.key });
1927
+ const writePhase = (stage, message) => {
1928
+ write ? write(`[${taskPrefix.scope}][${taskPrefix.system}][${taskPrefix.task}][${stage}] ${message}`, "stdout") : null;
1929
+ };
1930
+ writePhase("resolve-context", `Resolved Railway service ${deployService.serviceName ?? deployService.serviceId ?? deployService.key}.`);
1931
+ writePhase("sync-runtime-config", "Syncing Railway runtime configuration.");
1932
+ const runtimeConfiguration = await timedRailwayPhase(timings, "railway:sync-runtime-config", () => withRailwayPhaseTimeout(
1933
+ () => syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, deployService, { env: commandEnv, writePhase }),
1934
+ railwayPhaseTimeoutMs(commandEnv, "sync_runtime_config"),
1935
+ `Railway runtime configuration sync timed out for ${deployService.serviceName ?? deployService.key}.`
1936
+ ), { service: deployService.key });
2024
1937
  const cliDeployService = {
2025
1938
  ...deployService,
2026
1939
  projectId: runtimeConfiguration?.projectId ?? deployService.projectId,
@@ -2030,16 +1943,71 @@ async function deployRailwayService(tenantRoot, service, {
2030
1943
  serviceName: runtimeConfiguration?.serviceName ?? deployService.serviceName,
2031
1944
  railwayEnvironment: runtimeConfiguration?.environmentName ?? runtimeConfiguration?.environmentId ?? deployService.railwayEnvironment
2032
1945
  };
2033
- await timedRailwayPhase(timings, "railway:device-login-vars", () => syncRailwayApiDeviceLoginVariables(cliDeployService, commandEnv, write, taskPrefix), {
1946
+ writePhase("device-login-vars", "Syncing Railway device-login variables.");
1947
+ await timedRailwayPhase(timings, "railway:device-login-vars", () => withRailwayPhaseTimeout(
1948
+ () => syncRailwayApiDeviceLoginVariables(cliDeployService, commandEnv, write, taskPrefix),
1949
+ railwayPhaseTimeoutMs(commandEnv, "device_login_vars"),
1950
+ `Railway device-login variable sync timed out for ${cliDeployService.serviceName ?? cliDeployService.key}.`
1951
+ ), {
2034
1952
  service: cliDeployService.key
2035
1953
  });
1954
+ if (deployService.buildCommand && shouldRunRailwayPredeployBuild(commandEnv)) {
1955
+ const buildResult = await timedRailwayPhase(timings, "railway:predeploy-build", () => runPrefixedCommand("bash", ["-lc", deployService.buildCommand], {
1956
+ cwd: deployService.rootDir,
1957
+ env: commandEnv,
1958
+ write,
1959
+ prefix: { ...taskPrefix, stage: "build" }
1960
+ }), { service: deployService.key });
1961
+ if (buildResult.status !== 0) {
1962
+ throw new Error(`Railway ${deployService.key} build command failed.`);
1963
+ }
1964
+ }
1965
+ if (deployTransport !== "cli-fallback") {
1966
+ writePhase("deploy", `Deploying Railway service ${cliDeployService.serviceName ?? cliDeployService.serviceId ?? cliDeployService.key} through the Railway API.`);
1967
+ const apiDeploy = await timedRailwayPhase(timings, "railway:api-deploy", () => withRailwayPhaseTimeout(
1968
+ () => deployRailwayServiceInstance({
1969
+ serviceId: cliDeployService.serviceId,
1970
+ environmentId: cliDeployService.environmentId,
1971
+ env: commandEnv
1972
+ }),
1973
+ railwayPhaseTimeoutMs(commandEnv, "deploy"),
1974
+ `Railway API deploy phase timed out for ${cliDeployService.serviceName ?? cliDeployService.key}.`
1975
+ ), { service: cliDeployService.key });
1976
+ return {
1977
+ service: deployService.key,
1978
+ status: "deployed",
1979
+ command: "railway-api serviceInstanceDeployV2",
1980
+ cwd: deployService.rootDir,
1981
+ publicBaseUrl: deployService.publicBaseUrl,
1982
+ timings,
1983
+ deploymentId: apiDeploy.deploymentId,
1984
+ transport: {
1985
+ railway: {
1986
+ reconcile: "api",
1987
+ deploy: "api"
1988
+ }
1989
+ },
1990
+ runtimeConfiguration: runtimeConfiguration ? {
1991
+ updated: runtimeConfiguration.updated,
1992
+ healthcheckPath: runtimeConfiguration.instance?.healthcheckPath ?? null,
1993
+ healthcheckTimeoutSeconds: runtimeConfiguration.instance?.healthcheckTimeoutSeconds ?? null,
1994
+ runtimeMode: runtimeConfiguration.instance?.runtimeMode ?? null,
1995
+ volume: runtimeConfiguration.volume ?? null
1996
+ } : null
1997
+ };
1998
+ }
1999
+ const railway = resolveTreeseedToolCommand("railway", { env: commandEnv });
2000
+ if (!railway) {
2001
+ throw new Error("Railway CLI deploy fallback requested, but Railway CLI is unavailable.");
2002
+ }
2036
2003
  railwayDeployEnv = buildRailwayCliContextEnv(railwayDeployEnv, cliDeployService);
2037
2004
  const hasCommandApiToken = Boolean(configuredEnvValue(commandEnv, "RAILWAY_API_TOKEN"));
2038
2005
  let usesProjectToken = Boolean(configuredEnvValue(railwayDeployEnv, "RAILWAY_TOKEN"));
2039
2006
  if (usesProjectToken) {
2040
2007
  railwayDeployEnv = { ...railwayDeployEnv, RAILWAY_API_TOKEN: void 0 };
2041
2008
  }
2042
- await timedRailwayPhase(timings, "railway:project-token", async () => {
2009
+ writePhase("project-token", usesProjectToken || hasCommandApiToken ? "Using configured Railway authentication." : "Creating Railway project token.");
2010
+ await timedRailwayPhase(timings, "railway:project-token", () => withRailwayPhaseTimeout(async () => {
2043
2011
  if (usesProjectToken || hasCommandApiToken) {
2044
2012
  return null;
2045
2013
  }
@@ -2051,25 +2019,15 @@ async function deployRailwayService(tenantRoot, service, {
2051
2019
  throw new Error(`Railway CI deploy requires a project token for ${cliDeployService.serviceName ?? cliDeployService.key}. Automatic project token creation did not return a token.`);
2052
2020
  }
2053
2021
  return null;
2054
- }, { service: cliDeployService.key });
2022
+ }, railwayPhaseTimeoutMs(commandEnv, "project_token"), `Railway project-token phase timed out for ${cliDeployService.serviceName ?? cliDeployService.key}.`), { service: cliDeployService.key });
2055
2023
  const linkPlan = planRailwayServiceLink(cliDeployService, { env: commandEnv });
2056
2024
  const plan = planRailwayServiceDeploy(cliDeployService, { env, projectTokenMode: usesProjectToken });
2057
- if (deployService.buildCommand && shouldRunRailwayPredeployBuild(commandEnv)) {
2058
- const buildResult = await timedRailwayPhase(timings, "railway:predeploy-build", () => runPrefixedCommand("bash", ["-lc", deployService.buildCommand], {
2059
- cwd: deployService.rootDir,
2060
- env: commandEnv,
2061
- write,
2062
- prefix: { ...taskPrefix, stage: "build" }
2063
- }), { service: deployService.key });
2064
- if (buildResult.status !== 0) {
2065
- throw new Error(`Railway ${deployService.key} build command failed.`);
2066
- }
2067
- }
2068
2025
  const hasRailwayApiToken = Boolean(configuredEnvValue(commandEnv, "RAILWAY_API_TOKEN"));
2069
2026
  const cliConfig = configuredEnvValue(commandEnv, "CI") === "true" ? writeRailwayCliProjectConfig(cliDeployService, { env: railwayDeployEnv, cwd: plan.cwd }) : null;
2070
2027
  const effectiveLinkPlan = hasRailwayApiToken ? linkPlan : usesProjectToken ? planRailwayProjectEnvironmentLink(cliDeployService) : linkPlan;
2071
2028
  const railwayLinkEnv = hasRailwayApiToken ? buildRailwayLinkCommandEnv(commandEnv, cliDeployService) : railwayDeployEnv;
2072
- await timedRailwayPhase(timings, "railway:link", async () => {
2029
+ writePhase("link", `Linking Railway project context for ${cliDeployService.serviceName ?? cliDeployService.serviceId ?? cliDeployService.key}.`);
2030
+ await timedRailwayPhase(timings, "railway:link", () => withRailwayPhaseTimeout(async () => {
2073
2031
  if (cliConfig) {
2074
2032
  write ? write(`[${taskPrefix.scope}][${taskPrefix.system}][${taskPrefix.task}][link] Wrote Railway CLI project context for ${cliConfig.projectPath}.`, "stdout") : null;
2075
2033
  return;
@@ -2083,15 +2041,19 @@ async function deployRailwayService(tenantRoot, service, {
2083
2041
  if (linkResult.status !== 0) {
2084
2042
  throw new Error(linkResult.stderr?.trim() || linkResult.stdout?.trim() || `railway ${effectiveLinkPlan.args.join(" ")} failed with exit code ${linkResult.status ?? "unknown"} in ${effectiveLinkPlan.cwd}`);
2085
2043
  }
2086
- }, { service: cliDeployService.key });
2087
- await timedRailwayPhase(timings, "railway:deploy", async () => {
2044
+ }, railwayPhaseTimeoutMs(commandEnv, "link"), `Railway link phase timed out for ${cliDeployService.serviceName ?? cliDeployService.key}.`), { service: cliDeployService.key });
2045
+ await timedRailwayPhase(timings, "railway:deploy", () => withRailwayPhaseTimeout(async () => {
2088
2046
  let lastFailure = null;
2089
2047
  for (let attempt = 1; attempt <= 5; attempt += 1) {
2048
+ if (write && attempt === 1) {
2049
+ write(`[${taskPrefix.scope}][${taskPrefix.system}][${taskPrefix.task}][deploy] $ railway ${plan.args.join(" ")}`, "stdout");
2050
+ }
2090
2051
  const result = await runPrefixedCommand(railway.command, [...railway.argsPrefix, ...plan.args], {
2091
2052
  cwd: plan.cwd,
2092
2053
  env: railwayDeployEnv,
2093
2054
  write,
2094
- prefix: taskPrefix
2055
+ prefix: taskPrefix,
2056
+ timeoutMs: railwayDeployCommandTimeoutMs(commandEnv)
2095
2057
  });
2096
2058
  if (result.status === 0) {
2097
2059
  lastFailure = null;
@@ -2109,7 +2071,7 @@ async function deployRailwayService(tenantRoot, service, {
2109
2071
  if (lastFailure) {
2110
2072
  throw new Error(lastFailure.stderr?.trim() || lastFailure.stdout?.trim() || `railway ${plan.args.join(" ")} failed`);
2111
2073
  }
2112
- }, { service: cliDeployService.key });
2074
+ }, railwayPhaseTimeoutMs(commandEnv, "deploy"), `Railway deploy phase timed out for ${cliDeployService.serviceName ?? cliDeployService.key}.`), { service: cliDeployService.key });
2113
2075
  return {
2114
2076
  service: deployService.key,
2115
2077
  status: "deployed",
@@ -2117,6 +2079,12 @@ async function deployRailwayService(tenantRoot, service, {
2117
2079
  cwd: plan.cwd,
2118
2080
  publicBaseUrl: deployService.publicBaseUrl,
2119
2081
  timings,
2082
+ transport: {
2083
+ railway: {
2084
+ reconcile: "api",
2085
+ deploy: "cli-fallback"
2086
+ }
2087
+ },
2120
2088
  runtimeConfiguration: runtimeConfiguration ? {
2121
2089
  updated: runtimeConfiguration.updated,
2122
2090
  healthcheckPath: runtimeConfiguration.instance?.healthcheckPath ?? null,
@@ -2134,8 +2102,8 @@ export {
2134
2102
  configuredRailwayScheduledJobs,
2135
2103
  configuredRailwayServices,
2136
2104
  deployRailwayService,
2137
- deriveRailwayMarketOperationsRunnerServiceName,
2138
- deriveRailwayMarketOperationsRunnerVolumeName,
2105
+ deriveRailwayOperationsRunnerServiceName,
2106
+ deriveRailwayOperationsRunnerVolumeName,
2139
2107
  deriveRailwayWorkerRunnerServiceName,
2140
2108
  deriveRailwayWorkerRunnerVolumeName,
2141
2109
  ensureRailwayDatabaseServiceExists,
@@ -2144,10 +2112,10 @@ export {
2144
2112
  ensureRailwayProjectExists,
2145
2113
  ensureRailwayScheduledJobs,
2146
2114
  ensureRailwayServiceExists,
2147
- ensureRailwayServiceVolumeWithCliFallback,
2115
+ findStaleTreeseedOperationsRunnerResources,
2148
2116
  isRailwayTransientFailure,
2117
+ isTreeseedOperationsRunnerResourceName,
2149
2118
  isUsableRailwayToken,
2150
- listRailwayServiceVolumesWithCli,
2151
2119
  parseRailwayJsonOutput,
2152
2120
  planRailwayServiceDeploy,
2153
2121
  planRailwayServiceLink,