@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,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { createWriteStream, existsSync, mkdirSync, rmSync, renameSync, chmodSync, copyFileSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, mkdtempSync, rmSync, renameSync, chmodSync, copyFileSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { request as httpRequest } from "node:http";
|
|
4
4
|
import { request as httpsRequest } from "node:https";
|
|
5
5
|
import { platform as osPlatform, arch as osArch } from "node:os";
|
|
@@ -17,14 +17,40 @@ const GH_ASSETS = [
|
|
|
17
17
|
{ platform: "darwin", arch: "x64", assetName: `gh_${GH_VERSION}_macOS_amd64.zip`, archiveKind: "zip" },
|
|
18
18
|
{ platform: "darwin", arch: "arm64", assetName: `gh_${GH_VERSION}_macOS_arm64.zip`, archiveKind: "zip" }
|
|
19
19
|
];
|
|
20
|
-
const
|
|
21
|
-
|
|
20
|
+
const RAILWAY_VERSION = "5.23.2";
|
|
21
|
+
const RAILWAY_RELEASE_BASE_URL = `https://github.com/railwayapp/cli/releases/download/v${RAILWAY_VERSION}`;
|
|
22
|
+
const RAILWAY_ASSETS = [
|
|
22
23
|
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
platform: "linux",
|
|
25
|
+
arch: "x64",
|
|
26
|
+
assetName: `railway-v${RAILWAY_VERSION}-x86_64-unknown-linux-gnu.tar.gz`,
|
|
27
|
+
archiveKind: "tar.gz",
|
|
28
|
+
sha256: "ced014a566bc273ce87463678a2d1e5d9ee02f165a832d5d2fd27c201855145b"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
platform: "linux",
|
|
32
|
+
arch: "arm64",
|
|
33
|
+
assetName: `railway-v${RAILWAY_VERSION}-aarch64-unknown-linux-musl.tar.gz`,
|
|
34
|
+
archiveKind: "tar.gz",
|
|
35
|
+
sha256: "b6100a01d0bd5d349f39e796a907e77f45dadb42f99eee558a1aa7bb254b973e"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
platform: "darwin",
|
|
39
|
+
arch: "x64",
|
|
40
|
+
assetName: `railway-v${RAILWAY_VERSION}-x86_64-apple-darwin.tar.gz`,
|
|
41
|
+
archiveKind: "tar.gz",
|
|
42
|
+
sha256: "403721baa47c2afd0391190310ae8aaf9d671d3ac3d7db123219686b2762bea3"
|
|
27
43
|
},
|
|
44
|
+
{
|
|
45
|
+
platform: "darwin",
|
|
46
|
+
arch: "arm64",
|
|
47
|
+
assetName: `railway-v${RAILWAY_VERSION}-aarch64-apple-darwin.tar.gz`,
|
|
48
|
+
archiveKind: "tar.gz",
|
|
49
|
+
sha256: "83ddc35f9a5ec1a8adb4cf6a024f23227b109832b2d354633a9668c47acb02fa"
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
const NPM_TOOLS = [
|
|
53
|
+
{ name: "wrangler", packageName: "wrangler", binName: "wrangler", version: "4.86.0" },
|
|
28
54
|
{ name: "copilot", packageName: "@github/copilot", binName: "copilot", version: "1.0.39" },
|
|
29
55
|
{ name: "copilot-language-server", packageName: "@github/copilot-language-server", binName: "copilot-language-server", version: "1.480.0" }
|
|
30
56
|
];
|
|
@@ -76,9 +102,19 @@ function currentPlatformAsset() {
|
|
|
76
102
|
}
|
|
77
103
|
return GH_ASSETS.find((asset) => asset.platform === platform && asset.arch === arch) ?? null;
|
|
78
104
|
}
|
|
105
|
+
function currentRailwayPlatformAsset() {
|
|
106
|
+
const platform = osPlatform();
|
|
107
|
+
const arch = osArch();
|
|
108
|
+
if (platform !== "linux" && platform !== "darwin") return null;
|
|
109
|
+
if (arch !== "x64" && arch !== "arm64") return null;
|
|
110
|
+
return RAILWAY_ASSETS.find((asset) => asset.platform === platform && asset.arch === arch) ?? null;
|
|
111
|
+
}
|
|
79
112
|
function managedGhBin(env = process.env) {
|
|
80
113
|
return resolve(resolveToolsHome(env), "gh", GH_VERSION, platformKey(), "bin", "gh");
|
|
81
114
|
}
|
|
115
|
+
function managedRailwayBin(env = process.env) {
|
|
116
|
+
return resolve(resolveToolsHome(env), "railway", RAILWAY_VERSION, platformKey(), "bin", "railway");
|
|
117
|
+
}
|
|
82
118
|
function tokenEnv(env = process.env) {
|
|
83
119
|
const translated = withTreeseedServiceCredentialEnv(env);
|
|
84
120
|
const ghToken = translated.GH_TOKEN?.trim() || translated.GITHUB_TOKEN?.trim() || "";
|
|
@@ -247,28 +283,126 @@ function npmBackedDependenciesAvailable() {
|
|
|
247
283
|
}
|
|
248
284
|
}
|
|
249
285
|
function resolveNpmInstallCommand(env = process.env) {
|
|
286
|
+
const installArgs = ["install", "--ignore-scripts", "--prefer-offline", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
250
287
|
const npmCommandOverride = env.TREESEED_NPM_INSTALL_COMMAND;
|
|
251
288
|
if (npmCommandOverride?.trim()) {
|
|
252
289
|
return {
|
|
253
290
|
command: npmCommandOverride,
|
|
254
|
-
args:
|
|
255
|
-
display: [npmCommandOverride,
|
|
291
|
+
args: installArgs,
|
|
292
|
+
display: [npmCommandOverride, ...installArgs]
|
|
256
293
|
};
|
|
257
294
|
}
|
|
258
295
|
const npmExecPath = env.npm_execpath || env.NPM_EXEC_PATH;
|
|
259
296
|
if (npmExecPath?.trim()) {
|
|
260
297
|
return {
|
|
261
298
|
command: process.execPath,
|
|
262
|
-
args: [npmExecPath,
|
|
263
|
-
display: [process.execPath, npmExecPath,
|
|
299
|
+
args: [npmExecPath, ...installArgs],
|
|
300
|
+
display: [process.execPath, npmExecPath, ...installArgs]
|
|
264
301
|
};
|
|
265
302
|
}
|
|
266
303
|
return {
|
|
267
304
|
command: "npm",
|
|
268
|
-
args:
|
|
269
|
-
display: ["npm",
|
|
305
|
+
args: installArgs,
|
|
306
|
+
display: ["npm", ...installArgs]
|
|
270
307
|
};
|
|
271
308
|
}
|
|
309
|
+
function esbuildPlatformPackage() {
|
|
310
|
+
const platform = osPlatform();
|
|
311
|
+
const arch = osArch();
|
|
312
|
+
if (platform === "linux" && ["x64", "arm64", "arm", "ia32", "ppc64", "riscv64", "s390x"].includes(arch)) return `@esbuild/linux-${arch}`;
|
|
313
|
+
if (platform === "darwin" && ["x64", "arm64"].includes(arch)) return `@esbuild/darwin-${arch}`;
|
|
314
|
+
if (platform === "win32" && ["x64", "arm64", "ia32"].includes(arch)) return `@esbuild/win32-${arch}`;
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function collectInstalledNativeDependencyIssues(tenantRoot) {
|
|
318
|
+
return collectNativeDependencyRepairs(tenantRoot).map((repair) => repair.issue);
|
|
319
|
+
}
|
|
320
|
+
function collectNativeDependencyRepairs(tenantRoot) {
|
|
321
|
+
const binaryPackage = esbuildPlatformPackage();
|
|
322
|
+
if (!binaryPackage) return [];
|
|
323
|
+
try {
|
|
324
|
+
const lock = JSON.parse(readFileSync(resolve(tenantRoot, "package-lock.json"), "utf8"));
|
|
325
|
+
const packages = lock.packages ?? {};
|
|
326
|
+
const repairs = [];
|
|
327
|
+
for (const [packagePath, entry] of Object.entries(packages)) {
|
|
328
|
+
if (packagePath !== "node_modules/esbuild" && !packagePath.endsWith("/node_modules/esbuild")) continue;
|
|
329
|
+
if (!existsSync(resolve(tenantRoot, packagePath, "package.json"))) continue;
|
|
330
|
+
const binaryPath = `${packagePath.slice(0, -"esbuild".length)}${binaryPackage}`;
|
|
331
|
+
const expectedBinary = packages[binaryPath];
|
|
332
|
+
if (!expectedBinary?.version) continue;
|
|
333
|
+
const installedBinaryPath = resolve(tenantRoot, binaryPath, "package.json");
|
|
334
|
+
if (!existsSync(installedBinaryPath)) {
|
|
335
|
+
repairs.push({
|
|
336
|
+
packagePath: binaryPath,
|
|
337
|
+
packageName: binaryPackage,
|
|
338
|
+
version: expectedBinary.version,
|
|
339
|
+
integrity: expectedBinary.integrity,
|
|
340
|
+
issue: `${binaryPath}: missing native binary ${expectedBinary.version} for ${packagePath}`
|
|
341
|
+
});
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const installedBinary = JSON.parse(readFileSync(installedBinaryPath, "utf8"));
|
|
345
|
+
if (installedBinary.version !== expectedBinary.version || installedBinary.version !== entry.version) {
|
|
346
|
+
repairs.push({
|
|
347
|
+
packagePath: binaryPath,
|
|
348
|
+
packageName: binaryPackage,
|
|
349
|
+
version: expectedBinary.version,
|
|
350
|
+
integrity: expectedBinary.integrity,
|
|
351
|
+
issue: `${binaryPath}: native binary ${installedBinary.version ?? "(missing version)"} does not match host ${entry.version ?? "(missing version)"}`
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return repairs;
|
|
356
|
+
} catch {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function resolveNpmPackCommand(env, spec, destination) {
|
|
361
|
+
const args = ["pack", spec, "--json", "--ignore-scripts", "--pack-destination", destination];
|
|
362
|
+
const npmExecPath = env.npm_execpath || env.NPM_EXEC_PATH;
|
|
363
|
+
return npmExecPath?.trim() ? { command: process.execPath, args: [npmExecPath, ...args] } : { command: "npm", args };
|
|
364
|
+
}
|
|
365
|
+
function repairInstalledNativeDependencies(tenantRoot, options) {
|
|
366
|
+
const repairs = collectNativeDependencyRepairs(tenantRoot);
|
|
367
|
+
if (repairs.length === 0) return null;
|
|
368
|
+
const tempParent = resolve(tenantRoot, ".treeseed", "tmp");
|
|
369
|
+
mkdirSync(tempParent, { recursive: true });
|
|
370
|
+
const tempRoot = mkdtempSync(resolve(tempParent, "native-dependency-"));
|
|
371
|
+
try {
|
|
372
|
+
for (const repair of repairs) {
|
|
373
|
+
options.write?.(`Hydrating ${repair.packageName}@${repair.version} for ${repair.packagePath}.`);
|
|
374
|
+
const pack = resolveNpmPackCommand(options.env, `${repair.packageName}@${repair.version}`, tempRoot);
|
|
375
|
+
const packed = options.spawn(pack.command, pack.args, {
|
|
376
|
+
cwd: tenantRoot,
|
|
377
|
+
env: { ...options.env, TREESEED_MANAGED_NPM_INSTALL: "1" },
|
|
378
|
+
stdio: "pipe",
|
|
379
|
+
encoding: "utf8"
|
|
380
|
+
});
|
|
381
|
+
if (packed.status !== 0 || packed.error) return null;
|
|
382
|
+
let metadata;
|
|
383
|
+
try {
|
|
384
|
+
metadata = JSON.parse(String(packed.stdout ?? ""));
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const artifact = metadata[0];
|
|
389
|
+
if (!artifact?.filename || repair.integrity && artifact.integrity !== repair.integrity) return null;
|
|
390
|
+
const target = resolve(tenantRoot, repair.packagePath);
|
|
391
|
+
rmSync(target, { recursive: true, force: true });
|
|
392
|
+
mkdirSync(target, { recursive: true });
|
|
393
|
+
const extracted = options.spawn("tar", ["-xzf", resolve(tempRoot, artifact.filename), "-C", target, "--strip-components=1"], {
|
|
394
|
+
cwd: tenantRoot,
|
|
395
|
+
env: options.env,
|
|
396
|
+
stdio: "pipe",
|
|
397
|
+
encoding: "utf8"
|
|
398
|
+
});
|
|
399
|
+
if (extracted.status !== 0 || extracted.error) return null;
|
|
400
|
+
}
|
|
401
|
+
return collectNativeDependencyRepairs(tenantRoot).length === 0 ? repairs.length : null;
|
|
402
|
+
} finally {
|
|
403
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
272
406
|
function resolveNpmRebuildCommand(env = process.env, packageNames) {
|
|
273
407
|
const npmExecPath = env.npm_execpath || env.NPM_EXEC_PATH;
|
|
274
408
|
if (npmExecPath?.trim()) {
|
|
@@ -284,6 +418,15 @@ function resolveNpmRebuildCommand(env = process.env, packageNames) {
|
|
|
284
418
|
display: ["npm", "rebuild", ...packageNames]
|
|
285
419
|
};
|
|
286
420
|
}
|
|
421
|
+
function staleNpmGitClonePath(detail) {
|
|
422
|
+
const match = /destination path '([^']+\/_cacache\/tmp\/git-clone[^/]+\/\.git)' already exists/u.exec(detail);
|
|
423
|
+
if (!match?.[1]) return null;
|
|
424
|
+
const gitPath = resolve(match[1]);
|
|
425
|
+
const cloneRoot = dirname(gitPath);
|
|
426
|
+
if (basename(gitPath) !== ".git" || !basename(cloneRoot).startsWith("git-clone")) return null;
|
|
427
|
+
if (basename(dirname(cloneRoot)) !== "tmp" || basename(dirname(dirname(cloneRoot))) !== "_cacache") return null;
|
|
428
|
+
return cloneRoot;
|
|
429
|
+
}
|
|
287
430
|
function runNpmBootstrap(options) {
|
|
288
431
|
const tenantRoot = options.tenantRoot ? resolve(options.tenantRoot) : null;
|
|
289
432
|
const npmCommand = resolveNpmInstallCommand(options.env);
|
|
@@ -308,7 +451,20 @@ function runNpmBootstrap(options) {
|
|
|
308
451
|
const nodeModulesMissing = !existsSync(resolve(tenantRoot, "node_modules"));
|
|
309
452
|
const npmDepsMissing = !npmBackedDependenciesAvailable();
|
|
310
453
|
const missingRuntimeTools = npmToolsMissingRuntime();
|
|
311
|
-
|
|
454
|
+
const nativeDependencyIssues = nodeModulesMissing ? [] : collectInstalledNativeDependencyIssues(tenantRoot);
|
|
455
|
+
if (nativeDependencyIssues.length > 0) {
|
|
456
|
+
const repaired = repairInstalledNativeDependencies(tenantRoot, options);
|
|
457
|
+
if (repaired !== null) {
|
|
458
|
+
return [{
|
|
459
|
+
root: tenantRoot,
|
|
460
|
+
command: ["npm", "pack", "<lockfile-pinned-native-dependencies>"],
|
|
461
|
+
status: "installed",
|
|
462
|
+
exitCode: 0,
|
|
463
|
+
detail: `Hydrated ${repaired} lockfile-pinned native package artifact${repaired === 1 ? "" : "s"} without resolving the workspace dependency graph.`
|
|
464
|
+
}];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!nodeModulesMissing && npmDepsMissing && missingRuntimeTools.length > 0) {
|
|
312
468
|
return [{
|
|
313
469
|
root: tenantRoot,
|
|
314
470
|
command: npmCommand.display,
|
|
@@ -317,16 +473,16 @@ function runNpmBootstrap(options) {
|
|
|
317
473
|
detail: `npm dependencies are installed; rebuilding missing runtime tools: ${missingRuntimeTools.map((tool) => tool.packageName).join(", ")}.`
|
|
318
474
|
}];
|
|
319
475
|
}
|
|
320
|
-
if (!
|
|
476
|
+
if (!nodeModulesMissing && !npmDepsMissing && nativeDependencyIssues.length === 0) {
|
|
321
477
|
return [{
|
|
322
478
|
root: tenantRoot,
|
|
323
479
|
command: npmCommand.display,
|
|
324
480
|
status: "already-present",
|
|
325
481
|
exitCode: 0,
|
|
326
|
-
detail: "npm dependencies are already installed."
|
|
482
|
+
detail: options.force ? "npm dependencies are already installed; force is limited to Treeseed-managed tool repair." : "npm dependencies are already installed."
|
|
327
483
|
}];
|
|
328
484
|
}
|
|
329
|
-
options.write?.(`Installing npm dependencies in ${tenantRoot}...`);
|
|
485
|
+
options.write?.(nativeDependencyIssues.length > 0 ? `Repairing npm dependency integrity in ${tenantRoot}: ${nativeDependencyIssues.join("; ")}` : `Installing npm dependencies in ${tenantRoot}...`);
|
|
330
486
|
const testNpmInstallStatus = options.env.NODE_ENV === "test" ? options.env.TREESEED_TEST_NPM_INSTALL_STATUS : void 0;
|
|
331
487
|
if (testNpmInstallStatus === "installed" || testNpmInstallStatus === "failed") {
|
|
332
488
|
const ok2 = testNpmInstallStatus === "installed";
|
|
@@ -338,7 +494,7 @@ function runNpmBootstrap(options) {
|
|
|
338
494
|
detail: ok2 ? "npm install completed successfully." : "npm install failed."
|
|
339
495
|
}];
|
|
340
496
|
}
|
|
341
|
-
const
|
|
497
|
+
const spawnInstall = () => options.spawn(npmCommand.command, npmCommand.args, {
|
|
342
498
|
cwd: tenantRoot,
|
|
343
499
|
env: {
|
|
344
500
|
...options.env,
|
|
@@ -347,8 +503,17 @@ function runNpmBootstrap(options) {
|
|
|
347
503
|
stdio: "pipe",
|
|
348
504
|
encoding: "utf8"
|
|
349
505
|
});
|
|
350
|
-
|
|
506
|
+
let result = spawnInstall();
|
|
507
|
+
let detail = `${result.stderr ?? ""}
|
|
351
508
|
${result.stdout ?? ""}`.trim() || result.error?.message || "";
|
|
509
|
+
const staleClone = result.status !== 0 ? staleNpmGitClonePath(detail) : null;
|
|
510
|
+
if (staleClone) {
|
|
511
|
+
options.write?.(`Removing interrupted npm Git clone ${staleClone} and retrying dependency repair once.`);
|
|
512
|
+
rmSync(staleClone, { recursive: true, force: true });
|
|
513
|
+
result = spawnInstall();
|
|
514
|
+
detail = `${result.stderr ?? ""}
|
|
515
|
+
${result.stdout ?? ""}`.trim() || result.error?.message || "";
|
|
516
|
+
}
|
|
352
517
|
const ok = result.status === 0 && !result.error;
|
|
353
518
|
return [{
|
|
354
519
|
root: tenantRoot,
|
|
@@ -429,6 +594,10 @@ function resolveTreeseedToolBinary(toolName, options = {}) {
|
|
|
429
594
|
}
|
|
430
595
|
return locateSystemBinary("gh", spawnSync, options.env ?? process.env);
|
|
431
596
|
}
|
|
597
|
+
if (toolName === "railway") {
|
|
598
|
+
const managed = managedRailwayBin(options.env);
|
|
599
|
+
return existsSync(managed) ? managed : null;
|
|
600
|
+
}
|
|
432
601
|
const npmTool = findNpmTool(toolName);
|
|
433
602
|
if (npmTool) {
|
|
434
603
|
return resolveNpmToolRuntimeBinary(npmTool);
|
|
@@ -664,6 +833,95 @@ async function installGh(options) {
|
|
|
664
833
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
665
834
|
}
|
|
666
835
|
}
|
|
836
|
+
async function installRailway(options) {
|
|
837
|
+
const asset = currentRailwayPlatformAsset();
|
|
838
|
+
if (!asset) {
|
|
839
|
+
return report({
|
|
840
|
+
name: "railway",
|
|
841
|
+
kind: "download",
|
|
842
|
+
version: RAILWAY_VERSION,
|
|
843
|
+
source: "not-applicable",
|
|
844
|
+
status: "unsupported",
|
|
845
|
+
required: true,
|
|
846
|
+
detail: "Managed Railway CLI installation supports Linux and macOS on x64 or arm64."
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
const binaryPath = managedRailwayBin(options.env);
|
|
850
|
+
if (existsSync(binaryPath)) {
|
|
851
|
+
const check = checkCommand(binaryPath, ["--version"], {
|
|
852
|
+
cwd: options.tenantRoot,
|
|
853
|
+
env: options.env,
|
|
854
|
+
spawn: options.spawn
|
|
855
|
+
});
|
|
856
|
+
if (check.ok && check.stdout.includes(RAILWAY_VERSION)) {
|
|
857
|
+
return report({
|
|
858
|
+
name: "railway",
|
|
859
|
+
kind: "download",
|
|
860
|
+
version: RAILWAY_VERSION,
|
|
861
|
+
source: "managed-cache",
|
|
862
|
+
binaryPath,
|
|
863
|
+
status: options.force ? "repaired" : "already-present",
|
|
864
|
+
required: true,
|
|
865
|
+
detail: check.stdout.split("\n")[0] ?? `Railway CLI ${RAILWAY_VERSION} is installed.`
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const toolsHome = resolveToolsHome(options.env);
|
|
870
|
+
const tmpRoot = resolve(toolsHome, ".tmp", `railway-${process.pid}-${Date.now()}`);
|
|
871
|
+
const archivePath = resolve(tmpRoot, asset.assetName);
|
|
872
|
+
const extractRoot = resolve(tmpRoot, "extract");
|
|
873
|
+
const installRoot = dirname(dirname(binaryPath));
|
|
874
|
+
const stagingRoot = `${installRoot}.staging-${process.pid}-${Date.now()}`;
|
|
875
|
+
try {
|
|
876
|
+
options.write?.(`Installing Railway CLI ${RAILWAY_VERSION}...`);
|
|
877
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
878
|
+
mkdirSync(extractRoot, { recursive: true });
|
|
879
|
+
await options.downloadFile(`${RAILWAY_RELEASE_BASE_URL}/${asset.assetName}`, archivePath);
|
|
880
|
+
const assetHash = sha256File(archivePath);
|
|
881
|
+
if (assetHash !== asset.sha256) {
|
|
882
|
+
throw new Error(`Railway CLI archive hash mismatch for ${asset.assetName}: expected ${asset.sha256}, got ${assetHash}.`);
|
|
883
|
+
}
|
|
884
|
+
const tar = await import("tar");
|
|
885
|
+
await tar.x({ file: archivePath, cwd: extractRoot });
|
|
886
|
+
const extractedBinary = resolve(extractRoot, "railway");
|
|
887
|
+
if (!existsSync(extractedBinary)) throw new Error(`Unable to find railway binary in ${asset.assetName}.`);
|
|
888
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
889
|
+
mkdirSync(resolve(stagingRoot, "bin"), { recursive: true });
|
|
890
|
+
copyFileSync(extractedBinary, resolve(stagingRoot, "bin", "railway"));
|
|
891
|
+
chmodSync(resolve(stagingRoot, "bin", "railway"), 493);
|
|
892
|
+
rmSync(installRoot, { recursive: true, force: true });
|
|
893
|
+
mkdirSync(dirname(installRoot), { recursive: true });
|
|
894
|
+
renameSync(stagingRoot, installRoot);
|
|
895
|
+
const check = checkCommand(binaryPath, ["--version"], { cwd: options.tenantRoot, env: options.env, spawn: options.spawn });
|
|
896
|
+
if (!check.ok || !check.stdout.includes(RAILWAY_VERSION)) {
|
|
897
|
+
throw new Error(check.detail || `Railway CLI ${RAILWAY_VERSION} failed after installation.`);
|
|
898
|
+
}
|
|
899
|
+
return report({
|
|
900
|
+
name: "railway",
|
|
901
|
+
kind: "download",
|
|
902
|
+
version: RAILWAY_VERSION,
|
|
903
|
+
source: "managed-cache",
|
|
904
|
+
binaryPath,
|
|
905
|
+
status: options.force ? "repaired" : "installed",
|
|
906
|
+
required: true,
|
|
907
|
+
detail: check.stdout.split("\n")[0] ?? `Railway CLI ${RAILWAY_VERSION} installed.`
|
|
908
|
+
});
|
|
909
|
+
} catch (error) {
|
|
910
|
+
return report({
|
|
911
|
+
name: "railway",
|
|
912
|
+
kind: "download",
|
|
913
|
+
version: RAILWAY_VERSION,
|
|
914
|
+
source: "managed-cache",
|
|
915
|
+
binaryPath,
|
|
916
|
+
status: "failed",
|
|
917
|
+
required: true,
|
|
918
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
919
|
+
});
|
|
920
|
+
} finally {
|
|
921
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
922
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
667
925
|
function statusForNpmTool(tool, options) {
|
|
668
926
|
try {
|
|
669
927
|
const binaryPath = resolveNpmToolRuntimeBinary(tool);
|
|
@@ -808,6 +1066,7 @@ async function installTreeseedDependencies(options = {}) {
|
|
|
808
1066
|
const reports = [
|
|
809
1067
|
systemStatus("git", true, effectiveOptions),
|
|
810
1068
|
await installGh(effectiveOptions),
|
|
1069
|
+
await installRailway(effectiveOptions),
|
|
811
1070
|
...NPM_TOOLS.map((tool) => statusForNpmTool(tool, effectiveOptions)),
|
|
812
1071
|
...NPM_PACKAGES.map((pkg) => statusForNpmPackage(pkg)),
|
|
813
1072
|
systemStatus("docker", false, effectiveOptions),
|
|
@@ -844,9 +1103,21 @@ function collectTreeseedDependencyStatus(options = {}) {
|
|
|
844
1103
|
required: true,
|
|
845
1104
|
detail: `GitHub CLI ${GH_VERSION} is not installed in the Treeseed tool cache.`
|
|
846
1105
|
});
|
|
1106
|
+
const railwayBinary = managedRailwayBin(env);
|
|
1107
|
+
const railwayStatus = report({
|
|
1108
|
+
name: "railway",
|
|
1109
|
+
kind: "download",
|
|
1110
|
+
version: RAILWAY_VERSION,
|
|
1111
|
+
source: "managed-cache",
|
|
1112
|
+
binaryPath: railwayBinary,
|
|
1113
|
+
status: existsSync(railwayBinary) ? "already-present" : "missing",
|
|
1114
|
+
required: true,
|
|
1115
|
+
detail: existsSync(railwayBinary) ? `Railway CLI ${RAILWAY_VERSION} is installed in the Treeseed tool cache.` : `Railway CLI ${RAILWAY_VERSION} is not installed in the Treeseed tool cache.`
|
|
1116
|
+
});
|
|
847
1117
|
const reports = [
|
|
848
1118
|
systemStatus("git", true, options),
|
|
849
1119
|
ghStatus,
|
|
1120
|
+
railwayStatus,
|
|
850
1121
|
...NPM_TOOLS.map((tool) => statusForNpmTool(tool, options)),
|
|
851
1122
|
...NPM_PACKAGES.map((pkg) => statusForNpmPackage(pkg)),
|
|
852
1123
|
systemStatus("docker", false, options),
|
|
@@ -901,12 +1172,15 @@ function formatTreeseedDependencyReport(result) {
|
|
|
901
1172
|
].join("\n");
|
|
902
1173
|
}
|
|
903
1174
|
export {
|
|
1175
|
+
collectInstalledNativeDependencyIssues,
|
|
904
1176
|
collectTreeseedDependencyStatus,
|
|
905
1177
|
collectTreeseedToolStatus,
|
|
906
1178
|
createTreeseedManagedToolEnv,
|
|
907
1179
|
formatTreeseedDependencyFailureDetails,
|
|
908
1180
|
formatTreeseedDependencyReport,
|
|
909
1181
|
installTreeseedDependencies,
|
|
1182
|
+
repairInstalledNativeDependencies,
|
|
910
1183
|
resolveTreeseedToolBinary,
|
|
911
|
-
resolveTreeseedToolCommand
|
|
1184
|
+
resolveTreeseedToolCommand,
|
|
1185
|
+
staleNpmGitClonePath
|
|
912
1186
|
};
|
|
@@ -616,7 +616,7 @@ function defaultStateFromConfig(deployConfig, target) {
|
|
|
616
616
|
projectId: serviceConfig?.railway?.projectId ?? null,
|
|
617
617
|
projectName: serviceConfig?.railway?.projectName ?? sharedDeploymentName(identity),
|
|
618
618
|
serviceId: serviceConfig?.railway?.serviceId ?? null,
|
|
619
|
-
serviceName: serviceConfig?.railway?.serviceName ?? sharedDeploymentName(identity, serviceKey),
|
|
619
|
+
serviceName: serviceConfig?.environments?.[scope]?.serviceName ?? serviceConfig?.railway?.serviceName ?? sharedDeploymentName(identity, serviceKey),
|
|
620
620
|
workerName: serviceConfig?.cloudflare?.workerName ?? null,
|
|
621
621
|
rootDir: serviceConfig?.railway?.rootDir ?? serviceConfig?.rootDir ?? null,
|
|
622
622
|
environment: normalizeRailwayEnvironmentName(serviceConfig?.environments?.[scope]?.railwayEnvironment ?? scope),
|
|
@@ -2563,7 +2563,7 @@ function configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope) {
|
|
|
2563
2563
|
if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
|
|
2564
2564
|
continue;
|
|
2565
2565
|
}
|
|
2566
|
-
const baseServiceName = service.railway?.serviceName ?? `${identity.deploymentKey}-${serviceKey === "operationsRunner" ? "operations-runner" : serviceKey}`;
|
|
2566
|
+
const baseServiceName = service.environments?.[normalizedScope]?.serviceName ?? service.railway?.serviceName ?? `${identity.deploymentKey}-${serviceKey === "operationsRunner" ? "operations-runner" : serviceKey}`;
|
|
2567
2567
|
const runnerPool = serviceKey === "operationsRunner" && service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
|
|
2568
2568
|
const count = serviceKey === "operationsRunner" ? Math.max(1, Number.parseInt(String(runnerPool?.bootstrapCount ?? 1), 10) || 1) : 1;
|
|
2569
2569
|
for (let index = 1; index <= count; index += 1) {
|
|
@@ -2584,7 +2584,7 @@ function configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope) {
|
|
|
2584
2584
|
services.push({
|
|
2585
2585
|
key: "treeseedDatabase",
|
|
2586
2586
|
projectName: deployConfig.services?.api?.railway?.projectName ?? identity.deploymentKey,
|
|
2587
|
-
serviceName:
|
|
2587
|
+
serviceName: baseName.replace(/-(staging|prod|production)$/u, ""),
|
|
2588
2588
|
railwayEnvironment: normalizeRailwayEnvironmentName(normalizedScope),
|
|
2589
2589
|
domain: null,
|
|
2590
2590
|
volumeMountPath: null,
|
|
@@ -2662,7 +2662,7 @@ async function destroyRailwayResources(tenantRoot, deployConfig, target, { planO
|
|
|
2662
2662
|
if (planOnly) {
|
|
2663
2663
|
operations.push(resourceOperation("railway", "custom-domain", domain.domain, "planned", { id: domain.id }));
|
|
2664
2664
|
} else {
|
|
2665
|
-
const result = await deleteRailwayCustomDomain({ domainId: domain.id, env });
|
|
2665
|
+
const result = await deleteRailwayCustomDomain({ projectId: project.id, environmentId: environment.id, serviceId: railwayService.id, domainId: domain.id, env });
|
|
2666
2666
|
operations.push(resourceOperation("railway", "custom-domain", domain.domain, result.status, { id: domain.id }));
|
|
2667
2667
|
}
|
|
2668
2668
|
}
|
|
@@ -2678,7 +2678,7 @@ async function destroyRailwayResources(tenantRoot, deployConfig, target, { planO
|
|
|
2678
2678
|
if (planOnly) {
|
|
2679
2679
|
operations.push(resourceOperation("railway", "volume", volume.name, "planned", { id: volume.id, projectId: project.id }));
|
|
2680
2680
|
} else {
|
|
2681
|
-
const result = await deleteRailwayVolume({ volumeId: volume.id, env });
|
|
2681
|
+
const result = await deleteRailwayVolume({ projectId: project.id, environmentId: environment.id, volumeId: volume.id, env });
|
|
2682
2682
|
operations.push(resourceOperation("railway", "volume", volume.name, result.status, { id: volume.id, projectId: project.id }));
|
|
2683
2683
|
}
|
|
2684
2684
|
}
|
|
@@ -2697,7 +2697,7 @@ async function destroyRailwayResources(tenantRoot, deployConfig, target, { planO
|
|
|
2697
2697
|
if (planOnly) {
|
|
2698
2698
|
operations.push(resourceOperation("railway", "environment", environment.name, "planned", { id: environment.id, projectId: project.id }));
|
|
2699
2699
|
} else {
|
|
2700
|
-
const result = await deleteRailwayEnvironment({ environmentId: environment.id, env });
|
|
2700
|
+
const result = await deleteRailwayEnvironment({ projectId: project.id, environmentId: environment.id, env });
|
|
2701
2701
|
operations.push(resourceOperation("railway", "environment", environment.name, result.status, { id: environment.id, projectId: project.id }));
|
|
2702
2702
|
}
|
|
2703
2703
|
}
|
|
@@ -2,6 +2,7 @@ import { relative, resolve } from "node:path";
|
|
|
2
2
|
import { compileTreeseedHostingGraph, serializeHostingUnit } from "../../hosting/index.js";
|
|
3
3
|
import { discoverTreeseedPackageAdapters } from "./package-adapters.js";
|
|
4
4
|
import { configuredRailwayServices } from "./railway-deploy.js";
|
|
5
|
+
import { railwayEnvironmentQualifiedServiceName } from "./railway-source-policy.js";
|
|
5
6
|
function relRoot(tenantRoot, path) {
|
|
6
7
|
const value = relative(tenantRoot, path).split("\\").join("/");
|
|
7
8
|
return value || ".";
|
|
@@ -143,8 +144,8 @@ function collectTreeseedDeploymentReadiness(options) {
|
|
|
143
144
|
checks.push(check("hosting:api:healthcheckPath", api.config?.healthcheckPath, "/healthz", "API healthcheck path is /healthz.", "Set services.api.railway.healthcheckPath to /healthz.", "healthcheckPath"));
|
|
144
145
|
}
|
|
145
146
|
if (railwayApi) {
|
|
146
|
-
const expectedServiceName =
|
|
147
|
-
checks.push(check("railway-config:api:serviceName", railwayApi.serviceName, expectedServiceName, "Railway API service uses
|
|
147
|
+
const expectedServiceName = railwayEnvironmentQualifiedServiceName("treeseed-api", environment);
|
|
148
|
+
checks.push(check("railway-config:api:serviceName", railwayApi.serviceName, expectedServiceName, "Railway API service uses its environment-qualified identity.", `Set services.api.environments.${environment}.serviceName to ${expectedServiceName}.`, "serviceName"));
|
|
148
149
|
checks.push(check("railway-config:api:rootDirectory", relRoot(tenantRoot, railwayApi.rootDir), expectedApiRailwayRoot, "Railway API effective rootDirectory points at the API app.", "Set services.api.railway.rootDir relative to the owning API manifest.", "rootDirectory"));
|
|
149
150
|
}
|
|
150
151
|
if (!runner) {
|
|
@@ -177,8 +178,8 @@ function collectTreeseedDeploymentReadiness(options) {
|
|
|
177
178
|
checks.push(check("hosting:operationsRunner:volumeMountPath", runner.config?.volumeMountPath, "/data", "Runner volume mount path is stable.", "Set services.operationsRunner.railway.volumeMountPath to /data.", "volumeMountPath"));
|
|
178
179
|
}
|
|
179
180
|
if (railwayRunner) {
|
|
180
|
-
const expectedServiceName =
|
|
181
|
-
checks.push(check("railway-config:operationsRunner:serviceName", railwayRunner.serviceName, expectedServiceName, "Railway runner
|
|
181
|
+
const expectedServiceName = railwayEnvironmentQualifiedServiceName("treeseed-ops-01", environment);
|
|
182
|
+
checks.push(check("railway-config:operationsRunner:serviceName", railwayRunner.serviceName, expectedServiceName, "Railway runner uses its environment-qualified identity.", `Set services.operationsRunner.environments.${environment}.serviceName to ${expectedServiceName}.`, "serviceName"));
|
|
182
183
|
checks.push(check("railway-config:operationsRunner:rootDirectory", relRoot(tenantRoot, railwayRunner.rootDir), expectedRunnerRailwayRoot, "Railway runner effective rootDirectory points at the API app.", "Set services.operationsRunner.railway.rootDir relative to the owning API manifest.", "rootDirectory"));
|
|
183
184
|
}
|
|
184
185
|
if (!database) {
|
|
@@ -49,6 +49,8 @@ type RunOptions = {
|
|
|
49
49
|
export type TreeseedGitBatchOperation = RunOptions & {
|
|
50
50
|
args: string[];
|
|
51
51
|
};
|
|
52
|
+
export declare function treeseedGitCommandUsesRemote(args: string[]): boolean;
|
|
53
|
+
export declare function resolveTreeseedGitCommandTimeoutMs(args: string[], timeoutMs?: number): number | undefined;
|
|
52
54
|
export declare function classifyTreeseedGitMode(args: string[]): TreeseedGitRunnerMode;
|
|
53
55
|
export declare function inspectTreeseedGitLocks(cwd: string): TreeseedGitLockDiagnostic;
|
|
54
56
|
export declare function inspectTreeseedGitLockSet(cwd: string): TreeseedGitLockDiagnostic[];
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, relative, resolve } from "node:path";
|
|
4
|
+
const DEFAULT_REMOTE_GIT_TIMEOUT_MS = 6e4;
|
|
5
|
+
const REMOTE_GIT_COMMANDS = /* @__PURE__ */ new Set(["clone", "fetch", "ls-remote", "pull", "push"]);
|
|
6
|
+
function treeseedGitCommandUsesRemote(args) {
|
|
7
|
+
return REMOTE_GIT_COMMANDS.has(args[0] ?? "");
|
|
8
|
+
}
|
|
9
|
+
function resolveTreeseedGitCommandTimeoutMs(args, timeoutMs) {
|
|
10
|
+
return timeoutMs ?? (treeseedGitCommandUsesRemote(args) ? DEFAULT_REMOTE_GIT_TIMEOUT_MS : void 0);
|
|
11
|
+
}
|
|
4
12
|
function pidAlive(pid) {
|
|
5
13
|
try {
|
|
6
14
|
process.kill(pid, 0);
|
|
@@ -26,15 +34,26 @@ function gitSync(args, options) {
|
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
}
|
|
37
|
+
if (treeseedGitCommandUsesRemote(args) && !env.GIT_SSH_COMMAND) {
|
|
38
|
+
env.GIT_SSH_COMMAND = "ssh -o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=2";
|
|
39
|
+
}
|
|
40
|
+
const timeoutMs = resolveTreeseedGitCommandTimeoutMs(args, options.timeoutMs);
|
|
29
41
|
const spawnOptions = {
|
|
30
42
|
cwd: options.cwd,
|
|
31
43
|
env,
|
|
32
44
|
encoding: "utf8",
|
|
33
45
|
stdio: "pipe",
|
|
34
|
-
timeout:
|
|
46
|
+
timeout: timeoutMs,
|
|
35
47
|
maxBuffer: options.maxBuffer ?? 1024 * 1024 * 32
|
|
36
48
|
};
|
|
37
49
|
const result = spawnSync("git", args, spawnOptions);
|
|
50
|
+
if (result.error) {
|
|
51
|
+
const code = result.error.code;
|
|
52
|
+
if (code === "ETIMEDOUT") {
|
|
53
|
+
throw new Error(`Git remote command timed out after ${timeoutMs}ms: git ${args.join(" ")} in ${options.cwd}. Check network and repository access, then rerun the Treeseed command.`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Unable to run git ${args.join(" ")} in ${options.cwd}: ${result.error.message}`);
|
|
56
|
+
}
|
|
38
57
|
const output = {
|
|
39
58
|
status: result.status,
|
|
40
59
|
stdout: result.stdout ?? "",
|
|
@@ -472,8 +491,10 @@ export {
|
|
|
472
491
|
inspectTreeseedGitLocks,
|
|
473
492
|
inspectTreeseedWorkspaceGitLocks,
|
|
474
493
|
recoverTreeseedGitLocks,
|
|
494
|
+
resolveTreeseedGitCommandTimeoutMs,
|
|
475
495
|
runTreeseedGit,
|
|
476
496
|
runTreeseedGitBatch,
|
|
477
497
|
runTreeseedGitOk,
|
|
478
|
-
runTreeseedGitText
|
|
498
|
+
runTreeseedGitText,
|
|
499
|
+
treeseedGitCommandUsesRemote
|
|
479
500
|
};
|
|
@@ -236,6 +236,19 @@ function collectTreeseedHostedServiceChecks(options) {
|
|
|
236
236
|
status: isImmutableRailwayImageRef(service.imageRef) ? "passed" : "failed",
|
|
237
237
|
issues: isImmutableRailwayImageRef(service.imageRef) ? [] : [`Production API Railway service ${service.serviceName} is missing an immutable image ref.`]
|
|
238
238
|
}));
|
|
239
|
+
const observedGitSource = Boolean(observed?.deploymentRepo || observed?.deploymentBranch);
|
|
240
|
+
checks.push(check({
|
|
241
|
+
id: `railway:${service.instanceKey}:production-source-isolation`,
|
|
242
|
+
provider: "railway",
|
|
243
|
+
serviceKey: service.key,
|
|
244
|
+
serviceType,
|
|
245
|
+
target,
|
|
246
|
+
description: `Railway ${service.serviceName} production deployment has no Git source.`,
|
|
247
|
+
expected: { repo: null, branch: null },
|
|
248
|
+
observed: observed ? { repo: observed.deploymentRepo ?? null, branch: observed.deploymentBranch ?? null } : { skipped: true },
|
|
249
|
+
status: observed ? observedGitSource ? "failed" : "passed" : "skipped",
|
|
250
|
+
issues: observed && observedGitSource ? [`Production API Railway service ${service.serviceName} still reports Git source metadata.`] : []
|
|
251
|
+
}));
|
|
239
252
|
}
|
|
240
253
|
checks.push(check({
|
|
241
254
|
id: `railway:${service.instanceKey}:service`,
|
|
@@ -304,6 +317,21 @@ function collectTreeseedHostedServiceChecks(options) {
|
|
|
304
317
|
issues: !observed || sourceUploadDeployment || actualBranch === service.sourceBranch ? [] : [`Expected deployment branch=${service.sourceBranch}, observed ${actualBranch ?? "(unset)"}.`]
|
|
305
318
|
}));
|
|
306
319
|
}
|
|
320
|
+
if (service.sourceCommit) {
|
|
321
|
+
const actualCommit = observed?.deploymentCommitHash ?? null;
|
|
322
|
+
checks.push(check({
|
|
323
|
+
id: `railway:${service.instanceKey}:deployment-commit`,
|
|
324
|
+
provider: "railway",
|
|
325
|
+
serviceKey: service.key,
|
|
326
|
+
serviceType,
|
|
327
|
+
target,
|
|
328
|
+
description: `Railway ${service.serviceName} latest deployment uses the exact promoted commit.`,
|
|
329
|
+
expected: { commitHash: service.sourceCommit },
|
|
330
|
+
observed: observed ? { commitHash: actualCommit } : { skipped: true },
|
|
331
|
+
status: observed ? statusForMatch(actualCommit, service.sourceCommit) : "skipped",
|
|
332
|
+
issues: !observed || actualCommit === service.sourceCommit ? [] : [`Expected deployment commitHash=${service.sourceCommit}, observed ${actualCommit ?? "(unset)"}.`]
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
307
335
|
if (expectedRootDirectory) {
|
|
308
336
|
checks.push(check({
|
|
309
337
|
id: `railway:${service.instanceKey}:deployment-root-directory`,
|