@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
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 = input.environment
|
|
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,8 +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
|
|
180
|
-
const serviceName = typeof
|
|
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;
|
|
181
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 });
|
|
182
190
|
const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
|
|
183
191
|
const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
|
|
@@ -384,56 +392,82 @@ function buildProfileFromDeployConfig(input) {
|
|
|
384
392
|
const imageRefEnv = railwayImageRefEnvForService(serviceKey);
|
|
385
393
|
const imageRef = service.railway?.imageRef ?? (input.environment === "prod" && imageRefEnv ? launchEnv[imageRefEnv] ?? null : null) ?? defaultRailwayImageRefForService(serviceKey, input.environment) ?? null;
|
|
386
394
|
const sourcePolicy = railwaySourcePolicy(input, serviceKey, service, imageRef);
|
|
387
|
-
const environmentConfig = service.environments?.[input.environment];
|
|
388
395
|
const baseServiceName = service.railway?.serviceName ?? null;
|
|
389
|
-
const
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
429
448
|
},
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
}
|
|
434
468
|
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
437
471
|
}
|
|
438
472
|
if (config.cloudflare?.r2) {
|
|
439
473
|
services.push({
|
|
@@ -472,9 +506,11 @@ function buildProfileFromDeployConfig(input) {
|
|
|
472
506
|
const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config, launchEnv);
|
|
473
507
|
const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
|
|
474
508
|
const nodeIndex = offset + 1;
|
|
475
|
-
const
|
|
509
|
+
const logicalServiceName = indexedName("public-treedx-node", nodeIndex);
|
|
510
|
+
const serviceName = railwayTreeDxServiceName(nodeIndex, input.environment);
|
|
511
|
+
assertRailwayResourceNames(serviceName, `${serviceName}-volume`);
|
|
476
512
|
return {
|
|
477
|
-
id:
|
|
513
|
+
id: logicalServiceName,
|
|
478
514
|
label: `Public TreeDX node ${String(nodeIndex).padStart(2, "0")}`,
|
|
479
515
|
serviceType: "treedx-node",
|
|
480
516
|
placement: "knowledge-library",
|
|
@@ -660,7 +696,7 @@ function filterHostingUnits(units, filter) {
|
|
|
660
696
|
const serviceIds = normalizeFilterValues(filter?.serviceIds);
|
|
661
697
|
const placements = normalizeFilterValues(filter?.placements);
|
|
662
698
|
const hosts = normalizeFilterValues(filter?.hosts);
|
|
663
|
-
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)));
|
|
664
700
|
const missingServices = [...serviceIds].filter((serviceId) => !allServiceIds.has(serviceId));
|
|
665
701
|
if (missingServices.length > 0) {
|
|
666
702
|
throw new Error(`Unknown hosting service id${missingServices.length === 1 ? "" : "s"}: ${missingServices.join(", ")}.`);
|
|
@@ -668,7 +704,7 @@ function filterHostingUnits(units, filter) {
|
|
|
668
704
|
if (serviceIds.size === 0 && placements.size === 0 && hosts.size === 0) {
|
|
669
705
|
return units.filter((unit) => unit.metadata.deployByDefault !== false);
|
|
670
706
|
}
|
|
671
|
-
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)));
|
|
672
708
|
}
|
|
673
709
|
function compileSingleTreeseedHostingGraph(input, application) {
|
|
674
710
|
const environment = normalizeEnvironment(input.environment);
|
|
@@ -798,112 +834,6 @@ function railwayReconcileSystemsForUnits(units) {
|
|
|
798
834
|
}
|
|
799
835
|
return [...systems];
|
|
800
836
|
}
|
|
801
|
-
function isInfrastructureHostedUnit(unit) {
|
|
802
|
-
return ["railway", "cloudflare", "cloudflare-dns", "local-docker"].includes(unit.host.id);
|
|
803
|
-
}
|
|
804
|
-
function railwayEnvForHostingApply(input, graph) {
|
|
805
|
-
const seedValues = collectTreeseedConfigSeedValues(input.tenantRoot, graph.environment);
|
|
806
|
-
return {
|
|
807
|
-
...process.env,
|
|
808
|
-
...seedValues
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
async function applyTreeseedHostingGraph(input) {
|
|
812
|
-
const plan = await planTreeseedHostingGraph(input);
|
|
813
|
-
const graph = compileTreeseedHostingGraph(input);
|
|
814
|
-
const selectedSystems = railwayReconcileSystemsForUnits(graph.units);
|
|
815
|
-
const infrastructureUnits = graph.units.filter(isInfrastructureHostedUnit);
|
|
816
|
-
if (plan.planOnly) {
|
|
817
|
-
return {
|
|
818
|
-
environment: plan.environment,
|
|
819
|
-
planOnly: true,
|
|
820
|
-
selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
|
|
821
|
-
selectedSystems,
|
|
822
|
-
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." })),
|
|
823
|
-
results: plan.units.map((entry) => ({
|
|
824
|
-
unit: entry.unit,
|
|
825
|
-
plan: entry.plan,
|
|
826
|
-
result: entry.observed,
|
|
827
|
-
verification: entry.verification
|
|
828
|
-
})),
|
|
829
|
-
placements: plan.placements,
|
|
830
|
-
warnings: plan.warnings
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
if (infrastructureUnits.length > 0 && selectedSystems.length === 0) {
|
|
834
|
-
throw new Error(`Hosting apply selected infrastructure resources but no provider reconciliation system was selected: ${infrastructureUnits.map((unit) => `${unit.id} (${unit.host.id})`).join(", ")}.`);
|
|
835
|
-
}
|
|
836
|
-
const reconcile = await reconcileTreeseedTarget({
|
|
837
|
-
tenantRoot: graph.tenantRoot,
|
|
838
|
-
target: createPersistentDeployTarget(graph.environment),
|
|
839
|
-
systems: selectedSystems.length > 0 ? selectedSystems : void 0,
|
|
840
|
-
env: railwayEnvForHostingApply(input, graph),
|
|
841
|
-
planOnly: plan.planOnly
|
|
842
|
-
});
|
|
843
|
-
const resultByUnit = /* @__PURE__ */ new Map();
|
|
844
|
-
for (const entry of reconcile.results) {
|
|
845
|
-
resultByUnit.set(entry.unit.unitId, entry);
|
|
846
|
-
resultByUnit.set(entry.unit.logicalName, entry);
|
|
847
|
-
}
|
|
848
|
-
const results = plan.units.map((entry) => {
|
|
849
|
-
const unit = graph.units.find((candidate) => candidate.id === entry.unit.id) ?? entry.unit;
|
|
850
|
-
const reconcileResult = resultByUnit.get(entry.unit.id) ?? resultByUnit.get(unit.id) ?? resultByUnit.get(unit.logicalName) ?? null;
|
|
851
|
-
return {
|
|
852
|
-
unit,
|
|
853
|
-
plan: entry.plan,
|
|
854
|
-
result: {
|
|
855
|
-
status: reconcileResult?.verification?.ready || plan.planOnly ? "ready" : "blocked",
|
|
856
|
-
locators: reconcileResult?.resourceLocators ?? {},
|
|
857
|
-
state: reconcileResult?.state ?? {
|
|
858
|
-
unitId: unit.id,
|
|
859
|
-
action: plan.planOnly ? "plan" : "unmatched"
|
|
860
|
-
},
|
|
861
|
-
warnings: reconcileResult?.warnings ?? []
|
|
862
|
-
},
|
|
863
|
-
verification: reconcileResult?.verification ? {
|
|
864
|
-
unitId: unit.id,
|
|
865
|
-
status: reconcileResult.verification.verified ? "ready" : "blocked",
|
|
866
|
-
verified: reconcileResult.verification.verified,
|
|
867
|
-
checks: reconcileResult.verification.checks.map((check) => ({
|
|
868
|
-
key: check.key,
|
|
869
|
-
label: check.description,
|
|
870
|
-
ok: check.verified,
|
|
871
|
-
expected: check.expected,
|
|
872
|
-
observed: check.observed,
|
|
873
|
-
issues: check.issues
|
|
874
|
-
})),
|
|
875
|
-
warnings: reconcileResult.verification.warnings
|
|
876
|
-
} : {
|
|
877
|
-
unitId: unit.id,
|
|
878
|
-
status: plan.planOnly ? "ready" : "blocked",
|
|
879
|
-
verified: plan.planOnly,
|
|
880
|
-
checks: plan.planOnly ? [] : [{
|
|
881
|
-
key: "reconcile-result",
|
|
882
|
-
label: "Hosting unit matched a reconcile result",
|
|
883
|
-
ok: false,
|
|
884
|
-
issues: [`No reconcile result matched hosting unit ${unit.id}.`]
|
|
885
|
-
}],
|
|
886
|
-
warnings: []
|
|
887
|
-
}
|
|
888
|
-
};
|
|
889
|
-
});
|
|
890
|
-
return {
|
|
891
|
-
environment: plan.environment,
|
|
892
|
-
planOnly: plan.planOnly,
|
|
893
|
-
selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
|
|
894
|
-
selectedSystems,
|
|
895
|
-
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." })),
|
|
896
|
-
transport: selectedSystems.length > 0 ? {
|
|
897
|
-
railway: {
|
|
898
|
-
reconcile: "api",
|
|
899
|
-
deploy: process.env.TREESEED_RAILWAY_DEPLOY_TRANSPORT === "cli-fallback" ? "cli-fallback" : "api"
|
|
900
|
-
}
|
|
901
|
-
} : void 0,
|
|
902
|
-
results,
|
|
903
|
-
placements: plan.placements,
|
|
904
|
-
warnings: plan.warnings
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
837
|
function serializeHostingUnit(unit) {
|
|
908
838
|
return sanitizedUnitConfig(unit);
|
|
909
839
|
}
|
|
@@ -1019,58 +949,6 @@ function canonicalHostingReportFromPlan(plan) {
|
|
|
1019
949
|
}
|
|
1020
950
|
});
|
|
1021
951
|
}
|
|
1022
|
-
function canonicalHostingReportFromApplyResult(result) {
|
|
1023
|
-
const desiredGraph = result.results.map((entry) => canonicalHostingNode(entry.unit));
|
|
1024
|
-
const observedGraph = result.results.map((entry) => canonicalHostingNode(entry.unit, entry.result));
|
|
1025
|
-
const diff = result.results.flatMap((entry) => [
|
|
1026
|
-
...entry.plan.action && entry.plan.action !== "noop" ? [{
|
|
1027
|
-
id: `${entry.unit.id}:diff`,
|
|
1028
|
-
resourceId: entry.unit.id,
|
|
1029
|
-
severity: canonicalActionKind(entry.plan.action) === "blocked" ? "blocking" : "info",
|
|
1030
|
-
reason: hostingPlanReason(entry.plan, "Applied"),
|
|
1031
|
-
provider: entry.unit.host.id,
|
|
1032
|
-
type: entry.unit.serviceType.id,
|
|
1033
|
-
expected: serializeHostingUnit(entry.unit),
|
|
1034
|
-
observed: entry.result
|
|
1035
|
-
}] : [],
|
|
1036
|
-
...canonicalHostingDrift(entry.unit, entry.plan.blockedDrift, "Blocked provider drift.")
|
|
1037
|
-
]);
|
|
1038
|
-
const providerLimitations = result.results.flatMap((entry) => canonicalHostingDrift(entry.unit, entry.plan.providerLimitations, "Provider limitation."));
|
|
1039
|
-
const actions = result.results.map((entry) => ({
|
|
1040
|
-
id: `${entry.unit.id}:${entry.plan.action ?? "noop"}`,
|
|
1041
|
-
kind: canonicalActionKind(entry.plan.action),
|
|
1042
|
-
resourceId: entry.unit.id,
|
|
1043
|
-
reason: hostingPlanReason(entry.plan, "Applied"),
|
|
1044
|
-
provider: entry.unit.host.id,
|
|
1045
|
-
type: entry.unit.serviceType.id,
|
|
1046
|
-
before: entry.result,
|
|
1047
|
-
after: serializeHostingUnit(entry.unit)
|
|
1048
|
-
}));
|
|
1049
|
-
return createTreeseedCanonicalReconcileReport({
|
|
1050
|
-
desiredGraph,
|
|
1051
|
-
observedGraph,
|
|
1052
|
-
stateGraph: [],
|
|
1053
|
-
diff,
|
|
1054
|
-
actions,
|
|
1055
|
-
postconditions: result.results.map((entry) => canonicalHostingPostcondition(entry.unit, entry.verification)),
|
|
1056
|
-
selectedResources: result.results.map((entry) => entry.unit.id),
|
|
1057
|
-
skippedResources: result.skippedSystems.map((entry) => ({ id: entry.system, reason: entry.reason })),
|
|
1058
|
-
blockedDrift: diff.filter((entry) => entry.severity === "blocking"),
|
|
1059
|
-
providerLimitations,
|
|
1060
|
-
retainedResources: result.results.flatMap((entry) => (entry.plan.retainedResources ?? []).map((resource, index) => ({
|
|
1061
|
-
id: `${entry.unit.id}:retained:${index + 1}`,
|
|
1062
|
-
provider: entry.unit.host.id,
|
|
1063
|
-
type: "retained-resource",
|
|
1064
|
-
owner: entry.unit.application?.id ?? null,
|
|
1065
|
-
state: resource
|
|
1066
|
-
}))),
|
|
1067
|
-
liveVerification: {
|
|
1068
|
-
ok: result.results.every((entry) => entry.verification.verified === true),
|
|
1069
|
-
source: "hosting-apply",
|
|
1070
|
-
issues: result.results.filter((entry) => entry.verification.verified !== true).map((entry) => `${entry.unit.id}: verification did not pass after apply`)
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
952
|
function serializeHostingPlan(plan) {
|
|
1075
953
|
const selectedSystems = railwayReconcileSystemsForUnits(plan.units.map((entry) => entry.unit));
|
|
1076
954
|
const canonical = canonicalHostingReportFromPlan(plan);
|
|
@@ -1103,42 +981,13 @@ function serializeHostingPlan(plan) {
|
|
|
1103
981
|
warnings: plan.warnings
|
|
1104
982
|
};
|
|
1105
983
|
}
|
|
1106
|
-
function serializeHostingApplyResult(result) {
|
|
1107
|
-
const canonical = canonicalHostingReportFromApplyResult(result);
|
|
1108
|
-
return {
|
|
1109
|
-
environment: result.environment,
|
|
1110
|
-
planOnly: result.planOnly,
|
|
1111
|
-
...canonical,
|
|
1112
|
-
selectedApps: result.selectedApps ?? [],
|
|
1113
|
-
selectedSystems: result.selectedSystems ?? [],
|
|
1114
|
-
skippedSystems: result.skippedSystems ?? [],
|
|
1115
|
-
transport: result.transport,
|
|
1116
|
-
placements: result.placements,
|
|
1117
|
-
results: result.results.map((entry) => ({
|
|
1118
|
-
unit: serializeHostingUnit(entry.unit),
|
|
1119
|
-
desired: serializeHostingUnit(entry.unit),
|
|
1120
|
-
observed: entry.result,
|
|
1121
|
-
diff: entry.plan,
|
|
1122
|
-
actions: entry.plan.actions ?? [entry.plan.action],
|
|
1123
|
-
retainedResources: entry.plan.retainedResources ?? [],
|
|
1124
|
-
blockedDrift: entry.plan.blockedDrift ?? [],
|
|
1125
|
-
providerLimitations: entry.plan.providerLimitations ?? [],
|
|
1126
|
-
plan: entry.plan,
|
|
1127
|
-
result: entry.result,
|
|
1128
|
-
verification: entry.verification
|
|
1129
|
-
})),
|
|
1130
|
-
warnings: result.warnings
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
984
|
function hostingEnvironmentLabel(environment) {
|
|
1134
985
|
return ENVIRONMENT_NAMES[environment];
|
|
1135
986
|
}
|
|
1136
987
|
export {
|
|
1137
|
-
applyTreeseedHostingGraph,
|
|
1138
988
|
compileTreeseedHostingGraph,
|
|
1139
989
|
hostingEnvironmentLabel,
|
|
1140
990
|
planTreeseedHostingGraph,
|
|
1141
|
-
serializeHostingApplyResult,
|
|
1142
991
|
serializeHostingPlan,
|
|
1143
992
|
serializeHostingUnit
|
|
1144
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;
|