@treeseed/sdk 0.10.28 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -6
- package/dist/capacity-provider.d.ts +3 -1
- package/dist/capacity-provider.js +25 -5
- package/dist/control-plane.d.ts +1 -0
- package/dist/control-plane.js +38 -13
- package/dist/db/market-schema.d.ts +8860 -6172
- package/dist/db/market-schema.js +108 -0
- package/dist/db/node-sqlite.js +7 -2
- package/dist/hosting/apps.d.ts +12 -0
- package/dist/hosting/apps.js +107 -0
- package/dist/hosting/builtins.d.ts +25 -0
- package/dist/hosting/builtins.js +791 -0
- package/dist/hosting/contracts.d.ts +207 -0
- package/dist/hosting/contracts.js +0 -0
- package/dist/hosting/graph.d.ts +192 -0
- package/dist/hosting/graph.js +1106 -0
- package/dist/hosting/index.d.ts +4 -0
- package/dist/hosting/index.js +4 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +63 -6
- package/dist/managed-dependencies.js +1 -2
- package/dist/market-client.d.ts +63 -3
- package/dist/market-client.js +83 -11
- package/dist/operations/services/bootstrap-runner.d.ts +3 -1
- package/dist/operations/services/bootstrap-runner.js +22 -2
- package/dist/operations/services/config-runtime.d.ts +10 -5
- package/dist/operations/services/config-runtime.js +209 -66
- package/dist/operations/services/deploy.d.ts +70 -7
- package/dist/operations/services/deploy.js +579 -64
- package/dist/operations/services/deployment-readiness.d.ts +30 -0
- package/dist/operations/services/deployment-readiness.js +175 -0
- package/dist/operations/services/git-workflow.d.ts +2 -1
- package/dist/operations/services/git-workflow.js +9 -3
- package/dist/operations/services/github-actions-verification.d.ts +1 -0
- package/dist/operations/services/github-actions-verification.js +1 -0
- package/dist/operations/services/github-api.js +1 -1
- package/dist/operations/services/github-automation.d.ts +1 -1
- package/dist/operations/services/github-automation.js +4 -3
- package/dist/operations/services/github-credentials.d.ts +13 -0
- package/dist/operations/services/github-credentials.js +58 -0
- package/dist/operations/services/hosted-service-checks.d.ts +63 -0
- package/dist/operations/services/hosted-service-checks.js +327 -0
- package/dist/operations/services/hub-provider-launch.js +3 -3
- package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
- package/dist/operations/services/live-hosted-service-checks.js +350 -0
- package/dist/operations/services/managed-host-security.js +1 -1
- package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
- package/dist/operations/services/operations-runner-smoke.js +180 -0
- package/dist/operations/services/package-adapters.d.ts +95 -0
- package/dist/operations/services/package-adapters.js +288 -0
- package/dist/operations/services/package-reference-policy.d.ts +1 -0
- package/dist/operations/services/package-reference-policy.js +15 -2
- package/dist/operations/services/project-platform.d.ts +80 -22
- package/dist/operations/services/project-platform.js +49 -8
- package/dist/operations/services/project-web-monitor.js +26 -4
- package/dist/operations/services/railway-api.d.ts +88 -5
- package/dist/operations/services/railway-api.js +626 -35
- package/dist/operations/services/railway-deploy.d.ts +46 -40
- package/dist/operations/services/railway-deploy.js +261 -293
- package/dist/operations/services/release-candidate.d.ts +19 -0
- package/dist/operations/services/release-candidate.js +375 -38
- package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
- package/dist/operations/services/repository-save-orchestrator.js +279 -66
- package/dist/operations/services/runtime-tools.d.ts +1 -0
- package/dist/operations/services/runtime-tools.js +10 -9
- package/dist/operations/services/verification-cache.d.ts +25 -0
- package/dist/operations/services/verification-cache.js +71 -0
- package/dist/operations/services/workspace-dependency-mode.js +9 -1
- package/dist/operations/services/workspace-save.js +1 -1
- package/dist/operations/services/workspace-tools.js +2 -1
- package/dist/platform/contracts.d.ts +32 -1
- package/dist/platform/deploy-config.js +73 -8
- package/dist/platform/env.yaml +163 -35
- package/dist/platform/environment.d.ts +1 -0
- package/dist/platform/environment.js +74 -5
- package/dist/platform/plugin.d.ts +9 -0
- package/dist/platform-operation-store.js +2 -2
- package/dist/platform-operations.js +1 -1
- package/dist/reconcile/bootstrap-systems.js +2 -2
- package/dist/reconcile/builtin-adapters.js +372 -189
- package/dist/reconcile/contracts.d.ts +9 -5
- package/dist/reconcile/desired-state.d.ts +1 -0
- package/dist/reconcile/desired-state.js +5 -5
- package/dist/reconcile/engine.d.ts +5 -2
- package/dist/reconcile/engine.js +53 -32
- package/dist/reconcile/index.d.ts +2 -0
- package/dist/reconcile/index.js +2 -0
- package/dist/reconcile/live-acceptance.d.ts +79 -0
- package/dist/reconcile/live-acceptance.js +1615 -0
- package/dist/reconcile/platform.d.ts +104 -0
- package/dist/reconcile/platform.js +100 -0
- package/dist/reconcile/state.js +4 -4
- package/dist/reconcile/units.js +2 -2
- package/dist/scripts/deployment-readiness.js +20 -0
- package/dist/scripts/generate-treedx-openapi-types.js +186 -0
- package/dist/scripts/operations-runner-smoke.js +16 -0
- package/dist/scripts/release-verify.js +4 -1
- package/dist/scripts/tenant-workflow-action.js +10 -1
- package/dist/sdk-types.d.ts +169 -4
- package/dist/sdk-types.js +20 -2
- package/dist/sdk.d.ts +35 -24
- package/dist/sdk.js +186 -17
- package/dist/template-launch-requirements.js +9 -0
- package/dist/treedx/adapters.d.ts +6 -0
- package/dist/treedx/adapters.js +36 -0
- package/dist/treedx/client.d.ts +222 -0
- package/dist/treedx/client.js +871 -0
- package/dist/treedx/errors.d.ts +13 -0
- package/dist/treedx/errors.js +17 -0
- package/dist/treedx/federated-client.d.ts +27 -0
- package/dist/treedx/federated-client.js +158 -0
- package/dist/treedx/generated/openapi-types.d.ts +3558 -0
- package/dist/treedx/generated/openapi-types.js +0 -0
- package/dist/treedx/graph-adapter.d.ts +33 -0
- package/dist/treedx/graph-adapter.js +156 -0
- package/dist/treedx/index.d.ts +14 -0
- package/dist/treedx/index.js +48 -0
- package/dist/treedx/market-integration.d.ts +27 -0
- package/dist/treedx/market-integration.js +131 -0
- package/dist/treedx/ports.d.ts +166 -0
- package/dist/treedx/ports.js +231 -0
- package/dist/treedx/query-adapter.d.ts +19 -0
- package/dist/treedx/query-adapter.js +62 -0
- package/dist/treedx/registry-client.d.ts +11 -0
- package/dist/treedx/registry-client.js +19 -0
- package/dist/treedx/repository-adapter.d.ts +45 -0
- package/dist/treedx/repository-adapter.js +308 -0
- package/dist/treedx/sdk-integration.d.ts +27 -0
- package/dist/treedx/sdk-integration.js +63 -0
- package/dist/treedx/types.d.ts +1084 -0
- package/dist/treedx/types.js +8 -0
- package/dist/treedx/workspace-adapter.d.ts +27 -0
- package/dist/treedx/workspace-adapter.js +65 -0
- package/dist/treedx-backends.d.ts +218 -0
- package/dist/treedx-backends.js +632 -0
- package/dist/treedx-client.d.ts +86 -0
- package/dist/treedx-client.js +175 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +23 -23
- package/dist/workflow/operations.d.ts +119 -13
- package/dist/workflow/operations.js +309 -53
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.js +43 -26
- package/dist/workflow-support.d.ts +11 -3
- package/dist/workflow-support.js +67 -3
- package/dist/workflow.d.ts +5 -0
- package/drizzle/market/0004_treedx_market_integration.sql +99 -0
- package/package.json +34 -3
- package/templates/github/deploy-web.workflow.yml +39 -6
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
import { loadTreeseedDeployConfig } from "../platform/deploy-config.js";
|
|
2
|
+
import { loadTreeseedPlugins } from "../platform/plugins/runtime.js";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { createPersistentDeployTarget } from "../operations/services/deploy.js";
|
|
6
|
+
import { collectTreeseedConfigSeedValues } from "../operations/services/config-runtime.js";
|
|
7
|
+
import {
|
|
8
|
+
configuredRailwayServices,
|
|
9
|
+
deployRailwayService
|
|
10
|
+
} from "../operations/services/railway-deploy.js";
|
|
11
|
+
import {
|
|
12
|
+
deployRailwayServiceInstance,
|
|
13
|
+
deleteRailwayService,
|
|
14
|
+
ensureRailwayEnvironment,
|
|
15
|
+
ensureRailwayGeneratedServiceDomain,
|
|
16
|
+
ensureRailwayProject,
|
|
17
|
+
ensureRailwayService,
|
|
18
|
+
ensureRailwayServiceInstanceConfiguration,
|
|
19
|
+
ensureRailwayServiceVolume,
|
|
20
|
+
listRailwayVariables,
|
|
21
|
+
listRailwayServices,
|
|
22
|
+
normalizeRailwayEnvironmentName,
|
|
23
|
+
resolveRailwayWorkspaceContext,
|
|
24
|
+
updateRailwayServiceName,
|
|
25
|
+
upsertRailwayVariables
|
|
26
|
+
} from "../operations/services/railway-api.js";
|
|
27
|
+
import { createTreeseedCanonicalReconcileReport } from "../reconcile/index.js";
|
|
28
|
+
import { reconcileTreeseedTarget } from "../reconcile/index.js";
|
|
29
|
+
import { discoverTreeseedApplications, findTreeseedApplication } from "./apps.js";
|
|
30
|
+
import {
|
|
31
|
+
createDefaultHostAdapters,
|
|
32
|
+
createDefaultHostingProfiles,
|
|
33
|
+
createDefaultServiceTypeAdapters,
|
|
34
|
+
redactSensitiveConfig,
|
|
35
|
+
sanitizedUnitConfig,
|
|
36
|
+
summarizePlacementStatus
|
|
37
|
+
} from "./builtins.js";
|
|
38
|
+
const ENVIRONMENT_NAMES = {
|
|
39
|
+
local: "local",
|
|
40
|
+
staging: "staging",
|
|
41
|
+
prod: "production"
|
|
42
|
+
};
|
|
43
|
+
const PLACEMENT_LABELS = {
|
|
44
|
+
web: "Site Hosting",
|
|
45
|
+
api: "API Runtime",
|
|
46
|
+
database: "Database",
|
|
47
|
+
"knowledge-library": "Knowledge Library",
|
|
48
|
+
"runner-capacity": "Runner Capacity",
|
|
49
|
+
repository: "Repository",
|
|
50
|
+
"content-storage": "Content Storage",
|
|
51
|
+
email: "Email",
|
|
52
|
+
operations: "Operations",
|
|
53
|
+
custom: "Custom"
|
|
54
|
+
};
|
|
55
|
+
function mergeRecord(...records) {
|
|
56
|
+
return Object.assign({}, ...records.filter(Boolean));
|
|
57
|
+
}
|
|
58
|
+
function asPluginRecord(value) {
|
|
59
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
60
|
+
}
|
|
61
|
+
function normalizeEnvironment(value) {
|
|
62
|
+
return value === "prod" || value === "production" ? "prod" : value === "staging" ? "staging" : "local";
|
|
63
|
+
}
|
|
64
|
+
function indexedName(baseName, index) {
|
|
65
|
+
return `${baseName.replace(/-\d+$/u, "").replace(/-\d{2}$/u, "")}-${String(Math.max(1, index)).padStart(2, "0")}`;
|
|
66
|
+
}
|
|
67
|
+
function publicTreeDxNodePool(config) {
|
|
68
|
+
const nodePool = config.publicTreeDxFederation?.railway?.nodePool ?? {};
|
|
69
|
+
const bootstrapCount = Math.max(1, Number.parseInt(String(nodePool.bootstrapCount ?? 1), 10) || 1);
|
|
70
|
+
const maxNodes = Math.max(bootstrapCount, Number.parseInt(String(nodePool.maxNodes ?? 4), 10) || 4);
|
|
71
|
+
return { bootstrapCount, maxNodes };
|
|
72
|
+
}
|
|
73
|
+
function treeDxSecretBase() {
|
|
74
|
+
return randomBytes(48).toString("base64url");
|
|
75
|
+
}
|
|
76
|
+
function serviceKeyPlacement(serviceKey) {
|
|
77
|
+
if (serviceKey === "api") return "api";
|
|
78
|
+
if (serviceKey === "treeseedDatabase") return "database";
|
|
79
|
+
if (serviceKey === "operationsRunner") return "runner-capacity";
|
|
80
|
+
if (/runner|capacity/iu.test(serviceKey)) return "runner-capacity";
|
|
81
|
+
if (/database|postgres|db/iu.test(serviceKey)) return "database";
|
|
82
|
+
if (/email|smtp/iu.test(serviceKey)) return "email";
|
|
83
|
+
return "operations";
|
|
84
|
+
}
|
|
85
|
+
function serviceKeyType(serviceKey, service) {
|
|
86
|
+
if (serviceKey === "treeseedDatabase" || service.railway?.resourceType === "postgres") return "relational-database";
|
|
87
|
+
if (serviceKey === "operationsRunner" || /runner/iu.test(serviceKey)) return "runner-pool";
|
|
88
|
+
if (Array.isArray(service.railway?.schedule) || typeof service.railway?.schedule === "string") return "scheduled-job";
|
|
89
|
+
if (serviceKey === "api") return "container-api";
|
|
90
|
+
return service.railway?.volumeMountPath ? "stateful-container" : "container-api";
|
|
91
|
+
}
|
|
92
|
+
function collectPluginHostingContributions(input) {
|
|
93
|
+
const plugins = loadTreeseedPlugins(input.deployConfig);
|
|
94
|
+
const context = {
|
|
95
|
+
projectRoot: input.tenantRoot,
|
|
96
|
+
tenantConfig: void 0,
|
|
97
|
+
deployConfig: input.deployConfig,
|
|
98
|
+
pluginConfig: {}
|
|
99
|
+
};
|
|
100
|
+
const hostAdapters = {};
|
|
101
|
+
const serviceTypeAdapters = {};
|
|
102
|
+
const profiles = [];
|
|
103
|
+
for (const entry of plugins) {
|
|
104
|
+
const pluginContext = { ...context, pluginConfig: entry.config ?? {} };
|
|
105
|
+
const contribution = entry.plugin.hosting;
|
|
106
|
+
const resolved = typeof contribution === "function" ? contribution(pluginContext) : contribution;
|
|
107
|
+
if (!resolved || typeof resolved !== "object") continue;
|
|
108
|
+
Object.assign(hostAdapters, asPluginRecord(resolved.hostAdapters));
|
|
109
|
+
Object.assign(serviceTypeAdapters, asPluginRecord(resolved.serviceTypeAdapters));
|
|
110
|
+
const contributedProfiles = Array.isArray(resolved.profiles) ? resolved.profiles : [];
|
|
111
|
+
profiles.push(...contributedProfiles.filter(Boolean));
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
hostAdapters,
|
|
115
|
+
serviceTypeAdapters,
|
|
116
|
+
profiles
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function marketProjectGroup(environment, config) {
|
|
120
|
+
const railwayProjectName = Object.values(config.services ?? {}).map((service) => service && typeof service === "object" ? service.railway?.projectName : null).find((value) => typeof value === "string" && value.trim());
|
|
121
|
+
const projectName = railwayProjectName ?? config.slug ?? "treeseed-api";
|
|
122
|
+
return {
|
|
123
|
+
id: "treeseed-control-plane",
|
|
124
|
+
label: "Treeseed control plane",
|
|
125
|
+
hostId: environment === "local" ? "local-process" : "railway",
|
|
126
|
+
environments: {
|
|
127
|
+
local: { projectName: `${projectName}-local`, environmentName: "local" },
|
|
128
|
+
staging: { projectName, environmentName: "staging" },
|
|
129
|
+
prod: { projectName, environmentName: "production" }
|
|
130
|
+
},
|
|
131
|
+
metadata: { stableProjectName: projectName }
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function publicTreeDxProjectGroup(environment, config) {
|
|
135
|
+
const apiProjectName = marketProjectGroup(environment, config).environments.staging?.projectName ?? config.slug ?? "treeseed-api";
|
|
136
|
+
return {
|
|
137
|
+
id: "public-treedx-federation",
|
|
138
|
+
label: "Public TreeDX federation",
|
|
139
|
+
hostId: environment === "local" ? "local-docker" : "railway",
|
|
140
|
+
environments: {
|
|
141
|
+
local: { projectName: `${apiProjectName}-local`, environmentName: "local" },
|
|
142
|
+
staging: { projectName: apiProjectName, environmentName: "staging" },
|
|
143
|
+
prod: { projectName: apiProjectName, environmentName: "production" }
|
|
144
|
+
},
|
|
145
|
+
metadata: {
|
|
146
|
+
publicFederation: true,
|
|
147
|
+
ownedByAppProject: "api",
|
|
148
|
+
isolation: "railway-service-volume-domain"
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function privateTreeDxProjectGroup(teamId = "{teamId}") {
|
|
153
|
+
return {
|
|
154
|
+
id: "private-team-treedx",
|
|
155
|
+
label: "Private team TreeDX",
|
|
156
|
+
hostId: "railway",
|
|
157
|
+
environments: {
|
|
158
|
+
staging: { projectName: `treeseed-team-${teamId}-treedx`, environmentName: "staging" },
|
|
159
|
+
prod: { projectName: `treeseed-team-${teamId}-treedx`, environmentName: "production" }
|
|
160
|
+
},
|
|
161
|
+
metadata: { transferable: true, privateTeam: true }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function buildProfileFromDeployConfig(input) {
|
|
165
|
+
const config = input.deployConfig;
|
|
166
|
+
const environment = input.environment;
|
|
167
|
+
const services = [];
|
|
168
|
+
const projectGroups = [
|
|
169
|
+
marketProjectGroup(environment, config),
|
|
170
|
+
publicTreeDxProjectGroup(environment, config),
|
|
171
|
+
privateTreeDxProjectGroup()
|
|
172
|
+
];
|
|
173
|
+
if (config.surfaces?.web && config.surfaces.web.enabled !== false) {
|
|
174
|
+
services.push({
|
|
175
|
+
id: "web",
|
|
176
|
+
label: "Site Hosting",
|
|
177
|
+
serviceType: "web-site",
|
|
178
|
+
placement: "web",
|
|
179
|
+
projectGroupId: environment === "local" ? void 0 : "treeseed-control-plane",
|
|
180
|
+
config: {
|
|
181
|
+
rootDir: config.surfaces?.web?.rootDir ?? ".",
|
|
182
|
+
publicBaseUrl: config.surfaces?.web?.environments?.[environment]?.baseUrl ?? config.surfaces?.web?.publicBaseUrl ?? null,
|
|
183
|
+
domain: config.surfaces?.web?.environments?.[environment]?.domain ?? null,
|
|
184
|
+
cache: config.surfaces?.web?.cache ?? null,
|
|
185
|
+
cloudflare: config.cloudflare ? {
|
|
186
|
+
workerName: config.cloudflare.workerName ?? null,
|
|
187
|
+
pages: config.cloudflare.pages ?? null
|
|
188
|
+
} : null
|
|
189
|
+
},
|
|
190
|
+
environments: {
|
|
191
|
+
local: { hostId: "local-process", config: { hotReload: true, baseUrl: config.surfaces?.web?.localBaseUrl ?? "http://127.0.0.1:4321" } },
|
|
192
|
+
staging: { hostId: "cloudflare", projectGroupId: "treeseed-control-plane" },
|
|
193
|
+
prod: { hostId: "cloudflare", projectGroupId: "treeseed-control-plane" }
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
for (const [serviceKey, serviceValue] of Object.entries(config.services ?? {})) {
|
|
198
|
+
const service = serviceValue;
|
|
199
|
+
if (!service || service.enabled === false) continue;
|
|
200
|
+
const serviceType = serviceKeyType(serviceKey, service);
|
|
201
|
+
const placement = serviceKeyPlacement(serviceKey);
|
|
202
|
+
const defaultProjectGroup = service.provider === "railway" || service.railway ? "treeseed-control-plane" : void 0;
|
|
203
|
+
services.push({
|
|
204
|
+
id: serviceKey,
|
|
205
|
+
label: placement === "runner-capacity" ? "Runner Capacity" : serviceKey === "api" ? "API Runtime" : serviceKey,
|
|
206
|
+
serviceType,
|
|
207
|
+
placement,
|
|
208
|
+
projectGroupId: defaultProjectGroup,
|
|
209
|
+
config: {
|
|
210
|
+
rootDir: service.railway?.rootDir ?? service.rootDir ?? ".",
|
|
211
|
+
buildCommand: service.railway?.buildCommand ?? null,
|
|
212
|
+
startCommand: service.railway?.startCommand ?? null,
|
|
213
|
+
healthcheckPath: service.railway?.healthcheckPath ?? null,
|
|
214
|
+
runtimeMode: service.railway?.runtimeMode ?? null,
|
|
215
|
+
volumeMountPath: service.railway?.volumeMountPath ?? null,
|
|
216
|
+
runnerPool: service.railway?.runnerPool ?? null,
|
|
217
|
+
resourceType: service.railway?.resourceType ?? null,
|
|
218
|
+
serviceName: service.railway?.serviceName ?? null,
|
|
219
|
+
serviceTargets: service.railway?.serviceTargets ?? null
|
|
220
|
+
},
|
|
221
|
+
secretRefs: serviceKey === "treeseedDatabase" ? ["TREESEED_DATABASE_URL"] : [],
|
|
222
|
+
variableRefs: serviceKey === "operationsRunner" ? ["TREESEED_PLATFORM_RUNNER_ID", "TREESEED_PLATFORM_RUNNER_DATA_DIR", "TREESEED_PLATFORM_RUNNER_ENVIRONMENT"] : [],
|
|
223
|
+
environments: {
|
|
224
|
+
local: {
|
|
225
|
+
hostId: serviceType === "relational-database" || serviceType === "runner-pool" || service.railway?.volumeMountPath ? "local-docker" : "local-process",
|
|
226
|
+
projectGroupId: void 0,
|
|
227
|
+
config: service.environments?.local ?? {}
|
|
228
|
+
},
|
|
229
|
+
staging: {
|
|
230
|
+
hostId: service.provider ?? "railway",
|
|
231
|
+
projectGroupId: defaultProjectGroup,
|
|
232
|
+
config: service.environments?.staging ?? {}
|
|
233
|
+
},
|
|
234
|
+
prod: {
|
|
235
|
+
hostId: service.provider ?? "railway",
|
|
236
|
+
projectGroupId: defaultProjectGroup,
|
|
237
|
+
config: service.environments?.prod ?? {}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (config.cloudflare?.r2) {
|
|
243
|
+
services.push({
|
|
244
|
+
id: "content-storage",
|
|
245
|
+
label: "Content Storage",
|
|
246
|
+
serviceType: "object-store",
|
|
247
|
+
placement: "content-storage",
|
|
248
|
+
config: {
|
|
249
|
+
bucketName: config.cloudflare.r2.bucketName ?? null,
|
|
250
|
+
manifestKeyTemplate: config.cloudflare.r2.manifestKeyTemplate ?? null,
|
|
251
|
+
previewRootTemplate: config.cloudflare.r2.previewRootTemplate ?? null
|
|
252
|
+
},
|
|
253
|
+
environments: {
|
|
254
|
+
local: { hostId: "local-docker" },
|
|
255
|
+
staging: { hostId: "cloudflare" },
|
|
256
|
+
prod: { hostId: "cloudflare" }
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (config.smtp?.enabled === true) {
|
|
261
|
+
services.push({
|
|
262
|
+
id: "email",
|
|
263
|
+
label: "Email",
|
|
264
|
+
serviceType: "email-relay",
|
|
265
|
+
placement: "email",
|
|
266
|
+
secretRefs: ["SMTP_PASSWORD"],
|
|
267
|
+
environments: {
|
|
268
|
+
local: { hostId: "smtp" },
|
|
269
|
+
staging: { hostId: "smtp" },
|
|
270
|
+
prod: { hostId: "smtp" }
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (config.hosting?.kind === "treeseed_control_plane") {
|
|
275
|
+
const treeDxNodePool = publicTreeDxNodePool(config);
|
|
276
|
+
const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
|
|
277
|
+
const nodeIndex = offset + 1;
|
|
278
|
+
const serviceName = indexedName("public-treedx-node", nodeIndex);
|
|
279
|
+
return {
|
|
280
|
+
id: serviceName,
|
|
281
|
+
label: `Public TreeDX node ${String(nodeIndex).padStart(2, "0")}`,
|
|
282
|
+
serviceType: "treedx-node",
|
|
283
|
+
placement: "knowledge-library",
|
|
284
|
+
projectGroupId: "public-treedx-federation",
|
|
285
|
+
config: {
|
|
286
|
+
image: "treeseed/treedx",
|
|
287
|
+
imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF",
|
|
288
|
+
serviceName,
|
|
289
|
+
volumeName: `${serviceName}-volume`,
|
|
290
|
+
volumeMountPath: "/data",
|
|
291
|
+
runtimeMode: "replicated",
|
|
292
|
+
environmentVariables: {
|
|
293
|
+
PHX_SERVER: "true",
|
|
294
|
+
PORT: "4000",
|
|
295
|
+
TREEDX_DATA_DIR: "/data",
|
|
296
|
+
TREEDX_AUTH_MODE: "connected",
|
|
297
|
+
TREEDX_AUTH_VERIFIER: "hs256_dev",
|
|
298
|
+
TREEDX_ALLOW_DEV_VERIFIER_IN_PROD: "true",
|
|
299
|
+
TREEDX_EXEC_BACKEND: "container_sandbox",
|
|
300
|
+
TREEDX_FEDERATION_MODE: "connected_library",
|
|
301
|
+
TREEDX_JWT_AUDIENCE: "treedx-public-federation",
|
|
302
|
+
TREEDX_JWT_ISSUER: "https://api.treeseed.local/treedx",
|
|
303
|
+
TREESEED_TREEDX_SCOPE: "public_federation"
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
variableRefs: [
|
|
307
|
+
"TREESEED_PUBLIC_TREEDX_IMAGE_REF",
|
|
308
|
+
"PHX_HOST",
|
|
309
|
+
"PHX_SERVER",
|
|
310
|
+
"PORT",
|
|
311
|
+
"TREEDX_DATA_DIR",
|
|
312
|
+
"TREEDX_AUTH_MODE",
|
|
313
|
+
"TREEDX_AUTH_VERIFIER",
|
|
314
|
+
"TREEDX_ALLOW_DEV_VERIFIER_IN_PROD",
|
|
315
|
+
"TREEDX_EXEC_BACKEND",
|
|
316
|
+
"TREEDX_FEDERATION_MODE",
|
|
317
|
+
"TREEDX_JWT_AUDIENCE",
|
|
318
|
+
"TREEDX_JWT_ISSUER",
|
|
319
|
+
"TREESEED_TREEDX_SCOPE"
|
|
320
|
+
],
|
|
321
|
+
secretRefs: ["SECRET_KEY_BASE", "TREESEED_TREEDX_ADMIN_TOKEN", "TREEDX_JWT_HS256_SECRET"],
|
|
322
|
+
environments: {
|
|
323
|
+
local: { hostId: "local-docker", projectGroupId: "public-treedx-federation" },
|
|
324
|
+
staging: { hostId: "railway", projectGroupId: "public-treedx-federation" },
|
|
325
|
+
prod: { hostId: "railway", projectGroupId: "public-treedx-federation" }
|
|
326
|
+
},
|
|
327
|
+
metadata: {
|
|
328
|
+
publicFederation: true,
|
|
329
|
+
defaultNode: nodeIndex === 1,
|
|
330
|
+
nodeIndex,
|
|
331
|
+
maxNodes: treeDxNodePool.maxNodes,
|
|
332
|
+
retainVolumeOnScaleDown: true
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
services.push(
|
|
337
|
+
{
|
|
338
|
+
id: "public-treedx-federation",
|
|
339
|
+
label: "Public TreeDX federation",
|
|
340
|
+
serviceType: "treedx-federation",
|
|
341
|
+
placement: "knowledge-library",
|
|
342
|
+
projectGroupId: "public-treedx-federation",
|
|
343
|
+
dependencies: treeDxNodeUnits.map((unit) => unit.id),
|
|
344
|
+
config: {
|
|
345
|
+
projectName: marketProjectGroup(environment, config).environments.staging?.projectName ?? config.slug ?? "treeseed-api",
|
|
346
|
+
isolation: "same API Railway project, separate service, volume, and domain",
|
|
347
|
+
federationMode: "connected_library",
|
|
348
|
+
nodePool: treeDxNodePool
|
|
349
|
+
},
|
|
350
|
+
environments: {
|
|
351
|
+
local: { hostId: "local-docker", projectGroupId: "public-treedx-federation" },
|
|
352
|
+
staging: { hostId: "railway", projectGroupId: "public-treedx-federation" },
|
|
353
|
+
prod: { hostId: "railway", projectGroupId: "public-treedx-federation" }
|
|
354
|
+
},
|
|
355
|
+
metadata: { publicFederation: true, nodeCount: treeDxNodePool.bootstrapCount, maxNodes: treeDxNodePool.maxNodes }
|
|
356
|
+
},
|
|
357
|
+
...treeDxNodeUnits
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
id: `${config.slug}-compiled`,
|
|
362
|
+
label: `${config.name} hosting profile`,
|
|
363
|
+
services,
|
|
364
|
+
projectGroups,
|
|
365
|
+
metadata: {
|
|
366
|
+
source: "treeseed.site.yaml",
|
|
367
|
+
environment
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function assertCapabilityBinding(unit) {
|
|
372
|
+
const hostCapabilities = new Set(unit.host.capabilities.filter((capability) => capability.environments.includes(unit.environment)).map((capability) => capability.id));
|
|
373
|
+
const missing = unit.requiredCapabilities.filter((capability) => !hostCapabilities.has(capability));
|
|
374
|
+
if (missing.length > 0) {
|
|
375
|
+
throw new Error(`Hosting unit "${unit.id}" cannot bind ${unit.serviceType.id} to host "${unit.host.id}" in ${unit.environment}; missing capabilities: ${missing.join(", ")}.`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function orderUnits(units) {
|
|
379
|
+
const remaining = new Map(units.map((unit) => [unit.id, unit]));
|
|
380
|
+
const ordered = [];
|
|
381
|
+
while (remaining.size > 0) {
|
|
382
|
+
const ready = [...remaining.values()].filter((unit) => unit.dependencies.every((dependency) => !remaining.has(dependency) || ordered.some((orderedUnit) => orderedUnit.id === dependency)));
|
|
383
|
+
if (ready.length === 0) {
|
|
384
|
+
throw new Error(`Hosting graph contains a dependency cycle: ${[...remaining.keys()].join(", ")}.`);
|
|
385
|
+
}
|
|
386
|
+
for (const unit of ready) {
|
|
387
|
+
remaining.delete(unit.id);
|
|
388
|
+
ordered.push(unit);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return ordered;
|
|
392
|
+
}
|
|
393
|
+
function createUnit(service, environment, hosts, serviceTypes, projectGroups, application) {
|
|
394
|
+
const serviceType = serviceTypes[service.serviceType];
|
|
395
|
+
if (!serviceType) {
|
|
396
|
+
throw new Error(`Unknown hosting service type "${service.serviceType}" for service "${service.id}".`);
|
|
397
|
+
}
|
|
398
|
+
const binding = service.environments?.[environment];
|
|
399
|
+
if (binding?.enabled === false) return null;
|
|
400
|
+
const hostId = binding?.hostId ?? serviceType.defaultHostByEnvironment?.[environment];
|
|
401
|
+
if (!hostId) {
|
|
402
|
+
throw new Error(`Hosting service "${service.id}" does not define a host binding for ${environment}.`);
|
|
403
|
+
}
|
|
404
|
+
const host = hosts[hostId];
|
|
405
|
+
if (!host) {
|
|
406
|
+
throw new Error(`Unknown hosting host "${hostId}" for service "${service.id}".`);
|
|
407
|
+
}
|
|
408
|
+
const projectGroupId = binding?.projectGroupId ?? service.projectGroupId;
|
|
409
|
+
const projectGroup = projectGroupId ? projectGroups[projectGroupId] ?? null : null;
|
|
410
|
+
const unitId = application && application.relativeRoot !== "." && service.id === "web" ? application.id : service.id;
|
|
411
|
+
const unit = {
|
|
412
|
+
id: unitId,
|
|
413
|
+
label: service.label,
|
|
414
|
+
serviceType,
|
|
415
|
+
placement: service.placement ?? serviceType.placement,
|
|
416
|
+
host,
|
|
417
|
+
environment,
|
|
418
|
+
projectGroup,
|
|
419
|
+
dependencies: service.dependencies ?? [],
|
|
420
|
+
requiredCapabilities: serviceType.requiredCapabilities,
|
|
421
|
+
config: redactSensitiveConfig({
|
|
422
|
+
...service.config ?? {},
|
|
423
|
+
...binding?.config ?? {}
|
|
424
|
+
}),
|
|
425
|
+
secretRefs: service.secretRefs ?? [],
|
|
426
|
+
variableRefs: service.variableRefs ?? [],
|
|
427
|
+
metadata: redactSensitiveConfig(service.metadata ?? {}),
|
|
428
|
+
application
|
|
429
|
+
};
|
|
430
|
+
assertCapabilityBinding(unit);
|
|
431
|
+
return unit;
|
|
432
|
+
}
|
|
433
|
+
function summarizePlacements(units) {
|
|
434
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const unit of units) {
|
|
436
|
+
grouped.set(unit.placement, [...grouped.get(unit.placement) ?? [], unit]);
|
|
437
|
+
}
|
|
438
|
+
return [...grouped.entries()].map(([placement, placementUnits]) => ({
|
|
439
|
+
placement,
|
|
440
|
+
label: PLACEMENT_LABELS[placement] ?? placement,
|
|
441
|
+
serviceIds: placementUnits.map((unit) => unit.id),
|
|
442
|
+
hostIds: [...new Set(placementUnits.map((unit) => unit.host.id))],
|
|
443
|
+
status: summarizePlacementStatus(placementUnits.map(() => "pending")),
|
|
444
|
+
advanced: false
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
function normalizeFilterValues(values) {
|
|
448
|
+
return new Set((values ?? []).map((value) => value.trim()).filter(Boolean));
|
|
449
|
+
}
|
|
450
|
+
function filterHostingUnits(units, filter) {
|
|
451
|
+
const serviceIds = normalizeFilterValues(filter?.serviceIds);
|
|
452
|
+
const placements = normalizeFilterValues(filter?.placements);
|
|
453
|
+
const hosts = normalizeFilterValues(filter?.hosts);
|
|
454
|
+
if (serviceIds.size === 0 && placements.size === 0 && hosts.size === 0) return units;
|
|
455
|
+
const allServiceIds = new Set(units.map((unit) => unit.id));
|
|
456
|
+
const missingServices = [...serviceIds].filter((serviceId) => !allServiceIds.has(serviceId));
|
|
457
|
+
if (missingServices.length > 0) {
|
|
458
|
+
throw new Error(`Unknown hosting service id${missingServices.length === 1 ? "" : "s"}: ${missingServices.join(", ")}.`);
|
|
459
|
+
}
|
|
460
|
+
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)));
|
|
461
|
+
}
|
|
462
|
+
function compileSingleTreeseedHostingGraph(input, application) {
|
|
463
|
+
const environment = normalizeEnvironment(input.environment);
|
|
464
|
+
const deployConfig = input.deployConfig ?? loadTreeseedDeployConfig(resolve(input.tenantRoot, "treeseed.site.yaml"));
|
|
465
|
+
const pluginContributions = collectPluginHostingContributions({ ...input, deployConfig, environment });
|
|
466
|
+
const hosts = mergeRecord(createDefaultHostAdapters(), pluginContributions.hostAdapters, input.hostAdapters);
|
|
467
|
+
const serviceTypes = mergeRecord(createDefaultServiceTypeAdapters(), pluginContributions.serviceTypeAdapters, input.serviceTypeAdapters);
|
|
468
|
+
const compiledProfile = buildProfileFromDeployConfig({ ...input, deployConfig, environment });
|
|
469
|
+
const profiles = [
|
|
470
|
+
...createDefaultHostingProfiles(),
|
|
471
|
+
...pluginContributions.profiles,
|
|
472
|
+
compiledProfile,
|
|
473
|
+
...input.profiles ?? []
|
|
474
|
+
];
|
|
475
|
+
const projectGroups = Object.fromEntries(
|
|
476
|
+
profiles.flatMap((profile) => profile.projectGroups ?? []).map((group) => [group.id, group])
|
|
477
|
+
);
|
|
478
|
+
const services = profiles.flatMap((profile) => profile.services);
|
|
479
|
+
const applicationInfo = application ? {
|
|
480
|
+
id: application.id,
|
|
481
|
+
root: application.root,
|
|
482
|
+
relativeRoot: application.relativeRoot,
|
|
483
|
+
configPath: application.configPath,
|
|
484
|
+
roles: application.roles
|
|
485
|
+
} : void 0;
|
|
486
|
+
const units = filterHostingUnits(orderUnits(services.map((service) => createUnit(service, environment, hosts, serviceTypes, projectGroups, applicationInfo)).filter((unit) => Boolean(unit))), input.filter);
|
|
487
|
+
return {
|
|
488
|
+
tenantRoot: input.tenantRoot,
|
|
489
|
+
environment,
|
|
490
|
+
deployConfig,
|
|
491
|
+
applications: application ? [application] : void 0,
|
|
492
|
+
hosts,
|
|
493
|
+
serviceTypes,
|
|
494
|
+
profiles,
|
|
495
|
+
projectGroups,
|
|
496
|
+
units,
|
|
497
|
+
placements: summarizePlacements(units),
|
|
498
|
+
warnings: []
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function mergeTreeseedHostingGraphs(input, applications) {
|
|
502
|
+
const environment = normalizeEnvironment(input.environment);
|
|
503
|
+
const graphs = applications.map((application) => compileSingleTreeseedHostingGraph({
|
|
504
|
+
...input,
|
|
505
|
+
tenantRoot: application.root,
|
|
506
|
+
deployConfig: application.config,
|
|
507
|
+
filter: void 0
|
|
508
|
+
}, application));
|
|
509
|
+
const rootGraph = graphs.find((graph) => graph.tenantRoot === resolve(input.tenantRoot)) ?? graphs[0];
|
|
510
|
+
const hosts = mergeRecord(...graphs.map((graph) => graph.hosts), input.hostAdapters);
|
|
511
|
+
const serviceTypes = mergeRecord(...graphs.map((graph) => graph.serviceTypes), input.serviceTypeAdapters);
|
|
512
|
+
const projectGroups = mergeRecord(...graphs.map((graph) => graph.projectGroups));
|
|
513
|
+
const profiles = graphs.flatMap((graph) => graph.profiles);
|
|
514
|
+
const units = filterHostingUnits(orderUnits(graphs.flatMap((graph) => graph.units)), input.filter);
|
|
515
|
+
return {
|
|
516
|
+
tenantRoot: resolve(input.tenantRoot),
|
|
517
|
+
environment,
|
|
518
|
+
deployConfig: rootGraph.deployConfig,
|
|
519
|
+
applications,
|
|
520
|
+
hosts,
|
|
521
|
+
serviceTypes,
|
|
522
|
+
profiles,
|
|
523
|
+
projectGroups,
|
|
524
|
+
units,
|
|
525
|
+
placements: summarizePlacements(units),
|
|
526
|
+
warnings: graphs.flatMap((graph) => graph.warnings)
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function compileTreeseedHostingGraph(input) {
|
|
530
|
+
if (input.deployConfig) {
|
|
531
|
+
return compileSingleTreeseedHostingGraph(input);
|
|
532
|
+
}
|
|
533
|
+
const tenantRoot = resolve(input.tenantRoot);
|
|
534
|
+
if (input.appId) {
|
|
535
|
+
const application = findTreeseedApplication(tenantRoot, input.appId);
|
|
536
|
+
if (!application) {
|
|
537
|
+
throw new Error(`Unknown Treeseed application "${input.appId}".`);
|
|
538
|
+
}
|
|
539
|
+
return compileSingleTreeseedHostingGraph({
|
|
540
|
+
...input,
|
|
541
|
+
tenantRoot: application.root,
|
|
542
|
+
deployConfig: application.config
|
|
543
|
+
}, application);
|
|
544
|
+
}
|
|
545
|
+
const applications = discoverTreeseedApplications(tenantRoot);
|
|
546
|
+
if (applications.length > 1) {
|
|
547
|
+
return mergeTreeseedHostingGraphs(input, applications);
|
|
548
|
+
}
|
|
549
|
+
return compileSingleTreeseedHostingGraph(input, applications[0]);
|
|
550
|
+
}
|
|
551
|
+
async function planTreeseedHostingGraph(input) {
|
|
552
|
+
const graph = compileTreeseedHostingGraph(input);
|
|
553
|
+
const units = [];
|
|
554
|
+
for (const unit of graph.units) {
|
|
555
|
+
const observed = await unit.host.refresh({ environment: graph.environment, unit, graph, dryRun: input.dryRun !== false });
|
|
556
|
+
const plan = await unit.host.diff({ environment: graph.environment, unit, graph, observed, dryRun: input.dryRun !== false });
|
|
557
|
+
const verification = await unit.host.verify({ environment: graph.environment, unit, graph, observed, dryRun: input.dryRun !== false });
|
|
558
|
+
units.push({ unit, observed, plan, verification });
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
environment: graph.environment,
|
|
562
|
+
dryRun: input.dryRun !== false,
|
|
563
|
+
units,
|
|
564
|
+
placements: graph.placements,
|
|
565
|
+
warnings: graph.warnings
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function railwayReconcileSystemsForUnits(units) {
|
|
569
|
+
const systems = /* @__PURE__ */ new Set();
|
|
570
|
+
for (const unit of units) {
|
|
571
|
+
if (unit.host.id !== "railway") continue;
|
|
572
|
+
if (unit.placement === "api" || unit.id === "api" || unit.serviceType.id === "container-api") {
|
|
573
|
+
systems.add("api");
|
|
574
|
+
}
|
|
575
|
+
if (unit.placement === "runner-capacity" || unit.id === "operationsRunner" || unit.serviceType.id === "runner-pool") {
|
|
576
|
+
systems.add("agents");
|
|
577
|
+
}
|
|
578
|
+
if (unit.placement === "database" && units.some((candidate) => candidate.id === "api" || candidate.placement === "api")) {
|
|
579
|
+
systems.add("api");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return [...systems];
|
|
583
|
+
}
|
|
584
|
+
function railwayDeployServiceKeysForUnits(units) {
|
|
585
|
+
const keys = /* @__PURE__ */ new Set();
|
|
586
|
+
for (const unit of units) {
|
|
587
|
+
if (unit.host.id !== "railway") continue;
|
|
588
|
+
if (unit.id === "api" || unit.placement === "api" || unit.serviceType.id === "container-api") {
|
|
589
|
+
keys.add("api");
|
|
590
|
+
}
|
|
591
|
+
if (unit.id === "operationsRunner" || unit.placement === "runner-capacity" || unit.serviceType.id === "runner-pool") {
|
|
592
|
+
keys.add("operationsRunner");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return keys;
|
|
596
|
+
}
|
|
597
|
+
function railwayEnvForHostingApply(input, graph) {
|
|
598
|
+
const seedValues = collectTreeseedConfigSeedValues(input.tenantRoot, graph.environment);
|
|
599
|
+
return {
|
|
600
|
+
...process.env,
|
|
601
|
+
...seedValues
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
async function deploySelectedRailwayServices(input, graph) {
|
|
605
|
+
const selectedKeys = railwayDeployServiceKeysForUnits(graph.units);
|
|
606
|
+
if (selectedKeys.size === 0 || graph.environment === "local") {
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
const env = railwayEnvForHostingApply(input, graph);
|
|
610
|
+
const services = configuredRailwayServices(graph.tenantRoot, graph.environment).filter((service) => selectedKeys.has(service.key));
|
|
611
|
+
const results = [];
|
|
612
|
+
for (const service of services) {
|
|
613
|
+
results.push(await deployRailwayService(graph.tenantRoot, service, {
|
|
614
|
+
env,
|
|
615
|
+
prefix: {
|
|
616
|
+
scope: graph.environment,
|
|
617
|
+
system: service.key === "api" ? "api" : "agents",
|
|
618
|
+
task: `${service.key}-railway-deploy`,
|
|
619
|
+
stage: "deploy"
|
|
620
|
+
}
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
return results;
|
|
624
|
+
}
|
|
625
|
+
function valueFromUnitConfig(unit, key) {
|
|
626
|
+
const config = unit.config && typeof unit.config === "object" ? unit.config : {};
|
|
627
|
+
const value = config[key];
|
|
628
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
629
|
+
}
|
|
630
|
+
function traceHostingRailway(env, stage, message) {
|
|
631
|
+
if (env.TREESEED_RECONCILE_TRACE === "1" || process.env.TREESEED_RECONCILE_TRACE === "1") {
|
|
632
|
+
console.error(`[trsd][hosting][railway][${stage}] ${message}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async function treeDxStage(env, stage, task) {
|
|
636
|
+
traceHostingRailway(env, `treedx:${stage}:start`, stage);
|
|
637
|
+
try {
|
|
638
|
+
const result = await task();
|
|
639
|
+
traceHostingRailway(env, `treedx:${stage}:done`, stage);
|
|
640
|
+
return result;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
643
|
+
throw new Error(`Public TreeDX Railway reconcile failed during ${stage}: ${message}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function reconcilePublicTreeDxUnits(input, graph) {
|
|
647
|
+
if (graph.environment === "local") {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
const nodeUnits = graph.units.filter((unit) => unit.id.startsWith("public-treedx-node-") && unit.host.id === "railway");
|
|
651
|
+
if (nodeUnits.length === 0) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
const env = railwayEnvForHostingApply(input, graph);
|
|
655
|
+
const workspace = await resolveRailwayWorkspaceContext({ env });
|
|
656
|
+
const results = [];
|
|
657
|
+
for (const unit of nodeUnits) {
|
|
658
|
+
const projectName2 = unit.projectGroup?.environments?.[graph.environment]?.projectName || String(env.TREESEED_PUBLIC_TREEDX_RAILWAY_PROJECT_NAME ?? "").trim() || "treeseed-api";
|
|
659
|
+
const environmentName2 = normalizeRailwayEnvironmentName(unit.projectGroup?.environments?.[graph.environment]?.environmentName) || ENVIRONMENT_NAMES[graph.environment];
|
|
660
|
+
const configuredImage = valueFromUnitConfig(unit, "image") ?? "treeseed/treedx";
|
|
661
|
+
const imageRef = String(env.TREESEED_PUBLIC_TREEDX_IMAGE_REF ?? "").trim() || (configuredImage.includes(":") ? configuredImage : `${configuredImage}:latest`);
|
|
662
|
+
const serviceName = valueFromUnitConfig(unit, "serviceName") ?? unit.id;
|
|
663
|
+
const volumeName = valueFromUnitConfig(unit, "volumeName") ?? `${serviceName}-volume`;
|
|
664
|
+
const mountPath = valueFromUnitConfig(unit, "volumeMountPath") ?? "/data";
|
|
665
|
+
const deploymentRegion = String(env.TREESEED_PUBLIC_TREEDX_RAILWAY_REGION ?? env.TREESEED_RAILWAY_STATEFUL_REGION ?? "us-west2").trim();
|
|
666
|
+
const ensuredProject2 = await treeDxStage(env, "project", () => ensureRailwayProject({
|
|
667
|
+
projectName: projectName2,
|
|
668
|
+
defaultEnvironmentName: environmentName2,
|
|
669
|
+
workspace: workspace.name,
|
|
670
|
+
env
|
|
671
|
+
}));
|
|
672
|
+
const ensuredEnvironment = await treeDxStage(env, "environment", () => ensureRailwayEnvironment({
|
|
673
|
+
projectId: ensuredProject2.project.id,
|
|
674
|
+
environmentName: environmentName2,
|
|
675
|
+
env
|
|
676
|
+
}));
|
|
677
|
+
const ensuredService = await treeDxStage(env, "service", async () => {
|
|
678
|
+
const services2 = await listRailwayServices({ projectId: ensuredProject2.project.id, env });
|
|
679
|
+
const existing = services2.find((service) => service.name === serviceName) ?? null;
|
|
680
|
+
const legacy = unit.metadata.nodeIndex === 1 ? services2.find((service) => service.name === "public-treedx-node") ?? null : null;
|
|
681
|
+
if (!existing && legacy) {
|
|
682
|
+
const renamed = await updateRailwayServiceName({ serviceId: legacy.id, name: serviceName, env });
|
|
683
|
+
return { service: renamed, created: false, adopted: true };
|
|
684
|
+
}
|
|
685
|
+
return ensureRailwayService({
|
|
686
|
+
projectId: ensuredProject2.project.id,
|
|
687
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
688
|
+
serviceName,
|
|
689
|
+
imageRef,
|
|
690
|
+
env
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
const instance = await treeDxStage(env, "instance", () => ensureRailwayServiceInstanceConfiguration({
|
|
694
|
+
serviceId: ensuredService.service.id,
|
|
695
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
696
|
+
healthcheckPath: "/api/v1/health",
|
|
697
|
+
healthcheckTimeoutSeconds: 120,
|
|
698
|
+
runtimeMode: "replicated",
|
|
699
|
+
deploymentRegion,
|
|
700
|
+
env
|
|
701
|
+
}));
|
|
702
|
+
const currentVariables = await treeDxStage(env, "variables:observe", () => listRailwayVariables({
|
|
703
|
+
projectId: ensuredProject2.project.id,
|
|
704
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
705
|
+
serviceId: ensuredService.service.id,
|
|
706
|
+
env
|
|
707
|
+
}).catch(() => ({})));
|
|
708
|
+
await treeDxStage(env, "variables", () => upsertRailwayVariables({
|
|
709
|
+
projectId: ensuredProject2.project.id,
|
|
710
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
711
|
+
serviceId: ensuredService.service.id,
|
|
712
|
+
variables: {
|
|
713
|
+
TREESEED_PUBLIC_TREEDX_IMAGE_REF: imageRef,
|
|
714
|
+
PHX_HOST: `${serviceName}.railway.app`,
|
|
715
|
+
PHX_SERVER: "true",
|
|
716
|
+
PORT: "4000",
|
|
717
|
+
TREEDX_DATA_DIR: mountPath,
|
|
718
|
+
TREEDX_AUTH_MODE: "connected",
|
|
719
|
+
TREEDX_AUTH_VERIFIER: "hs256_dev",
|
|
720
|
+
TREEDX_ALLOW_DEV_VERIFIER_IN_PROD: "true",
|
|
721
|
+
TREEDX_EXEC_BACKEND: "container_sandbox",
|
|
722
|
+
TREEDX_FEDERATION_MODE: "connected_library",
|
|
723
|
+
TREEDX_JWT_AUDIENCE: "treedx-public-federation",
|
|
724
|
+
TREEDX_JWT_ISSUER: `https://${serviceName}.railway.app/treedx`,
|
|
725
|
+
TREESEED_TREEDX_SCOPE: "public_federation",
|
|
726
|
+
...typeof currentVariables.SECRET_KEY_BASE === "string" && currentVariables.SECRET_KEY_BASE.trim() ? {} : { SECRET_KEY_BASE: treeDxSecretBase() },
|
|
727
|
+
...typeof currentVariables.TREEDX_JWT_HS256_SECRET === "string" && currentVariables.TREEDX_JWT_HS256_SECRET.trim() ? {} : { TREEDX_JWT_HS256_SECRET: treeDxSecretBase() },
|
|
728
|
+
...typeof env.TREESEED_TREEDX_ADMIN_TOKEN === "string" && env.TREESEED_TREEDX_ADMIN_TOKEN.trim() ? { TREESEED_TREEDX_ADMIN_TOKEN: env.TREESEED_TREEDX_ADMIN_TOKEN } : {}
|
|
729
|
+
},
|
|
730
|
+
env
|
|
731
|
+
}));
|
|
732
|
+
const volume = await treeDxStage(env, "volume", () => ensureRailwayServiceVolume({
|
|
733
|
+
projectId: ensuredProject2.project.id,
|
|
734
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
735
|
+
serviceId: ensuredService.service.id,
|
|
736
|
+
name: volumeName,
|
|
737
|
+
mountPath,
|
|
738
|
+
env
|
|
739
|
+
}));
|
|
740
|
+
const domain = await treeDxStage(env, "domain", () => ensureRailwayGeneratedServiceDomain({
|
|
741
|
+
projectId: ensuredProject2.project.id,
|
|
742
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
743
|
+
serviceId: ensuredService.service.id,
|
|
744
|
+
env
|
|
745
|
+
}));
|
|
746
|
+
await treeDxStage(env, "variables:domain", () => upsertRailwayVariables({
|
|
747
|
+
projectId: ensuredProject2.project.id,
|
|
748
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
749
|
+
serviceId: ensuredService.service.id,
|
|
750
|
+
variables: {
|
|
751
|
+
PHX_HOST: domain.domain.domain,
|
|
752
|
+
TREEDX_JWT_ISSUER: `https://${domain.domain.domain}/treedx`
|
|
753
|
+
},
|
|
754
|
+
env
|
|
755
|
+
}));
|
|
756
|
+
const deployment = await treeDxStage(env, "deploy", () => deployRailwayServiceInstance({
|
|
757
|
+
serviceId: ensuredService.service.id,
|
|
758
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
759
|
+
env
|
|
760
|
+
}));
|
|
761
|
+
results.push({
|
|
762
|
+
status: "ready",
|
|
763
|
+
locators: {
|
|
764
|
+
hostId: "railway",
|
|
765
|
+
projectGroupId: unit.projectGroup?.id ?? null,
|
|
766
|
+
projectName: projectName2,
|
|
767
|
+
serviceName,
|
|
768
|
+
domain: domain.domain.domain
|
|
769
|
+
},
|
|
770
|
+
state: {
|
|
771
|
+
unitId: unit.id,
|
|
772
|
+
serviceType: unit.serviceType.id,
|
|
773
|
+
placement: unit.placement,
|
|
774
|
+
projectId: ensuredProject2.project.id,
|
|
775
|
+
environmentId: ensuredEnvironment.environment.id,
|
|
776
|
+
serviceId: ensuredService.service.id,
|
|
777
|
+
imageRef,
|
|
778
|
+
volumeMountPath: volume.instance?.mountPath ?? mountPath,
|
|
779
|
+
healthcheckPath: instance.instance.healthcheckPath,
|
|
780
|
+
deploymentId: deployment.deploymentId
|
|
781
|
+
},
|
|
782
|
+
warnings: []
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const firstUnit = nodeUnits[0];
|
|
786
|
+
const desiredIndexes = new Set(nodeUnits.map((unit) => Number(unit.metadata.nodeIndex ?? 1)).filter((index) => Number.isFinite(index) && index > 0));
|
|
787
|
+
const projectName = firstUnit.projectGroup?.environments?.[graph.environment]?.projectName || String(env.TREESEED_PUBLIC_TREEDX_RAILWAY_PROJECT_NAME ?? "").trim() || "treeseed-api";
|
|
788
|
+
const environmentName = normalizeRailwayEnvironmentName(firstUnit.projectGroup?.environments?.[graph.environment]?.environmentName) || ENVIRONMENT_NAMES[graph.environment];
|
|
789
|
+
const ensuredProject = await treeDxStage(env, "scale-down-project", () => ensureRailwayProject({
|
|
790
|
+
projectName,
|
|
791
|
+
defaultEnvironmentName: environmentName,
|
|
792
|
+
workspace: workspace.name,
|
|
793
|
+
env
|
|
794
|
+
}));
|
|
795
|
+
await treeDxStage(env, "scale-down-environment", () => ensureRailwayEnvironment({
|
|
796
|
+
projectId: ensuredProject.project.id,
|
|
797
|
+
environmentName,
|
|
798
|
+
env
|
|
799
|
+
}));
|
|
800
|
+
const services = await treeDxStage(env, "scale-down-services", () => listRailwayServices({ projectId: ensuredProject.project.id, env }));
|
|
801
|
+
for (const service of services) {
|
|
802
|
+
const match = /^public-treedx-node-(\d{2,})$/u.exec(service.name);
|
|
803
|
+
const index = match ? Number.parseInt(match[1], 10) : null;
|
|
804
|
+
const staleSingleton = service.name === "public-treedx-node" && desiredIndexes.has(1);
|
|
805
|
+
if (index !== null && !desiredIndexes.has(index) || staleSingleton) {
|
|
806
|
+
await treeDxStage(env, `scale-down-service:${service.name}`, () => deleteRailwayService({ serviceId: service.id, env }));
|
|
807
|
+
results.push({
|
|
808
|
+
status: "ready",
|
|
809
|
+
locators: {
|
|
810
|
+
hostId: "railway",
|
|
811
|
+
projectGroupId: firstUnit.projectGroup?.id ?? null,
|
|
812
|
+
projectName,
|
|
813
|
+
serviceName: service.name
|
|
814
|
+
},
|
|
815
|
+
state: {
|
|
816
|
+
unitId: service.name,
|
|
817
|
+
action: "delete",
|
|
818
|
+
retainedResources: [{
|
|
819
|
+
kind: "volume",
|
|
820
|
+
name: `${service.name}-volume`,
|
|
821
|
+
reason: "Stateful TreeDX volumes are retained across scale-down for later reclaim."
|
|
822
|
+
}]
|
|
823
|
+
},
|
|
824
|
+
warnings: [`Destroyed scaled-down TreeDX service ${service.name}; retained ${service.name}-volume.`]
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return results;
|
|
829
|
+
}
|
|
830
|
+
async function applyTreeseedHostingGraph(input) {
|
|
831
|
+
const plan = await planTreeseedHostingGraph(input);
|
|
832
|
+
const graph = compileTreeseedHostingGraph(input);
|
|
833
|
+
const selectedSystems = railwayReconcileSystemsForUnits(graph.units);
|
|
834
|
+
const usesDefaultRailwayAdapter = !input.hostAdapters?.railway && !(Array.isArray(input.profiles) && input.profiles.length > 0);
|
|
835
|
+
if (!plan.dryRun && selectedSystems.length > 0 && usesDefaultRailwayAdapter) {
|
|
836
|
+
await reconcileTreeseedTarget({
|
|
837
|
+
tenantRoot: graph.tenantRoot,
|
|
838
|
+
target: createPersistentDeployTarget(graph.environment),
|
|
839
|
+
systems: selectedSystems,
|
|
840
|
+
env: railwayEnvForHostingApply(input, graph)
|
|
841
|
+
});
|
|
842
|
+
await reconcilePublicTreeDxUnits(input, graph);
|
|
843
|
+
await deploySelectedRailwayServices(input, graph);
|
|
844
|
+
}
|
|
845
|
+
const results = [];
|
|
846
|
+
for (const entry of plan.units) {
|
|
847
|
+
const unit = graph.units.find((candidate) => candidate.id === entry.unit.id) ?? entry.unit;
|
|
848
|
+
const result = await unit.host.apply({ environment: graph.environment, unit, graph, plan: entry.plan, dryRun: plan.dryRun });
|
|
849
|
+
const verification = await unit.host.verify({ environment: graph.environment, unit, graph, observed: result, dryRun: plan.dryRun });
|
|
850
|
+
results.push({ unit, plan: entry.plan, result, verification });
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
environment: plan.environment,
|
|
854
|
+
dryRun: plan.dryRun,
|
|
855
|
+
selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
|
|
856
|
+
selectedSystems,
|
|
857
|
+
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." })),
|
|
858
|
+
transport: selectedSystems.length > 0 ? {
|
|
859
|
+
railway: {
|
|
860
|
+
reconcile: "api",
|
|
861
|
+
deploy: process.env.TREESEED_RAILWAY_DEPLOY_TRANSPORT === "cli-fallback" ? "cli-fallback" : "api"
|
|
862
|
+
}
|
|
863
|
+
} : void 0,
|
|
864
|
+
results,
|
|
865
|
+
placements: plan.placements,
|
|
866
|
+
warnings: plan.warnings
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function serializeHostingUnit(unit) {
|
|
870
|
+
return sanitizedUnitConfig(unit);
|
|
871
|
+
}
|
|
872
|
+
function canonicalActionKind(value) {
|
|
873
|
+
const allowed = /* @__PURE__ */ new Set(["noop", "create", "update", "replace", "delete", "adopt", "rename", "reattach", "retain", "taint", "blocked"]);
|
|
874
|
+
return typeof value === "string" && allowed.has(value) ? value : "noop";
|
|
875
|
+
}
|
|
876
|
+
function canonicalHostingNode(unit, value) {
|
|
877
|
+
return {
|
|
878
|
+
id: unit.id,
|
|
879
|
+
provider: unit.host.id,
|
|
880
|
+
type: unit.serviceType.id,
|
|
881
|
+
owner: unit.application?.id ?? null,
|
|
882
|
+
environment: unit.environment,
|
|
883
|
+
spec: serializeHostingUnit(unit),
|
|
884
|
+
state: value,
|
|
885
|
+
locators: {
|
|
886
|
+
hostId: unit.host.id,
|
|
887
|
+
projectGroupId: unit.projectGroup?.id ?? null,
|
|
888
|
+
serviceTypeId: unit.serviceType.id
|
|
889
|
+
},
|
|
890
|
+
metadata: {
|
|
891
|
+
placement: unit.placement,
|
|
892
|
+
logicalName: unit.logicalName
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function canonicalHostingDrift(unit, entries, fallbackReason) {
|
|
897
|
+
const rawEntries = Array.isArray(entries) ? entries : [];
|
|
898
|
+
if (rawEntries.length === 0) return [];
|
|
899
|
+
return rawEntries.map((entry, index) => ({
|
|
900
|
+
id: `${unit.id}:drift:${index + 1}`,
|
|
901
|
+
resourceId: unit.id,
|
|
902
|
+
severity: "blocking",
|
|
903
|
+
reason: typeof entry === "string" ? entry : fallbackReason,
|
|
904
|
+
provider: unit.host.id,
|
|
905
|
+
type: unit.serviceType.id,
|
|
906
|
+
observed: entry
|
|
907
|
+
}));
|
|
908
|
+
}
|
|
909
|
+
function canonicalHostingPostcondition(unit, verification) {
|
|
910
|
+
const issues = [
|
|
911
|
+
...Array.isArray(verification.issues) ? verification.issues.map(String) : [],
|
|
912
|
+
...Array.isArray(verification.checks) ? verification.checks.flatMap((check) => {
|
|
913
|
+
if (!check || typeof check !== "object") return [];
|
|
914
|
+
const maybeIssues = check.issues;
|
|
915
|
+
return Array.isArray(maybeIssues) ? maybeIssues.map(String) : [];
|
|
916
|
+
}) : []
|
|
917
|
+
];
|
|
918
|
+
return {
|
|
919
|
+
id: `${unit.id}:verified`,
|
|
920
|
+
resourceId: unit.id,
|
|
921
|
+
description: `Live postconditions pass for ${unit.logicalName}.`,
|
|
922
|
+
source: "sdk",
|
|
923
|
+
required: true,
|
|
924
|
+
ok: verification.verified === true,
|
|
925
|
+
issues,
|
|
926
|
+
observed: verification
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function hostingPlanReason(plan, prefix) {
|
|
930
|
+
return plan.reasons?.length ? plan.reasons.join("; ") : `${prefix} ${String(plan.action ?? "noop")}.`;
|
|
931
|
+
}
|
|
932
|
+
function canonicalHostingReportFromPlan(plan) {
|
|
933
|
+
const desiredGraph = plan.units.map((entry) => canonicalHostingNode(entry.unit));
|
|
934
|
+
const observedGraph = plan.units.map((entry) => canonicalHostingNode(entry.unit, entry.observed));
|
|
935
|
+
const diff = plan.units.flatMap((entry) => [
|
|
936
|
+
...entry.plan.action && entry.plan.action !== "noop" ? [{
|
|
937
|
+
id: `${entry.unit.id}:diff`,
|
|
938
|
+
resourceId: entry.unit.id,
|
|
939
|
+
severity: canonicalActionKind(entry.plan.action) === "blocked" ? "blocking" : "info",
|
|
940
|
+
reason: hostingPlanReason(entry.plan, "Planned"),
|
|
941
|
+
provider: entry.unit.host.id,
|
|
942
|
+
type: entry.unit.serviceType.id,
|
|
943
|
+
expected: serializeHostingUnit(entry.unit),
|
|
944
|
+
observed: entry.observed
|
|
945
|
+
}] : [],
|
|
946
|
+
...canonicalHostingDrift(entry.unit, entry.plan.blockedDrift, "Blocked provider drift.")
|
|
947
|
+
]);
|
|
948
|
+
const providerLimitations = plan.units.flatMap((entry) => canonicalHostingDrift(entry.unit, entry.plan.providerLimitations, "Provider limitation."));
|
|
949
|
+
const actions = plan.units.map((entry) => ({
|
|
950
|
+
id: `${entry.unit.id}:${entry.plan.action ?? "noop"}`,
|
|
951
|
+
kind: canonicalActionKind(entry.plan.action),
|
|
952
|
+
resourceId: entry.unit.id,
|
|
953
|
+
reason: hostingPlanReason(entry.plan, "Planned"),
|
|
954
|
+
provider: entry.unit.host.id,
|
|
955
|
+
type: entry.unit.serviceType.id,
|
|
956
|
+
before: entry.observed,
|
|
957
|
+
after: serializeHostingUnit(entry.unit)
|
|
958
|
+
}));
|
|
959
|
+
return createTreeseedCanonicalReconcileReport({
|
|
960
|
+
desiredGraph,
|
|
961
|
+
observedGraph,
|
|
962
|
+
stateGraph: [],
|
|
963
|
+
diff,
|
|
964
|
+
actions,
|
|
965
|
+
postconditions: plan.units.map((entry) => canonicalHostingPostcondition(entry.unit, entry.verification)),
|
|
966
|
+
selectedResources: plan.units.map((entry) => entry.unit.id),
|
|
967
|
+
skippedResources: [],
|
|
968
|
+
blockedDrift: diff.filter((entry) => entry.severity === "blocking"),
|
|
969
|
+
providerLimitations,
|
|
970
|
+
retainedResources: plan.units.flatMap((entry) => (entry.plan.retainedResources ?? []).map((resource, index) => ({
|
|
971
|
+
id: `${entry.unit.id}:retained:${index + 1}`,
|
|
972
|
+
provider: entry.unit.host.id,
|
|
973
|
+
type: "retained-resource",
|
|
974
|
+
owner: entry.unit.application?.id ?? null,
|
|
975
|
+
state: resource
|
|
976
|
+
}))),
|
|
977
|
+
liveVerification: {
|
|
978
|
+
ok: plan.units.every((entry) => entry.verification.verified === true),
|
|
979
|
+
source: "hosting-plan",
|
|
980
|
+
issues: plan.units.filter((entry) => entry.verification.verified !== true).map((entry) => `${entry.unit.id}: verification did not pass`)
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function canonicalHostingReportFromApplyResult(result) {
|
|
985
|
+
const desiredGraph = result.results.map((entry) => canonicalHostingNode(entry.unit));
|
|
986
|
+
const observedGraph = result.results.map((entry) => canonicalHostingNode(entry.unit, entry.result));
|
|
987
|
+
const diff = result.results.flatMap((entry) => [
|
|
988
|
+
...entry.plan.action && entry.plan.action !== "noop" ? [{
|
|
989
|
+
id: `${entry.unit.id}:diff`,
|
|
990
|
+
resourceId: entry.unit.id,
|
|
991
|
+
severity: canonicalActionKind(entry.plan.action) === "blocked" ? "blocking" : "info",
|
|
992
|
+
reason: hostingPlanReason(entry.plan, "Applied"),
|
|
993
|
+
provider: entry.unit.host.id,
|
|
994
|
+
type: entry.unit.serviceType.id,
|
|
995
|
+
expected: serializeHostingUnit(entry.unit),
|
|
996
|
+
observed: entry.result
|
|
997
|
+
}] : [],
|
|
998
|
+
...canonicalHostingDrift(entry.unit, entry.plan.blockedDrift, "Blocked provider drift.")
|
|
999
|
+
]);
|
|
1000
|
+
const providerLimitations = result.results.flatMap((entry) => canonicalHostingDrift(entry.unit, entry.plan.providerLimitations, "Provider limitation."));
|
|
1001
|
+
const actions = result.results.map((entry) => ({
|
|
1002
|
+
id: `${entry.unit.id}:${entry.plan.action ?? "noop"}`,
|
|
1003
|
+
kind: canonicalActionKind(entry.plan.action),
|
|
1004
|
+
resourceId: entry.unit.id,
|
|
1005
|
+
reason: hostingPlanReason(entry.plan, "Applied"),
|
|
1006
|
+
provider: entry.unit.host.id,
|
|
1007
|
+
type: entry.unit.serviceType.id,
|
|
1008
|
+
before: entry.result,
|
|
1009
|
+
after: serializeHostingUnit(entry.unit)
|
|
1010
|
+
}));
|
|
1011
|
+
return createTreeseedCanonicalReconcileReport({
|
|
1012
|
+
desiredGraph,
|
|
1013
|
+
observedGraph,
|
|
1014
|
+
stateGraph: [],
|
|
1015
|
+
diff,
|
|
1016
|
+
actions,
|
|
1017
|
+
postconditions: result.results.map((entry) => canonicalHostingPostcondition(entry.unit, entry.verification)),
|
|
1018
|
+
selectedResources: result.results.map((entry) => entry.unit.id),
|
|
1019
|
+
skippedResources: result.skippedSystems.map((entry) => ({ id: entry.system, reason: entry.reason })),
|
|
1020
|
+
blockedDrift: diff.filter((entry) => entry.severity === "blocking"),
|
|
1021
|
+
providerLimitations,
|
|
1022
|
+
retainedResources: result.results.flatMap((entry) => (entry.plan.retainedResources ?? []).map((resource, index) => ({
|
|
1023
|
+
id: `${entry.unit.id}:retained:${index + 1}`,
|
|
1024
|
+
provider: entry.unit.host.id,
|
|
1025
|
+
type: "retained-resource",
|
|
1026
|
+
owner: entry.unit.application?.id ?? null,
|
|
1027
|
+
state: resource
|
|
1028
|
+
}))),
|
|
1029
|
+
liveVerification: {
|
|
1030
|
+
ok: result.results.every((entry) => entry.verification.verified === true),
|
|
1031
|
+
source: "hosting-apply",
|
|
1032
|
+
issues: result.results.filter((entry) => entry.verification.verified !== true).map((entry) => `${entry.unit.id}: verification did not pass after apply`)
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function serializeHostingPlan(plan) {
|
|
1037
|
+
const selectedSystems = railwayReconcileSystemsForUnits(plan.units.map((entry) => entry.unit));
|
|
1038
|
+
const canonical = canonicalHostingReportFromPlan(plan);
|
|
1039
|
+
return {
|
|
1040
|
+
environment: plan.environment,
|
|
1041
|
+
dryRun: plan.dryRun,
|
|
1042
|
+
...canonical,
|
|
1043
|
+
selectedApps: [...new Set(plan.units.map((entry) => entry.unit.application?.id).filter((value) => Boolean(value)))],
|
|
1044
|
+
selectedSystems,
|
|
1045
|
+
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." })),
|
|
1046
|
+
transport: selectedSystems.length > 0 ? {
|
|
1047
|
+
railway: {
|
|
1048
|
+
reconcile: "api",
|
|
1049
|
+
deploy: process.env.TREESEED_RAILWAY_DEPLOY_TRANSPORT === "cli-fallback" ? "cli-fallback" : "api"
|
|
1050
|
+
}
|
|
1051
|
+
} : void 0,
|
|
1052
|
+
placements: plan.placements,
|
|
1053
|
+
units: plan.units.map((entry) => ({
|
|
1054
|
+
unit: serializeHostingUnit(entry.unit),
|
|
1055
|
+
desired: serializeHostingUnit(entry.unit),
|
|
1056
|
+
observed: entry.observed,
|
|
1057
|
+
diff: entry.plan,
|
|
1058
|
+
actions: entry.plan.actions ?? [entry.plan.action],
|
|
1059
|
+
retainedResources: entry.plan.retainedResources ?? [],
|
|
1060
|
+
blockedDrift: entry.plan.blockedDrift ?? [],
|
|
1061
|
+
providerLimitations: entry.plan.providerLimitations ?? [],
|
|
1062
|
+
plan: entry.plan,
|
|
1063
|
+
verification: entry.verification
|
|
1064
|
+
})),
|
|
1065
|
+
warnings: plan.warnings
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function serializeHostingApplyResult(result) {
|
|
1069
|
+
const canonical = canonicalHostingReportFromApplyResult(result);
|
|
1070
|
+
return {
|
|
1071
|
+
environment: result.environment,
|
|
1072
|
+
dryRun: result.dryRun,
|
|
1073
|
+
...canonical,
|
|
1074
|
+
selectedApps: result.selectedApps ?? [],
|
|
1075
|
+
selectedSystems: result.selectedSystems ?? [],
|
|
1076
|
+
skippedSystems: result.skippedSystems ?? [],
|
|
1077
|
+
transport: result.transport,
|
|
1078
|
+
placements: result.placements,
|
|
1079
|
+
results: result.results.map((entry) => ({
|
|
1080
|
+
unit: serializeHostingUnit(entry.unit),
|
|
1081
|
+
desired: serializeHostingUnit(entry.unit),
|
|
1082
|
+
observed: entry.result,
|
|
1083
|
+
diff: entry.plan,
|
|
1084
|
+
actions: entry.plan.actions ?? [entry.plan.action],
|
|
1085
|
+
retainedResources: entry.plan.retainedResources ?? [],
|
|
1086
|
+
blockedDrift: entry.plan.blockedDrift ?? [],
|
|
1087
|
+
providerLimitations: entry.plan.providerLimitations ?? [],
|
|
1088
|
+
plan: entry.plan,
|
|
1089
|
+
result: entry.result,
|
|
1090
|
+
verification: entry.verification
|
|
1091
|
+
})),
|
|
1092
|
+
warnings: result.warnings
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function hostingEnvironmentLabel(environment) {
|
|
1096
|
+
return ENVIRONMENT_NAMES[environment];
|
|
1097
|
+
}
|
|
1098
|
+
export {
|
|
1099
|
+
applyTreeseedHostingGraph,
|
|
1100
|
+
compileTreeseedHostingGraph,
|
|
1101
|
+
hostingEnvironmentLabel,
|
|
1102
|
+
planTreeseedHostingGraph,
|
|
1103
|
+
serializeHostingApplyResult,
|
|
1104
|
+
serializeHostingPlan,
|
|
1105
|
+
serializeHostingUnit
|
|
1106
|
+
};
|