@treeseed/sdk 0.10.27 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +207 -6
  2. package/dist/capacity-provider.d.ts +3 -1
  3. package/dist/capacity-provider.js +25 -5
  4. package/dist/control-plane.d.ts +1 -0
  5. package/dist/control-plane.js +38 -13
  6. package/dist/db/market-schema.d.ts +8860 -6172
  7. package/dist/db/market-schema.js +108 -0
  8. package/dist/db/node-sqlite.js +7 -2
  9. package/dist/hosting/apps.d.ts +12 -0
  10. package/dist/hosting/apps.js +107 -0
  11. package/dist/hosting/builtins.d.ts +25 -0
  12. package/dist/hosting/builtins.js +791 -0
  13. package/dist/hosting/contracts.d.ts +207 -0
  14. package/dist/hosting/contracts.js +0 -0
  15. package/dist/hosting/graph.d.ts +192 -0
  16. package/dist/hosting/graph.js +1106 -0
  17. package/dist/hosting/index.d.ts +4 -0
  18. package/dist/hosting/index.js +4 -0
  19. package/dist/index.d.ts +11 -4
  20. package/dist/index.js +71 -7
  21. package/dist/managed-dependencies.js +1 -2
  22. package/dist/market-client.d.ts +63 -3
  23. package/dist/market-client.js +83 -11
  24. package/dist/operations/services/bootstrap-runner.d.ts +3 -1
  25. package/dist/operations/services/bootstrap-runner.js +22 -2
  26. package/dist/operations/services/config-runtime.d.ts +10 -5
  27. package/dist/operations/services/config-runtime.js +209 -66
  28. package/dist/operations/services/deploy.d.ts +70 -7
  29. package/dist/operations/services/deploy.js +579 -64
  30. package/dist/operations/services/deployment-readiness.d.ts +30 -0
  31. package/dist/operations/services/deployment-readiness.js +175 -0
  32. package/dist/operations/services/git-workflow.d.ts +2 -1
  33. package/dist/operations/services/git-workflow.js +9 -3
  34. package/dist/operations/services/github-actions-verification.d.ts +1 -0
  35. package/dist/operations/services/github-actions-verification.js +1 -0
  36. package/dist/operations/services/github-api.js +1 -1
  37. package/dist/operations/services/github-automation.d.ts +1 -1
  38. package/dist/operations/services/github-automation.js +4 -3
  39. package/dist/operations/services/github-credentials.d.ts +13 -0
  40. package/dist/operations/services/github-credentials.js +58 -0
  41. package/dist/operations/services/hosted-service-checks.d.ts +63 -0
  42. package/dist/operations/services/hosted-service-checks.js +327 -0
  43. package/dist/operations/services/hub-provider-launch.js +3 -3
  44. package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
  45. package/dist/operations/services/live-hosted-service-checks.js +350 -0
  46. package/dist/operations/services/managed-host-security.js +1 -1
  47. package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
  48. package/dist/operations/services/operations-runner-smoke.js +180 -0
  49. package/dist/operations/services/package-adapters.d.ts +95 -0
  50. package/dist/operations/services/package-adapters.js +288 -0
  51. package/dist/operations/services/package-reference-policy.d.ts +1 -0
  52. package/dist/operations/services/package-reference-policy.js +15 -2
  53. package/dist/operations/services/project-platform.d.ts +80 -22
  54. package/dist/operations/services/project-platform.js +49 -8
  55. package/dist/operations/services/project-web-monitor.js +26 -4
  56. package/dist/operations/services/railway-api.d.ts +88 -5
  57. package/dist/operations/services/railway-api.js +626 -35
  58. package/dist/operations/services/railway-deploy.d.ts +46 -40
  59. package/dist/operations/services/railway-deploy.js +261 -293
  60. package/dist/operations/services/release-candidate.d.ts +19 -0
  61. package/dist/operations/services/release-candidate.js +375 -38
  62. package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
  63. package/dist/operations/services/repository-save-orchestrator.js +279 -66
  64. package/dist/operations/services/runtime-tools.d.ts +1 -0
  65. package/dist/operations/services/runtime-tools.js +10 -9
  66. package/dist/operations/services/template-registry.js +14 -7
  67. package/dist/operations/services/verification-cache.d.ts +25 -0
  68. package/dist/operations/services/verification-cache.js +71 -0
  69. package/dist/operations/services/workspace-dependency-mode.js +9 -1
  70. package/dist/operations/services/workspace-save.js +1 -1
  71. package/dist/operations/services/workspace-tools.js +2 -1
  72. package/dist/platform/contracts.d.ts +32 -1
  73. package/dist/platform/deploy-config.js +73 -8
  74. package/dist/platform/env.yaml +163 -35
  75. package/dist/platform/environment.d.ts +1 -0
  76. package/dist/platform/environment.js +74 -5
  77. package/dist/platform/plugin.d.ts +9 -0
  78. package/dist/platform-operation-store.js +2 -2
  79. package/dist/platform-operations.js +1 -1
  80. package/dist/reconcile/bootstrap-systems.js +2 -2
  81. package/dist/reconcile/builtin-adapters.js +372 -189
  82. package/dist/reconcile/contracts.d.ts +9 -5
  83. package/dist/reconcile/desired-state.d.ts +1 -0
  84. package/dist/reconcile/desired-state.js +5 -5
  85. package/dist/reconcile/engine.d.ts +5 -2
  86. package/dist/reconcile/engine.js +53 -32
  87. package/dist/reconcile/index.d.ts +2 -0
  88. package/dist/reconcile/index.js +2 -0
  89. package/dist/reconcile/live-acceptance.d.ts +79 -0
  90. package/dist/reconcile/live-acceptance.js +1615 -0
  91. package/dist/reconcile/platform.d.ts +104 -0
  92. package/dist/reconcile/platform.js +100 -0
  93. package/dist/reconcile/state.js +4 -4
  94. package/dist/reconcile/units.js +2 -2
  95. package/dist/scripts/deployment-readiness.js +20 -0
  96. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  97. package/dist/scripts/operations-runner-smoke.js +16 -0
  98. package/dist/scripts/release-verify.js +4 -1
  99. package/dist/scripts/template-catalog.test.js +7 -7
  100. package/dist/scripts/tenant-workflow-action.js +10 -1
  101. package/dist/sdk-types.d.ts +172 -5
  102. package/dist/sdk-types.js +28 -3
  103. package/dist/sdk.d.ts +35 -24
  104. package/dist/sdk.js +186 -17
  105. package/dist/template-launch-requirements.js +9 -0
  106. package/dist/treedx/adapters.d.ts +6 -0
  107. package/dist/treedx/adapters.js +36 -0
  108. package/dist/treedx/client.d.ts +222 -0
  109. package/dist/treedx/client.js +871 -0
  110. package/dist/treedx/errors.d.ts +13 -0
  111. package/dist/treedx/errors.js +17 -0
  112. package/dist/treedx/federated-client.d.ts +27 -0
  113. package/dist/treedx/federated-client.js +158 -0
  114. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  115. package/dist/treedx/generated/openapi-types.js +0 -0
  116. package/dist/treedx/graph-adapter.d.ts +33 -0
  117. package/dist/treedx/graph-adapter.js +156 -0
  118. package/dist/treedx/index.d.ts +14 -0
  119. package/dist/treedx/index.js +48 -0
  120. package/dist/treedx/market-integration.d.ts +27 -0
  121. package/dist/treedx/market-integration.js +131 -0
  122. package/dist/treedx/ports.d.ts +166 -0
  123. package/dist/treedx/ports.js +231 -0
  124. package/dist/treedx/query-adapter.d.ts +19 -0
  125. package/dist/treedx/query-adapter.js +62 -0
  126. package/dist/treedx/registry-client.d.ts +11 -0
  127. package/dist/treedx/registry-client.js +19 -0
  128. package/dist/treedx/repository-adapter.d.ts +45 -0
  129. package/dist/treedx/repository-adapter.js +308 -0
  130. package/dist/treedx/sdk-integration.d.ts +27 -0
  131. package/dist/treedx/sdk-integration.js +63 -0
  132. package/dist/treedx/types.d.ts +1084 -0
  133. package/dist/treedx/types.js +8 -0
  134. package/dist/treedx/workspace-adapter.d.ts +27 -0
  135. package/dist/treedx/workspace-adapter.js +65 -0
  136. package/dist/treedx-backends.d.ts +218 -0
  137. package/dist/treedx-backends.js +632 -0
  138. package/dist/treedx-client.d.ts +86 -0
  139. package/dist/treedx-client.js +175 -0
  140. package/dist/treeseed/template-catalog/catalog.fixture.json +497 -138
  141. package/dist/workflow/operations.d.ts +119 -13
  142. package/dist/workflow/operations.js +309 -53
  143. package/dist/workflow-state.d.ts +13 -0
  144. package/dist/workflow-state.js +43 -26
  145. package/dist/workflow-support.d.ts +11 -3
  146. package/dist/workflow-support.js +67 -3
  147. package/dist/workflow.d.ts +5 -0
  148. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  149. package/package.json +34 -3
  150. package/templates/github/deploy-web.workflow.yml +39 -6
  151. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.d.ts +0 -3
  152. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.ts +0 -6
  153. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +0 -35
  154. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +0 -4
  155. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +0 -65
  156. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +0 -22
  157. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/empty/.gitkeep +0 -1
  158. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/knowledge/handbook/index.mdx +0 -11
  159. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/pages/welcome.mdx +0 -11
  160. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +0 -11
  161. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +0 -17
  162. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.d.ts +0 -1
  163. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.ts +0 -3
  164. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/env.yaml +0 -1
  165. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +0 -26
  166. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +0 -74
  167. package/dist/treeseed/template-catalog/templates/starter-basic/template/tsconfig.json +0 -9
  168. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +0 -103
