@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.
- package/README.md +207 -6
- package/dist/capacity-provider.d.ts +3 -1
- package/dist/capacity-provider.js +25 -5
- package/dist/control-plane.d.ts +1 -0
- package/dist/control-plane.js +38 -13
- package/dist/db/market-schema.d.ts +8860 -6172
- package/dist/db/market-schema.js +108 -0
- package/dist/db/node-sqlite.js +7 -2
- package/dist/hosting/apps.d.ts +12 -0
- package/dist/hosting/apps.js +107 -0
- package/dist/hosting/builtins.d.ts +25 -0
- package/dist/hosting/builtins.js +791 -0
- package/dist/hosting/contracts.d.ts +207 -0
- package/dist/hosting/contracts.js +0 -0
- package/dist/hosting/graph.d.ts +192 -0
- package/dist/hosting/graph.js +1106 -0
- package/dist/hosting/index.d.ts +4 -0
- package/dist/hosting/index.js +4 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +71 -7
- package/dist/managed-dependencies.js +1 -2
- package/dist/market-client.d.ts +63 -3
- package/dist/market-client.js +83 -11
- package/dist/operations/services/bootstrap-runner.d.ts +3 -1
- package/dist/operations/services/bootstrap-runner.js +22 -2
- package/dist/operations/services/config-runtime.d.ts +10 -5
- package/dist/operations/services/config-runtime.js +209 -66
- package/dist/operations/services/deploy.d.ts +70 -7
- package/dist/operations/services/deploy.js +579 -64
- package/dist/operations/services/deployment-readiness.d.ts +30 -0
- package/dist/operations/services/deployment-readiness.js +175 -0
- package/dist/operations/services/git-workflow.d.ts +2 -1
- package/dist/operations/services/git-workflow.js +9 -3
- package/dist/operations/services/github-actions-verification.d.ts +1 -0
- package/dist/operations/services/github-actions-verification.js +1 -0
- package/dist/operations/services/github-api.js +1 -1
- package/dist/operations/services/github-automation.d.ts +1 -1
- package/dist/operations/services/github-automation.js +4 -3
- package/dist/operations/services/github-credentials.d.ts +13 -0
- package/dist/operations/services/github-credentials.js +58 -0
- package/dist/operations/services/hosted-service-checks.d.ts +63 -0
- package/dist/operations/services/hosted-service-checks.js +327 -0
- package/dist/operations/services/hub-provider-launch.js +3 -3
- package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
- package/dist/operations/services/live-hosted-service-checks.js +350 -0
- package/dist/operations/services/managed-host-security.js +1 -1
- package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
- package/dist/operations/services/operations-runner-smoke.js +180 -0
- package/dist/operations/services/package-adapters.d.ts +95 -0
- package/dist/operations/services/package-adapters.js +288 -0
- package/dist/operations/services/package-reference-policy.d.ts +1 -0
- package/dist/operations/services/package-reference-policy.js +15 -2
- package/dist/operations/services/project-platform.d.ts +80 -22
- package/dist/operations/services/project-platform.js +49 -8
- package/dist/operations/services/project-web-monitor.js +26 -4
- package/dist/operations/services/railway-api.d.ts +88 -5
- package/dist/operations/services/railway-api.js +626 -35
- package/dist/operations/services/railway-deploy.d.ts +46 -40
- package/dist/operations/services/railway-deploy.js +261 -293
- package/dist/operations/services/release-candidate.d.ts +19 -0
- package/dist/operations/services/release-candidate.js +375 -38
- package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
- package/dist/operations/services/repository-save-orchestrator.js +279 -66
- package/dist/operations/services/runtime-tools.d.ts +1 -0
- package/dist/operations/services/runtime-tools.js +10 -9
- package/dist/operations/services/template-registry.js +14 -7
- package/dist/operations/services/verification-cache.d.ts +25 -0
- package/dist/operations/services/verification-cache.js +71 -0
- package/dist/operations/services/workspace-dependency-mode.js +9 -1
- package/dist/operations/services/workspace-save.js +1 -1
- package/dist/operations/services/workspace-tools.js +2 -1
- package/dist/platform/contracts.d.ts +32 -1
- package/dist/platform/deploy-config.js +73 -8
- package/dist/platform/env.yaml +163 -35
- package/dist/platform/environment.d.ts +1 -0
- package/dist/platform/environment.js +74 -5
- package/dist/platform/plugin.d.ts +9 -0
- package/dist/platform-operation-store.js +2 -2
- package/dist/platform-operations.js +1 -1
- package/dist/reconcile/bootstrap-systems.js +2 -2
- package/dist/reconcile/builtin-adapters.js +372 -189
- package/dist/reconcile/contracts.d.ts +9 -5
- package/dist/reconcile/desired-state.d.ts +1 -0
- package/dist/reconcile/desired-state.js +5 -5
- package/dist/reconcile/engine.d.ts +5 -2
- package/dist/reconcile/engine.js +53 -32
- package/dist/reconcile/index.d.ts +2 -0
- package/dist/reconcile/index.js +2 -0
- package/dist/reconcile/live-acceptance.d.ts +79 -0
- package/dist/reconcile/live-acceptance.js +1615 -0
- package/dist/reconcile/platform.d.ts +104 -0
- package/dist/reconcile/platform.js +100 -0
- package/dist/reconcile/state.js +4 -4
- package/dist/reconcile/units.js +2 -2
- package/dist/scripts/deployment-readiness.js +20 -0
- package/dist/scripts/generate-treedx-openapi-types.js +186 -0
- package/dist/scripts/operations-runner-smoke.js +16 -0
- package/dist/scripts/release-verify.js +4 -1
- package/dist/scripts/template-catalog.test.js +7 -7
- package/dist/scripts/tenant-workflow-action.js +10 -1
- package/dist/sdk-types.d.ts +172 -5
- package/dist/sdk-types.js +28 -3
- package/dist/sdk.d.ts +35 -24
- package/dist/sdk.js +186 -17
- package/dist/template-launch-requirements.js +9 -0
- package/dist/treedx/adapters.d.ts +6 -0
- package/dist/treedx/adapters.js +36 -0
- package/dist/treedx/client.d.ts +222 -0
- package/dist/treedx/client.js +871 -0
- package/dist/treedx/errors.d.ts +13 -0
- package/dist/treedx/errors.js +17 -0
- package/dist/treedx/federated-client.d.ts +27 -0
- package/dist/treedx/federated-client.js +158 -0
- package/dist/treedx/generated/openapi-types.d.ts +3558 -0
- package/dist/treedx/generated/openapi-types.js +0 -0
- package/dist/treedx/graph-adapter.d.ts +33 -0
- package/dist/treedx/graph-adapter.js +156 -0
- package/dist/treedx/index.d.ts +14 -0
- package/dist/treedx/index.js +48 -0
- package/dist/treedx/market-integration.d.ts +27 -0
- package/dist/treedx/market-integration.js +131 -0
- package/dist/treedx/ports.d.ts +166 -0
- package/dist/treedx/ports.js +231 -0
- package/dist/treedx/query-adapter.d.ts +19 -0
- package/dist/treedx/query-adapter.js +62 -0
- package/dist/treedx/registry-client.d.ts +11 -0
- package/dist/treedx/registry-client.js +19 -0
- package/dist/treedx/repository-adapter.d.ts +45 -0
- package/dist/treedx/repository-adapter.js +308 -0
- package/dist/treedx/sdk-integration.d.ts +27 -0
- package/dist/treedx/sdk-integration.js +63 -0
- package/dist/treedx/types.d.ts +1084 -0
- package/dist/treedx/types.js +8 -0
- package/dist/treedx/workspace-adapter.d.ts +27 -0
- package/dist/treedx/workspace-adapter.js +65 -0
- package/dist/treedx-backends.d.ts +218 -0
- package/dist/treedx-backends.js +632 -0
- package/dist/treedx-client.d.ts +86 -0
- package/dist/treedx-client.js +175 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +497 -138
- package/dist/workflow/operations.d.ts +119 -13
- package/dist/workflow/operations.js +309 -53
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.js +43 -26
- package/dist/workflow-support.d.ts +11 -3
- package/dist/workflow-support.js +67 -3
- package/dist/workflow.d.ts +5 -0
- package/drizzle/market/0004_treedx_market_integration.sql +99 -0
- package/package.json +34 -3
- package/templates/github/deploy-web.workflow.yml +39 -6
- package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.d.ts +0 -3
- package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.ts +0 -6
- package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +0 -35
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +0 -4
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +0 -65
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +0 -22
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/empty/.gitkeep +0 -1
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/knowledge/handbook/index.mdx +0 -11
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/pages/welcome.mdx +0 -11
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +0 -11
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +0 -17
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.d.ts +0 -1
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.ts +0 -3
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/env.yaml +0 -1
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +0 -26
- package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +0 -74
- package/dist/treeseed/template-catalog/templates/starter-basic/template/tsconfig.json +0 -9
- 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:
|
|
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 ${
|
|
754
|
+
throw new Error(`Railway service create did not return a usable service for ${serviceName}.`);
|
|
680
755
|
}
|
|
681
|
-
return
|
|
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((
|
|
983
|
+
const existing = services.find((service) => service.name === desiredServiceName || service.id === desiredServiceName) ?? null;
|
|
728
984
|
if (existing) {
|
|
729
|
-
|
|
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
|
|
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
|
-
|
|
734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
753
|
-
|
|
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
|
|
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:
|
|
912
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
1523
|
-
|
|
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
|
};
|