@treeseed/sdk 0.12.60 → 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 -245
- 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 +5 -5
- 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 +51 -15
- 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 +2 -2
- package/dist/operations/services/railway-deploy.js +36 -91
- 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 +519 -684
- 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 +147 -0
- package/dist/reconcile/providers/railway-iac.js +289 -16
- 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 +4 -0
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +3 -1
- package/package.json +1 -2
package/dist/hosting/graph.js
CHANGED
|
@@ -3,12 +3,10 @@ import { loadTreeseedPlugins } from "../platform/plugins/runtime.js";
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { parse as parseYaml } from "yaml";
|
|
6
|
-
import {
|
|
7
|
-
import { collectTreeseedConfigSeedValues, resolveTreeseedMachineEnvironmentValues } from "../operations/services/config-runtime.js";
|
|
6
|
+
import { resolveTreeseedMachineEnvironmentValues } from "../operations/services/config-runtime.js";
|
|
8
7
|
import { classifyTreeseedGitMode, runTreeseedGitText } from "../operations/services/git-runner.js";
|
|
9
|
-
import { apiRailwayDefaultDockerfilePath, apiRailwayDefaultSourceRepo, assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService } from "../operations/services/railway-source-policy.js";
|
|
8
|
+
import { apiRailwayDefaultDockerfilePath, apiRailwayDefaultSourceRepo, assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService, railwayEnvironmentQualifiedServiceName, railwayTreeDxServiceName } from "../operations/services/railway-source-policy.js";
|
|
10
9
|
import { createTreeseedCanonicalReconcileReport } from "../reconcile/index.js";
|
|
11
|
-
import { reconcileTreeseedTarget } from "../reconcile/index.js";
|
|
12
10
|
import { discoverTreeseedApplications, findTreeseedApplication } from "./apps.js";
|
|
13
11
|
import {
|
|
14
12
|
createDefaultHostAdapters,
|
|
@@ -18,6 +16,16 @@ import {
|
|
|
18
16
|
sanitizedUnitConfig,
|
|
19
17
|
summarizePlacementStatus
|
|
20
18
|
} from "./builtins.js";
|
|
19
|
+
const RAILWAY_SERVICE_NAME_MAX_LENGTH = 32;
|
|
20
|
+
const RAILWAY_VOLUME_NAME_MAX_LENGTH = 48;
|
|
21
|
+
function assertRailwayResourceNames(serviceName, volumeName) {
|
|
22
|
+
if (serviceName.length > RAILWAY_SERVICE_NAME_MAX_LENGTH) {
|
|
23
|
+
throw new Error(`Railway service name ${serviceName} exceeds the provider limit of ${RAILWAY_SERVICE_NAME_MAX_LENGTH} characters.`);
|
|
24
|
+
}
|
|
25
|
+
if (volumeName && volumeName.length > RAILWAY_VOLUME_NAME_MAX_LENGTH) {
|
|
26
|
+
throw new Error(`Railway volume name ${volumeName} exceeds the provider limit of ${RAILWAY_VOLUME_NAME_MAX_LENGTH} characters.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
21
29
|
const ENVIRONMENT_NAMES = {
|
|
22
30
|
local: "local",
|
|
23
31
|
staging: "staging",
|
|
@@ -83,8 +91,8 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
|
|
|
83
91
|
imageRef,
|
|
84
92
|
imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF"
|
|
85
93
|
};
|
|
86
|
-
const serviceName =
|
|
87
|
-
assertApiRailwaySourcePolicy(input.environment, { key:
|
|
94
|
+
const serviceName = railwayTreeDxServiceName(1, input.environment);
|
|
95
|
+
assertApiRailwaySourcePolicy(input.environment, { key: "public-treedx-node-01", serviceName, ...policy2 });
|
|
88
96
|
return policy2;
|
|
89
97
|
}
|
|
90
98
|
const policy = {
|
|
@@ -98,7 +106,7 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
|
|
|
98
106
|
};
|
|
99
107
|
assertApiRailwaySourcePolicy(input.environment, {
|
|
100
108
|
key: "public-treedx-node-01",
|
|
101
|
-
serviceName:
|
|
109
|
+
serviceName: railwayTreeDxServiceName(1, input.environment),
|
|
102
110
|
dockerfilePath: railway.dockerfilePath ?? "/Dockerfile",
|
|
103
111
|
...policy
|
|
104
112
|
});
|
|
@@ -176,7 +184,8 @@ function railwaySourcePolicy(input, serviceKey, service, imageRef) {
|
|
|
176
184
|
const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
|
|
177
185
|
const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
|
|
178
186
|
const baseServiceName = service.railway?.serviceName ?? null;
|
|
179
|
-
const
|
|
187
|
+
const environmentConfig = service.environments?.[input.environment];
|
|
188
|
+
const serviceName = typeof environmentConfig?.serviceName === "string" && environmentConfig.serviceName.trim() ? environmentConfig.serviceName.trim() : isApiRailwaySourcePolicyService({ key: serviceKey, serviceName: baseServiceName }) && baseServiceName ? railwayEnvironmentQualifiedServiceName(baseServiceName, input.environment) : baseServiceName;
|
|
180
189
|
const repository = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(resolveRailwayServiceSourceRoot(input, serviceKey, service)) ?? readPackageRepository(input.tenantRoot) ?? apiRailwayDefaultSourceRepo({ key: serviceKey, serviceName });
|
|
181
190
|
const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
|
|
182
191
|
const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
|
|
@@ -384,54 +393,81 @@ function buildProfileFromDeployConfig(input) {
|
|
|
384
393
|
const imageRef = service.railway?.imageRef ?? (input.environment === "prod" && imageRefEnv ? launchEnv[imageRefEnv] ?? null : null) ?? defaultRailwayImageRefForService(serviceKey, input.environment) ?? null;
|
|
385
394
|
const sourcePolicy = railwaySourcePolicy(input, serviceKey, service, imageRef);
|
|
386
395
|
const baseServiceName = service.railway?.serviceName ?? null;
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
396
|
+
const environmentConfig = service.environments?.[input.environment];
|
|
397
|
+
const effectiveServiceName = typeof environmentConfig?.serviceName === "string" && environmentConfig.serviceName.trim() ? environmentConfig.serviceName.trim() : (serviceKey === "operationsRunner" || isApiRailwaySourcePolicyService({ key: serviceKey, serviceName: baseServiceName })) && baseServiceName ? railwayEnvironmentQualifiedServiceName(baseServiceName, input.environment) : baseServiceName;
|
|
398
|
+
const runnerPool = serviceKey === "operationsRunner" && service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
|
|
399
|
+
const instanceCount = runnerPool ? Math.max(1, Number.parseInt(String(runnerPool.bootstrapCount ?? 1), 10) || 1) : 1;
|
|
400
|
+
const maxRunners = runnerPool ? Math.max(1, Number.parseInt(String(runnerPool.maxRunners ?? instanceCount), 10) || instanceCount) : 1;
|
|
401
|
+
if (runnerPool && instanceCount > maxRunners) {
|
|
402
|
+
throw new Error(`services.operationsRunner.railway.runnerPool.bootstrapCount (${instanceCount}) cannot exceed maxRunners (${maxRunners}).`);
|
|
403
|
+
}
|
|
404
|
+
for (let offset = 0; offset < instanceCount; offset += 1) {
|
|
405
|
+
const runnerIndex = offset + 1;
|
|
406
|
+
const instanceId = serviceKey === "operationsRunner" && runnerIndex > 1 ? `${serviceKey}-${String(runnerIndex).padStart(2, "0")}` : serviceKey;
|
|
407
|
+
const instanceServiceName = serviceKey === "operationsRunner" && effectiveServiceName ? indexedName(effectiveServiceName, runnerIndex) : effectiveServiceName;
|
|
408
|
+
const volumeMountPath = serviceKey === "operationsRunner" ? service.railway?.volumeMountPath ?? runnerPool?.volumeMountPath ?? "/data" : service.railway?.volumeMountPath ?? null;
|
|
409
|
+
if (instanceServiceName && (service.provider === "railway" || service.railway)) {
|
|
410
|
+
assertRailwayResourceNames(instanceServiceName, volumeMountPath ? `${instanceServiceName}-volume` : null);
|
|
411
|
+
}
|
|
412
|
+
const environmentBinding = (bindingEnvironment) => {
|
|
413
|
+
const configured = service.environments?.[bindingEnvironment] ?? {};
|
|
414
|
+
if (!runnerPool) return configured;
|
|
415
|
+
const configuredName = typeof configured.serviceName === "string" && configured.serviceName.trim() ? configured.serviceName.trim() : baseServiceName ? railwayEnvironmentQualifiedServiceName(baseServiceName, bindingEnvironment) : instanceServiceName;
|
|
416
|
+
const serviceName = configuredName ? indexedName(configuredName, runnerIndex) : instanceServiceName;
|
|
417
|
+
return { ...configured, serviceName, railwayServiceName: serviceName };
|
|
418
|
+
};
|
|
419
|
+
services.push({
|
|
420
|
+
id: instanceId,
|
|
421
|
+
label: placement === "runner-capacity" ? `Runner Capacity ${String(runnerIndex).padStart(2, "0")}` : serviceKey === "api" ? "API Runtime" : serviceKey,
|
|
422
|
+
serviceType,
|
|
423
|
+
placement,
|
|
424
|
+
projectGroupId: defaultProjectGroup,
|
|
425
|
+
config: {
|
|
426
|
+
rootDir: service.railway?.rootDir ?? service.rootDir ?? ".",
|
|
427
|
+
imageRef: sourcePolicy.imageRef,
|
|
428
|
+
imageRefEnv: sourcePolicy.sourceMode === "image" ? imageRefEnv : null,
|
|
429
|
+
sourceMode: sourcePolicy.sourceMode,
|
|
430
|
+
sourceRepo: sourcePolicy.sourceRepo,
|
|
431
|
+
sourceBranch: sourcePolicy.sourceBranch,
|
|
432
|
+
sourceCommit: sourcePolicy.sourceCommit,
|
|
433
|
+
sourceRootDirectory: sourcePolicy.sourceRootDirectory,
|
|
434
|
+
dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName: service.railway?.serviceName ?? null }) : null,
|
|
435
|
+
buildCommand: sourcePolicy.imageRef || sourcePolicy.sourceMode === "git" && service.railway?.dockerfilePath ? null : service.railway?.buildCommand ?? null,
|
|
436
|
+
startCommand: sourcePolicy.imageRef ? null : service.railway?.startCommand ?? null,
|
|
437
|
+
healthcheckPath: service.railway?.healthcheckPath ?? null,
|
|
438
|
+
runtimeMode: service.railway?.runtimeMode ?? null,
|
|
439
|
+
volumeMountPath,
|
|
440
|
+
volumeName: volumeMountPath && instanceServiceName ? `${instanceServiceName}-volume` : null,
|
|
441
|
+
runnerPool: runnerPool ? { ...runnerPool, bootstrapCount: instanceCount, maxRunners } : null,
|
|
442
|
+
runnerIndex: runnerPool ? runnerIndex : null,
|
|
443
|
+
poolKey: runnerPool ? serviceKey : null,
|
|
444
|
+
runnerId: runnerPool ? instanceServiceName : null,
|
|
445
|
+
resourceType: service.railway?.resourceType ?? null,
|
|
446
|
+
serviceName: instanceServiceName,
|
|
447
|
+
serviceTargets: service.railway?.serviceTargets ?? null
|
|
427
448
|
},
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
449
|
+
secretRefs: serviceKey === "treeseedDatabase" ? ["TREESEED_DATABASE_URL"] : [],
|
|
450
|
+
variableRefs: serviceKey === "operationsRunner" ? ["TREESEED_PLATFORM_RUNNER_ID", "TREESEED_PLATFORM_RUNNER_DATA_DIR", "TREESEED_PLATFORM_RUNNER_ENVIRONMENT"] : [],
|
|
451
|
+
metadata: String(serviceKey).startsWith("capacityProvider") ? { capacityProvider: true, deployByDefault: false } : runnerPool ? { poolKey: serviceKey, runnerIndex } : {},
|
|
452
|
+
environments: {
|
|
453
|
+
local: {
|
|
454
|
+
hostId: serviceType === "relational-database" || serviceType === "runner-pool" || service.railway?.volumeMountPath ? "local-docker" : "local-process",
|
|
455
|
+
projectGroupId: void 0,
|
|
456
|
+
config: environmentBinding("local")
|
|
457
|
+
},
|
|
458
|
+
staging: {
|
|
459
|
+
hostId: service.provider ?? "railway",
|
|
460
|
+
projectGroupId: defaultProjectGroup,
|
|
461
|
+
config: environmentBinding("staging")
|
|
462
|
+
},
|
|
463
|
+
prod: {
|
|
464
|
+
hostId: service.provider ?? "railway",
|
|
465
|
+
projectGroupId: defaultProjectGroup,
|
|
466
|
+
config: environmentBinding("prod")
|
|
467
|
+
}
|
|
432
468
|
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
435
471
|
}
|
|
436
472
|
if (config.cloudflare?.r2) {
|
|
437
473
|
services.push({
|
|
@@ -470,9 +506,11 @@ function buildProfileFromDeployConfig(input) {
|
|
|
470
506
|
const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config, launchEnv);
|
|
471
507
|
const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
|
|
472
508
|
const nodeIndex = offset + 1;
|
|
473
|
-
const
|
|
509
|
+
const logicalServiceName = indexedName("public-treedx-node", nodeIndex);
|
|
510
|
+
const serviceName = railwayTreeDxServiceName(nodeIndex, input.environment);
|
|
511
|
+
assertRailwayResourceNames(serviceName, `${serviceName}-volume`);
|
|
474
512
|
return {
|
|
475
|
-
id:
|
|
513
|
+
id: logicalServiceName,
|
|
476
514
|
label: `Public TreeDX node ${String(nodeIndex).padStart(2, "0")}`,
|
|
477
515
|
serviceType: "treedx-node",
|
|
478
516
|
placement: "knowledge-library",
|
|
@@ -658,7 +696,7 @@ function filterHostingUnits(units, filter) {
|
|
|
658
696
|
const serviceIds = normalizeFilterValues(filter?.serviceIds);
|
|
659
697
|
const placements = normalizeFilterValues(filter?.placements);
|
|
660
698
|
const hosts = normalizeFilterValues(filter?.hosts);
|
|
661
|
-
const allServiceIds = new Set(units.
|
|
699
|
+
const allServiceIds = new Set(units.flatMap((unit) => [unit.id, typeof unit.config.poolKey === "string" ? unit.config.poolKey : null]).filter((value) => Boolean(value)));
|
|
662
700
|
const missingServices = [...serviceIds].filter((serviceId) => !allServiceIds.has(serviceId));
|
|
663
701
|
if (missingServices.length > 0) {
|
|
664
702
|
throw new Error(`Unknown hosting service id${missingServices.length === 1 ? "" : "s"}: ${missingServices.join(", ")}.`);
|
|
@@ -666,7 +704,7 @@ function filterHostingUnits(units, filter) {
|
|
|
666
704
|
if (serviceIds.size === 0 && placements.size === 0 && hosts.size === 0) {
|
|
667
705
|
return units.filter((unit) => unit.metadata.deployByDefault !== false);
|
|
668
706
|
}
|
|
669
|
-
return units.filter((unit) => (serviceIds.size === 0 || serviceIds.has(unit.id)) && (placements.size === 0 || placements.has(unit.placement)) && (hosts.size === 0 || hosts.has(unit.host.id)));
|
|
707
|
+
return units.filter((unit) => (serviceIds.size === 0 || serviceIds.has(unit.id) || typeof unit.config.poolKey === "string" && serviceIds.has(unit.config.poolKey)) && (placements.size === 0 || placements.has(unit.placement)) && (hosts.size === 0 || hosts.has(unit.host.id)));
|
|
670
708
|
}
|
|
671
709
|
function compileSingleTreeseedHostingGraph(input, application) {
|
|
672
710
|
const environment = normalizeEnvironment(input.environment);
|
|
@@ -796,112 +834,6 @@ function railwayReconcileSystemsForUnits(units) {
|
|
|
796
834
|
}
|
|
797
835
|
return [...systems];
|
|
798
836
|
}
|
|
799
|
-
function isInfrastructureHostedUnit(unit) {
|
|
800
|
-
return ["railway", "cloudflare", "cloudflare-dns", "local-docker"].includes(unit.host.id);
|
|
801
|
-
}
|
|
802
|
-
function railwayEnvForHostingApply(input, graph) {
|
|
803
|
-
const seedValues = collectTreeseedConfigSeedValues(input.tenantRoot, graph.environment);
|
|
804
|
-
return {
|
|
805
|
-
...process.env,
|
|
806
|
-
...seedValues
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
async function applyTreeseedHostingGraph(input) {
|
|
810
|
-
const plan = await planTreeseedHostingGraph(input);
|
|
811
|
-
const graph = compileTreeseedHostingGraph(input);
|
|
812
|
-
const selectedSystems = railwayReconcileSystemsForUnits(graph.units);
|
|
813
|
-
const infrastructureUnits = graph.units.filter(isInfrastructureHostedUnit);
|
|
814
|
-
if (plan.planOnly) {
|
|
815
|
-
return {
|
|
816
|
-
environment: plan.environment,
|
|
817
|
-
planOnly: true,
|
|
818
|
-
selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
|
|
819
|
-
selectedSystems,
|
|
820
|
-
skippedSystems: ["web", "data", "github"].filter((system) => !selectedSystems.includes(system)).map((system) => ({ system, reason: selectedSystems.length > 0 ? "Not selected by hosting app filter." : "No Railway reconciliation selected." })),
|
|
821
|
-
results: plan.units.map((entry) => ({
|
|
822
|
-
unit: entry.unit,
|
|
823
|
-
plan: entry.plan,
|
|
824
|
-
result: entry.observed,
|
|
825
|
-
verification: entry.verification
|
|
826
|
-
})),
|
|
827
|
-
placements: plan.placements,
|
|
828
|
-
warnings: plan.warnings
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
if (infrastructureUnits.length > 0 && selectedSystems.length === 0) {
|
|
832
|
-
throw new Error(`Hosting apply selected infrastructure resources but no provider reconciliation system was selected: ${infrastructureUnits.map((unit) => `${unit.id} (${unit.host.id})`).join(", ")}.`);
|
|
833
|
-
}
|
|
834
|
-
const reconcile = await reconcileTreeseedTarget({
|
|
835
|
-
tenantRoot: graph.tenantRoot,
|
|
836
|
-
target: createPersistentDeployTarget(graph.environment),
|
|
837
|
-
systems: selectedSystems.length > 0 ? selectedSystems : void 0,
|
|
838
|
-
env: railwayEnvForHostingApply(input, graph),
|
|
839
|
-
planOnly: plan.planOnly
|
|
840
|
-
});
|
|
841
|
-
const resultByUnit = /* @__PURE__ */ new Map();
|
|
842
|
-
for (const entry of reconcile.results) {
|
|
843
|
-
resultByUnit.set(entry.unit.unitId, entry);
|
|
844
|
-
resultByUnit.set(entry.unit.logicalName, entry);
|
|
845
|
-
}
|
|
846
|
-
const results = plan.units.map((entry) => {
|
|
847
|
-
const unit = graph.units.find((candidate) => candidate.id === entry.unit.id) ?? entry.unit;
|
|
848
|
-
const reconcileResult = resultByUnit.get(entry.unit.id) ?? resultByUnit.get(unit.id) ?? resultByUnit.get(unit.logicalName) ?? null;
|
|
849
|
-
return {
|
|
850
|
-
unit,
|
|
851
|
-
plan: entry.plan,
|
|
852
|
-
result: {
|
|
853
|
-
status: reconcileResult?.verification?.ready || plan.planOnly ? "ready" : "blocked",
|
|
854
|
-
locators: reconcileResult?.resourceLocators ?? {},
|
|
855
|
-
state: reconcileResult?.state ?? {
|
|
856
|
-
unitId: unit.id,
|
|
857
|
-
action: plan.planOnly ? "plan" : "unmatched"
|
|
858
|
-
},
|
|
859
|
-
warnings: reconcileResult?.warnings ?? []
|
|
860
|
-
},
|
|
861
|
-
verification: reconcileResult?.verification ? {
|
|
862
|
-
unitId: unit.id,
|
|
863
|
-
status: reconcileResult.verification.verified ? "ready" : "blocked",
|
|
864
|
-
verified: reconcileResult.verification.verified,
|
|
865
|
-
checks: reconcileResult.verification.checks.map((check) => ({
|
|
866
|
-
key: check.key,
|
|
867
|
-
label: check.description,
|
|
868
|
-
ok: check.verified,
|
|
869
|
-
expected: check.expected,
|
|
870
|
-
observed: check.observed,
|
|
871
|
-
issues: check.issues
|
|
872
|
-
})),
|
|
873
|
-
warnings: reconcileResult.verification.warnings
|
|
874
|
-
} : {
|
|
875
|
-
unitId: unit.id,
|
|
876
|
-
status: plan.planOnly ? "ready" : "blocked",
|
|
877
|
-
verified: plan.planOnly,
|
|
878
|
-
checks: plan.planOnly ? [] : [{
|
|
879
|
-
key: "reconcile-result",
|
|
880
|
-
label: "Hosting unit matched a reconcile result",
|
|
881
|
-
ok: false,
|
|
882
|
-
issues: [`No reconcile result matched hosting unit ${unit.id}.`]
|
|
883
|
-
}],
|
|
884
|
-
warnings: []
|
|
885
|
-
}
|
|
886
|
-
};
|
|
887
|
-
});
|
|
888
|
-
return {
|
|
889
|
-
environment: plan.environment,
|
|
890
|
-
planOnly: plan.planOnly,
|
|
891
|
-
selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
|
|
892
|
-
selectedSystems,
|
|
893
|
-
skippedSystems: ["web", "data", "github"].filter((system) => !selectedSystems.includes(system)).map((system) => ({ system, reason: selectedSystems.length > 0 ? "Not selected by hosting app filter." : "No Railway reconciliation selected." })),
|
|
894
|
-
transport: selectedSystems.length > 0 ? {
|
|
895
|
-
railway: {
|
|
896
|
-
reconcile: "api",
|
|
897
|
-
deploy: process.env.TREESEED_RAILWAY_DEPLOY_TRANSPORT === "cli-fallback" ? "cli-fallback" : "api"
|
|
898
|
-
}
|
|
899
|
-
} : void 0,
|
|
900
|
-
results,
|
|
901
|
-
placements: plan.placements,
|
|
902
|
-
warnings: plan.warnings
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
837
|
function serializeHostingUnit(unit) {
|
|
906
838
|
return sanitizedUnitConfig(unit);
|
|
907
839
|
}
|
|
@@ -1017,58 +949,6 @@ function canonicalHostingReportFromPlan(plan) {
|
|
|
1017
949
|
}
|
|
1018
950
|
});
|
|
1019
951
|
}
|
|
1020
|
-
function canonicalHostingReportFromApplyResult(result) {
|
|
1021
|
-
const desiredGraph = result.results.map((entry) => canonicalHostingNode(entry.unit));
|
|
1022
|
-
const observedGraph = result.results.map((entry) => canonicalHostingNode(entry.unit, entry.result));
|
|
1023
|
-
const diff = result.results.flatMap((entry) => [
|
|
1024
|
-
...entry.plan.action && entry.plan.action !== "noop" ? [{
|
|
1025
|
-
id: `${entry.unit.id}:diff`,
|
|
1026
|
-
resourceId: entry.unit.id,
|
|
1027
|
-
severity: canonicalActionKind(entry.plan.action) === "blocked" ? "blocking" : "info",
|
|
1028
|
-
reason: hostingPlanReason(entry.plan, "Applied"),
|
|
1029
|
-
provider: entry.unit.host.id,
|
|
1030
|
-
type: entry.unit.serviceType.id,
|
|
1031
|
-
expected: serializeHostingUnit(entry.unit),
|
|
1032
|
-
observed: entry.result
|
|
1033
|
-
}] : [],
|
|
1034
|
-
...canonicalHostingDrift(entry.unit, entry.plan.blockedDrift, "Blocked provider drift.")
|
|
1035
|
-
]);
|
|
1036
|
-
const providerLimitations = result.results.flatMap((entry) => canonicalHostingDrift(entry.unit, entry.plan.providerLimitations, "Provider limitation."));
|
|
1037
|
-
const actions = result.results.map((entry) => ({
|
|
1038
|
-
id: `${entry.unit.id}:${entry.plan.action ?? "noop"}`,
|
|
1039
|
-
kind: canonicalActionKind(entry.plan.action),
|
|
1040
|
-
resourceId: entry.unit.id,
|
|
1041
|
-
reason: hostingPlanReason(entry.plan, "Applied"),
|
|
1042
|
-
provider: entry.unit.host.id,
|
|
1043
|
-
type: entry.unit.serviceType.id,
|
|
1044
|
-
before: entry.result,
|
|
1045
|
-
after: serializeHostingUnit(entry.unit)
|
|
1046
|
-
}));
|
|
1047
|
-
return createTreeseedCanonicalReconcileReport({
|
|
1048
|
-
desiredGraph,
|
|
1049
|
-
observedGraph,
|
|
1050
|
-
stateGraph: [],
|
|
1051
|
-
diff,
|
|
1052
|
-
actions,
|
|
1053
|
-
postconditions: result.results.map((entry) => canonicalHostingPostcondition(entry.unit, entry.verification)),
|
|
1054
|
-
selectedResources: result.results.map((entry) => entry.unit.id),
|
|
1055
|
-
skippedResources: result.skippedSystems.map((entry) => ({ id: entry.system, reason: entry.reason })),
|
|
1056
|
-
blockedDrift: diff.filter((entry) => entry.severity === "blocking"),
|
|
1057
|
-
providerLimitations,
|
|
1058
|
-
retainedResources: result.results.flatMap((entry) => (entry.plan.retainedResources ?? []).map((resource, index) => ({
|
|
1059
|
-
id: `${entry.unit.id}:retained:${index + 1}`,
|
|
1060
|
-
provider: entry.unit.host.id,
|
|
1061
|
-
type: "retained-resource",
|
|
1062
|
-
owner: entry.unit.application?.id ?? null,
|
|
1063
|
-
state: resource
|
|
1064
|
-
}))),
|
|
1065
|
-
liveVerification: {
|
|
1066
|
-
ok: result.results.every((entry) => entry.verification.verified === true),
|
|
1067
|
-
source: "hosting-apply",
|
|
1068
|
-
issues: result.results.filter((entry) => entry.verification.verified !== true).map((entry) => `${entry.unit.id}: verification did not pass after apply`)
|
|
1069
|
-
}
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
952
|
function serializeHostingPlan(plan) {
|
|
1073
953
|
const selectedSystems = railwayReconcileSystemsForUnits(plan.units.map((entry) => entry.unit));
|
|
1074
954
|
const canonical = canonicalHostingReportFromPlan(plan);
|
|
@@ -1101,42 +981,13 @@ function serializeHostingPlan(plan) {
|
|
|
1101
981
|
warnings: plan.warnings
|
|
1102
982
|
};
|
|
1103
983
|
}
|
|
1104
|
-
function serializeHostingApplyResult(result) {
|
|
1105
|
-
const canonical = canonicalHostingReportFromApplyResult(result);
|
|
1106
|
-
return {
|
|
1107
|
-
environment: result.environment,
|
|
1108
|
-
planOnly: result.planOnly,
|
|
1109
|
-
...canonical,
|
|
1110
|
-
selectedApps: result.selectedApps ?? [],
|
|
1111
|
-
selectedSystems: result.selectedSystems ?? [],
|
|
1112
|
-
skippedSystems: result.skippedSystems ?? [],
|
|
1113
|
-
transport: result.transport,
|
|
1114
|
-
placements: result.placements,
|
|
1115
|
-
results: result.results.map((entry) => ({
|
|
1116
|
-
unit: serializeHostingUnit(entry.unit),
|
|
1117
|
-
desired: serializeHostingUnit(entry.unit),
|
|
1118
|
-
observed: entry.result,
|
|
1119
|
-
diff: entry.plan,
|
|
1120
|
-
actions: entry.plan.actions ?? [entry.plan.action],
|
|
1121
|
-
retainedResources: entry.plan.retainedResources ?? [],
|
|
1122
|
-
blockedDrift: entry.plan.blockedDrift ?? [],
|
|
1123
|
-
providerLimitations: entry.plan.providerLimitations ?? [],
|
|
1124
|
-
plan: entry.plan,
|
|
1125
|
-
result: entry.result,
|
|
1126
|
-
verification: entry.verification
|
|
1127
|
-
})),
|
|
1128
|
-
warnings: result.warnings
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
984
|
function hostingEnvironmentLabel(environment) {
|
|
1132
985
|
return ENVIRONMENT_NAMES[environment];
|
|
1133
986
|
}
|
|
1134
987
|
export {
|
|
1135
|
-
applyTreeseedHostingGraph,
|
|
1136
988
|
compileTreeseedHostingGraph,
|
|
1137
989
|
hostingEnvironmentLabel,
|
|
1138
990
|
planTreeseedHostingGraph,
|
|
1139
|
-
serializeHostingApplyResult,
|
|
1140
991
|
serializeHostingPlan,
|
|
1141
992
|
serializeHostingUnit
|
|
1142
993
|
};
|
|
@@ -31,6 +31,31 @@ async function waitForPidsToExit(pids, timeoutMs = 3e3) {
|
|
|
31
31
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
function processGroupAlive(pid) {
|
|
35
|
+
if (process.platform === "win32") return pidAlive(pid);
|
|
36
|
+
try {
|
|
37
|
+
process.kill(-pid, 0);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function terminateManagedProcess(pid) {
|
|
44
|
+
const signal = (value) => {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(process.platform === "win32" ? pid : -pid, value);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
signal("SIGTERM");
|
|
51
|
+
const startedAt = Date.now();
|
|
52
|
+
while (processGroupAlive(pid) && Date.now() - startedAt < 3e3) {
|
|
53
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
|
|
54
|
+
}
|
|
55
|
+
if (processGroupAlive(pid)) {
|
|
56
|
+
signal("SIGKILL");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
34
59
|
async function stopConflictingPortListeners(spec, allowedPid = null) {
|
|
35
60
|
const conflicts = portListenerPids(spec.port).filter((pid) => pid !== process.pid && pid !== allowedPid);
|
|
36
61
|
if (conflicts.length === 0) return [];
|
|
@@ -249,7 +274,7 @@ function instanceFromSpec(spec) {
|
|
|
249
274
|
return {
|
|
250
275
|
...spec,
|
|
251
276
|
pid: Number.isFinite(pid) ? pid : null,
|
|
252
|
-
running:
|
|
277
|
+
running: Number.isFinite(pid) ? processGroupAlive(pid) : false,
|
|
253
278
|
startedAt: typeof record?.startedAt === "string" ? record.startedAt : null
|
|
254
279
|
};
|
|
255
280
|
}
|
|
@@ -296,13 +321,10 @@ function listTreeseedDevInstances(input = {}) {
|
|
|
296
321
|
if (!existsSync(instanceDir)) return [];
|
|
297
322
|
return readdirSync(instanceDir).filter((entry) => entry.endsWith(".json")).map((entry) => entry.slice(0, -".json".length)).map((surface) => readTreeseedDevInstance({ cwd: input.cwd, surface })).filter((entry) => Boolean(entry));
|
|
298
323
|
}
|
|
299
|
-
function stopSpec(spec) {
|
|
324
|
+
async function stopSpec(spec) {
|
|
300
325
|
const instance = instanceFromSpec(spec);
|
|
301
326
|
if (instance.pid && instance.running) {
|
|
302
|
-
|
|
303
|
-
process.kill(instance.pid, "SIGTERM");
|
|
304
|
-
} catch {
|
|
305
|
-
}
|
|
327
|
+
await terminateManagedProcess(instance.pid);
|
|
306
328
|
}
|
|
307
329
|
rmSync(spec.pidPath, { force: true });
|
|
308
330
|
rmSync(spec.instancePath, { force: true });
|
|
@@ -314,7 +336,7 @@ async function startSpec(spec, force = false, forceConflicts = false) {
|
|
|
314
336
|
return existing;
|
|
315
337
|
}
|
|
316
338
|
if (existing.running && force) {
|
|
317
|
-
stopSpec(spec);
|
|
339
|
+
await stopSpec(spec);
|
|
318
340
|
}
|
|
319
341
|
if (forceConflicts) {
|
|
320
342
|
await stopConflictingPortListeners(spec, existing.pid);
|
|
@@ -359,7 +381,7 @@ async function startTreeseedManagedDev(options = {}) {
|
|
|
359
381
|
}
|
|
360
382
|
async function stopTreeseedManagedDev(options = {}) {
|
|
361
383
|
const plan = createTreeseedIntegratedDevPlan(options);
|
|
362
|
-
const instances = plan.processes.map((spec) => stopSpec(spec));
|
|
384
|
+
const instances = await Promise.all(plan.processes.map((spec) => stopSpec(spec)));
|
|
363
385
|
return { ok: true, action: "stop", plan, instances };
|
|
364
386
|
}
|
|
365
387
|
function stopTreeseedDevInstance(options = {}) {
|
|
@@ -70,6 +70,9 @@ export declare function createTreeseedManagedToolEnv(env?: NodeJS.ProcessEnv): {
|
|
|
70
70
|
DOCKERHUB_USERNAME?: string;
|
|
71
71
|
CODEX_API_KEY?: string;
|
|
72
72
|
};
|
|
73
|
+
export declare function collectInstalledNativeDependencyIssues(tenantRoot: string): string[];
|
|
74
|
+
export declare function repairInstalledNativeDependencies(tenantRoot: string, options: Required<Pick<DependencyInstallerOptions, 'env' | 'spawn'>> & Pick<DependencyInstallerOptions, 'write'>): number | null;
|
|
75
|
+
export declare function staleNpmGitClonePath(detail: string): string | null;
|
|
73
76
|
export declare function formatTreeseedDependencyFailureDetails(result: Pick<TreeseedDependencyInstallResult, 'npmInstalls' | 'reports'>): string;
|
|
74
77
|
export declare function resolveTreeseedToolBinary(toolName: TreeseedManagedToolName, options?: {
|
|
75
78
|
env?: NodeJS.ProcessEnv;
|