@@ -1,8 +1,6 @@
1
1
  import { request as httpsRequest } from "node:https";
2
2
  const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
3
3
  const DEFAULT_RAILWAY_WORKSPACE = "knowledge-coop";
4
- const RAILWAY_POSTGRES_TEMPLATE_ID = "b55da7dc-09be-4140-bc65-1284d15d349c";
5
- const RAILWAY_POSTGRES_TEMPLATE_SERVICE_ID = "b55da7dc-09be-4140-bc65-1284b15d349b";
6
4
  function normalizeRailwayEnvironmentName(value) {
7
5
  const normalized = typeof value === "string" ? value.trim() : "";
8
6
  if (!normalized) {
@@ -195,6 +193,31 @@ function normalizeRailwayCustomDomain(node) {
195
193
  dnsRecords
196
194
  };
197
195
  }
196
+ function normalizeRailwayDomain(node, kind = "service") {
197
+ if (!node || typeof node !== "object") {
198
+ return null;
199
+ }
200
+ const record = node;
201
+ const id = railwayConnectionLabel(record.id) || railwayConnectionLabel(record.domain);
202
+ const domain = railwayConnectionLabel(record.domain);
203
+ if (!id || !domain) {
204
+ return null;
205
+ }
206
+ return {
207
+ id,
208
+ domain,
209
+ kind,
210
+ environmentId: railwayConnectionLabel(record.environmentId),
211
+ serviceId: railwayConnectionLabel(record.serviceId),
212
+ targetPort: normalizeRailwayNumber(record.targetPort)
213
+ };
214
+ }
215
+ function normalizeRailwayDomainList(value, kind) {
216
+ if (!Array.isArray(value)) {
217
+ return [];
218
+ }
219
+ return value.map((entry) => normalizeRailwayDomain(entry, kind)).filter(Boolean);
220
+ }
198
221
  function normalizeRailwayVolumeInstance(node) {
199
222
  const id = railwayConnectionLabel(node.id);
200
223
  if (!id) {
@@ -641,6 +664,8 @@ async function ensureRailwayService({
641
664
  projectId,
642
665
  serviceName,
643
666
  serviceId,
667
+ environmentId,
668
+ imageRef,
644
669
  env = process.env,
645
670
  fetchImpl = fetch
646
671
  }) {
@@ -651,11 +676,55 @@ async function ensureRailwayService({
651
676
  (service2) => desiredServiceId && service2.id === desiredServiceId || desiredServiceName && service2.name === desiredServiceName
652
677
  ) ?? null;
653
678
  if (existing) {
679
+ const desiredImageRef = railwayConnectionLabel(imageRef);
680
+ if (desiredImageRef) {
681
+ try {
682
+ await updateRailwayServiceImageSource({
683
+ serviceId: existing.id,
684
+ imageRef: desiredImageRef,
685
+ env,
686
+ fetchImpl
687
+ });
688
+ } catch (error) {
689
+ if (!looksLikeRailwayImageSourceUpdateUnsupported(error)) {
690
+ throw error;
691
+ }
692
+ await deleteRailwayService({ serviceId: existing.id, env, fetchImpl });
693
+ await waitForRailwayServiceMissing({ projectId, serviceId: existing.id, env, fetchImpl });
694
+ const replacement = await createRailwayImageService({
695
+ projectId,
696
+ environmentId,
697
+ serviceName: desiredServiceName || existing.name,
698
+ imageRef: desiredImageRef,
699
+ env,
700
+ fetchImpl
701
+ });
702
+ return { service: replacement, created: true, replaced: true };
703
+ }
704
+ }
654
705
  return { service: existing, created: false };
655
706
  }
656
707
  if (!desiredServiceName) {
657
708
  throw new Error("Railway service creation requires a service name.");
658
709
  }
710
+ const service = await createRailwayImageService({
711
+ projectId,
712
+ environmentId,
713
+ serviceName: desiredServiceName,
714
+ imageRef,
715
+ env,
716
+ fetchImpl
717
+ });
718
+ return { service, created: true };
719
+ }
720
+ async function createRailwayImageService({
721
+ projectId,
722
+ serviceName,
723
+ environmentId,
724
+ imageRef,
725
+ env = process.env,
726
+ fetchImpl = fetch
727
+ }) {
659
728
  const created = await railwayGraphqlRequest({
660
729
  query: `
661
730
  mutation TreeseedRailwayServiceCreate($input: ServiceCreateInput!) {
@@ -668,7 +737,13 @@ mutation TreeseedRailwayServiceCreate($input: ServiceCreateInput!) {
668
737
  variables: {
669
738
  input: {
670
739
  projectId,
671
- name: desiredServiceName
740
+ name: serviceName,
741
+ ...railwayConnectionLabel(environmentId) ? { environmentId: railwayConnectionLabel(environmentId) } : {},
742
+ ...railwayConnectionLabel(imageRef) ? {
743
+ source: {
744
+ image: railwayConnectionLabel(imageRef)
745
+ }
746
+ } : {}
672
747
  }
673
748
  },
674
749
  env,
@@ -676,9 +751,189 @@ mutation TreeseedRailwayServiceCreate($input: ServiceCreateInput!) {
676
751
  });
677
752
  const service = created.data?.serviceCreate ? normalizeService(created.data.serviceCreate) : null;
678
753
  if (!service) {
679
- throw new Error(`Railway service create did not return a usable service for ${desiredServiceName}.`);
754
+ throw new Error(`Railway service create did not return a usable service for ${serviceName}.`);
680
755
  }
681
- return { service, created: true };
756
+ return service;
757
+ }
758
+ async function waitForRailwayServiceMissing({
759
+ projectId,
760
+ serviceId,
761
+ env = process.env,
762
+ fetchImpl = fetch,
763
+ attempts = 20,
764
+ delayMs = 1500
765
+ }) {
766
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
767
+ const services = await listRailwayServices({ projectId, env, fetchImpl }).catch(() => []);
768
+ if (!services.some((service) => service.id === serviceId)) {
769
+ return true;
770
+ }
771
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
772
+ }
773
+ throw new Error(`Railway service ${serviceId} was not deleted before image source replacement.`);
774
+ }
775
+ function looksLikeRailwayImageSourceUpdateUnsupported(error) {
776
+ const message = error instanceof Error ? error.message : String(error ?? "");
777
+ return /Problem processing request|source|image|ServiceUpdateInput/iu.test(message);
778
+ }
779
+ async function updateRailwayServiceImageSource({
780
+ serviceId,
781
+ imageRef,
782
+ env = process.env,
783
+ fetchImpl = fetch
784
+ }) {
785
+ const desiredImage = railwayConnectionLabel(imageRef);
786
+ if (!serviceId || !desiredImage) {
787
+ throw new Error("Railway service image source update requires a service id and image reference.");
788
+ }
789
+ const payload = await railwayGraphqlRequest({
790
+ query: `
791
+ mutation TreeseedRailwayServiceImageSourceUpdate($id: String!, $input: ServiceUpdateInput!) {
792
+ serviceUpdate(id: $id, input: $input) {
793
+ id
794
+ name
795
+ }
796
+ }
797
+ `.trim(),
798
+ variables: {
799
+ id: serviceId,
800
+ input: {
801
+ source: {
802
+ image: desiredImage
803
+ }
804
+ }
805
+ },
806
+ env,
807
+ fetchImpl
808
+ });
809
+ const service = payload.data?.serviceUpdate ? normalizeService(payload.data.serviceUpdate) : null;
810
+ if (!service) {
811
+ throw new Error(`Railway service image source update did not return a usable service for ${serviceId}.`);
812
+ }
813
+ return service;
814
+ }
815
+ async function ensureRailwayGeneratedServiceDomain({
816
+ projectId,
817
+ environmentId,
818
+ serviceId,
819
+ targetPort,
820
+ env = process.env,
821
+ fetchImpl = fetch
822
+ }) {
823
+ const domains = await listRailwayServiceDomains({ projectId, environmentId, serviceId, env, fetchImpl });
824
+ const existing = domains.find((domain2) => domain2.kind === "service") ?? domains.find((domain2) => domain2.domain.endsWith(".railway.app")) ?? null;
825
+ if (existing) {
826
+ return { domain: existing, created: false };
827
+ }
828
+ const query = `
829
+ mutation TreeseedRailwayServiceDomainCreate($input: ServiceDomainCreateInput!) {
830
+ serviceDomainCreate(input: $input) {
831
+ id
832
+ domain
833
+ serviceId
834
+ environmentId
835
+ targetPort
836
+ }
837
+ }
838
+ `.trim();
839
+ const inputWithProject = {
840
+ projectId,
841
+ environmentId,
842
+ serviceId,
843
+ ...Number.isFinite(Number(targetPort)) ? { targetPort: Number(targetPort) } : {}
844
+ };
845
+ const createPayload = async (input) => railwayGraphqlRequest({
846
+ query,
847
+ variables: {
848
+ input: {
849
+ ...input
850
+ }
851
+ },
852
+ env,
853
+ fetchImpl
854
+ });
855
+ let payload;
856
+ try {
857
+ payload = await createPayload(inputWithProject);
858
+ } catch (error) {
859
+ const message = error instanceof Error ? error.message : String(error ?? "");
860
+ if (!/Problem processing request|projectId|not defined by type/iu.test(message)) {
861
+ throw error;
862
+ }
863
+ payload = await createPayload({
864
+ environmentId,
865
+ serviceId,
866
+ ...Number.isFinite(Number(targetPort)) ? { targetPort: Number(targetPort) } : {}
867
+ });
868
+ }
869
+ const domain = normalizeRailwayDomain(payload.data?.serviceDomainCreate);
870
+ if (!domain) {
871
+ throw new Error("Railway service domain create did not return a usable domain.");
872
+ }
873
+ return { domain, created: true };
874
+ }
875
+ async function listRailwayServiceDomains({
876
+ projectId,
877
+ environmentId,
878
+ serviceId,
879
+ env = process.env,
880
+ fetchImpl = fetch
881
+ }) {
882
+ const payload = await railwayGraphqlRequest({
883
+ query: `
884
+ query TreeseedRailwayServiceDomains($projectId: String!, $environmentId: String!, $serviceId: String!) {
885
+ domains(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId) {
886
+ serviceDomains {
887
+ id
888
+ domain
889
+ serviceId
890
+ environmentId
891
+ targetPort
892
+ }
893
+ customDomains {
894
+ id
895
+ domain
896
+ serviceId
897
+ environmentId
898
+ targetPort
899
+ }
900
+ }
901
+ }
902
+ `.trim(),
903
+ variables: { projectId, environmentId, serviceId },
904
+ env,
905
+ fetchImpl
906
+ });
907
+ const domains = payload.data?.domains && typeof payload.data.domains === "object" ? payload.data.domains : {};
908
+ return [
909
+ ...normalizeRailwayDomainList(domains.serviceDomains, "service"),
910
+ ...normalizeRailwayDomainList(domains.customDomains, "custom")
911
+ ];
912
+ }
913
+ async function deployRailwayServiceInstance({
914
+ serviceId,
915
+ environmentId,
916
+ env = process.env,
917
+ fetchImpl = fetch
918
+ }) {
919
+ const payload = await railwayGraphqlRequest({
920
+ query: `
921
+ mutation TreeseedRailwayServiceInstanceDeploy($serviceId: String!, $environmentId: String!) {
922
+ serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
923
+ }
924
+ `.trim(),
925
+ variables: { serviceId, environmentId },
926
+ env,
927
+ fetchImpl
928
+ });
929
+ const value = payload.data?.serviceInstanceDeployV2;
930
+ if (typeof value === "string" && value.trim()) {
931
+ return { deploymentId: value.trim() };
932
+ }
933
+ if (value && typeof value === "object") {
934
+ return { deploymentId: railwayConnectionLabel(value.id) || null };
935
+ }
936
+ return { deploymentId: null };
682
937
  }
683
938
  async function updateRailwayServiceName({
684
939
  serviceId,
@@ -717,43 +972,212 @@ async function ensureRailwayPostgresService({
717
972
  environmentId,
718
973
  serviceName,
719
974
  env = process.env,
720
- fetchImpl = fetch
975
+ fetchImpl = fetch,
976
+ maxAttempts = 40
721
977
  }) {
722
978
  const desiredServiceName = railwayConnectionLabel(serviceName);
723
979
  if (!desiredServiceName) {
724
980
  throw new Error("Railway Postgres service creation requires a service name.");
725
981
  }
726
982
  const services = await listRailwayServices({ projectId, env, fetchImpl });
727
- const existing = services.find((service2) => service2.name === desiredServiceName || service2.id === desiredServiceName) ?? null;
983
+ const existing = services.find((service) => service.name === desiredServiceName || service.id === desiredServiceName) ?? null;
728
984
  if (existing) {
729
- return { service: existing, created: false };
985
+ const proof = await inspectRailwayPostgresService({ projectId, environmentId, serviceId: existing.id, env, fetchImpl });
986
+ if (proof.ok) {
987
+ return { service: existing, created: false, proof };
988
+ }
989
+ await deleteRailwayService({ serviceId: existing.id, env, fetchImpl });
730
990
  }
731
- const created = await railwayGraphqlRequest({
991
+ const template = await getRailwayTemplateByCode({ code: "postgres", env, fetchImpl });
992
+ await deployRailwayTemplate({
993
+ templateId: template.id,
994
+ serializedConfig: template.serializedConfig,
995
+ projectId,
996
+ environmentId,
997
+ env,
998
+ fetchImpl
999
+ });
1000
+ const settled = await waitForRailwayPostgresTemplateService({
1001
+ projectId,
1002
+ environmentId,
1003
+ desiredServiceName,
1004
+ env,
1005
+ fetchImpl,
1006
+ maxAttempts
1007
+ });
1008
+ return { service: settled.service, created: true, proof: settled.proof };
1009
+ }
1010
+ async function getRailwayTemplateByCode({
1011
+ code,
1012
+ env = process.env,
1013
+ fetchImpl = fetch
1014
+ }) {
1015
+ const payload = await railwayGraphqlRequest({
732
1016
  query: `
733
- mutation TreeseedRailwayPostgresServiceCreate($input: ServiceCreateInput!) {
734
- serviceCreate(input: $input) {
1017
+ query TreeseedRailwayTemplate($code: String!) {
1018
+ template(code: $code) {
735
1019
  id
1020
+ code
736
1021
  name
1022
+ serializedConfig
1023
+ }
1024
+ }
1025
+ `.trim(),
1026
+ variables: { code },
1027
+ env,
1028
+ fetchImpl,
1029
+ timeoutMs: 15e3,
1030
+ retries: 1
1031
+ });
1032
+ const template = payload.data?.template;
1033
+ const id = railwayConnectionLabel(template?.id);
1034
+ if (!id || !template || typeof template !== "object") {
1035
+ throw new Error(`Railway Postgres template "${code}" was not found through the Railway API.`);
1036
+ }
1037
+ return {
1038
+ id,
1039
+ code: railwayConnectionLabel(template.code) || null,
1040
+ name: railwayConnectionLabel(template.name) || null,
1041
+ serializedConfig: normalizeTemplateSerializedConfig(template.serializedConfig)
1042
+ };
1043
+ }
1044
+ function normalizeTemplateSerializedConfig(value) {
1045
+ if (typeof value === "string") {
1046
+ try {
1047
+ const parsed = JSON.parse(value);
1048
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1049
+ } catch {
1050
+ return {};
1051
+ }
1052
+ }
1053
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1054
+ }
1055
+ async function deployRailwayTemplate({
1056
+ templateId,
1057
+ serializedConfig,
1058
+ projectId,
1059
+ environmentId,
1060
+ env = process.env,
1061
+ fetchImpl = fetch
1062
+ }) {
1063
+ const workspace = await resolveRailwayWorkspaceContext({ env, fetchImpl });
1064
+ const payload = await railwayGraphqlRequest({
1065
+ query: `
1066
+ mutation TreeseedRailwayTemplateDeploy($input: TemplateDeployV2Input!) {
1067
+ templateDeployV2(input: $input) {
1068
+ projectId
1069
+ workflowId
737
1070
  }
738
1071
  }
739
1072
  `.trim(),
740
1073
  variables: {
741
1074
  input: {
1075
+ templateId,
1076
+ serializedConfig,
742
1077
  projectId,
743
1078
  environmentId,
744
- name: desiredServiceName,
745
- templateId: RAILWAY_POSTGRES_TEMPLATE_ID,
746
- templateServiceId: RAILWAY_POSTGRES_TEMPLATE_SERVICE_ID
1079
+ workspaceId: workspace.id
747
1080
  }
748
1081
  },
749
1082
  env,
750
- fetchImpl
1083
+ fetchImpl,
1084
+ timeoutMs: 2e4,
1085
+ retries: 1
751
1086
  });
752
- const service = created.data?.serviceCreate ? normalizeService(created.data.serviceCreate) : null;
753
- if (!service) {
754
- throw new Error(`Railway Postgres service create did not return a usable service for ${desiredServiceName}.`);
1087
+ if (!payload.data?.templateDeployV2?.projectId) {
1088
+ throw new Error("Railway Postgres template deployment did not return a project id.");
755
1089
  }
756
- return { service, created: true };
1090
+ return payload.data.templateDeployV2;
1091
+ }
1092
+ async function waitForRailwayPostgresTemplateService({
1093
+ projectId,
1094
+ environmentId,
1095
+ desiredServiceName,
1096
+ env = process.env,
1097
+ fetchImpl = fetch,
1098
+ maxAttempts = 40
1099
+ }) {
1100
+ let lastProof = null;
1101
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1102
+ const services = await listRailwayServices({ projectId, env, fetchImpl });
1103
+ for (const service of services) {
1104
+ const proof = await inspectRailwayPostgresService({
1105
+ projectId,
1106
+ environmentId,
1107
+ serviceId: service.id,
1108
+ env,
1109
+ fetchImpl
1110
+ });
1111
+ if (proof.ok) {
1112
+ const renamed = service.name === desiredServiceName ? service : await updateRailwayServiceName({ serviceId: service.id, name: desiredServiceName, env, fetchImpl });
1113
+ return { service: renamed, proof };
1114
+ }
1115
+ lastProof = proof;
1116
+ }
1117
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
1118
+ }
1119
+ throw new Error(`Railway Postgres template deployment did not produce a managed PostgreSQL service named ${desiredServiceName}. Last proof: ${lastProof?.message ?? "no candidate service observed"}`);
1120
+ }
1121
+ async function inspectRailwayPostgresService({
1122
+ projectId,
1123
+ environmentId,
1124
+ serviceId,
1125
+ env = process.env,
1126
+ fetchImpl = fetch
1127
+ }) {
1128
+ const variables = await listRailwayVariables({ projectId, environmentId, serviceId, env, fetchImpl }).catch(() => ({}));
1129
+ const hasConnectionVars = typeof variables.DATABASE_URL === "string" && typeof variables.PGHOST === "string" && typeof variables.PGUSER === "string" && typeof variables.PGPASSWORD === "string" && typeof variables.PGDATABASE === "string";
1130
+ const volumes = await listRailwayVolumes({ projectId, env, fetchImpl }).catch(() => []);
1131
+ const volume = volumes.find((candidate) => candidate.instances.some(
1132
+ (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === "/var/lib/postgresql/data"
1133
+ )) ?? null;
1134
+ const deployment = await inspectRailwayServiceDeploymentHealth({ serviceId, environmentId, env, fetchImpl }).catch((error) => ({
1135
+ ok: false,
1136
+ status: null,
1137
+ message: error instanceof Error ? error.message : String(error ?? "Unable to inspect PostgreSQL deployment health.")
1138
+ }));
1139
+ return {
1140
+ ok: hasConnectionVars && Boolean(volume) && deployment.ok,
1141
+ variableKeys: Object.keys(variables).sort(),
1142
+ volumeId: volume?.id ?? null,
1143
+ deploymentStatus: deployment.status,
1144
+ message: hasConnectionVars ? volume ? deployment.ok ? "Railway managed PostgreSQL markers are present and the deployment is healthy." : `Railway managed PostgreSQL markers are present, but deployment health is not ready. ${deployment.message}` : "PostgreSQL connection variables exist, but the managed data volume is missing." : "PostgreSQL connection variables are missing."
1145
+ };
1146
+ }
1147
+ async function inspectRailwayServiceDeploymentHealth({
1148
+ serviceId,
1149
+ environmentId,
1150
+ env = process.env,
1151
+ fetchImpl = fetch
1152
+ }) {
1153
+ const payload = await railwayGraphqlRequest({
1154
+ query: `
1155
+ query TreeseedRailwayServiceDeploymentHealth($serviceId: String!, $environmentId: String!) {
1156
+ serviceInstance(serviceId: $serviceId, environmentId: $environmentId) {
1157
+ latestDeployment {
1158
+ status
1159
+ deploymentStopped
1160
+ instances {
1161
+ status
1162
+ }
1163
+ }
1164
+ }
1165
+ }
1166
+ `.trim(),
1167
+ variables: { serviceId, environmentId },
1168
+ env,
1169
+ fetchImpl
1170
+ });
1171
+ const deployment = payload.data?.serviceInstance?.latestDeployment;
1172
+ const status = railwayConnectionLabel(deployment?.status)?.toUpperCase() ?? null;
1173
+ const instanceStatuses = Array.isArray(deployment?.instances) ? deployment.instances.map((instance) => railwayConnectionLabel(instance?.status)?.toUpperCase()).filter(Boolean) : [];
1174
+ const stopped = deployment?.deploymentStopped === true;
1175
+ const ok = status === "SUCCESS" && !stopped && (instanceStatuses.length === 0 || instanceStatuses.some((candidate) => candidate === "RUNNING"));
1176
+ return {
1177
+ ok,
1178
+ status,
1179
+ message: ok ? "Deployment is healthy." : `Latest deployment status is ${status ?? "unknown"}${stopped ? " and stopped" : ""}${instanceStatuses.length ? `; instances=${instanceStatuses.join(",")}` : ""}.`
1180
+ };
757
1181
  }
758
1182
  async function listRailwayServices({
759
1183
  projectId,
@@ -884,6 +1308,7 @@ async function ensureRailwayServiceInstanceConfiguration({
884
1308
  healthcheckIntervalSeconds,
885
1309
  restartPolicy,
886
1310
  runtimeMode,
1311
+ deploymentRegion,
887
1312
  env = process.env,
888
1313
  fetchImpl = fetch,
889
1314
  settleAttempts = 60,
@@ -899,6 +1324,8 @@ async function ensureRailwayServiceInstanceConfiguration({
899
1324
  if (!current.id) {
900
1325
  return { instance: current, updated: false };
901
1326
  }
1327
+ const desiredRuntimeMode = railwayConnectionLabel(runtimeMode) === "service" ? "replicated" : railwayConnectionLabel(runtimeMode) || null;
1328
+ const desiredDeploymentRegion = railwayConnectionLabel(deploymentRegion) || null;
902
1329
  const desired = {
903
1330
  buildCommand: railwayConnectionLabel(buildCommand) || null,
904
1331
  startCommand: railwayConnectionLabel(startCommand) || null,
@@ -908,10 +1335,11 @@ async function ensureRailwayServiceInstanceConfiguration({
908
1335
  healthcheckTimeoutSeconds: normalizeRailwayNumber(healthcheckTimeoutSeconds),
909
1336
  healthcheckIntervalSeconds: normalizeRailwayNumber(healthcheckIntervalSeconds),
910
1337
  restartPolicy: railwayConnectionLabel(restartPolicy) || null,
911
- runtimeMode: railwayConnectionLabel(runtimeMode) || null,
912
- sleepApplication: railwayConnectionLabel(runtimeMode) === "serverless" ? true : railwayConnectionLabel(runtimeMode) === "replicated" ? false : null
1338
+ runtimeMode: desiredRuntimeMode,
1339
+ deploymentRegion: desiredDeploymentRegion,
1340
+ sleepApplication: desiredRuntimeMode === "serverless" ? true : desiredRuntimeMode === "replicated" ? false : null
913
1341
  };
914
- const needsRuntimeConfig = desired.healthcheckPath !== null || desired.healthcheckTimeoutSeconds !== null || desired.runtimeMode !== null;
1342
+ const needsRuntimeConfig = desired.healthcheckPath !== null || desired.healthcheckTimeoutSeconds !== null || desired.runtimeMode !== null || desired.deploymentRegion !== null;
915
1343
  if (needsRuntimeConfig && current.runtimeConfigSupported !== true) {
916
1344
  throw new Error("Railway service instance runtime settings are unsupported by the current Railway API schema.");
917
1345
  }
@@ -921,7 +1349,7 @@ async function ensureRailwayServiceInstanceConfiguration({
921
1349
  if (desired.restartPolicy !== null) {
922
1350
  throw new Error("Railway service instance restart policies are unsupported by the current Railway API schema.");
923
1351
  }
924
- const drifted = desired.buildCommand !== null && desired.buildCommand !== current.buildCommand || desired.startCommand !== null && desired.startCommand !== current.startCommand || desired.cronSchedule !== null && desired.cronSchedule !== current.cronSchedule || desired.rootDirectory !== null && desired.rootDirectory !== current.rootDirectory || desired.healthcheckPath !== null && desired.healthcheckPath !== current.healthcheckPath || desired.healthcheckTimeoutSeconds !== null && desired.healthcheckTimeoutSeconds !== current.healthcheckTimeoutSeconds || desired.runtimeMode !== null && desired.runtimeMode !== current.runtimeMode;
1352
+ const drifted = desired.buildCommand !== null && desired.buildCommand !== current.buildCommand || desired.startCommand !== null && desired.startCommand !== current.startCommand || desired.cronSchedule !== null && desired.cronSchedule !== current.cronSchedule || desired.rootDirectory !== null && desired.rootDirectory !== current.rootDirectory || desired.healthcheckPath !== null && desired.healthcheckPath !== current.healthcheckPath || desired.healthcheckTimeoutSeconds !== null && desired.healthcheckTimeoutSeconds !== current.healthcheckTimeoutSeconds || desired.runtimeMode !== null && desired.runtimeMode !== current.runtimeMode || desired.deploymentRegion !== null;
925
1353
  if (!drifted) {
926
1354
  return { instance: current, updated: false };
927
1355
  }
@@ -947,7 +1375,12 @@ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $enviro
947
1375
  ...desired.rootDirectory !== null ? { rootDirectory: desired.rootDirectory } : {},
948
1376
  ...desired.healthcheckPath !== null ? { healthcheckPath: desired.healthcheckPath } : {},
949
1377
  ...desired.healthcheckTimeoutSeconds !== null ? { healthcheckTimeout: desired.healthcheckTimeoutSeconds } : {},
950
- ...desired.sleepApplication !== null ? { sleepApplication: desired.sleepApplication } : {}
1378
+ ...desired.sleepApplication !== null ? { sleepApplication: desired.sleepApplication } : {},
1379
+ ...desired.deploymentRegion !== null ? {
1380
+ multiRegionConfig: {
1381
+ [desired.deploymentRegion]: { numReplicas: 1 }
1382
+ }
1383
+ } : {}
951
1384
  }
952
1385
  },
953
1386
  env,
@@ -1248,7 +1681,9 @@ async function ensureRailwayServiceVolume({
1248
1681
  name,
1249
1682
  mountPath,
1250
1683
  env = process.env,
1251
- fetchImpl = fetch
1684
+ fetchImpl = fetch,
1685
+ settleAttempts = 24,
1686
+ settleDelayMs = 5e3
1252
1687
  }) {
1253
1688
  if (!mountPath.startsWith("/")) {
1254
1689
  throw new Error(`Railway volume mount path must be absolute: ${mountPath}`);
@@ -1258,7 +1693,22 @@ async function ensureRailwayServiceVolume({
1258
1693
  ...candidate,
1259
1694
  instances: candidate.instances.filter(isActiveRailwayVolumeInstance)
1260
1695
  })).filter((candidate) => candidate.instances.length > 0);
1261
- let volume = findRailwayVolumeForService(volumes, serviceId, environmentId) ?? activeVolumes.find(
1696
+ const exactVolume = activeVolumes.find(
1697
+ (candidate) => candidate.name === name && candidate.instances.some(
1698
+ (instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId && instance2.mountPath === mountPath
1699
+ )
1700
+ ) ?? null;
1701
+ if (exactVolume) {
1702
+ return {
1703
+ volume: exactVolume,
1704
+ instance: exactVolume.instances.find(
1705
+ (instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId && instance2.mountPath === mountPath
1706
+ ) ?? null,
1707
+ created: false,
1708
+ updated: false
1709
+ };
1710
+ }
1711
+ let volume = findRailwayVolumeForService(volumes, serviceId, environmentId) ?? findRailwayVolumeForService(volumes, serviceId) ?? activeVolumes.find(
1262
1712
  (candidate) => candidate.name === name && candidate.instances.some((instance2) => instance2.environmentId === environmentId)
1263
1713
  ) ?? null;
1264
1714
  let created = false;
@@ -1282,7 +1732,7 @@ async function ensureRailwayServiceVolume({
1282
1732
  for (let attempt = 0; attempt < 8; attempt += 1) {
1283
1733
  await new Promise((resolve) => setTimeout(resolve, 1500));
1284
1734
  const refreshed = await listRailwayVolumes({ projectId, env, fetchImpl });
1285
- const existing = findRailwayVolumeForService(refreshed, serviceId, environmentId);
1735
+ const existing = findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? findRailwayVolumeForService(refreshed, serviceId) ?? findSoleActiveRailwayVolumeForEnvironment(refreshed, environmentId);
1286
1736
  if (existing) {
1287
1737
  return existing;
1288
1738
  }
@@ -1313,10 +1763,33 @@ async function ensureRailwayServiceVolume({
1313
1763
  await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1314
1764
  volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1315
1765
  } catch (error) {
1316
- if (!looksLikeRailwayMissingResource(error)) {
1766
+ if (looksLikeRailwayVolumeCreateRace(error)) {
1767
+ volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then(
1768
+ (refreshed) => findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? volume
1769
+ );
1770
+ } else if (!looksLikeRailwayMissingResource(error)) {
1317
1771
  throw error;
1772
+ } else {
1773
+ volume = await createReplacementVolume();
1774
+ }
1775
+ }
1776
+ instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1777
+ updated = true;
1778
+ }
1779
+ if (!instance) {
1780
+ try {
1781
+ await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1782
+ volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1783
+ } catch (error) {
1784
+ if (looksLikeRailwayVolumeCreateRace(error)) {
1785
+ volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then(
1786
+ (refreshed) => findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? volume
1787
+ );
1788
+ } else if (!looksLikeRailwayMissingResource(error)) {
1789
+ throw error;
1790
+ } else {
1791
+ volume = await createReplacementVolume();
1318
1792
  }
1319
- volume = await createReplacementVolume();
1320
1793
  }
1321
1794
  instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1322
1795
  updated = true;
@@ -1337,18 +1810,68 @@ async function ensureRailwayServiceVolume({
1337
1810
  }
1338
1811
  updated = true;
1339
1812
  }
1813
+ const settled = await waitForRailwayVolumeMount({
1814
+ projectId,
1815
+ volume,
1816
+ serviceId,
1817
+ environmentId,
1818
+ mountPath,
1819
+ env,
1820
+ fetchImpl,
1821
+ settleAttempts,
1822
+ settleDelayMs
1823
+ });
1824
+ if (settled) {
1825
+ volume = settled.volume;
1826
+ instance = settled.instance;
1827
+ }
1828
+ if (!instance || instance.serviceId !== serviceId || instance.environmentId !== environmentId || instance.mountPath !== mountPath) {
1829
+ throw new Error(`Railway API volume reconciliation did not observe ${name} mounted on service ${serviceId} at ${mountPath}.`);
1830
+ }
1340
1831
  return { volume, instance, created, updated };
1341
1832
  }
1833
+ async function waitForRailwayVolumeMount({
1834
+ projectId,
1835
+ volume,
1836
+ serviceId,
1837
+ environmentId,
1838
+ mountPath,
1839
+ env,
1840
+ fetchImpl,
1841
+ settleAttempts,
1842
+ settleDelayMs
1843
+ }) {
1844
+ const mountedInstance = (candidate) => candidate.instances.find(
1845
+ (entry) => entry.serviceId === serviceId && entry.environmentId === environmentId && entry.mountPath === mountPath && isActiveRailwayVolumeInstance(entry)
1846
+ ) ?? null;
1847
+ let instance = mountedInstance(volume);
1848
+ if (instance) {
1849
+ return { volume, instance };
1850
+ }
1851
+ for (let attempt = 0; attempt < settleAttempts; attempt += 1) {
1852
+ await new Promise((resolve) => setTimeout(resolve, settleDelayMs));
1853
+ const refreshed = await listRailwayVolumes({ projectId, env, fetchImpl });
1854
+ const refreshedVolume = refreshed.find((candidate) => candidate.id === volume.id) ?? findRailwayVolumeForService(refreshed, serviceId, environmentId) ?? null;
1855
+ if (!refreshedVolume) {
1856
+ continue;
1857
+ }
1858
+ instance = mountedInstance(refreshedVolume);
1859
+ if (instance) {
1860
+ return { volume: refreshedVolume, instance };
1861
+ }
1862
+ }
1863
+ return null;
1864
+ }
1342
1865
  function findRailwayVolumeForService(volumes, serviceId, environmentId) {
1343
1866
  return volumes.find(
1344
1867
  (candidate) => candidate.instances.some(
1345
- (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && isActiveRailwayVolumeInstance(instance)
1868
+ (instance) => instance.serviceId === serviceId && (!environmentId || instance.environmentId === environmentId) && isActiveRailwayVolumeInstance(instance)
1346
1869
  )
1347
1870
  ) ?? null;
1348
1871
  }
1349
1872
  function looksLikeRailwayVolumeCreateRace(error) {
1350
1873
  const message = error instanceof Error ? error.message : String(error ?? "");
1351
- return /would have \d+ volumes attached|can only have one volume|not authorized/iu.test(message);
1874
+ 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);
1352
1875
  }
1353
1876
  async function listRailwayCustomDomains({
1354
1877
  projectId,
@@ -1471,12 +1994,16 @@ async function railwayDeleteMutation({
1471
1994
  missingResult
1472
1995
  }) {
1473
1996
  try {
1474
- await railwayGraphqlRequest({
1997
+ const payload = await railwayGraphqlRequest({
1475
1998
  query,
1476
1999
  variables,
1477
2000
  env,
1478
2001
  fetchImpl
1479
2002
  });
2003
+ const mutationResult = Object.values(payload.data ?? {})[0];
2004
+ if (mutationResult === false || mutationResult == null) {
2005
+ throw new Error("Railway delete mutation returned no successful deletion result.");
2006
+ }
1480
2007
  return { status: "deleted" };
1481
2008
  } catch (error) {
1482
2009
  if (looksLikeRailwayMissingResource(error)) {
@@ -1506,6 +2033,27 @@ mutation TreeseedRailwayCustomDomainDelete($id: String!) {
1506
2033
  missingResult: { status: "missing", id: domainId }
1507
2034
  });
1508
2035
  }
2036
+ async function deleteRailwayService({
2037
+ serviceId,
2038
+ env = process.env,
2039
+ fetchImpl = fetch
2040
+ }) {
2041
+ if (!railwayConnectionLabel(serviceId)) {
2042
+ return { status: "missing", id: serviceId };
2043
+ }
2044
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_SERVICE_DELETE_MUTATION") || `
2045
+ mutation TreeseedRailwayServiceDelete($id: String!) {
2046
+ serviceDelete(id: $id)
2047
+ }
2048
+ `.trim();
2049
+ return railwayDeleteMutation({
2050
+ query: mutation,
2051
+ variables: { id: serviceId },
2052
+ env,
2053
+ fetchImpl,
2054
+ missingResult: { status: "missing", id: serviceId }
2055
+ });
2056
+ }
1509
2057
  async function deleteRailwayVolume({
1510
2058
  volumeId,
1511
2059
  env = process.env,
@@ -1514,18 +2062,55 @@ async function deleteRailwayVolume({
1514
2062
  if (!railwayConnectionLabel(volumeId)) {
1515
2063
  return { status: "missing", id: volumeId };
1516
2064
  }
1517
- const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_DELETE_MUTATION") || `
2065
+ const configuredMutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_DELETE_MUTATION");
2066
+ if (configuredMutation) {
2067
+ return railwayDeleteMutation({
2068
+ query: configuredMutation,
2069
+ variables: { volumeId, id: volumeId },
2070
+ env,
2071
+ fetchImpl,
2072
+ missingResult: { status: "missing", id: volumeId }
2073
+ });
2074
+ }
2075
+ const primaryMutation = `
1518
2076
  mutation TreeseedRailwayVolumeDelete($volumeId: String!) {
1519
2077
  volumeDelete(volumeId: $volumeId)
1520
2078
  }
1521
2079
  `.trim();
1522
- return railwayDeleteMutation({
1523
- query: mutation,
2080
+ const fallbackMutation = `
2081
+ mutation TreeseedRailwayVolumeDeleteById($id: String!) {
2082
+ volumeDelete(volumeId: $id)
2083
+ }
2084
+ `.trim();
2085
+ const primary = await railwayDeleteMutation({
2086
+ query: primaryMutation,
1524
2087
  variables: { volumeId },
1525
2088
  env,
1526
2089
  fetchImpl,
1527
2090
  missingResult: { status: "missing", id: volumeId }
2091
+ }).catch((error) => {
2092
+ if (looksLikeRailwayVolumeDeleteShapeUnsupported(error)) {
2093
+ return null;
2094
+ }
2095
+ throw error;
2096
+ });
2097
+ const fallback = await railwayDeleteMutation({
2098
+ query: fallbackMutation,
2099
+ variables: { id: volumeId },
2100
+ env,
2101
+ fetchImpl,
2102
+ missingResult: { status: "missing", id: volumeId }
2103
+ }).catch((error) => {
2104
+ if (looksLikeRailwayVolumeDeleteShapeUnsupported(error) && primary) {
2105
+ return primary;
2106
+ }
2107
+ throw error;
1528
2108
  });
2109
+ return fallback ?? primary ?? { status: "deleted" };
2110
+ }
2111
+ function looksLikeRailwayVolumeDeleteShapeUnsupported(error) {
2112
+ const message = error instanceof Error ? error.message : String(error ?? "");
2113
+ return /Unknown argument|Cannot query field|Unknown field|Field .* is not defined|volumeDelete.*argument|Problem processing request/iu.test(message);
1529
2114
  }
1530
2115
  async function deleteRailwayEnvironment({
1531
2116
  environmentId,
@@ -1573,9 +2158,12 @@ export {
1573
2158
  deleteRailwayCustomDomain,
1574
2159
  deleteRailwayEnvironment,
1575
2160
  deleteRailwayProject,
2161
+ deleteRailwayService,
1576
2162
  deleteRailwayVolume,
2163
+ deployRailwayServiceInstance,
1577
2164
  ensureRailwayCustomDomain,
1578
2165
  ensureRailwayEnvironment,
2166
+ ensureRailwayGeneratedServiceDomain,
1579
2167
  ensureRailwayPostgresService,
1580
2168
  ensureRailwayProject,
1581
2169
  ensureRailwayService,
@@ -1584,10 +2172,12 @@ export {
1584
2172
  getRailwayAuthProfile,
1585
2173
  getRailwayProject,
1586
2174
  getRailwayServiceInstance,
2175
+ inspectRailwayServiceDeploymentHealth,
1587
2176
  isUsableRailwayToken,
1588
2177
  listRailwayCustomDomains,
1589
2178
  listRailwayEnvironments,
1590
2179
  listRailwayProjects,
2180
+ listRailwayServiceDomains,
1591
2181
  listRailwayServices,
1592
2182
  listRailwayVariables,
1593
2183
  listRailwayVolumes,
@@ -1597,6 +2187,7 @@ export {
1597
2187
  resolveRailwayApiUrl,
1598
2188
  resolveRailwayWorkspace,
1599
2189
  resolveRailwayWorkspaceContext,
2190
+ updateRailwayServiceImageSource,
1600
2191
  updateRailwayServiceName,
1601
2192
  upsertRailwayVariables
1602
2193
  };