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