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