@treeseed/sdk 0.12.60 → 0.12.61

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 (44) hide show
  1. package/dist/guarantees/index.js +59 -56
  2. package/dist/hosting/contracts.d.ts +0 -19
  3. package/dist/hosting/graph.d.ts +1 -86
  4. package/dist/hosting/graph.js +96 -245
  5. package/dist/local-dev/managed-dev.js +30 -8
  6. package/dist/managed-dependencies.d.ts +3 -0
  7. package/dist/managed-dependencies.js +294 -20
  8. package/dist/operations/services/deploy.js +5 -5
  9. package/dist/operations/services/deployment-readiness.js +5 -4
  10. package/dist/operations/services/git-runner.d.ts +2 -0
  11. package/dist/operations/services/git-runner.js +23 -2
  12. package/dist/operations/services/hosted-service-checks.js +28 -0
  13. package/dist/operations/services/live-hosted-service-checks.js +51 -15
  14. package/dist/operations/services/local-cleanup.d.ts +1 -0
  15. package/dist/operations/services/local-cleanup.js +28 -9
  16. package/dist/operations/services/package-adapters.js +3 -3
  17. package/dist/operations/services/railway-api.d.ts +72 -28
  18. package/dist/operations/services/railway-api.js +321 -876
  19. package/dist/operations/services/railway-cli.d.ts +47 -0
  20. package/dist/operations/services/railway-cli.js +142 -0
  21. package/dist/operations/services/railway-deploy.d.ts +2 -2
  22. package/dist/operations/services/railway-deploy.js +36 -91
  23. package/dist/operations/services/railway-source-policy.d.ts +6 -0
  24. package/dist/operations/services/railway-source-policy.js +52 -9
  25. package/dist/operations/services/repository-save-orchestrator.d.ts +2 -0
  26. package/dist/operations/services/repository-save-orchestrator.js +45 -14
  27. package/dist/operations-types.d.ts +3 -1
  28. package/dist/platform/contracts.d.ts +1 -0
  29. package/dist/platform/deploy-config.js +2 -1
  30. package/dist/reconcile/builtin-adapters.js +519 -684
  31. package/dist/reconcile/desired-state.js +5 -3
  32. package/dist/reconcile/engine.js +34 -28
  33. package/dist/reconcile/live-acceptance.js +2 -11
  34. package/dist/reconcile/providers/railway-iac.d.ts +147 -0
  35. package/dist/reconcile/providers/railway-iac.js +289 -16
  36. package/dist/scenes/runner.js +11 -11
  37. package/dist/scripts/build-dist.js +22 -0
  38. package/dist/workflow/operations.d.ts +12 -0
  39. package/dist/workflow/operations.js +265 -90
  40. package/dist/workflow/runs.d.ts +4 -0
  41. package/dist/workflow/runs.js +4 -0
  42. package/dist/workflow-support.d.ts +1 -1
  43. package/dist/workflow-support.js +3 -1
  44. package/package.json +1 -2
@@ -1,7 +1,28 @@
1
- import { request as httpsRequest } from "node:https";
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { resolve } from "node:path";
4
+ import { IacClient } from "railway";
5
+ import { connectRailwayServiceSourceWithCli, runRailwayCliJson } from "./railway-cli.js";
2
6
  import { resolveTreeseedRailwayApiToken } from "../../service-credentials.js";
3
7
  const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
4
8
  const DEFAULT_RAILWAY_WORKSPACE = "knowledge-coop";
