@treeseed/sdk 0.5.2 → 0.6.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 (66) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +46 -0
  3. package/dist/operations/providers/default.js +1 -1
  4. package/dist/operations/services/config-runtime.d.ts +49 -42
  5. package/dist/operations/services/config-runtime.js +465 -142
  6. package/dist/operations/services/deploy.d.ts +298 -0
  7. package/dist/operations/services/deploy.js +381 -137
  8. package/dist/operations/services/git-workflow.d.ts +9 -0
  9. package/dist/operations/services/git-workflow.js +32 -0
  10. package/dist/operations/services/github-api.d.ts +115 -0
  11. package/dist/operations/services/github-api.js +455 -0
  12. package/dist/operations/services/github-automation.d.ts +19 -33
  13. package/dist/operations/services/github-automation.js +44 -131
  14. package/dist/operations/services/key-agent.d.ts +20 -1
  15. package/dist/operations/services/key-agent.js +267 -102
  16. package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
  17. package/dist/operations/services/knowledge-coop-launch.js +26 -12
  18. package/dist/operations/services/project-platform.d.ts +157 -150
  19. package/dist/operations/services/project-platform.js +129 -26
  20. package/dist/operations/services/railway-api.d.ts +244 -0
  21. package/dist/operations/services/railway-api.js +882 -0
  22. package/dist/operations/services/railway-deploy.d.ts +171 -27
  23. package/dist/operations/services/railway-deploy.js +672 -172
  24. package/dist/operations/services/runtime-tools.d.ts +18 -0
  25. package/dist/operations/services/runtime-tools.js +19 -6
  26. package/dist/operations/services/workspace-preflight.js +2 -2
  27. package/dist/platform/contracts.d.ts +7 -0
  28. package/dist/platform/deploy-config.js +23 -0
  29. package/dist/platform/deploy-runtime.d.ts +1 -0
  30. package/dist/platform/deploy-runtime.js +7 -9
  31. package/dist/platform/env.yaml +10 -9
  32. package/dist/platform/environment.js +4 -0
  33. package/dist/platform/plugin.d.ts +6 -0
  34. package/dist/platform/plugins/constants.d.ts +1 -0
  35. package/dist/platform/plugins/constants.js +1 -0
  36. package/dist/platform/plugins/runtime.d.ts +4 -0
  37. package/dist/platform/plugins/runtime.js +8 -1
  38. package/dist/platform/published-content.js +27 -4
  39. package/dist/platform/tenant/runtime-config.js +33 -24
  40. package/dist/plugin-default.d.ts +1 -0
  41. package/dist/plugin-default.js +1 -0
  42. package/dist/reconcile/builtin-adapters.d.ts +3 -0
  43. package/dist/reconcile/builtin-adapters.js +2093 -0
  44. package/dist/reconcile/contracts.d.ts +155 -0
  45. package/dist/reconcile/contracts.js +0 -0
  46. package/dist/reconcile/desired-state.d.ts +179 -0
  47. package/dist/reconcile/desired-state.js +319 -0
  48. package/dist/reconcile/engine.d.ts +405 -0
  49. package/dist/reconcile/engine.js +356 -0
  50. package/dist/reconcile/errors.d.ts +5 -0
  51. package/dist/reconcile/errors.js +13 -0
  52. package/dist/reconcile/index.d.ts +7 -0
  53. package/dist/reconcile/index.js +7 -0
  54. package/dist/reconcile/registry.d.ts +7 -0
  55. package/dist/reconcile/registry.js +64 -0
  56. package/dist/reconcile/state.d.ts +7 -0
  57. package/dist/reconcile/state.js +303 -0
  58. package/dist/reconcile/units.d.ts +6 -0
  59. package/dist/reconcile/units.js +68 -0
  60. package/dist/scripts/config-treeseed.js +27 -19
  61. package/dist/scripts/tenant-deploy.js +35 -14
  62. package/dist/workflow/operations.js +127 -22
  63. package/dist/workflow-support.d.ts +3 -1
  64. package/dist/workflow-support.js +50 -0
  65. package/dist/workflow.d.ts +2 -0
  66. package/package.json +7 -1
@@ -1,13 +1,37 @@
1
1
  import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { relative, resolve } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { loadCliDeployConfig } from "./runtime-tools.js";
5
- const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
5
+ import { createPersistentDeployTarget, resolveTreeseedResourceIdentity } from "./deploy.js";
6
+ import {
7
+ ensureRailwayEnvironment,
8
+ ensureRailwayProject,
9
+ ensureRailwayService,
10
+ ensureRailwayServiceInstanceConfiguration,
11
+ listRailwayEnvironments,
12
+ listRailwayProjects,
13
+ listRailwayServices,
14
+ normalizeRailwayEnvironmentName,
15
+ railwayGraphqlRequest,
16
+ resolveRailwayApiToken,
17
+ resolveRailwayApiUrl,
18
+ resolveRailwayWorkspace,
19
+ resolveRailwayWorkspaceContext
20
+ } from "./railway-api.js";
6
21
  function normalizeScope(scope) {
7
22
  return scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
8
23
  }
24
+ function resolveRailwayEnvironmentForScope(scope, configuredEnvironment) {
25
+ return normalizeRailwayEnvironmentName(configuredEnvironment || normalizeScope(scope));
26
+ }
9
27
  const RAILWAY_SERVICE_KEYS = ["api", "manager", "worker", "workdayStart", "workdayReport"];
10
28
  const HOSTED_PROJECT_SERVICE_KEYS = ["api", "manager", "worker"];