9
+ let railwayReadActive = false;
10
+ const railwayReadWaiters = [];
11
+ let railwayReadCooldownUntil = 0;
12
+ async function acquireRailwayReadSlot() {
13
+ if (railwayReadActive) await new Promise((resolve2) => railwayReadWaiters.push(resolve2));
14
+ railwayReadActive = true;
15
+ const cooldownMs = Math.max(0, railwayReadCooldownUntil - Date.now());
16
+ if (cooldownMs > 0) await new Promise((resolve2) => setTimeout(resolve2, cooldownMs));
17
+ return () => {
18
+ const next = railwayReadWaiters.shift();
19
+ if (next) next();
20
+ else railwayReadActive = false;
21
+ };
22
+ }
23
+ function extendRailwayReadCooldown(delayMs) {
24
+ railwayReadCooldownUntil = Math.max(railwayReadCooldownUntil, Date.now() + Math.max(0, delayMs));
25
+ }
5
26
  function normalizeRailwayEnvironmentName(value) {
6
27
  const normalized = typeof value === "string" ? value.trim() : "";
7
28
  if (!normalized) {
@@ -9,6 +30,20 @@ function normalizeRailwayEnvironmentName(value) {
9
30
  }
10
31
  return normalized === "prod" ? "production" : normalized;
11
32
  }
33
+ function createRailwayEnvironmentPatchClient({
34
+ env,
35
+ fetchImpl
36
+ }) {
37
+ const token = resolveRailwayApiToken(env);
38
+ if (!token) {
39
+ throw new Error("Railway API token is required for environment reconciliation.");
40
+ }
41
+ return new IacClient({
42
+ token,
43
+ endpoint: resolveRailwayApiUrl(env),
44
+ fetch: fetchImpl
45
+ });
46
+ }
12
47
  function configuredEnvValue(env, name) {
13
48
  const value = env?.[name];
14
49
  return typeof value === "string" && value.trim() ? value.trim() : "";
@@ -55,6 +90,7 @@ function parseRetryAfterMs(value) {
55
90
  function markRailwayTransientError(error, options = {}) {
56
91
  const tagged = error;
57
92
  tagged.treeseedTransient = true;
93
+ if (options.rateLimited) tagged.treeseedRateLimited = true;
58
94
  if (typeof options.retryAfterMs === "number" && Number.isFinite(options.retryAfterMs) && options.retryAfterMs >= 0) {
59
95
  tagged.treeseedRetryAfterMs = options.retryAfterMs;
60
96
  }
@@ -270,6 +306,24 @@ function normalizeVolumeInstances(value) {
270
306
  }
271
307
  return normalizeConnectionNodes(value, normalizeRailwayVolumeInstance);
272
308
  }
309
+ function mergeRailwayVolumeInstances(instances) {
310
+ const byId = /* @__PURE__ */ new Map();
311
+ for (const instance of instances) {
312
+ const existing = byId.get(instance.id);
313
+ byId.set(instance.id, existing ? {
314
+ ...existing,
315
+ serviceId: existing.serviceId || instance.serviceId,
316
+ environmentId: existing.environmentId || instance.environmentId,
317
+ mountPath: existing.mountPath || instance.mountPath,
318
+ state: [existing.state, instance.state].find((state) => /^(?:DELETED|DELETING)$/u.test(String(state ?? "").toUpperCase())) ?? existing.state ?? instance.state,
319
+ isPendingDeletion: existing.isPendingDeletion || instance.isPendingDeletion,
320
+ deletedAt: existing.deletedAt || instance.deletedAt,
321
+ sizeGb: existing.sizeGb ?? instance.sizeGb,
322
+ usedGb: existing.usedGb ?? instance.usedGb
323
+ } : instance);
324
+ }
325
+ return [...byId.values()];
326
+ }
273
327
  function normalizeRailwayVolume(node) {
274
328
  const id = railwayConnectionLabel(node.id);
275
329
  if (!id) {
@@ -279,11 +333,11 @@ function normalizeRailwayVolume(node) {
279
333
  id,
280
334
  name: railwayConnectionLabel(node.name),
281
335
  projectId: railwayConnectionLabel(node.projectId) || null,
282
- instances: [
336
+ instances: mergeRailwayVolumeInstances([
283
337
  ...normalizeVolumeInstances(node.instances),
284
338
  ...normalizeVolumeInstances(node.volumeInstances),
285
339
  ...normalizeVolumeInstances(node.volume_instances)
286
- ]
340
+ ])
287
341
  };
288
342
  }
289
343
  function collectRailwayVolumes(value, seen = /* @__PURE__ */ new Set()) {
@@ -319,7 +373,7 @@ function collectRailwayVolumes(value, seen = /* @__PURE__ */ new Set()) {
319
373
  ...existing,
320
374
  name: existing.name || volume.name,
321
375
  projectId: existing.projectId || volume.projectId,
322
- instances: [...existing.instances, ...volume.instances]
376
+ instances: mergeRailwayVolumeInstances([...existing.instances, ...volume.instances])
323
377
  } : volume);
324
378
  }
325
379
  return [...byId.values()];
@@ -329,7 +383,13 @@ function railwayApiTimeoutMs(env, explicitTimeoutMs) {
329
383
  return explicitTimeoutMs;
330
384
  }
331
385
  const configured = Number.parseInt(String(env.TREESEED_RAILWAY_API_TIMEOUT_MS ?? "").trim(), 10);
332
- return Number.isFinite(configured) && configured > 0 ? configured : 12e4;
386
+ return Number.isFinite(configured) && configured > 0 ? Math.min(configured, 15e3) : 15e3;
387
+ }
388
+ function assertRailwayGraphqlReadOnly(document) {
389
+ const operation = document.replace(/^\uFEFF/u, "").replace(/#[^\r\n]*/gu, "").trimStart();
390
+ if (!operation.startsWith("query ") && !operation.startsWith("query\n") && !operation.startsWith("{")) {
391
+ throw new Error("Direct Railway GraphQL is read-only. Use the official Railway SDK/IaC client, or the managed Railway CLI when the public SDK lacks the operation.");
392
+ }
333
393
  }
334
394
  async function railwayGraphqlRequest({
335
395
  query,
@@ -339,19 +399,26 @@ async function railwayGraphqlRequest({
339
399
  apiUrl,
340
400
  fetchImpl = fetch,
341
401
  timeoutMs,
342
- retries = 4
402
+ retries = 3
343
403
  }) {
404
+ assertRailwayGraphqlReadOnly(query);
344
405
  const token = apiToken || resolveRailwayApiToken(env);
345
406
  if (!token) {
346
407
  throw new Error("Configure TREESEED_RAILWAY_API_TOKEN before invoking Railway APIs.");
347
408
  }
348
409
  const requestTimeoutMs = railwayApiTimeoutMs(env, timeoutMs);
410
+ if (env.TREESEED_RECONCILE_TRACE === "1") {
411
+ process.stderr.write(`[trsd][railway][api:request] timeoutMs=${requestTimeoutMs} retries=${retries}
412
+ `);
413
+ }
349
414
  let attempt = 0;
350
415
  for (; ; ) {
351
416
  const controller = new AbortController();
352
417
  let timer = null;
418
+ let releaseReadSlot = null;
353
419
  try {
354
- const response = fetchImpl === fetch ? await railwayGraphqlHttpsRequest(apiUrl || resolveRailwayApiUrl(env), token, { query, variables }, requestTimeoutMs) : await Promise.race([
420
+ releaseReadSlot = await acquireRailwayReadSlot();
421
+ const response = await Promise.race([
355
422
  fetchImpl(apiUrl || resolveRailwayApiUrl(env), {
356
423
  method: "POST",
357
424
  headers: {
@@ -381,7 +448,9 @@ async function railwayGraphqlRequest({
381
448
  const shouldRetry = isRetryableRailwayStatus(response.status) || /rate limit|too many requests/iu.test(message);
382
449
  const error = new Error(message);
383
450
  if (shouldRetry || hasGraphqlErrors && /rate limit|too many requests/iu.test(message)) {
384
- throw markRailwayTransientError(error, { retryAfterMs });
451
+ const rateLimited = response.status === 429 || /rate limit|too many requests/iu.test(message);
452
+ if (rateLimited) extendRailwayReadCooldown(retryAfterMs ?? 15e3);
453
+ throw markRailwayTransientError(error, { retryAfterMs, rateLimited });
385
454
  }
386
455
  throw error;
387
456
  }
@@ -392,76 +461,28 @@ async function railwayGraphqlRequest({
392
461
  }
393
462
  attempt += 1;
394
463
  const retryAfterMs = error && typeof error === "object" && typeof error.treeseedRetryAfterMs === "number" ? Math.max(0, Number(error.treeseedRetryAfterMs)) : null;
395
- const backoffMs = retryAfterMs ?? Math.min(500 * 2 ** (attempt - 1), 4e3);
396
- await new Promise((resolve) => setTimeout(resolve, backoffMs));
464
+ const rateLimited = error && typeof error === "object" && error.treeseedRateLimited === true;
465
+ const backoffMs = retryAfterMs !== null ? Math.min(retryAfterMs, 18e4) : rateLimited ? [15e3, 45e3, 9e4][attempt - 1] ?? 9e4 : Math.min(500 * 2 ** (attempt - 1), 4e3);
466
+ if (rateLimited) process.stderr.write(`[trsd][railway][api:rate-limit] attempt=${attempt + 1} waitMs=${backoffMs}
467
+ `);
468
+ let remainingMs = backoffMs;
469
+ while (remainingMs > 0) {
470
+ const sliceMs = Math.min(15e3, remainingMs);
471
+ await new Promise((resolve2) => setTimeout(resolve2, sliceMs));
472
+ remainingMs -= sliceMs;
473
+ if (rateLimited && remainingMs > 0) {
474
+ process.stderr.write(`[trsd][railway][api:rate-limit] cooldownRemainingMs=${remainingMs}
475
+ `);
476
+ }
477
+ }
397
478
  } finally {
479
+ releaseReadSlot?.();
398
480
  if (timer) {
399
481
  clearTimeout(timer);
400
482
  }
401
483
  }
402
484
  }
403
485
  }
404
- async function railwayGraphqlHttpsRequest(url, token, body, timeoutMs) {
405
- const rawBody = JSON.stringify(body);
406
- return new Promise((resolve, reject) => {
407
- let settled = false;
408
- let req = null;
409
- const hardTimeout = setTimeout(() => {
410
- if (settled) return;
411
- settled = true;
412
- const error = markRailwayTransientError(new Error(`Railway API request timed out after ${timeoutMs}ms.`));
413
- if (req) {
414
- req.destroy(error);
415
- }
416
- reject(error);
417
- }, timeoutMs);
418
- const finish = (callback) => {
419
- if (settled) return;
420
- settled = true;
421
- clearTimeout(hardTimeout);
422
- callback();
423
- };
424
- req = httpsRequest(url, {
425
- method: "POST",
426
- headers: {
427
- authorization: `Bearer ${token}`,
428
- "content-type": "application/json",
429
- "content-length": Buffer.byteLength(rawBody)
430
- },
431
- timeout: timeoutMs
432
- }, (res) => {
433
- const chunks = [];
434
- res.setEncoding("utf8");
435
- res.on("data", (chunk) => chunks.push(chunk));
436
- res.on("end", () => {
437
- const text = chunks.join("");
438
- let payload = {};
439
- try {
440
- payload = text ? JSON.parse(text) : {};
441
- } catch {
442
- payload = {};
443
- }
444
- const status = res.statusCode ?? 0;
445
- const retryAfterHeader = res.headers["retry-after"];
446
- finish(() => resolve({
447
- ok: status >= 200 && status < 300,
448
- status,
449
- payload,
450
- retryAfter: Array.isArray(retryAfterHeader) ? retryAfterHeader[0] ?? null : retryAfterHeader ?? null
451
- }));
452
- });
453
- });
454
- req.on("timeout", () => {
455
- const error = markRailwayTransientError(new Error(`Railway API request timed out after ${timeoutMs}ms.`));
456
- if (!settled) {
457
- req?.destroy(error);
458
- }
459
- });
460
- req.on("error", (error) => finish(() => reject(error)));
461
- req.write(rawBody);
462
- req.end();
463
- });
464
- }
465
486
  async function getRailwayAuthProfile({
466
487
  env = process.env,
467
488
  fetchImpl = fetch
@@ -606,44 +627,14 @@ async function ensureRailwayProject({
606
627
  if (!desiredProjectName) {
607
628
  throw new Error("Railway project creation requires a project name.");
608
629
  }
609
- const created = await railwayGraphqlRequest({
610
- query: `
611
- mutation TreeseedRailwayProjectCreate($input: ProjectCreateInput!) {
612
- projectCreate(input: $input) {
613
- id
614
- name
615
- workspaceId
616
- deletedAt
617
- environments(first: 50) {
618
- edges {
619
- node {
620
- id
621
- name
622
- }
623
- }
624
- }
625
- services(first: 50) {
626
- edges {
627
- node {
628
- id
629
- name
630
- }
631
- }
632
- }
633
- }
634
- }
635
- `.trim(),
636
- variables: {
637
- input: {
638
- name: desiredProjectName,
639
- workspaceId: workspaceContext.id,
640
- defaultEnvironmentName
641
- }
642
- },
643
- env,
644
- fetchImpl
645
- });
646
- const project = created.data?.projectCreate ? normalizeProject(created.data.projectCreate) : null;
630
+ void defaultEnvironmentName;
631
+ const tempRoot = mkdtempSync(resolve(tmpdir(), "treeseed-railway-init-"));
632
+ try {
633
+ await runRailwayCliJson({ args: ["init", "--name", desiredProjectName, "--workspace", workspaceContext.id, "--json"], env, cwd: tempRoot });
634
+ } finally {
635
+ rmSync(tempRoot, { recursive: true, force: true });
636
+ }
637
+ const project = (await listRailwayProjects({ env, workspaceId: workspaceContext.id, fetchImpl })).find((entry) => entry.name === desiredProjectName && !entry.deletedAt) ?? null;
647
638
  if (!project) {
648
639
  throw new Error(`Railway project create did not return a usable project for ${desiredProjectName}.`);
649
640
  }
@@ -660,26 +651,14 @@ async function ensureRailwayEnvironment({
660
651
  if (existing) {
661
652
  return { environment: existing, created: false };
662
653
  }
663
- const created = await railwayGraphqlRequest({
664
- query: `
665
- mutation TreeseedRailwayEnvironmentCreate($input: EnvironmentCreateInput!) {
666
- environmentCreate(input: $input) {
667
- id
668
- name
669
- }
670
- }
671
- `.trim(),
672
- variables: {
673
- input: {
674
- projectId,
675
- name: environmentName,
676
- skipInitialDeploys: true
677
- }
678
- },
679
- env,
680
- fetchImpl
681
- });
682
- const environment = created.data?.environmentCreate ? normalizeEnvironment(created.data.environmentCreate) : null;
654
+ const tempRoot = mkdtempSync(resolve(tmpdir(), "treeseed-railway-environment-"));
655
+ try {
656
+ await runRailwayCliJson({ args: ["link", projectId, "--json"], env, cwd: tempRoot });
657
+ await runRailwayCliJson({ args: ["environment", "new", environmentName, "--json"], env, cwd: tempRoot });
658
+ } finally {
659
+ rmSync(tempRoot, { recursive: true, force: true });
660
+ }
661
+ const environment = (await listRailwayEnvironments({ projectId, env, fetchImpl })).find((entry) => entry.name === environmentName) ?? null;
683
662
  if (!environment) {
684
663
  throw new Error(`Railway environment create did not return a usable environment for ${environmentName}.`);
685
664
  }
@@ -774,7 +753,9 @@ async function ensureRailwayService({
774
753
  if (desiredSourceRepo) {
775
754
  try {
776
755
  await updateRailwayServiceGitSource({
756
+ projectId,
777
757
  serviceId: existing.id,
758
+ environmentId,
778
759
  sourceRepo: desiredSourceRepo,
779
760
  sourceBranch,
780
761
  env,
@@ -793,7 +774,9 @@ async function ensureRailwayService({
793
774
  if (desiredImageRef) {
794
775
  try {
795
776
  await updateRailwayServiceImageSource({
777
+ projectId,
796
778
  serviceId: existing.id,
779
+ environmentId,
797
780
  imageRef: desiredImageRef,
798
781
  env,
799
782
  fetchImpl
@@ -836,36 +819,27 @@ async function createRailwayImageService({
836
819
  }) {
837
820
  const desiredSourceRepo = railwayConnectionLabel(sourceRepo);
838
821
  const desiredImageRef = railwayConnectionLabel(imageRef);
839
- const created = await railwayGraphqlRequest({
840
- query: `
841
- mutation TreeseedRailwayServiceCreate($input: ServiceCreateInput!) {
842
- serviceCreate(input: $input) {
843
- id
844
- name
845
- }
846
- }
847
- `.trim(),
848
- variables: {
849
- input: {
850
- projectId,
851
- name: serviceName,
852
- ...railwayConnectionLabel(environmentId) ? { environmentId: railwayConnectionLabel(environmentId) } : {},
853
- ...desiredSourceRepo ? {
854
- source: {
855
- repo: desiredSourceRepo
856
- },
857
- ...railwayConnectionLabel(sourceBranch) ? { branch: railwayConnectionLabel(sourceBranch) } : {}
858
- } : desiredImageRef ? {
859
- source: {
860
- image: desiredImageRef
861
- }
862
- } : {}
822
+ const targetEnvironmentId = railwayConnectionLabel(environmentId);
823
+ if (!targetEnvironmentId) throw new Error(`Railway service creation requires an environment id for ${serviceName}.`);
824
+ const client = createRailwayEnvironmentPatchClient({ env, fetchImpl });
825
+ await client.stageEnvironmentChanges({
826
+ environmentId: targetEnvironmentId,
827
+ merge: true,
828
+ patch: {
829
+ services: {
830
+ [serviceName]: {
831
+ isCreated: true,
832
+ source: desiredSourceRepo ? { repo: desiredSourceRepo, branch: railwayConnectionLabel(sourceBranch) || null, image: null } : desiredImageRef ? { image: desiredImageRef, repo: null, branch: null } : null
833
+ }
863
834
  }
864
- },
865
- env,
866
- fetchImpl
835
+ }
836
+ });
837
+ await client.commitStagedPatch({
838
+ environmentId: targetEnvironmentId,
839
+ message: `Treeseed create service ${serviceName}`,
840
+ skipDeploys: true
867
841
  });
868
- const service = created.data?.serviceCreate ? normalizeService(created.data.serviceCreate) : null;
842
+ const service = (await listRailwayServices({ projectId, env, fetchImpl })).find((entry) => entry.name === serviceName) ?? null;
869
843
  if (!service) {
870
844
  throw new Error(`Railway service create did not return a usable service for ${serviceName}.`);
871
845
  }
@@ -876,87 +850,50 @@ function looksLikeRailwayImageSourceUpdateUnsupported(error) {
876
850
  return /Problem processing request|source|image|ServiceUpdateInput/iu.test(message);
877
851
  }
878
852
  async function updateRailwayServiceImageSource({
853
+ projectId,
879
854
  serviceId,
855
+ environmentId,
880
856
  imageRef,
881
- env = process.env,
882
- fetchImpl = fetch
857
+ env = process.env
883
858
  }) {
884
859
  const desiredImage = railwayConnectionLabel(imageRef);
885
860
  if (!serviceId || !desiredImage) {
886
861
  throw new Error("Railway service image source update requires a service id and image reference.");
887
862
  }
888
- const payload = await railwayGraphqlRequest({
889
- query: `
890
- mutation TreeseedRailwayServiceImageSourceUpdate($id: String!, $input: ServiceConnectInput!) {
891
- serviceConnect(id: $id, input: $input) {
892
- id
893
- name
894
- }
895
- }
896
- `.trim(),
897
- variables: {
898
- id: serviceId,
899
- input: {
900
- image: desiredImage
901
- }
902
- },
903
- env,
904
- fetchImpl
863
+ const targetEnvironmentId = railwayConnectionLabel(environmentId);
864
+ if (!targetEnvironmentId) throw new Error(`Railway service image source update requires an environment id for ${serviceId}.`);
865
+ await connectRailwayServiceSourceWithCli({
866
+ projectId,
867
+ environmentId: targetEnvironmentId,
868
+ serviceId,
869
+ image: desiredImage,
870
+ env
905
871
  });
906
- const service = payload.data?.serviceConnect ? normalizeService(payload.data.serviceConnect) : null;
907
- if (!service) {
908
- throw new Error(`Railway service image source update did not return a usable service for ${serviceId}.`);
909
- }
910
- return service;
872
+ return { id: serviceId, name: serviceId };
911
873
  }
912
874
  async function updateRailwayServiceGitSource({
875
+ projectId,
913
876
  serviceId,
877
+ environmentId,
914
878
  sourceRepo,
915
879
  sourceBranch,
916
- env = process.env,
917
- fetchImpl = fetch
880
+ env = process.env
918
881
  }) {
919
882
  const desiredRepo = railwayConnectionLabel(sourceRepo);
920
883
  if (!serviceId || !desiredRepo) {
921
884
  throw new Error("Railway service Git source update requires a service id and repository slug.");
922
885
  }
923
- const connect = async (repo) => {
924
- const payload2 = await railwayGraphqlRequest({
925
- query: `
926
- mutation TreeseedRailwayServiceGitSourceUpdate($id: String!, $input: ServiceConnectInput!) {
927
- serviceConnect(id: $id, input: $input) {
928
- id
929
- name
930
- }
931
- }
932
- `.trim(),
933
- variables: {
934
- id: serviceId,
935
- input: {
936
- repo,
937
- ...railwayConnectionLabel(sourceBranch) ? { branch: railwayConnectionLabel(sourceBranch) } : {}
938
- }
939
- },
940
- env,
941
- fetchImpl
942
- });
943
- return payload2;
944
- };
945
- let payload;
946
- try {
947
- payload = await connect(desiredRepo);
948
- } catch (error) {
949
- const message = error instanceof Error ? error.message : String(error ?? "");
950
- if (!/User does not have access to the repo/iu.test(message) || /^https?:\/\//iu.test(desiredRepo)) {
951
- throw error;
952
- }
953
- payload = await connect(`https://github.com/${desiredRepo}`);
954
- }
955
- const service = payload.data?.serviceConnect ? normalizeService(payload.data.serviceConnect) : null;
956
- if (!service) {
957
- throw new Error(`Railway service Git source update did not return a usable service for ${serviceId}.`);
958
- }
959
- return service;
886
+ const targetEnvironmentId = railwayConnectionLabel(environmentId);
887
+ if (!targetEnvironmentId) throw new Error(`Railway service Git source update requires an environment id for ${serviceId}.`);
888
+ await connectRailwayServiceSourceWithCli({
889
+ projectId,
890
+ environmentId: targetEnvironmentId,
891
+ serviceId,
892
+ repo: desiredRepo,
893
+ branch: railwayConnectionLabel(sourceBranch) || null,
894
+ env
895
+ });
896
+ return { id: serviceId, name: serviceId };
960
897
  }
961
898
  async function ensureRailwayGeneratedServiceDomain({
962
899
  projectId,
@@ -971,48 +908,21 @@ async function ensureRailwayGeneratedServiceDomain({
971
908
  if (existing) {
972
909
  return { domain: existing, created: false };
973
910
  }
974
- const query = `
975
- mutation TreeseedRailwayServiceDomainCreate($input: ServiceDomainCreateInput!) {
976
- serviceDomainCreate(input: $input) {
977
- id
978
- domain
979
- serviceId
980
- environmentId
981
- targetPort
982
- }
983
- }
984
- `.trim();
985
- const inputWithProject = {
986
- projectId,
987
- environmentId,
988
- serviceId,
989
- ...Number.isFinite(Number(targetPort)) ? { targetPort: Number(targetPort) } : {}
990
- };
991
- const createPayload = async (input) => railwayGraphqlRequest({
992
- query,
993
- variables: {
994
- input: {
995
- ...input
996
- }
997
- },
998
- env,
999
- fetchImpl
1000
- });
1001
- let payload;
1002
- try {
1003
- payload = await createPayload(inputWithProject);
1004
- } catch (error) {
1005
- const message = error instanceof Error ? error.message : String(error ?? "");
1006
- if (!/Problem processing request|projectId|not defined by type/iu.test(message)) {
1007
- throw error;
1008
- }
1009
- payload = await createPayload({
911
+ await runRailwayCliJson({
912
+ args: [
913
+ "domain",
914
+ "--project",
915
+ projectId,
916
+ "--environment",
1010
917
  environmentId,
918
+ "--service",
1011
919
  serviceId,
1012
- ...Number.isFinite(Number(targetPort)) ? { targetPort: Number(targetPort) } : {}
1013
- });
1014
- }
1015
- const domain = normalizeRailwayDomain(payload.data?.serviceDomainCreate);
920
+ ...Number.isFinite(Number(targetPort)) ? ["--port", String(Number(targetPort))] : [],
921
+ "--json"
922
+ ],
923
+ env
924
+ });
925
+ const domain = (await listRailwayServiceDomains({ projectId, environmentId, serviceId, env, fetchImpl })).find((entry) => entry.kind === "service" || entry.domain.endsWith(".railway.app")) ?? null;
1016
926
  if (!domain) {
1017
927
  throw new Error("Railway service domain create did not return a usable domain.");
1018
928
  }
@@ -1057,29 +967,20 @@ query TreeseedRailwayServiceDomains($projectId: String!, $environmentId: String!
1057
967
  ];
1058
968
  }
1059
969
  async function deployRailwayServiceInstance({
970
+ projectId,
1060
971
  serviceId,
1061
972
  environmentId,
1062
973
  env = process.env,
1063
974
  fetchImpl = fetch
1064
975
  }) {
1065
- const payload = await railwayGraphqlRequest({
1066
- query: `
1067
- mutation TreeseedRailwayServiceInstanceDeploy($serviceId: String!, $environmentId: String!) {
1068
- serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
1069
- }
1070
- `.trim(),
1071
- variables: { serviceId, environmentId },
1072
- env,
1073
- fetchImpl
976
+ void fetchImpl;
977
+ const targetProjectId = railwayConnectionLabel(projectId) || configuredEnvValue(env, "TREESEED_RAILWAY_PROJECT_ID");
978
+ if (!targetProjectId) throw new Error(`Railway CLI redeploy requires a project id for service ${serviceId}.`);
979
+ const result = await runRailwayCliJson({
980
+ args: ["service", "redeploy", "--project", targetProjectId, "--environment", environmentId, "--service", serviceId, "--from-source", "--yes", "--json"],
981
+ env
1074
982
  });
1075
- const value = payload.data?.serviceInstanceDeployV2;
1076
- if (typeof value === "string" && value.trim()) {
1077
- return { deploymentId: value.trim() };
1078
- }
1079
- if (value && typeof value === "object") {
1080
- return { deploymentId: railwayConnectionLabel(value.id) || null };
1081
- }
1082
- return { deploymentId: null };
983
+ return { deploymentId: railwayConnectionLabel(result.deploymentId ?? result.id) || null };
1083
984
  }
1084
985
  async function updateRailwayServiceName({
1085
986
  serviceId,
@@ -1091,27 +992,9 @@ async function updateRailwayServiceName({
1091
992
  if (!serviceId || !desiredName) {
1092
993
  throw new Error("Railway service rename requires a service id and name.");
1093
994
  }
1094
- const payload = await railwayGraphqlRequest({
1095
- query: `
1096
- mutation TreeseedRailwayServiceUpdate($id: String!, $input: ServiceUpdateInput!) {
1097
- serviceUpdate(id: $id, input: $input) {
1098
- id
1099
- name
1100
- }
1101
- }
1102
- `.trim(),
1103
- variables: {
1104
- id: serviceId,
1105
- input: { name: desiredName }
1106
- },
1107
- env,
1108
- fetchImpl
1109
- });
1110
- const service = payload.data?.serviceUpdate ? normalizeService(payload.data.serviceUpdate) : null;
1111
- if (!service) {
1112
- throw new Error(`Railway service rename did not return a usable service for ${desiredName}.`);
1113
- }
1114
- return service;
995
+ void env;
996
+ void fetchImpl;
997
+ throw new Error(`Railway service rename ${serviceId} -> ${desiredName} is not exposed by the official SDK or CLI; direct GraphQL mutation is prohibited.`);
1115
998
  }
1116
999
  async function ensureRailwayPostgresService({
1117
1000
  projectId,
@@ -1208,34 +1091,10 @@ async function deployRailwayTemplate({
1208
1091
  env = process.env,
1209
1092
  fetchImpl = fetch
1210
1093
  }) {
1211
- const workspace = await resolveRailwayWorkspaceContext({ env, fetchImpl });
1212
- const payload = await railwayGraphqlRequest({
1213
- query: `
1214
- mutation TreeseedRailwayTemplateDeploy($input: TemplateDeployV2Input!) {
1215
- templateDeployV2(input: $input) {
1216
- projectId
1217
- workflowId
1218
- }
1219
- }
1220
- `.trim(),
1221
- variables: {
1222
- input: {
1223
- templateId,
1224
- serializedConfig,
1225
- projectId,
1226
- environmentId,
1227
- workspaceId: workspace.id
1228
- }
1229
- },
1230
- env,
1231
- fetchImpl,
1232
- timeoutMs: 2e4,
1233
- retries: 1
1234
- });
1235
- if (!payload.data?.templateDeployV2?.projectId) {
1236
- throw new Error("Railway Postgres template deployment did not return a project id.");
1237
- }
1238
- return payload.data.templateDeployV2;
1094
+ void serializedConfig;
1095
+ void env;
1096
+ void fetchImpl;
1097
+ throw new Error(`Railway template deployment ${templateId} into ${projectId}/${environmentId} is not exposed non-interactively by the official SDK or CLI; direct GraphQL mutation is prohibited.`);
1239
1098
  }
1240
1099
  async function waitForRailwayPostgresTemplateService({
1241
1100
  projectId,
@@ -1262,7 +1121,7 @@ async function waitForRailwayPostgresTemplateService({
1262
1121
  }
1263
1122
  lastProof = proof;
1264
1123
  }
1265
- await new Promise((resolve) => setTimeout(resolve, 3e3));
1124
+ await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1266
1125
  }
1267
1126
  throw new Error(`Railway Postgres template deployment did not produce a managed PostgreSQL service named ${desiredServiceName}. Last proof: ${lastProof?.message ?? "no candidate service observed"}`);
1268
1127
  }
@@ -1325,10 +1184,13 @@ query TreeseedRailwayServiceDeploymentHealth($serviceId: String!, $environmentId
1325
1184
  return {
1326
1185
  ok,
1327
1186
  status,
1187
+ deploymentStopped: stopped,
1188
+ instanceStatuses,
1328
1189
  branch: railwayConnectionLabel(deployment?.meta?.branch) || null,
1329
1190
  repo: railwayConnectionLabel(deployment?.meta?.repo) || null,
1330
1191
  rootDirectory: railwayConnectionLabel(deployment?.meta?.rootDirectory) || null,
1331
1192
  commitHash: railwayConnectionLabel(deployment?.meta?.commitHash) || null,
1193
+ image: railwayConnectionLabel(deployment?.meta?.image) || null,
1332
1194
  requiredMountPath: railwayConnectionLabel(deployment?.meta?.serviceManifest?.deploy?.requiredMountPath) || null,
1333
1195
  volumeMounts: Array.isArray(deployment?.meta?.volumeMounts) ? deployment.meta.volumeMounts.map((entry) => railwayConnectionLabel(entry)).filter(Boolean) : [],
1334
1196
  message: ok ? "Deployment is healthy." : `Latest deployment status is ${status ?? "unknown"}${stopped ? " and stopped" : ""}${instanceStatuses.length ? `; instances=${instanceStatuses.join(",")}` : ""}.`
@@ -1485,7 +1347,7 @@ async function ensureRailwayServiceInstanceConfiguration({
1485
1347
  let current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
1486
1348
  if (!current.id) {
1487
1349
  for (let attempt = 0; attempt < settleAttempts && !current.id; attempt += 1) {
1488
- await new Promise((resolve) => setTimeout(resolve, settleDelayMs));
1350
+ await new Promise((resolve2) => setTimeout(resolve2, settleDelayMs));
1489
1351
  current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
1490
1352
  }
1491
1353
  }
@@ -1523,48 +1385,40 @@ async function ensureRailwayServiceInstanceConfiguration({
1523
1385
  if (!drifted) {
1524
1386
  return { instance: current, updated: false };
1525
1387
  }
1526
- const mutationQuery = needsRuntimeConfig ? `
1527
- mutation TreeseedRailwayServiceInstanceUpdate($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) {
1528
- serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input)
1529
- }
1530
- `.trim() : `
1531
- mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) {
1532
- serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input)
1533
- }
1534
- `.trim();
1535
- try {
1536
- await railwayGraphqlRequest({
1537
- query: mutationQuery,
1538
- variables: {
1539
- serviceId,
1540
- environmentId,
1541
- input: {
1542
- ...desired.buildCommand !== null || clearSourceConfiguration && current.buildCommand !== null ? { buildCommand: desired.buildCommand } : {},
1543
- ...desired.dockerfilePath !== null || clearSourceConfiguration && current.dockerfilePath !== null ? { dockerfilePath: desired.dockerfilePath } : {},
1544
- ...desired.railwayConfigFile !== null || clearSourceConfiguration && current.railwayConfigFile !== null ? { railwayConfigFile: desired.railwayConfigFile } : {},
1545
- ...desired.startCommand !== null || clearSourceConfiguration && current.startCommand !== null ? { startCommand: desired.startCommand } : {},
1546
- ...desired.cronSchedule !== null ? { cronSchedule: desired.cronSchedule } : {},
1547
- ...desired.rootDirectory !== null || clearSourceConfiguration && current.rootDirectory !== null ? { rootDirectory: desired.rootDirectory } : {},
1548
- ...desired.healthcheckPath !== null ? { healthcheckPath: desired.healthcheckPath } : {},
1549
- ...desired.healthcheckTimeoutSeconds !== null ? { healthcheckTimeout: desired.healthcheckTimeoutSeconds } : {},
1550
- ...desired.sleepApplication !== null ? { sleepApplication: desired.sleepApplication } : {},
1551
- ...desired.deploymentRegion !== null ? {
1552
- multiRegionConfig: {
1553
- [desired.deploymentRegion]: { numReplicas: 1 }
1554
- }
1555
- } : {}
1388
+ const client = createRailwayEnvironmentPatchClient({ env, fetchImpl });
1389
+ await client.stageEnvironmentChanges({
1390
+ environmentId,
1391
+ merge: true,
1392
+ patch: {
1393
+ services: {
1394
+ [serviceId]: {
1395
+ build: {
1396
+ ...desired.buildCommand !== null || clearSourceConfiguration ? { buildCommand: desired.buildCommand } : {},
1397
+ ...desired.dockerfilePath !== null || clearSourceConfiguration ? { dockerfilePath: desired.dockerfilePath } : {}
1398
+ },
1399
+ ...desired.railwayConfigFile !== null || clearSourceConfiguration ? { configFile: desired.railwayConfigFile } : {},
1400
+ ...desired.rootDirectory !== null || clearSourceConfiguration ? { source: { rootDirectory: desired.rootDirectory } } : {},
1401
+ deploy: {
1402
+ ...desired.startCommand !== null || clearSourceConfiguration ? { startCommand: desired.startCommand } : {},
1403
+ ...desired.cronSchedule !== null ? { cronSchedule: desired.cronSchedule } : {},
1404
+ ...desired.healthcheckPath !== null ? { healthcheckPath: desired.healthcheckPath } : {},
1405
+ ...desired.healthcheckTimeoutSeconds !== null ? { healthcheckTimeout: desired.healthcheckTimeoutSeconds } : {},
1406
+ ...desired.sleepApplication !== null ? { sleepApplication: desired.sleepApplication } : {},
1407
+ ...desired.runtimeMode !== null ? { runtime: desired.runtimeMode } : {},
1408
+ ...desired.deploymentRegion !== null ? {
1409
+ region: desired.deploymentRegion,
1410
+ multiRegionConfig: { [desired.deploymentRegion]: { numReplicas: 1 } }
1411
+ } : {}
1412
+ }
1556
1413
  }
1557
- },
1558
- env,
1559
- fetchImpl
1560
- });
1561
- } catch (error) {
1562
- const message = error instanceof Error ? error.message : String(error ?? "");
1563
- if (needsRuntimeConfig && /Field .* is not defined by type .*ServiceInstanceUpdateInput|Unknown argument|Cannot query field/iu.test(message)) {
1564
- throw new Error("Railway service instance runtime settings are unsupported by the current Railway API schema.");
1414
+ }
1565
1415
  }
1566
- throw error;
1567
- }
1416
+ });
1417
+ await client.commitStagedPatch({
1418
+ environmentId,
1419
+ message: `Treeseed reconcile service configuration ${serviceId}`,
1420
+ skipDeploys: true
1421
+ });
1568
1422
  let instance = current;
1569
1423
  for (let attempt = 0; attempt <= settleAttempts; attempt += 1) {
1570
1424
  instance = await getRailwayServiceInstance({
@@ -1576,7 +1430,7 @@ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $enviro
1576
1430
  if (!serviceInstanceDrifted(instance, desired, clearSourceConfiguration) || attempt >= settleAttempts) {
1577
1431
  break;
1578
1432
  }
1579
- await new Promise((resolve) => setTimeout(resolve, settleDelayMs));
1433
+ await new Promise((resolve2) => setTimeout(resolve2, settleDelayMs));
1580
1434
  }
1581
1435
  return {
1582
1436
  instance: {
@@ -1635,67 +1489,34 @@ async function upsertRailwayVariables({
1635
1489
  if (Object.keys(variables).length === 0) {
1636
1490
  return;
1637
1491
  }
1638
- const query = `
1639
- mutation TreeseedRailwayVariableCollectionUpsert($input: VariableCollectionUpsertInput!) {
1640
- variableCollectionUpsert(input: $input)
1641
- }
1642
- `.trim();
1643
- const input = {
1644
- projectId,
1645
- environmentId,
1646
- serviceId: serviceId || null,
1647
- variables,
1648
- replace: false,
1649
- skipDeploys: true
1650
- };
1651
- const upsertOne = (key, value) => railwayGraphqlRequest({
1652
- query,
1653
- variables: {
1654
- input: {
1655
- projectId,
1656
- environmentId,
1657
- serviceId: serviceId || null,
1658
- variables: { [key]: value },
1659
- replace: false,
1660
- skipDeploys: true
1661
- }
1662
- },
1663
- env,
1664
- fetchImpl
1665
- });
1666
- try {
1667
- await railwayGraphqlRequest({
1668
- query,
1669
- variables: { input },
1670
- env,
1671
- fetchImpl
1492
+ const client = createRailwayEnvironmentPatchClient({ env, fetchImpl });
1493
+ const applyVariablePatch = async (keys) => {
1494
+ const variablePatch = Object.fromEntries(keys.map((key) => [key, { value: variables[key] }]));
1495
+ await client.stageEnvironmentChanges({
1496
+ environmentId,
1497
+ merge: true,
1498
+ patch: serviceId ? { services: { [serviceId]: { variables: variablePatch } } } : { sharedVariables: variablePatch }
1672
1499
  });
1673
- } catch (error) {
1674
- const message = error instanceof Error ? error.message : String(error ?? "");
1675
- if (!/Problem processing request/iu.test(message) || Object.keys(variables).length <= 1) {
1676
- throw error;
1677
- }
1678
- for (const [key, value] of Object.entries(variables)) {
1679
- await upsertOne(key, value);
1680
- }
1681
- }
1500
+ await client.commitStagedPatch({
1501
+ environmentId,
1502
+ message: `Treeseed update ${keys.length} Railway variable${keys.length === 1 ? "" : "s"}`,
1503
+ skipDeploys: true
1504
+ });
1505
+ };
1506
+ await applyVariablePatch(Object.keys(variables));
1682
1507
  const expectedKeys = Object.keys(variables);
1683
1508
  const mismatchedKeys = (observed2) => expectedKeys.filter((key) => observed2[key] !== variables[key]);
1684
1509
  const observed = await listRailwayVariables({ projectId, environmentId, serviceId, env, fetchImpl }).catch(() => ({}));
1685
1510
  const missingOrMismatched = mismatchedKeys(observed);
1686
- for (const key of missingOrMismatched) {
1687
- await upsertOne(key, variables[key]);
1688
- }
1511
+ if (missingOrMismatched.length > 0) await applyVariablePatch(missingOrMismatched);
1689
1512
  let retried = missingOrMismatched.length > 0 ? await listRailwayVariables({ projectId, environmentId, serviceId, env, fetchImpl }).catch(() => ({})) : observed;
1690
1513
  let stillMismatched = mismatchedKeys(retried);
1691
1514
  for (let attempt = 0; stillMismatched.length > 0 && attempt < 12; attempt += 1) {
1692
- await new Promise((resolve) => setTimeout(resolve, 2500));
1515
+ await new Promise((resolve2) => setTimeout(resolve2, 2500));
1693
1516
  retried = await listRailwayVariables({ projectId, environmentId, serviceId, env, fetchImpl }).catch(() => ({}));
1694
1517
  stillMismatched = mismatchedKeys(retried);
1695
1518
  if (stillMismatched.length > 0 && attempt === 5) {
1696
- for (const key of stillMismatched) {
1697
- await upsertOne(key, variables[key]);
1698
- }
1519
+ await applyVariablePatch(stillMismatched);
1699
1520
  }
1700
1521
  }
1701
1522
  if (stillMismatched.length > 0) {
@@ -1744,145 +1565,13 @@ query TreeseedRailwayVolumeList($projectId: String!) {
1744
1565
  });
1745
1566
  return collectRailwayVolumes(payload.data);
1746
1567
  }
1747
- async function createRailwayVolume({
1748
- projectId,
1749
- environmentId,
1750
- serviceId,
1751
- name,
1752
- mountPath,
1753
- env = process.env,
1754
- fetchImpl = fetch
1755
- }) {
1756
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_CREATE_MUTATION") || `
1757
- mutation TreeseedRailwayVolumeCreate($input: VolumeCreateInput!) {
1758
- volumeCreate(input: $input) {
1759
- id
1760
- name
1761
- projectId
1762
- volumeInstances {
1763
- edges {
1764
- node {
1765
- id
1766
- serviceId
1767
- environmentId
1768
- mountPath
1769
- state
1770
- isPendingDeletion
1771
- deletedAt
1772
- }
1773
- }
1774
- }
1775
- }
1776
- }
1777
- `.trim();
1778
- const payload = await railwayGraphqlRequest({
1779
- query: mutation,
1780
- variables: {
1781
- input: {
1782
- projectId,
1783
- environmentId,
1784
- serviceId,
1785
- mountPath
1786
- }
1787
- },
1788
- env,
1789
- fetchImpl
1790
- });
1791
- const volume = collectRailwayVolumes(payload.data)[0] ?? null;
1792
- if (!volume) {
1793
- throw new Error(`Railway volume create did not return a usable volume for ${name}.`);
1794
- }
1795
- if (name && volume.name !== name) {
1796
- try {
1797
- const renamed = await updateRailwayVolumeName({
1798
- volumeId: volume.id,
1799
- name,
1800
- env,
1801
- fetchImpl
1802
- });
1803
- return {
1804
- ...volume,
1805
- name: renamed.name || name
1806
- };
1807
- } catch {
1808
- return {
1809
- ...volume,
1810
- name: volume.name || name
1811
- };
1812
- }
1813
- }
1814
- return volume;
1815
- }
1816
- async function updateRailwayVolumeName({
1817
- volumeId,
1818
- name,
1819
- env = process.env,
1820
- fetchImpl = fetch
1821
- }) {
1822
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_UPDATE_MUTATION") || `
1823
- mutation TreeseedRailwayVolumeUpdate($volumeId: String!, $input: VolumeUpdateInput!) {
1824
- volumeUpdate(volumeId: $volumeId, input: $input) {
1825
- id
1826
- name
1827
- projectId
1828
- volumeInstances {
1829
- edges {
1830
- node {
1831
- id
1832
- serviceId
1833
- environmentId
1834
- mountPath
1835
- state
1836
- isPendingDeletion
1837
- deletedAt
1838
- }
1839
- }
1840
- }
1841
- }
1842
- }
1843
- `.trim();
1844
- const payload = await railwayGraphqlRequest({
1845
- query: mutation,
1846
- variables: {
1847
- volumeId,
1848
- input: { name }
1849
- },
1850
- env,
1851
- fetchImpl
1852
- });
1853
- return collectRailwayVolumes(payload.data)[0] ?? null;
1854
- }
1855
- async function updateRailwayVolumeInstanceMountPath({
1856
- volumeId,
1857
- serviceId,
1858
- mountPath,
1859
- env = process.env,
1860
- fetchImpl = fetch
1861
- }) {
1862
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_INSTANCE_UPDATE_MUTATION") || `
1863
- mutation TreeseedRailwayVolumeInstanceUpdate($volumeId: String!, $input: VolumeInstanceUpdateInput!) {
1864
- volumeInstanceUpdate(volumeId: $volumeId, input: $input)
1865
- }
1866
- `.trim();
1867
- await railwayGraphqlRequest({
1868
- query: mutation,
1869
- variables: {
1870
- volumeId,
1871
- input: {
1872
- ...serviceId !== void 0 ? { serviceId } : {},
1873
- mountPath
1874
- }
1875
- },
1876
- env,
1877
- fetchImpl
1878
- });
1879
- }
1880
1568
  async function ensureRailwayServiceVolume({
1881
1569
  projectId,
1882
1570
  environmentId,
1883
1571
  serviceId,
1884
1572
  name,
1885
1573
  mountPath,
1574
+ adoptVolumeId,
1886
1575
  env = process.env,
1887
1576
  fetchImpl = fetch,
1888
1577
  settleAttempts = 24,
@@ -1891,189 +1580,52 @@ async function ensureRailwayServiceVolume({
1891
1580
  if (!mountPath.startsWith("/")) {
1892
1581
  throw new Error(`Railway volume mount path must be absolute: ${mountPath}`);
1893
1582
  }
1894
- const volumes = await listRailwayVolumes({ projectId, env, fetchImpl });
1895
- const activeVolumes = volumes.map((candidate) => ({
1896
- ...candidate,
1897
- instances: candidate.instances.filter(isActiveRailwayVolumeInstance)
1898
- })).filter((candidate) => candidate.instances.length > 0);
1899
- const exactVolume = activeVolumes.find(
1900
- (candidate) => candidate.name === name && candidate.instances.some(
1901
- (instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId && instance2.mountPath === mountPath
1902
- )
1903
- ) ?? null;
1904
- if (exactVolume) {
1905
- return {
1906
- volume: exactVolume,
1907
- instance: exactVolume.instances.find(
1908
- (instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId && instance2.mountPath === mountPath
1909
- ) ?? null,
1910
- created: false,
1911
- updated: false
1912
- };
1913
- }
1914
- let volume = findRailwayVolumeForService(volumes, serviceId, environmentId) ?? findRailwayVolumeForService(volumes, serviceId) ?? activeVolumes.find(
1915
- (candidate) => candidate.name === name && candidate.instances.some((instance2) => instance2.environmentId === environmentId)
1916
- ) ?? activeVolumes.find(
1917
- (candidate) => candidate.name === name && (candidate.instances.length === 0 || candidate.instances.some((instance2) => instance2.environmentId === environmentId))
1918
- ) ?? null;
1919
- let created = false;
1920
- let updated = false;
1921
- const createReplacementVolume = async () => {
1922
- let replacement;
1923
- try {
1924
- replacement = await createRailwayVolume({
1925
- projectId,
1926
- environmentId,
1927
- serviceId,
1928
- name,
1929
- mountPath,
1930
- env,
1931
- fetchImpl
1932
- });
1933
- } catch (error) {
1934
- if (!looksLikeRailwayVolumeCreateRace(error)) {
1935
- throw error;
1936
- }
1937
- for (let attempt = 0; attempt < 8; attempt += 1) {
1938
- await new Promise((resolve) => setTimeout(resolve, 1500));
1939
- const refreshed = await listRailwayVolumes({ projectId, env, fetchImpl });
1940
- const activeRefreshed = refreshed.map((candidate) => ({
1941
- ...candidate,
1942
- instances: candidate.instances.filter(isActiveRailwayVolumeInstance)
1943
- })).filter((candidate) => candidate.instances.length > 0);
1944
- const existing = findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? findRailwayVolumeForService(refreshed, serviceId) ?? activeRefreshed.find(
1945
- (candidate) => candidate.name === name && (candidate.instances.length === 0 || candidate.instances.some((instance2) => instance2.environmentId === environmentId))
1946
- ) ?? findSoleActiveRailwayVolumeForEnvironment(refreshed, environmentId);
1947
- if (existing) {
1948
- return existing;
1949
- }
1950
- }
1951
- throw error;
1952
- }
1953
- created = true;
1954
- return replacement;
1955
- };
1956
- if (!volume) {
1957
- volume = await createReplacementVolume();
1958
- }
1959
- const desiredNameInUse = volumes.some(
1960
- (candidate) => candidate.id !== volume?.id && candidate.name === name && candidate.instances.some(isActiveRailwayVolumeInstance)
1961
- );
1962
- if (volume.name && volume.name !== name && !desiredNameInUse) {
1963
- try {
1964
- volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1965
- } catch (error) {
1966
- if (!looksLikeRailwayMissingResource(error)) {
1967
- throw error;
1968
- }
1969
- volume = await createReplacementVolume();
1970
- }
1971
- updated = true;
1972
- }
1973
- let instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1974
- if (!instance && volume.instances.some((entry) => entry.environmentId === environmentId)) {
1975
- try {
1976
- await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1977
- volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1978
- } catch (error) {
1979
- if (looksLikeRailwayVolumeCreateRace(error)) {
1980
- volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then(
1981
- (refreshed) => findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? volume
1982
- );
1983
- } else if (!looksLikeRailwayMissingResource(error)) {
1984
- throw error;
1985
- } else {
1986
- volume = await createReplacementVolume();
1987
- }
1988
- }
1989
- instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1990
- updated = true;
1991
- }
1992
- if (!instance) {
1993
- try {
1994
- await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1995
- volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1996
- } catch (error) {
1997
- if (looksLikeRailwayVolumeCreateRace(error)) {
1998
- volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then(
1999
- (refreshed) => findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? volume
2000
- );
2001
- } else if (!looksLikeRailwayMissingResource(error)) {
2002
- throw error;
2003
- } else {
2004
- volume = await createReplacementVolume();
2005
- }
2006
- }
2007
- instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
2008
- updated = true;
2009
- }
2010
- if (instance && instance.mountPath !== mountPath) {
2011
- try {
2012
- await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, mountPath, env, fetchImpl });
2013
- volume = {
2014
- ...volume,
2015
- instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
1583
+ {
1584
+ const observed = await listRailwayVolumes({ projectId, env, fetchImpl });
1585
+ const exact = observed.find(
1586
+ (candidate) => candidate.name === name && candidate.instances.some(
1587
+ (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath && isActiveRailwayVolumeInstance(instance)
1588
+ )
1589
+ ) ?? null;
1590
+ if (exact) {
1591
+ return {
1592
+ volume: exact,
1593
+ instance: exact.instances.find((instance) => instance.serviceId === serviceId && instance.environmentId === environmentId) ?? null,
1594
+ created: false,
1595
+ updated: false
2016
1596
  };
2017
- } catch (error) {
2018
- if (!looksLikeRailwayMissingResource(error)) {
2019
- throw error;
2020
- }
2021
- volume = await createReplacementVolume();
2022
- instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
2023
1597
  }
2024
- updated = true;
2025
- }
2026
- const settled = await waitForRailwayVolumeMount({
2027
- projectId,
2028
- volume,
2029
- serviceId,
2030
- environmentId,
2031
- mountPath,
2032
- env,
2033
- fetchImpl,
2034
- settleAttempts,
2035
- settleDelayMs
2036
- });
2037
- if (settled) {
2038
- volume = settled.volume;
2039
- instance = settled.instance;
2040
- }
2041
- if (!instance || instance.serviceId !== serviceId || instance.environmentId !== environmentId || instance.mountPath !== mountPath) {
2042
- throw new Error(`Railway API volume reconciliation did not observe ${name} mounted on service ${serviceId} at ${mountPath}.`);
2043
- }
2044
- return { volume, instance, created, updated };
2045
- }
2046
- async function waitForRailwayVolumeMount({
2047
- projectId,
2048
- volume,
2049
- serviceId,
2050
- environmentId,
2051
- mountPath,
2052
- env,
2053
- fetchImpl,
2054
- settleAttempts,
2055
- settleDelayMs
2056
- }) {
2057
- const mountedInstance = (candidate) => candidate.instances.find(
2058
- (entry) => entry.serviceId === serviceId && entry.environmentId === environmentId && entry.mountPath === mountPath && isActiveRailwayVolumeInstance(entry)
2059
- ) ?? null;
2060
- let instance = mountedInstance(volume);
2061
- if (instance) {
2062
- return { volume, instance };
2063
- }
2064
- for (let attempt = 0; attempt < settleAttempts; attempt += 1) {
2065
- await new Promise((resolve) => setTimeout(resolve, settleDelayMs));
2066
- const refreshed = await listRailwayVolumes({ projectId, env, fetchImpl });
2067
- const refreshedVolume = refreshed.find((candidate) => candidate.id === volume.id) ?? findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? null;
2068
- if (!refreshedVolume) {
2069
- continue;
1598
+ const requestedAdoption = railwayConnectionLabel(adoptVolumeId);
1599
+ const adoptable = requestedAdoption ? observed.find((candidate) => candidate.id === requestedAdoption) ?? null : observed.find((candidate) => candidate.name === name) ?? findRailwayVolumeForService(observed, serviceId, environmentId) ?? null;
1600
+ if (requestedAdoption && !adoptable) {
1601
+ throw new Error(`Railway volume ${requestedAdoption} cannot be adopted because it was not found; refusing to create an empty replacement volume.`);
2070
1602
  }
2071
- instance = mountedInstance(refreshedVolume);
2072
- if (instance) {
2073
- return { volume: refreshedVolume, instance };
1603
+ const volumeKey = adoptable?.id ?? name;
1604
+ const client = createRailwayEnvironmentPatchClient({ env, fetchImpl });
1605
+ await client.stageEnvironmentChanges({
1606
+ environmentId,
1607
+ merge: true,
1608
+ patch: {
1609
+ volumes: { [volumeKey]: adoptable ? { isDeleted: false } : { isCreated: true } },
1610
+ services: { [serviceId]: { volumeMounts: { [volumeKey]: { mountPath } } } }
1611
+ }
1612
+ });
1613
+ await client.commitStagedPatch({
1614
+ environmentId,
1615
+ message: `Treeseed reconcile volume ${name}`,
1616
+ skipDeploys: true
1617
+ });
1618
+ for (let attempt = 0; attempt <= settleAttempts; attempt += 1) {
1619
+ if (attempt > 0) await new Promise((resolve2) => setTimeout(resolve2, settleDelayMs));
1620
+ const refreshed = await listRailwayVolumes({ projectId, env, fetchImpl });
1621
+ const volume = refreshed.find((candidate) => candidate.id === adoptable?.id || candidate.name === name) ?? null;
1622
+ const instance = volume?.instances.find(
1623
+ (entry) => entry.serviceId === serviceId && entry.environmentId === environmentId && entry.mountPath === mountPath && isActiveRailwayVolumeInstance(entry)
1624
+ ) ?? null;
1625
+ if (volume && instance) return { volume, instance, created: !adoptable, updated: Boolean(adoptable) };
2074
1626
  }
1627
+ throw new Error(`Railway SDK volume reconciliation did not observe ${name} mounted on service ${serviceId} at ${mountPath}.`);
2075
1628
  }
2076
- return null;
2077
1629
  }
2078
1630
  function findRailwayVolumeForService(volumes, serviceId, environmentId) {
2079
1631
  return volumes.find(
@@ -2082,19 +1634,6 @@ function findRailwayVolumeForService(volumes, serviceId, environmentId) {
2082
1634
  )
2083
1635
  ) ?? null;
2084
1636
  }
2085
- function findSoleActiveRailwayVolumeForEnvironment(volumes, environmentId) {
2086
- const active = volumes.map((candidate) => ({
2087
- ...candidate,
2088
- instances: candidate.instances.filter(
2089
- (instance) => instance.environmentId === environmentId && isActiveRailwayVolumeInstance(instance)
2090
- )
2091
- })).filter((candidate) => candidate.instances.length > 0);
2092
- return active.length === 1 ? active[0] : null;
2093
- }
2094
- function looksLikeRailwayVolumeCreateRace(error) {
2095
- const message = error instanceof Error ? error.message : String(error ?? "");
2096
- return /already has a volume attached|would have \d+ volumes attached|can only have one volume|volume named .* already exists|already exists in this project|not authorized/iu.test(message);
2097
- }
2098
1637
  async function listRailwayCustomDomains({
2099
1638
  projectId,
2100
1639
  environmentId,
@@ -2159,46 +1698,11 @@ async function ensureRailwayCustomDomain({
2159
1698
  if (matched) {
2160
1699
  return { domain: matched, created: false };
2161
1700
  }
2162
- const payload = await railwayGraphqlRequest({
2163
- query: `
2164
- mutation TreeseedRailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
2165
- customDomainCreate(input: $input) {
2166
- id
2167
- domain
2168
- environmentId
2169
- serviceId
2170
- targetPort
2171
- status {
2172
- verified
2173
- certificateStatus
2174
- verificationDnsHost
2175
- verificationToken
2176
- dnsRecords {
2177
- fqdn
2178
- hostlabel
2179
- recordType
2180
- requiredValue
2181
- currentValue
2182
- status
2183
- zone
2184
- purpose
2185
- }
2186
- }
2187
- }
2188
- }
2189
- `.trim(),
2190
- variables: {
2191
- input: {
2192
- projectId,
2193
- environmentId,
2194
- serviceId,
2195
- domain: normalizedDomain
2196
- }
2197
- },
2198
- env,
2199
- fetchImpl
1701
+ await runRailwayCliJson({
1702
+ args: ["domain", normalizedDomain, "--project", projectId, "--environment", environmentId, "--service", serviceId, "--json"],
1703
+ env
2200
1704
  });
2201
- const created = payload.data?.customDomainCreate ? normalizeRailwayCustomDomain(payload.data.customDomainCreate) : null;
1705
+ const created = (await listRailwayCustomDomains({ projectId, environmentId, serviceId, env, fetchImpl })).find((entry) => entry.domain === normalizedDomain) ?? null;
2202
1706
  if (!created) {
2203
1707
  throw new Error(`Railway custom domain create did not return a usable domain for ${normalizedDomain}.`);
2204
1708
  }
@@ -2241,12 +1745,15 @@ async function railwayDeleteMutation({
2241
1745
  throw error;
2242
1746
  }
2243
1747
  lastError = error;
2244
- await new Promise((resolve) => setTimeout(resolve, 2500 * (attempt + 1)));
1748
+ await new Promise((resolve2) => setTimeout(resolve2, 2500 * (attempt + 1)));
2245
1749
  }
2246
1750
  }
2247
1751
  throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "Railway delete mutation did not complete."));
2248
1752
  }
2249
1753
  async function deleteRailwayCustomDomain({
1754
+ projectId,
1755
+ environmentId,
1756
+ serviceId,
2250
1757
  domainId,
2251
1758
  env = process.env,
2252
1759
  fetchImpl = fetch
@@ -2254,20 +1761,14 @@ async function deleteRailwayCustomDomain({
2254
1761
  if (!railwayConnectionLabel(domainId)) {
2255
1762
  return { status: "missing", id: domainId };
2256
1763
  }
2257
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_CUSTOM_DOMAIN_DELETE_MUTATION") || `
2258
- mutation TreeseedRailwayCustomDomainDelete($id: String!) {
2259
- customDomainDelete(id: $id)
2260
- }
2261
- `.trim();
2262
- return railwayDeleteMutation({
2263
- query: mutation,
2264
- variables: { id: domainId },
2265
- env,
2266
- fetchImpl,
2267
- missingResult: { status: "missing", id: domainId }
2268
- });
1764
+ void fetchImpl;
1765
+ if (!projectId || !environmentId || !serviceId) throw new Error(`Railway CLI domain deletion requires project, environment, and service ids for ${domainId}.`);
1766
+ await runRailwayCliJson({ args: ["domain", "delete", domainId, "--project", projectId, "--environment", environmentId, "--service", serviceId, "--yes", "--json"], env });
1767
+ return { status: "deleted" };
2269
1768
  }
2270
1769
  async function deleteRailwayService({
1770
+ projectId,
1771
+ environmentId,
2271
1772
  serviceId,
2272
1773
  env = process.env,
2273
1774
  fetchImpl = fetch
@@ -2275,20 +1776,14 @@ async function deleteRailwayService({
2275
1776
  if (!railwayConnectionLabel(serviceId)) {
2276
1777
  return { status: "missing", id: serviceId };
2277
1778
  }
2278
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_SERVICE_DELETE_MUTATION") || `
2279
- mutation TreeseedRailwayServiceDelete($id: String!) {
2280
- serviceDelete(id: $id)
2281
- }
2282
- `.trim();
2283
- return railwayDeleteMutation({
2284
- query: mutation,
2285
- variables: { id: serviceId },
2286
- env,
2287
- fetchImpl,
2288
- missingResult: { status: "missing", id: serviceId }
2289
- });
1779
+ void fetchImpl;
1780
+ if (!projectId || !environmentId) throw new Error(`Railway CLI service deletion requires project and environment ids for ${serviceId}.`);
1781
+ await runRailwayCliJson({ args: ["service", "delete", "--project", projectId, "--environment", environmentId, "--service", serviceId, "--yes", "--json"], env });
1782
+ return { status: "deleted" };
2290
1783
  }
2291
1784
  async function deleteRailwayVolume({
1785
+ projectId,
1786
+ environmentId,
2292
1787
  volumeId,
2293
1788
  env = process.env,
2294
1789
  fetchImpl = fetch
@@ -2296,57 +1791,17 @@ async function deleteRailwayVolume({
2296
1791
  if (!railwayConnectionLabel(volumeId)) {
2297
1792
  return { status: "missing", id: volumeId };
2298
1793
  }
2299
- const configuredMutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_DELETE_MUTATION");
2300
- if (configuredMutation) {
2301
- return railwayDeleteMutation({
2302
- query: configuredMutation,
2303
- variables: { volumeId, id: volumeId },
2304
- env,
2305
- fetchImpl,
2306
- missingResult: { status: "missing", id: volumeId }
2307
- });
2308
- }
2309
- const primaryMutation = `
2310
- mutation TreeseedRailwayVolumeDelete($volumeId: String!) {
2311
- volumeDelete(volumeId: $volumeId)
2312
- }
2313
- `.trim();
2314
- const fallbackMutation = `
2315
- mutation TreeseedRailwayVolumeDeleteById($id: String!) {
2316
- volumeDelete(volumeId: $id)
2317
- }
2318
- `.trim();
2319
- const primary = await railwayDeleteMutation({
2320
- query: primaryMutation,
2321
- variables: { volumeId },
2322
- env,
2323
- fetchImpl,
2324
- missingResult: { status: "missing", id: volumeId }
2325
- }).catch((error) => {
2326
- if (looksLikeRailwayVolumeDeleteShapeUnsupported(error)) {
2327
- return null;
2328
- }
2329
- throw error;
2330
- });
2331
- const fallback = await railwayDeleteMutation({
2332
- query: fallbackMutation,
2333
- variables: { id: volumeId },
2334
- env,
2335
- fetchImpl,
2336
- missingResult: { status: "missing", id: volumeId }
2337
- }).catch((error) => {
2338
- if (looksLikeRailwayVolumeDeleteShapeUnsupported(error) && primary) {
2339
- return primary;
2340
- }
2341
- throw error;
2342
- });
2343
- return fallback ?? primary ?? { status: "deleted" };
1794
+ void fetchImpl;
1795
+ if (!projectId || !environmentId) throw new Error(`Railway CLI volume deletion requires project and environment ids for ${volumeId}.`);
1796
+ await runRailwayCliJson({ args: ["volume", "--project", projectId, "--environment", environmentId, "delete", "--volume", volumeId, "--yes", "--json"], env });
1797
+ return { status: "deleted" };
2344
1798
  }
2345
1799
  function looksLikeRailwayVolumeDeleteShapeUnsupported(error) {
2346
1800
  const message = error instanceof Error ? error.message : String(error ?? "");
2347
1801
  return /Unknown argument|Cannot query field|Unknown field|Field .* is not defined|volumeDelete.*argument|Problem processing request/iu.test(message);
2348
1802
  }
2349
1803
  async function deleteRailwayEnvironment({
1804
+ projectId,
2350
1805
  environmentId,
2351
1806
  env = process.env,
2352
1807
  fetchImpl = fetch
@@ -2354,18 +1809,16 @@ async function deleteRailwayEnvironment({
2354
1809
  if (!railwayConnectionLabel(environmentId)) {
2355
1810
  return { status: "missing", id: environmentId };
2356
1811
  }
2357
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_ENVIRONMENT_DELETE_MUTATION") || `
2358
- mutation TreeseedRailwayEnvironmentDelete($id: String!) {
2359
- environmentDelete(id: $id)
2360
- }
2361
- `.trim();
2362
- return railwayDeleteMutation({
2363
- query: mutation,
2364
- variables: { id: environmentId },
2365
- env,
2366
- fetchImpl,
2367
- missingResult: { status: "missing", id: environmentId }
2368
- });
1812
+ void fetchImpl;
1813
+ if (!projectId) throw new Error(`Railway CLI environment deletion requires a project id for ${environmentId}.`);
1814
+ const tempRoot = mkdtempSync(resolve(tmpdir(), "treeseed-railway-environment-delete-"));
1815
+ try {
1816
+ await runRailwayCliJson({ args: ["link", projectId, "--json"], env, cwd: tempRoot });
1817
+ await runRailwayCliJson({ args: ["environment", "delete", environmentId, "--yes", "--json"], env, cwd: tempRoot });
1818
+ } finally {
1819
+ rmSync(tempRoot, { recursive: true, force: true });
1820
+ }
1821
+ return { status: "deleted" };
2369
1822
  }
2370
1823
  async function deleteRailwayProject({
2371
1824
  projectId,
@@ -2375,20 +1828,12 @@ async function deleteRailwayProject({
2375
1828
  if (!railwayConnectionLabel(projectId)) {
2376
1829
  return { status: "missing", id: projectId };
2377
1830
  }
2378
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_PROJECT_DELETE_MUTATION") || `
2379
- mutation TreeseedRailwayProjectDelete($id: String!) {
2380
- projectDelete(id: $id)
2381
- }
2382
- `.trim();
2383
- return railwayDeleteMutation({
2384
- query: mutation,
2385
- variables: { id: projectId },
2386
- env,
2387
- fetchImpl,
2388
- missingResult: { status: "missing", id: projectId }
2389
- });
1831
+ void fetchImpl;
1832
+ await runRailwayCliJson({ args: ["project", "delete", "--project", projectId, "--yes", "--json"], env });
1833
+ return { status: "deleted" };
2390
1834
  }
2391
1835
  export {
1836
+ assertRailwayGraphqlReadOnly,
2392
1837
  deleteRailwayCustomDomain,
2393
1838
  deleteRailwayEnvironment,
2394
1839
  deleteRailwayProject,