29
+ function shouldManageRailwaySchedules(scope, phase = "deploy") {
30
+ return phase === "deploy" && normalizeRailwayEnvironmentName(scope) === "production";
31
+ }
32
+ function railwayServiceNameSuffix(serviceKey) {
33
+ return serviceKey === "workdayStart" ? "workday-start" : serviceKey === "workdayReport" ? "workday-report" : serviceKey;
34
+ }
11
35
  function normalizeScheduleExpressions(value) {
12
36
  if (typeof value === "string" && value.trim()) {
13
37
  return [value.trim()];
@@ -21,6 +45,30 @@ function envValue(name) {
21
45
  const value = process.env[name];
22
46
  return typeof value === "string" && value.trim() ? value.trim() : "";
23
47
  }
48
+ function relativeRailwayRootDir(tenantRoot, serviceRoot) {
49
+ const resolved = relative(tenantRoot, serviceRoot).replace(/\\/gu, "/");
50
+ return !resolved || resolved === "" ? "." : resolved;
51
+ }
52
+ function configuredEnvValue(env, name) {
53
+ const value = env?.[name];
54
+ return typeof value === "string" && value.trim() ? value.trim() : "";
55
+ }
56
+ function isUsableRailwayToken(value) {
57
+ return typeof value === "string" && value.trim().length >= 8;
58
+ }
59
+ function resolveRailwayAuthToken(env = process.env) {
60
+ return resolveRailwayApiToken(env);
61
+ }
62
+ function buildRailwayCommandEnv(env = process.env) {
63
+ const merged = { ...env };
64
+ const token = resolveRailwayAuthToken(merged);
65
+ if (token) {
66
+ merged.RAILWAY_API_TOKEN = token;
67
+ } else {
68
+ delete merged.RAILWAY_API_TOKEN;
69
+ }
70
+ return merged;
71
+ }
24
72
  function normalizeRailwaySchedule(schedule) {
25
73
  if (!schedule || typeof schedule !== "object") {
26
74
  return null;
@@ -70,31 +118,46 @@ function collectRailwaySchedules(value, seen = /* @__PURE__ */ new Set()) {
70
118
  visit(value);
71
119
  return matches;
72
120
  }
73
- async function railwayGraphqlRequest({
74
- query,
75
- variables,
76
- apiToken = envValue("RAILWAY_API_TOKEN") || envValue("RAILWAY_TOKEN"),
77
- apiUrl = envValue("TREESEED_RAILWAY_API_URL") || DEFAULT_RAILWAY_API_URL,
78
- fetchImpl = fetch
79
- }) {
80
- if (!apiToken) {
81
- throw new Error("Configure RAILWAY_API_TOKEN before invoking Railway GraphQL APIs.");
82
- }
83
- const response = await fetchImpl(apiUrl, {
84
- method: "POST",
85
- headers: {
86
- authorization: `Bearer ${apiToken}`,
87
- "content-type": "application/json"
88
- },
89
- body: JSON.stringify({ query, variables })
90
- });
91
- const payload = await response.json().catch(() => ({}));
92
- if (!response.ok || Array.isArray(payload?.errors) && payload.errors.length > 0) {
93
- throw new Error(
94
- payload?.errors?.[0]?.message ?? `Railway GraphQL request failed with ${response.status}.`
95
- );
121
+ function normalizeRailwayProjectList(payload) {
122
+ try {
123
+ const parsed = JSON.parse(payload);
124
+ if (!Array.isArray(parsed)) {
125
+ return [];
126
+ }
127
+ return parsed.map((entry) => {
128
+ if (!entry || typeof entry !== "object") {
129
+ return null;
130
+ }
131
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
132
+ const name = typeof entry.name === "string" ? entry.name.trim() : "";
133
+ if (!id && !name) {
134
+ return null;
135
+ }
136
+ return { id, name };
137
+ }).filter(Boolean);
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
142
+ function railwayMessage(result) {
143
+ return `${result?.stderr ?? ""}
144
+ ${result?.stdout ?? ""}`.trim();
145
+ }
146
+ function isRailwayAlreadyExistsMessage(result) {
147
+ return /already exists|already taken|duplicate|has already been taken/iu.test(railwayMessage(result));
148
+ }
149
+ function isRailwayTransientFailure(result) {
150
+ return /timed out|failed to fetch|temporarily unavailable|econnreset|etimedout/iu.test(railwayMessage(result));
151
+ }
152
+ function sleepSync(milliseconds) {
153
+ if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
154
+ return;
96
155
  }
97
- return payload;
156
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
157
+ }
158
+ function isRailwayScheduleCapabilityError(error) {
159
+ const message = error instanceof Error ? error.message : String(error ?? "");
160
+ return /cronTriggers|cronTriggerCreate|cronTriggerUpdate/iu.test(message);
98
161
  }
99
162
  function defaultRailwayScheduleQueries() {
100
163
  return {
@@ -164,27 +227,222 @@ mutation TreeseedScheduleUpdate($id: String!, $name: String!, $schedule: String!
164
227
  `.trim()
165
228
  };
166
229
  }
167
- function runRailway(args, { cwd, capture = false, allowFailure = false } = {}) {
168
- const result = spawnSync("railway", args, {
230
+ function runRailway(args, { cwd, capture = false, allowFailure = false, input, env } = {}) {
231
+ const effectiveEnv = buildRailwayCommandEnv({ ...process.env, ...env ?? {} });
232
+ const runWithEnv = (spawnEnv) => spawnSync("railway", args, {
169
233
  cwd,
170
- stdio: capture ? "pipe" : "inherit",
234
+ stdio: input !== void 0 ? ["pipe", capture ? "pipe" : "inherit", capture ? "pipe" : "inherit"] : capture ? "pipe" : "inherit",
171
235
  encoding: "utf8",
172
- env: { ...process.env }
236
+ env: spawnEnv,
237
+ input
173
238
  });
239
+ const result = runWithEnv(effectiveEnv);
174
240
  if (result.status !== 0 && !allowFailure) {
175
241
  throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${args.join(" ")} failed`);
176
242
  }
177
243
  return result;
178
244
  }
245
+ function shellEscape(value) {
246
+ return `'${String(value).replace(/'/gu, `'\\''`)}'`;
247
+ }
248
+ function setRailwaySecretVariable({ cwd, service, environment, key, value, env = process.env, capture = false, allowFailure = false }) {
249
+ const effectiveEnv = buildRailwayCommandEnv({
250
+ ...process.env,
251
+ ...env ?? {},
252
+ TREESEED_RAILWAY_SECRET_VALUE: value
253
+ });
254
+ const command = [
255
+ 'printf %s\\\\n "$TREESEED_RAILWAY_SECRET_VALUE"',
256
+ "|",
257
+ "railway variable set",
258
+ "--service",
259
+ shellEscape(service),
260
+ "--environment",
261
+ shellEscape(environment),
262
+ "--stdin",
263
+ "--skip-deploys",
264
+ shellEscape(key)
265
+ ].join(" ");
266
+ let result = null;
267
+ for (let attempt = 0; attempt < 3; attempt += 1) {
268
+ result = spawnSync("bash", ["-lc", command], {
269
+ cwd,
270
+ stdio: capture ? "pipe" : "inherit",
271
+ encoding: "utf8",
272
+ env: effectiveEnv
273
+ });
274
+ if (result.status === 0) {
275
+ return result;
276
+ }
277
+ if (!isRailwayTransientFailure(result)) {
278
+ break;
279
+ }
280
+ }
281
+ if (result?.status !== 0 && !allowFailure) {
282
+ throw new Error(result?.stderr?.trim() || result?.stdout?.trim() || `railway variable set --stdin ${key} failed`);
283
+ }
284
+ return result;
285
+ }
286
+ function ensureRailwayProjectExists(service, { env = process.env } = {}) {
287
+ const projectName = typeof service?.projectName === "string" ? service.projectName.trim() : "";
288
+ if (!projectName) {
289
+ throw new Error(`Railway service ${service?.key ?? service?.serviceName ?? service?.serviceId ?? "(unknown)"} is missing a projectName.`);
290
+ }
291
+ const listed = runRailway(["list", "--json"], {
292
+ cwd: service.rootDir,
293
+ capture: true,
294
+ allowFailure: true,
295
+ env
296
+ });
297
+ if (listed.status === 0) {
298
+ const match = normalizeRailwayProjectList(listed.stdout ?? "").find((entry) => entry.name === projectName || entry.id === projectName);
299
+ if (match) {
300
+ return match;
301
+ }
302
+ }
303
+ const args = ["init", "--name", projectName, "--json"];
304
+ const workspace = resolveRailwayWorkspace(env);
305
+ if (workspace) {
306
+ args.push("--workspace", workspace);
307
+ }
308
+ const created = runRailway(args, {
309
+ cwd: service.rootDir,
310
+ capture: true,
311
+ allowFailure: true,
312
+ env
313
+ });
314
+ if (created.status !== 0 && !isRailwayAlreadyExistsMessage(created)) {
315
+ throw new Error(railwayMessage(created) || `railway ${args.join(" ")} failed`);
316
+ }
317
+ return null;
318
+ }
319
+ function ensureRailwayEnvironmentExists(service, { env = process.env } = {}) {
320
+ const environmentName = normalizeRailwayEnvironmentName(service?.railwayEnvironment);
321
+ if (!environmentName) {
322
+ return null;
323
+ }
324
+ const linkResult = runRailway(["environment", "link", environmentName, "--json"], {
325
+ cwd: service.rootDir,
326
+ capture: true,
327
+ allowFailure: true,
328
+ env
329
+ });
330
+ if (linkResult.status === 0) {
331
+ return linkResult;
332
+ }
333
+ const createResult = runRailway(["environment", "new", environmentName, "--json"], {
334
+ cwd: service.rootDir,
335
+ capture: true,
336
+ allowFailure: true,
337
+ env
338
+ });
339
+ if (createResult.status !== 0 && !isRailwayAlreadyExistsMessage(createResult)) {
340
+ throw new Error(railwayMessage(createResult) || `railway environment new ${environmentName} failed`);
341
+ }
342
+ const relinkResult = runRailway(["environment", "link", environmentName, "--json"], {
343
+ cwd: service.rootDir,
344
+ capture: true,
345
+ allowFailure: true,
346
+ env
347
+ });
348
+ if (relinkResult.status !== 0) {
349
+ throw new Error(railwayMessage(relinkResult) || `railway environment link ${environmentName} failed`);
350
+ }
351
+ return relinkResult;
352
+ }
353
+ function ensureRailwayServiceExists(service, { env = process.env } = {}) {
354
+ const serviceSelector = typeof (service?.serviceName ?? service?.serviceId) === "string" ? String(service.serviceName ?? service.serviceId).trim() : "";
355
+ if (!serviceSelector) {
356
+ throw new Error(`Railway service ${service?.key ?? "(unknown)"} is missing a service selector.`);
357
+ }
358
+ const statusArgs = ["service", "status", "--service", serviceSelector, "--environment", service.railwayEnvironment, "--json"];
359
+ const statusResult = runRailway(statusArgs, {
360
+ cwd: service.rootDir,
361
+ capture: true,
362
+ allowFailure: true,
363
+ env
364
+ });
365
+ if (statusResult.status === 0) {
366
+ return statusResult;
367
+ }
368
+ const createResult = runRailway(["add", "--service", serviceSelector, "--json"], {
369
+ cwd: service.rootDir,
370
+ capture: true,
371
+ allowFailure: true,
372
+ env
373
+ });
374
+ if (createResult.status !== 0 && !isRailwayAlreadyExistsMessage(createResult)) {
375
+ throw new Error(railwayMessage(createResult) || `railway add --service ${serviceSelector} failed`);
376
+ }
377
+ const refreshed = runRailway(statusArgs, {
378
+ cwd: service.rootDir,
379
+ capture: true,
380
+ allowFailure: true,
381
+ env
382
+ });
383
+ if (refreshed.status !== 0) {
384
+ throw new Error(railwayMessage(refreshed) || `railway service status --service ${serviceSelector} failed`);
385
+ }
386
+ return refreshed;
387
+ }
388
+ function ensureRailwayProjectContext(service, { env = process.env, allowFailure = false, capture = false } = {}) {
389
+ ensureRailwayProjectExists(service, { env });
390
+ let projectSelector = service?.projectId ?? "";
391
+ if ((!projectSelector || !String(projectSelector).trim()) && service?.projectName) {
392
+ const listed = runRailway(["list", "--json"], {
393
+ cwd: service.rootDir,
394
+ capture: true,
395
+ allowFailure: true,
396
+ env
397
+ });
398
+ if (listed.status === 0) {
399
+ const match = normalizeRailwayProjectList(listed.stdout ?? "").find((entry) => entry.name === service.projectName || entry.id === service.projectName);
400
+ projectSelector = match?.id || match?.name || service.projectName;
401
+ } else {
402
+ projectSelector = service.projectName;
403
+ }
404
+ }
405
+ projectSelector = typeof projectSelector === "string" ? projectSelector.trim() : "";
406
+ if (typeof projectSelector !== "string" || projectSelector.trim().length === 0) {
407
+ throw new Error(`Railway service ${service?.key ?? service?.serviceName ?? service?.serviceId ?? "(unknown)"} is missing a project selector.`);
408
+ }
409
+ const args = ["link", "--project", projectSelector];
410
+ const workspace = resolveRailwayWorkspace(env);
411
+ if (workspace) {
412
+ args.push("--workspace", workspace);
413
+ }
414
+ const environmentName = normalizeRailwayEnvironmentName(service?.railwayEnvironment);
415
+ if (environmentName) {
416
+ args.push("--environment", environmentName);
417
+ }
418
+ runRailway(args, {
419
+ cwd: service.rootDir,
420
+ capture,
421
+ allowFailure,
422
+ env
423
+ });
424
+ if (environmentName) {
425
+ ensureRailwayEnvironmentExists(service, { env });
426
+ return runRailway(["environment", "link", environmentName, "--json"], {
427
+ cwd: service.rootDir,
428
+ capture,
429
+ allowFailure,
430
+ env
431
+ });
432
+ }
433
+ return null;
434
+ }
179
435
  function configuredRailwayServices(tenantRoot, scope) {
180
436
  const deployConfig = loadCliDeployConfig(tenantRoot);
181
437
  const normalizedScope = normalizeScope(scope);
438
+ const identity = resolveTreeseedResourceIdentity(deployConfig, createPersistentDeployTarget(normalizedScope));
182
439
  const managedRuntime = deployConfig.runtime?.mode === "treeseed_managed";
183
440
  const hostingKind = deployConfig.hosting?.kind ?? (managedRuntime ? "hosted_project" : "self_hosted_project");
184
441
  if (!managedRuntime) {
185
442
  return [];
186
443
  }
187
- const serviceKeys = hostingKind === "hosted_project" ? HOSTED_PROJECT_SERVICE_KEYS : RAILWAY_SERVICE_KEYS;
444
+ const configuredOptionalServiceKeys = Object.keys(deployConfig.services ?? {}).filter((serviceKey) => RAILWAY_SERVICE_KEYS.includes(serviceKey));
445
+ const serviceKeys = hostingKind === "hosted_project" ? [.../* @__PURE__ */ new Set([...HOSTED_PROJECT_SERVICE_KEYS, ...configuredOptionalServiceKeys])] : RAILWAY_SERVICE_KEYS;
188
446
  return serviceKeys.map((serviceKey) => {
189
447
  const service = deployConfig.services?.[serviceKey];
190
448
  if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
@@ -192,26 +450,37 @@ function configuredRailwayServices(tenantRoot, scope) {
192
450
  }
193
451
  const defaultRootDir = ["api", "manager", "worker", "workdayStart", "workdayReport"].includes(serviceKey) ? "." : "packages/core";
194
452
  const serviceRoot = resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir);
195
- const railwayEnvironment = service.environments?.[normalizedScope]?.railwayEnvironment ?? normalizedScope;
453
+ const railwayEnvironment = resolveRailwayEnvironmentForScope(
454
+ normalizedScope,
455
+ service.environments?.[normalizedScope]?.railwayEnvironment
456
+ );
196
457
  const publicBaseUrl = service.environments?.[normalizedScope]?.baseUrl ?? service.publicBaseUrl ?? null;
197
458
  return {
198
459
  key: serviceKey,
199
460
  scope: normalizedScope,
200
461
  projectId: service.railway?.projectId ?? null,
201
- projectName: service.railway?.projectName ?? null,
462
+ projectName: service.railway?.projectName ?? identity.deploymentKey,
202
463
  serviceId: service.railway?.serviceId ?? null,
203
- serviceName: service.railway?.serviceName ?? null,
464
+ serviceName: service.railway?.serviceName ?? `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`,
204
465
  rootDir: serviceRoot,
205
466
  publicBaseUrl,
206
467
  railwayEnvironment,
207
468
  buildCommand: service.railway?.buildCommand ?? null,
208
469
  startCommand: service.railway?.startCommand ?? null,
470
+ healthcheckPath: service.railway?.healthcheckPath ?? null,
471
+ healthcheckTimeoutSeconds: service.railway?.healthcheckTimeoutSeconds ?? null,
472
+ healthcheckIntervalSeconds: service.railway?.healthcheckIntervalSeconds ?? null,
473
+ restartPolicy: service.railway?.restartPolicy ?? null,
474
+ runtimeMode: service.railway?.runtimeMode ?? null,
209
475
  schedule: normalizeScheduleExpressions(service.railway?.schedule),
210
476
  hostingKind
211
477
  };
212
478
  }).filter(Boolean);
213
479
  }
214
- function configuredRailwayScheduledJobs(tenantRoot, scope) {
480
+ function configuredRailwayScheduledJobs(tenantRoot, scope, { phase = "deploy" } = {}) {
481
+ if (!shouldManageRailwaySchedules(scope, phase)) {
482
+ return [];
483
+ }
215
484
  return configuredRailwayServices(tenantRoot, scope).filter((service) => Array.isArray(service.schedule) && service.schedule.length > 0).flatMap(
216
485
  (service) => service.schedule.map((expression, index) => ({
217
486
  service: service.key,
@@ -219,7 +488,7 @@ function configuredRailwayScheduledJobs(tenantRoot, scope) {
219
488
  projectName: service.projectName,
220
489
  serviceId: service.serviceId,
221
490
  serviceName: service.serviceName,
222
- environment: service.railwayEnvironment,
491
+ environment: normalizeRailwayEnvironmentName(service.railwayEnvironment),
223
492
  environmentId: envValue("TREESEED_RAILWAY_ENVIRONMENT_ID") || null,
224
493
  expression,
225
494
  command: service.startCommand,
@@ -228,12 +497,59 @@ function configuredRailwayScheduledJobs(tenantRoot, scope) {
228
497
  }))
229
498
  );
230
499
  }
500
+ async function resolveRailwayScheduleTarget(schedule, {
501
+ env = process.env,
502
+ fetchImpl = fetch,
503
+ ensure = false
504
+ } = {}) {
505
+ const workspace = await resolveRailwayWorkspaceContext({ env, fetchImpl });
506
+ const projects = await listRailwayProjects({
507
+ env,
508
+ workspaceId: workspace.id,
509
+ fetchImpl
510
+ });
511
+ let project = projects.find((entry) => entry.id === schedule.projectId || entry.name === schedule.projectName) ?? null;
512
+ if (!project && ensure) {
513
+ project = (await ensureRailwayProject({
514
+ projectId: schedule.projectId,
515
+ projectName: schedule.projectName,
516
+ defaultEnvironmentName: schedule.environment,
517
+ env,
518
+ workspace: workspace.name,
519
+ fetchImpl
520
+ })).project;
521
+ }
522
+ if (!project) {
523
+ return { workspace, project: null, environment: null, service: null };
524
+ }
525
+ let environment = project.environments.find((entry) => entry.id === schedule.environmentId || entry.name === schedule.environment) ?? null;
526
+ if (!environment) {
527
+ environment = ensure ? (await ensureRailwayEnvironment({
528
+ projectId: project.id,
529
+ environmentName: schedule.environment,
530
+ env,
531
+ fetchImpl
532
+ })).environment : (await listRailwayEnvironments({ projectId: project.id, env, fetchImpl })).find((entry) => entry.id === schedule.environmentId || entry.name === schedule.environment) ?? null;
533
+ }
534
+ let service = project.services.find((entry) => entry.id === schedule.serviceId || entry.name === schedule.serviceName) ?? null;
535
+ if (!service) {
536
+ service = ensure ? (await ensureRailwayService({
537
+ projectId: project.id,
538
+ serviceId: schedule.serviceId,
539
+ serviceName: schedule.serviceName,
540
+ env,
541
+ fetchImpl
542
+ })).service : (await listRailwayServices({ projectId: project.id, env, fetchImpl })).find((entry) => entry.id === schedule.serviceId || entry.name === schedule.serviceName) ?? null;
543
+ }
544
+ return { workspace, project, environment, service };
545
+ }
231
546
  function resolveRailwayDeploymentProfile(tenantRoot) {
232
547
  const deployConfig = loadCliDeployConfig(tenantRoot);
233
548
  const hostingKind = deployConfig.hosting?.kind ?? (deployConfig.runtime?.mode === "treeseed_managed" ? "hosted_project" : "self_hosted_project");
549
+ const configuredOptionalServiceKeys = Object.keys(deployConfig.services ?? {}).filter((serviceKey) => RAILWAY_SERVICE_KEYS.includes(serviceKey));
234
550
  return {
235
551
  hostingKind,
236
- managedTopology: deployConfig.runtime?.mode === "treeseed_managed" ? hostingKind === "hosted_project" ? [...HOSTED_PROJECT_SERVICE_KEYS] : [...RAILWAY_SERVICE_KEYS] : []
552
+ managedTopology: deployConfig.runtime?.mode === "treeseed_managed" ? hostingKind === "hosted_project" ? [.../* @__PURE__ */ new Set([...HOSTED_PROJECT_SERVICE_KEYS, ...configuredOptionalServiceKeys])] : [...RAILWAY_SERVICE_KEYS] : []
237
553
  };
238
554
  }
239
555
  function validateRailwayServiceConfiguration(tenantRoot, scope) {
@@ -268,168 +584,245 @@ function validateRailwayServiceConfiguration(tenantRoot, scope) {
268
584
  }
269
585
  return {
270
586
  services,
271
- schedules: configuredRailwayScheduledJobs(tenantRoot, scope),
587
+ schedules: configuredRailwayScheduledJobs(tenantRoot, scope, { phase: "deploy" }),
272
588
  hostingKind,
273
589
  managedTopology
274
590
  };
275
591
  }
276
- function validateRailwayDeployPrerequisites(tenantRoot, scope) {
592
+ function validateRailwayDeployPrerequisites(tenantRoot, scope, { env = process.env } = {}) {
277
593
  const validation = validateRailwayServiceConfiguration(tenantRoot, scope);
278
- const token = process.env.RAILWAY_API_TOKEN;
594
+ const token = resolveRailwayAuthToken(env);
279
595
  if (typeof token !== "string" || token.trim().length === 0) {
280
596
  throw new Error("Configure RAILWAY_API_TOKEN before deploying Railway-managed services.");
281
597
  }
282
598
  return validation;
283
599
  }
284
- async function ensureRailwayScheduledJobs(tenantRoot, scope, { dryRun = false, fetchImpl = fetch, apiToken, apiUrl } = {}) {
285
- const { schedules } = validateRailwayDeployPrerequisites(tenantRoot, scope);
600
+ async function ensureRailwayScheduledJobs(tenantRoot, scope, { dryRun = false, fetchImpl = fetch, apiToken, apiUrl, env = process.env } = {}) {
601
+ const { schedules } = validateRailwayServiceConfiguration(tenantRoot, scope);
602
+ if (schedules.length === 0) {
603
+ return [];
604
+ }
605
+ const effectiveApiToken = apiToken || resolveRailwayAuthToken(env);
606
+ const effectiveApiUrl = apiUrl || resolveRailwayApiUrl(env);
607
+ if (typeof effectiveApiToken !== "string" || effectiveApiToken.trim().length === 0) {
608
+ throw new Error("Configure RAILWAY_API_TOKEN before deploying Railway-managed services.");
609
+ }
286
610
  const queries = defaultRailwayScheduleQueries();
287
611
  const results = [];
288
- for (const schedule of schedules) {
289
- if (!schedule.serviceId || !schedule.environmentId) {
290
- results.push({
291
- ...schedule,
292
- id: null,
293
- status: "skipped_missing_identifiers",
294
- enabled: schedule.enabled !== false,
295
- command: schedule.command
612
+ try {
613
+ for (const schedule of schedules) {
614
+ const target = await resolveRailwayScheduleTarget(schedule, {
615
+ env,
616
+ fetchImpl,
617
+ ensure: !dryRun
296
618
  });
297
- continue;
298
- }
299
- const variables = {
300
- projectId: schedule.projectId,
301
- serviceId: schedule.serviceId,
302
- environmentId: schedule.environmentId
303
- };
304
- const listed = await railwayGraphqlRequest({
305
- query: queries.listQuery,
306
- variables,
307
- apiToken,
308
- apiUrl,
309
- fetchImpl
310
- });
311
- const existing = collectRailwaySchedules(listed?.data).find(
312
- (entry) => entry.id && entry.id === schedule.id || entry.name && entry.name === schedule.logicalName || entry.expression === schedule.expression && entry.serviceId === schedule.serviceId && (!schedule.environmentId || entry.environmentId === schedule.environmentId)
313
- );
314
- const desired = {
315
- name: schedule.logicalName,
316
- schedule: schedule.expression,
317
- command: schedule.command,
318
- enabled: schedule.enabled !== false
319
- };
320
- const drifted = Boolean(
321
- existing && (existing.expression !== desired.schedule || (existing.command ?? null) !== (desired.command ?? null) || existing.enabled !== desired.enabled)
322
- );
323
- if (dryRun) {
324
- results.push({
325
- ...schedule,
326
- id: existing?.id ?? null,
327
- status: existing ? drifted ? "planned_update" : "planned_noop" : "planned_create",
328
- enabled: desired.enabled,
329
- command: desired.command
330
- });
331
- continue;
332
- }
333
- if (!existing) {
334
- const created = await railwayGraphqlRequest({
335
- query: queries.createMutation,
336
- variables: {
337
- ...variables,
338
- name: desired.name,
339
- schedule: desired.schedule,
340
- command: desired.command,
341
- enabled: desired.enabled
342
- },
343
- apiToken,
344
- apiUrl,
345
- fetchImpl
346
- });
347
- const createdSchedule = collectRailwaySchedules(created?.data)[0];
348
- if (!createdSchedule?.id) {
349
- throw new Error(`Railway schedule create did not return an id for ${schedule.logicalName}.`);
619
+ if (!target.project || !target.environment || !target.service) {
620
+ results.push({
621
+ ...schedule,
622
+ id: null,
623
+ projectId: target.project?.id ?? null,
624
+ serviceId: target.service?.id ?? null,
625
+ environmentId: target.environment?.id ?? null,
626
+ status: "skipped_missing_identifiers",
627
+ enabled: schedule.enabled !== false,
628
+ command: schedule.command
629
+ });
630
+ continue;
350
631
  }
351
- results.push({
352
- ...schedule,
353
- id: createdSchedule.id,
354
- status: "created",
355
- enabled: createdSchedule.enabled,
356
- command: createdSchedule.command ?? desired.command
357
- });
358
- continue;
359
- }
360
- if (drifted) {
361
- const updated = await railwayGraphqlRequest({
362
- query: queries.updateMutation,
363
- variables: {
364
- id: existing.id,
365
- name: desired.name,
366
- schedule: desired.schedule,
367
- command: desired.command,
368
- enabled: desired.enabled
369
- },
370
- apiToken,
371
- apiUrl,
632
+ const variables = {
633
+ projectId: target.project.id,
634
+ serviceId: target.service.id,
635
+ environmentId: target.environment.id
636
+ };
637
+ const listed = await railwayGraphqlRequest({
638
+ query: queries.listQuery,
639
+ variables,
640
+ apiToken: effectiveApiToken,
641
+ apiUrl: effectiveApiUrl,
372
642
  fetchImpl
373
643
  });
374
- const updatedSchedule = collectRailwaySchedules(updated?.data)[0];
375
- if (!updatedSchedule?.id) {
376
- throw new Error(`Railway schedule update did not return an id for ${schedule.logicalName}.`);
644
+ const existing = collectRailwaySchedules(listed?.data).find(
645
+ (entry) => entry.id && entry.id === schedule.id || entry.name && entry.name === schedule.logicalName || entry.expression === schedule.expression && entry.serviceId === schedule.serviceId && (!schedule.environmentId || entry.environmentId === schedule.environmentId)
646
+ );
647
+ const desired = {
648
+ name: schedule.logicalName,
649
+ schedule: schedule.expression,
650
+ command: schedule.command,
651
+ enabled: schedule.enabled !== false
652
+ };
653
+ const drifted = Boolean(
654
+ existing && (existing.expression !== desired.schedule || (existing.command ?? null) !== (desired.command ?? null) || existing.enabled !== desired.enabled)
655
+ );
656
+ if (dryRun) {
657
+ results.push({
658
+ ...schedule,
659
+ projectId: variables.projectId,
660
+ id: existing?.id ?? null,
661
+ status: existing ? drifted ? "planned_update" : "planned_noop" : "planned_create",
662
+ enabled: desired.enabled,
663
+ command: desired.command,
664
+ serviceId: variables.serviceId,
665
+ environmentId: variables.environmentId
666
+ });
667
+ continue;
668
+ }
669
+ if (!existing) {
670
+ const created = await railwayGraphqlRequest({
671
+ query: queries.createMutation,
672
+ variables: {
673
+ ...variables,
674
+ name: desired.name,
675
+ schedule: desired.schedule,
676
+ command: desired.command,
677
+ enabled: desired.enabled
678
+ },
679
+ apiToken: effectiveApiToken,
680
+ apiUrl: effectiveApiUrl,
681
+ fetchImpl
682
+ });
683
+ const createdSchedule = collectRailwaySchedules(created?.data)[0];
684
+ if (!createdSchedule?.id) {
685
+ throw new Error(`Railway schedule create did not return an id for ${schedule.logicalName}.`);
686
+ }
687
+ results.push({
688
+ ...schedule,
689
+ projectId: variables.projectId,
690
+ id: createdSchedule.id,
691
+ status: "created",
692
+ enabled: createdSchedule.enabled,
693
+ command: createdSchedule.command ?? desired.command,
694
+ serviceId: variables.serviceId,
695
+ environmentId: variables.environmentId
696
+ });
697
+ continue;
698
+ }
699
+ if (drifted) {
700
+ const updated = await railwayGraphqlRequest({
701
+ query: queries.updateMutation,
702
+ variables: {
703
+ id: existing.id,
704
+ name: desired.name,
705
+ schedule: desired.schedule,
706
+ command: desired.command,
707
+ enabled: desired.enabled
708
+ },
709
+ apiToken: effectiveApiToken,
710
+ apiUrl: effectiveApiUrl,
711
+ fetchImpl
712
+ });
713
+ const updatedSchedule = collectRailwaySchedules(updated?.data)[0];
714
+ if (!updatedSchedule?.id) {
715
+ throw new Error(`Railway schedule update did not return an id for ${schedule.logicalName}.`);
716
+ }
717
+ results.push({
718
+ ...schedule,
719
+ projectId: variables.projectId,
720
+ id: updatedSchedule.id,
721
+ status: "updated",
722
+ enabled: updatedSchedule.enabled,
723
+ command: updatedSchedule.command ?? desired.command,
724
+ serviceId: variables.serviceId,
725
+ environmentId: variables.environmentId
726
+ });
727
+ continue;
377
728
  }
378
729
  results.push({
379
730
  ...schedule,
380
- id: updatedSchedule.id,
381
- status: "updated",
382
- enabled: updatedSchedule.enabled,
383
- command: updatedSchedule.command ?? desired.command
731
+ projectId: variables.projectId,
732
+ id: existing.id,
733
+ status: "noop",
734
+ enabled: existing.enabled,
735
+ command: existing.command ?? desired.command,
736
+ serviceId: variables.serviceId,
737
+ environmentId: variables.environmentId
384
738
  });
385
- continue;
386
739
  }
387
- results.push({
740
+ } catch (error) {
741
+ if (!isRailwayScheduleCapabilityError(error)) {
742
+ throw error;
743
+ }
744
+ return schedules.map((schedule) => ({
388
745
  ...schedule,
389
- id: existing.id,
390
- status: "noop",
391
- enabled: existing.enabled,
392
- command: existing.command ?? desired.command
393
- });
746
+ id: null,
747
+ status: "unsupported",
748
+ enabled: schedule.enabled !== false,
749
+ command: schedule.command,
750
+ message: "Railway GraphQL no longer exposes cron trigger resources for this account. Schedule reconciliation is not currently supported."
751
+ }));
394
752
  }
395
753
  return results;
396
754
  }
397
- async function verifyRailwayScheduledJobs(tenantRoot, scope, { fetchImpl = fetch, apiToken, apiUrl } = {}) {
755
+ async function verifyRailwayScheduledJobs(tenantRoot, scope, { fetchImpl = fetch, apiToken, apiUrl, env = process.env } = {}) {
756
+ const effectiveApiToken = apiToken || env?.RAILWAY_API_TOKEN;
757
+ const effectiveApiUrl = apiUrl || resolveRailwayApiUrl(env);
398
758
  const configured = configuredRailwayScheduledJobs(tenantRoot, scope);
399
759
  const queries = defaultRailwayScheduleQueries();
400
760
  const checks = [];
401
- for (const schedule of configured) {
402
- if (!schedule.serviceId || !schedule.environmentId) {
761
+ try {
762
+ for (const schedule of configured) {
763
+ const target = await resolveRailwayScheduleTarget(schedule, {
764
+ env,
765
+ fetchImpl,
766
+ ensure: false
767
+ });
768
+ if (!target.project || !target.environment || !target.service) {
769
+ checks.push({
770
+ ...schedule,
771
+ id: null,
772
+ ok: false,
773
+ status: "skipped_missing_identifiers",
774
+ message: `Railway schedule target is missing in workspace ${target.workspace.name}.`
775
+ });
776
+ continue;
777
+ }
778
+ const listed = await railwayGraphqlRequest({
779
+ query: queries.listQuery,
780
+ variables: {
781
+ projectId: target.project.id,
782
+ serviceId: target.service.id,
783
+ environmentId: target.environment.id
784
+ },
785
+ apiToken: effectiveApiToken,
786
+ apiUrl: effectiveApiUrl,
787
+ fetchImpl
788
+ });
789
+ const existing = collectRailwaySchedules(listed?.data).find(
790
+ (entry) => entry.name && entry.name === schedule.logicalName || entry.expression === schedule.expression && entry.serviceId === schedule.serviceId && (!schedule.environmentId || entry.environmentId === schedule.environmentId)
791
+ );
403
792
  checks.push({
404
793
  ...schedule,
405
- id: null,
406
- ok: false,
407
- status: "skipped_missing_identifiers"
794
+ id: existing?.id ?? null,
795
+ projectId: target.project.id,
796
+ serviceId: target.service.id,
797
+ environmentId: target.environment.id,
798
+ ok: Boolean(
799
+ existing && existing.expression === schedule.expression && (existing.command ?? null) === (schedule.command ?? null) && existing.enabled !== false
800
+ ),
801
+ status: existing ? "checked" : "missing",
802
+ observed: existing ? {
803
+ expression: existing.expression,
804
+ command: existing.command ?? null,
805
+ enabled: existing.enabled
806
+ } : null,
807
+ message: existing ? void 0 : `Railway schedule ${schedule.logicalName} is missing for ${target.service.name} in ${target.environment.name}.`
408
808
  });
409
- continue;
410
809
  }
411
- const listed = await railwayGraphqlRequest({
412
- query: queries.listQuery,
413
- variables: {
414
- projectId: schedule.projectId,
415
- serviceId: schedule.serviceId,
416
- environmentId: schedule.environmentId
417
- },
418
- apiToken,
419
- apiUrl,
420
- fetchImpl
421
- });
422
- const existing = collectRailwaySchedules(listed?.data).find(
423
- (entry) => entry.name && entry.name === schedule.logicalName || entry.expression === schedule.expression && entry.serviceId === schedule.serviceId && (!schedule.environmentId || entry.environmentId === schedule.environmentId)
424
- );
425
- checks.push({
426
- ...schedule,
427
- id: existing?.id ?? null,
428
- ok: Boolean(
429
- existing && existing.expression === schedule.expression && (existing.command ?? null) === (schedule.command ?? null) && existing.enabled !== false
430
- ),
431
- status: existing ? "checked" : "missing"
432
- });
810
+ } catch (error) {
811
+ if (!isRailwayScheduleCapabilityError(error)) {
812
+ throw error;
813
+ }
814
+ return {
815
+ ok: true,
816
+ unsupported: true,
817
+ message: "Railway GraphQL no longer exposes cron trigger resources for this account. Schedule verification is skipped.",
818
+ checks: configured.map((schedule) => ({
819
+ ...schedule,
820
+ id: null,
821
+ ok: true,
822
+ status: "unsupported",
823
+ message: "Railway GraphQL no longer exposes cron trigger resources for this account. Schedule verification is skipped."
824
+ }))
825
+ };
433
826
  }
434
827
  return {
435
828
  ok: checks.every((entry) => entry.ok === true),
@@ -438,8 +831,9 @@ async function verifyRailwayScheduledJobs(tenantRoot, scope, { fetchImpl = fetch
438
831
  }
439
832
  function planRailwayServiceDeploy(service) {
440
833
  const args = ["up", "--service", service.serviceName ?? service.serviceId, "--ci"];
441
- if (service.railwayEnvironment) {
442
- args.push("--environment", service.railwayEnvironment);
834
+ const environmentName = normalizeRailwayEnvironmentName(service.railwayEnvironment);
835
+ if (environmentName) {
836
+ args.push("--environment", environmentName);
443
837
  }
444
838
  return {
445
839
  command: "railway",
@@ -447,7 +841,66 @@ function planRailwayServiceDeploy(service) {
447
841
  cwd: service.rootDir
448
842
  };
449
843
  }
450
- function deployRailwayService(tenantRoot, service, { dryRun = false } = {}) {
844
+ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, service, { env = process.env } = {}) {
845
+ 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;
846
+ if (!wantsInstanceConfig) {
847
+ return null;
848
+ }
849
+ const workspace = await resolveRailwayWorkspaceContext({ env });
850
+ let project = null;
851
+ if (service.projectId) {
852
+ project = await ensureRailwayProject({
853
+ projectId: service.projectId,
854
+ projectName: service.projectName,
855
+ defaultEnvironmentName: service.railwayEnvironment,
856
+ env,
857
+ workspace: workspace.id
858
+ }).then((result) => result.project);
859
+ } else {
860
+ const projects = await listRailwayProjects({ env, workspaceId: workspace.id });
861
+ project = projects.find((entry) => entry.name === service.projectName) ?? null;
862
+ if (!project) {
863
+ project = await ensureRailwayProject({
864
+ projectName: service.projectName,
865
+ defaultEnvironmentName: service.railwayEnvironment,
866
+ env,
867
+ workspace: workspace.id
868
+ }).then((result) => result.project);
869
+ }
870
+ }
871
+ const environmentName = normalizeRailwayEnvironmentName(service.railwayEnvironment);
872
+ let environment = project.environments.find((entry) => entry.name === environmentName || entry.id === environmentName) ?? null;
873
+ if (!environment) {
874
+ environment = await ensureRailwayEnvironment({
875
+ projectId: project.id,
876
+ environmentName,
877
+ env
878
+ }).then((result) => result.environment);
879
+ }
880
+ let railwayService = project.services.find((entry) => entry.id === service.serviceId || entry.name === service.serviceName) ?? null;
881
+ if (!railwayService) {
882
+ railwayService = await ensureRailwayService({
883
+ projectId: project.id,
884
+ serviceId: service.serviceId,
885
+ serviceName: service.serviceName,
886
+ env
887
+ }).then((result) => result.service);
888
+ }
889
+ return await ensureRailwayServiceInstanceConfiguration({
890
+ serviceId: railwayService.id,
891
+ environmentId: environment.id,
892
+ buildCommand: service.buildCommand,
893
+ startCommand: service.startCommand,
894
+ rootDirectory: relativeRailwayRootDir(tenantRoot, service.rootDir),
895
+ healthcheckPath: service.healthcheckPath,
896
+ healthcheckTimeoutSeconds: service.healthcheckTimeoutSeconds,
897
+ healthcheckIntervalSeconds: service.healthcheckIntervalSeconds,
898
+ restartPolicy: service.restartPolicy,
899
+ runtimeMode: service.runtimeMode,
900
+ env
901
+ });
902
+ }
903
+ async function deployRailwayService(tenantRoot, service, { dryRun = false } = {}) {
451
904
  const plan = planRailwayServiceDeploy(service);
452
905
  if (dryRun) {
453
906
  return {
@@ -468,22 +921,69 @@ function deployRailwayService(tenantRoot, service, { dryRun = false } = {}) {
468
921
  throw new Error(`Railway ${service.key} build command failed.`);
469
922
  }
470
923
  }
471
- runRailway(plan.args, { cwd: service.rootDir });
924
+ let lastFailure = null;
925
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
926
+ const result = runRailway(plan.args, {
927
+ cwd: service.rootDir,
928
+ capture: true,
929
+ allowFailure: true
930
+ });
931
+ if (result.stdout) {
932
+ process.stdout.write(result.stdout);
933
+ }
934
+ if (result.stderr) {
935
+ process.stderr.write(result.stderr);
936
+ }
937
+ if (result.status === 0) {
938
+ lastFailure = null;
939
+ break;
940
+ }
941
+ lastFailure = result;
942
+ if (!isRailwayTransientFailure(result) || attempt === 5) {
943
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${plan.args.join(" ")} failed`);
944
+ }
945
+ const backoffMs = 5e3 * attempt;
946
+ console.warn(
947
+ `Railway deploy for ${service.serviceName ?? service.serviceId ?? service.key} hit a transient failure; retrying in ${Math.round(backoffMs / 1e3)}s...`
948
+ );
949
+ sleepSync(backoffMs);
950
+ }
951
+ if (lastFailure) {
952
+ throw new Error(lastFailure.stderr?.trim() || lastFailure.stdout?.trim() || `railway ${plan.args.join(" ")} failed`);
953
+ }
954
+ const runtimeConfiguration = await syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, service, {
955
+ env: buildRailwayCommandEnv(process.env)
956
+ });
472
957
  return {
473
958
  service: service.key,
474
959
  status: "deployed",
475
960
  command: [plan.command, ...plan.args].join(" "),
476
961
  cwd: plan.cwd,
477
- publicBaseUrl: service.publicBaseUrl
962
+ publicBaseUrl: service.publicBaseUrl,
963
+ runtimeConfiguration: runtimeConfiguration ? {
964
+ updated: runtimeConfiguration.updated,
965
+ healthcheckPath: runtimeConfiguration.instance.healthcheckPath,
966
+ healthcheckTimeoutSeconds: runtimeConfiguration.instance.healthcheckTimeoutSeconds,
967
+ runtimeMode: runtimeConfiguration.instance.runtimeMode
968
+ } : null
478
969
  };
479
970
  }
480
971
  export {
972
+ buildRailwayCommandEnv,
481
973
  configuredRailwayScheduledJobs,
482
974
  configuredRailwayServices,
483
975
  deployRailwayService,
976
+ ensureRailwayEnvironmentExists,
977
+ ensureRailwayProjectContext,
978
+ ensureRailwayProjectExists,
484
979
  ensureRailwayScheduledJobs,
980
+ ensureRailwayServiceExists,
981
+ isUsableRailwayToken,
485
982
  planRailwayServiceDeploy,
983
+ resolveRailwayAuthToken,
486
984
  resolveRailwayDeploymentProfile,
985
+ runRailway,
986
+ setRailwaySecretVariable,
487
987
  validateRailwayDeployPrerequisites,
488
988
  validateRailwayServiceConfiguration,
489
989
  verifyRailwayScheduledJobs