@treeseed/sdk 0.6.1 → 0.6.2
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/operations/services/bootstrap-runner.d.ts +33 -0
- package/dist/operations/services/bootstrap-runner.js +136 -0
- package/dist/operations/services/config-runtime.d.ts +27 -8
- package/dist/operations/services/config-runtime.js +297 -124
- package/dist/operations/services/github-api.d.ts +33 -0
- package/dist/operations/services/github-api.js +118 -4
- package/dist/operations/services/github-automation.d.ts +30 -0
- package/dist/operations/services/github-automation.js +107 -1
- package/dist/operations/services/project-platform.d.ts +38 -2
- package/dist/operations/services/project-platform.js +281 -9
- package/dist/operations/services/railway-deploy.d.ts +6 -2
- package/dist/operations/services/railway-deploy.js +26 -18
- package/dist/operations/services/runtime-tools.d.ts +0 -2
- package/dist/operations/services/runtime-tools.js +0 -2
- package/dist/platform/env.yaml +68 -96
- package/dist/platform/environment.js +51 -0
- package/dist/reconcile/bootstrap-systems.d.ts +32 -0
- package/dist/reconcile/bootstrap-systems.js +175 -0
- package/dist/reconcile/builtin-adapters.js +1 -9
- package/dist/reconcile/desired-state.js +16 -14
- package/dist/reconcile/engine.d.ts +9 -4
- package/dist/reconcile/engine.js +57 -14
- package/dist/reconcile/index.d.ts +1 -0
- package/dist/reconcile/index.js +1 -0
- package/dist/scripts/config-treeseed.js +30 -0
- package/dist/scripts/package-tools.js +0 -2
- package/dist/scripts/tenant-deploy.js +16 -36
- package/dist/scripts/test-cloudflare-local.js +0 -2
- package/dist/workflow/operations.js +23 -4
- package/dist/workflow.d.ts +5 -0
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +15 -15
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const TREESEED_BOOTSTRAP_SYSTEMS = ["github", "data", "web", "api", "agents"];
|
|
2
|
+
const BOOTSTRAP_SYSTEM_SET = new Set(TREESEED_BOOTSTRAP_SYSTEMS);
|
|
3
|
+
const SYSTEM_DEPENDENCIES = {
|
|
4
|
+
github: [],
|
|
5
|
+
data: [],
|
|
6
|
+
web: ["data"],
|
|
7
|
+
api: ["data"],
|
|
8
|
+
agents: ["data"]
|
|
9
|
+
};
|
|
10
|
+
function uniqueSystems(values) {
|
|
11
|
+
return [...new Set(values)];
|
|
12
|
+
}
|
|
13
|
+
function parseTreeseedBootstrapSystems(value) {
|
|
14
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : ["all"];
|
|
15
|
+
const systems = raw.flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean);
|
|
16
|
+
if (systems.length === 0 || systems.includes("all")) {
|
|
17
|
+
return ["all"];
|
|
18
|
+
}
|
|
19
|
+
const invalid = systems.filter((system) => !BOOTSTRAP_SYSTEM_SET.has(system));
|
|
20
|
+
if (invalid.length > 0) {
|
|
21
|
+
throw new Error(`Unknown Treeseed bootstrap system "${invalid[0]}". Expected one of all, ${TREESEED_BOOTSTRAP_SYSTEMS.join(", ")}.`);
|
|
22
|
+
}
|
|
23
|
+
return uniqueSystems(systems);
|
|
24
|
+
}
|
|
25
|
+
function expandBootstrapSystems(systems) {
|
|
26
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
27
|
+
const visit = (system) => {
|
|
28
|
+
for (const dependency of SYSTEM_DEPENDENCIES[system]) {
|
|
29
|
+
visit(dependency);
|
|
30
|
+
}
|
|
31
|
+
expanded.add(system);
|
|
32
|
+
};
|
|
33
|
+
for (const system of systems) {
|
|
34
|
+
visit(system);
|
|
35
|
+
}
|
|
36
|
+
return [...expanded];
|
|
37
|
+
}
|
|
38
|
+
function serviceEnabled(config, serviceKey) {
|
|
39
|
+
const service = config.services?.[serviceKey];
|
|
40
|
+
return Boolean(service && service.enabled !== false && (service.provider ?? "railway") === "railway");
|
|
41
|
+
}
|
|
42
|
+
function apiSystemDisabled(config) {
|
|
43
|
+
if (config.runtime?.mode === "none") {
|
|
44
|
+
return "runtime.mode is none.";
|
|
45
|
+
}
|
|
46
|
+
if (config.surfaces?.api?.enabled === false) {
|
|
47
|
+
return "surfaces.api.enabled is false.";
|
|
48
|
+
}
|
|
49
|
+
if (!serviceEnabled(config, "api")) {
|
|
50
|
+
return "services.api is not enabled for Railway.";
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function agentsSystemDisabled(config) {
|
|
55
|
+
if (config.runtime?.mode === "none") {
|
|
56
|
+
return "runtime.mode is none.";
|
|
57
|
+
}
|
|
58
|
+
const enabled = ["manager", "worker", "workdayStart", "workdayReport"].some((serviceKey) => serviceEnabled(config, serviceKey));
|
|
59
|
+
return enabled ? null : "No agent Railway services are enabled.";
|
|
60
|
+
}
|
|
61
|
+
function hasValue(env, key) {
|
|
62
|
+
return typeof env[key] === "string" && String(env[key]).trim().length > 0;
|
|
63
|
+
}
|
|
64
|
+
function missingForSystem(system, env) {
|
|
65
|
+
switch (system) {
|
|
66
|
+
case "github":
|
|
67
|
+
return hasValue(env, "GH_TOKEN") || hasValue(env, "GITHUB_TOKEN") ? [] : ["GH_TOKEN"];
|
|
68
|
+
case "data":
|
|
69
|
+
case "web":
|
|
70
|
+
return hasValue(env, "CLOUDFLARE_API_TOKEN") ? [] : ["CLOUDFLARE_API_TOKEN"];
|
|
71
|
+
case "api":
|
|
72
|
+
case "agents":
|
|
73
|
+
return hasValue(env, "RAILWAY_API_TOKEN") ? [] : ["RAILWAY_API_TOKEN"];
|
|
74
|
+
default:
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function resolveTreeseedBootstrapSelection({
|
|
79
|
+
deployConfig,
|
|
80
|
+
env,
|
|
81
|
+
systems,
|
|
82
|
+
skipUnavailable
|
|
83
|
+
}) {
|
|
84
|
+
const requested = parseTreeseedBootstrapSystems(systems);
|
|
85
|
+
const explicit = !(systems === void 0 || systems === null) && requested.length > 0;
|
|
86
|
+
const selected = requested.includes("all") ? [...TREESEED_BOOTSTRAP_SYSTEMS] : requested;
|
|
87
|
+
const expanded = expandBootstrapSystems(selected);
|
|
88
|
+
const defaultSkipUnavailable = requested.includes("all");
|
|
89
|
+
const effectiveSkipUnavailable = skipUnavailable ?? defaultSkipUnavailable;
|
|
90
|
+
const canSkipUnavailable = (system) => effectiveSkipUnavailable && (skipUnavailable === true || system === "api" || system === "agents");
|
|
91
|
+
const statuses = [];
|
|
92
|
+
const configDisabled = [];
|
|
93
|
+
const unavailable = [];
|
|
94
|
+
const skipped = [];
|
|
95
|
+
const runnable = [];
|
|
96
|
+
for (const system of expanded) {
|
|
97
|
+
const disabledReason = system === "api" ? apiSystemDisabled(deployConfig) : system === "agents" ? agentsSystemDisabled(deployConfig) : null;
|
|
98
|
+
if (disabledReason) {
|
|
99
|
+
const status2 = { system, status: "config_disabled", reason: disabledReason, missing: [] };
|
|
100
|
+
configDisabled.push(status2);
|
|
101
|
+
skipped.push(status2);
|
|
102
|
+
statuses.push(status2);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const missing = missingForSystem(system, env);
|
|
106
|
+
if (missing.length > 0) {
|
|
107
|
+
const reason = `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not configured.`;
|
|
108
|
+
const status2 = {
|
|
109
|
+
system,
|
|
110
|
+
status: "unavailable",
|
|
111
|
+
reason,
|
|
112
|
+
missing
|
|
113
|
+
};
|
|
114
|
+
if (!canSkipUnavailable(system) && (selected.includes(system) || !requested.includes("all"))) {
|
|
115
|
+
unavailable.push(status2);
|
|
116
|
+
statuses.push(status2);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
unavailable.push(status2);
|
|
120
|
+
skipped.push(status2);
|
|
121
|
+
statuses.push(status2);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const status = {
|
|
125
|
+
system,
|
|
126
|
+
status: selected.includes(system) ? "selected" : "included_dependency",
|
|
127
|
+
reason: selected.includes(system) ? "Selected for bootstrap." : "Included as a dependency.",
|
|
128
|
+
missing: []
|
|
129
|
+
};
|
|
130
|
+
runnable.push(system);
|
|
131
|
+
statuses.push(status);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
requested,
|
|
135
|
+
explicit,
|
|
136
|
+
skipUnavailable: effectiveSkipUnavailable,
|
|
137
|
+
selected,
|
|
138
|
+
expanded,
|
|
139
|
+
runnable,
|
|
140
|
+
configDisabled,
|
|
141
|
+
unavailable,
|
|
142
|
+
skipped,
|
|
143
|
+
statuses
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function bootstrapSystemForUnit(unit) {
|
|
147
|
+
const metadataSystem = unit.metadata?.bootstrapSystem;
|
|
148
|
+
if (typeof metadataSystem === "string" && BOOTSTRAP_SYSTEM_SET.has(metadataSystem)) {
|
|
149
|
+
return metadataSystem;
|
|
150
|
+
}
|
|
151
|
+
if (unit.unitType === "queue" || unit.unitType === "database" || unit.unitType === "content-store") {
|
|
152
|
+
return "data";
|
|
153
|
+
}
|
|
154
|
+
if (unit.unitType === "api-runtime" || unit.unitType === "railway-service:api" || unit.unitType === "custom-domain:api") {
|
|
155
|
+
return "api";
|
|
156
|
+
}
|
|
157
|
+
if (unit.unitType.startsWith("railway-service:") || unit.unitType.endsWith("-runtime")) {
|
|
158
|
+
return "agents";
|
|
159
|
+
}
|
|
160
|
+
return "web";
|
|
161
|
+
}
|
|
162
|
+
function filterTreeseedDesiredUnitsByBootstrapSystems(units, systems) {
|
|
163
|
+
if (!systems || systems.length === 0) {
|
|
164
|
+
return units;
|
|
165
|
+
}
|
|
166
|
+
const allowed = new Set(systems);
|
|
167
|
+
return units.filter((unit) => allowed.has(bootstrapSystemForUnit(unit)));
|
|
168
|
+
}
|
|
169
|
+
export {
|
|
170
|
+
TREESEED_BOOTSTRAP_SYSTEMS,
|
|
171
|
+
bootstrapSystemForUnit,
|
|
172
|
+
filterTreeseedDesiredUnitsByBootstrapSystems,
|
|
173
|
+
parseTreeseedBootstrapSystems,
|
|
174
|
+
resolveTreeseedBootstrapSelection
|
|
175
|
+
};
|
|
@@ -168,12 +168,6 @@ function resolveReconcileEnvironmentValues(input, scope) {
|
|
|
168
168
|
...normalizeEnvironmentValues(process.env),
|
|
169
169
|
...normalizeEnvironmentValues(input.context.launchEnv)
|
|
170
170
|
};
|
|
171
|
-
const launchRailwayAlias = input.context.launchEnv.RAILWAY_API_KEY;
|
|
172
|
-
if (!input.context.launchEnv.RAILWAY_API_TOKEN && typeof launchRailwayAlias === "string" && launchRailwayAlias.length > 0) {
|
|
173
|
-
values.RAILWAY_API_TOKEN = launchRailwayAlias;
|
|
174
|
-
} else if (!values.RAILWAY_API_TOKEN && values.RAILWAY_API_KEY) {
|
|
175
|
-
values.RAILWAY_API_TOKEN = values.RAILWAY_API_KEY;
|
|
176
|
-
}
|
|
177
171
|
return values;
|
|
178
172
|
}
|
|
179
173
|
function buildCloudflareEnv(input) {
|
|
@@ -192,9 +186,7 @@ function buildRailwayEnv(input, scope) {
|
|
|
192
186
|
const token = [
|
|
193
187
|
values.RAILWAY_API_TOKEN,
|
|
194
188
|
input.context.launchEnv.RAILWAY_API_TOKEN,
|
|
195
|
-
|
|
196
|
-
process.env.RAILWAY_API_TOKEN,
|
|
197
|
-
process.env.RAILWAY_API_KEY
|
|
189
|
+
process.env.RAILWAY_API_TOKEN
|
|
198
190
|
].find((value) => typeof value === "string" && value.trim().length > 0)?.trim() ?? "";
|
|
199
191
|
return {
|
|
200
192
|
RAILWAY_API_TOKEN: token,
|
|
@@ -49,7 +49,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
49
49
|
binding: legacyState.queues.agentWork.binding
|
|
50
50
|
},
|
|
51
51
|
secrets: {},
|
|
52
|
-
metadata: {}
|
|
52
|
+
metadata: { bootstrapSystem: "data" }
|
|
53
53
|
});
|
|
54
54
|
const databaseId = add({
|
|
55
55
|
unitId: createTreeseedReconcileUnitId("database", legacyState.d1Databases.SITE_DATA_DB.databaseName),
|
|
@@ -64,7 +64,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
64
64
|
binding: "SITE_DATA_DB"
|
|
65
65
|
},
|
|
66
66
|
secrets: {},
|
|
67
|
-
metadata: {}
|
|
67
|
+
metadata: { bootstrapSystem: "data" }
|
|
68
68
|
});
|
|
69
69
|
const formGuardKvId = add({
|
|
70
70
|
unitId: createTreeseedReconcileUnitId("kv-form-guard", legacyState.kvNamespaces.FORM_GUARD_KV.name),
|
|
@@ -76,7 +76,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
76
76
|
dependencies: [],
|
|
77
77
|
spec: { binding: "FORM_GUARD_KV", name: legacyState.kvNamespaces.FORM_GUARD_KV.name },
|
|
78
78
|
secrets: {},
|
|
79
|
-
metadata: {}
|
|
79
|
+
metadata: { bootstrapSystem: "web" }
|
|
80
80
|
});
|
|
81
81
|
const sessionKvId = add({
|
|
82
82
|
unitId: createTreeseedReconcileUnitId("kv-session", legacyState.kvNamespaces.SESSION.name),
|
|
@@ -88,7 +88,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
88
88
|
dependencies: [],
|
|
89
89
|
spec: { binding: "SESSION", name: legacyState.kvNamespaces.SESSION.name },
|
|
90
90
|
secrets: {},
|
|
91
|
-
metadata: {}
|
|
91
|
+
metadata: { bootstrapSystem: "web" }
|
|
92
92
|
});
|
|
93
93
|
const contentStoreId = add({
|
|
94
94
|
unitId: createTreeseedReconcileUnitId("content-store", legacyState.content.bucketName ?? deployConfig.slug),
|
|
@@ -106,7 +106,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
106
106
|
previewRootTemplate: legacyState.content.previewRootTemplate
|
|
107
107
|
},
|
|
108
108
|
secrets: {},
|
|
109
|
-
metadata: { shared: true }
|
|
109
|
+
metadata: { shared: true, bootstrapSystem: "data" }
|
|
110
110
|
});
|
|
111
111
|
const pagesProjectId = add({
|
|
112
112
|
unitId: createTreeseedReconcileUnitId("pages-project", legacyState.pages.projectName),
|
|
@@ -123,7 +123,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
123
123
|
buildOutputDir: legacyState.pages.buildOutputDir
|
|
124
124
|
},
|
|
125
125
|
secrets: {},
|
|
126
|
-
metadata: {}
|
|
126
|
+
metadata: { bootstrapSystem: "web" }
|
|
127
127
|
});
|
|
128
128
|
const edgeWorkerId = add({
|
|
129
129
|
unitId: createTreeseedReconcileUnitId("edge-worker", legacyState.workerName),
|
|
@@ -137,7 +137,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
137
137
|
workerName: legacyState.workerName
|
|
138
138
|
},
|
|
139
139
|
secrets: {},
|
|
140
|
-
metadata: {}
|
|
140
|
+
metadata: { bootstrapSystem: "web" }
|
|
141
141
|
});
|
|
142
142
|
if (deployConfig.surfaces?.web?.enabled !== false) {
|
|
143
143
|
const scope2 = target.kind === "persistent" ? target.scope : "staging";
|
|
@@ -155,7 +155,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
155
155
|
projectName: legacyState.pages.projectName
|
|
156
156
|
},
|
|
157
157
|
secrets: {},
|
|
158
|
-
metadata: { surface: "web" }
|
|
158
|
+
metadata: { surface: "web", bootstrapSystem: "web" }
|
|
159
159
|
}) : null;
|
|
160
160
|
const webDnsUnitId = webDomain ? add({
|
|
161
161
|
unitId: createTreeseedReconcileUnitId("dns-record", `web:${webDomain}`),
|
|
@@ -175,7 +175,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
175
175
|
targetKind: "pages-project"
|
|
176
176
|
},
|
|
177
177
|
secrets: {},
|
|
178
|
-
metadata: { surface: "web" }
|
|
178
|
+
metadata: { surface: "web", bootstrapSystem: "web" }
|
|
179
179
|
}) : null;
|
|
180
180
|
add({
|
|
181
181
|
unitId: createTreeseedReconcileUnitId("web-ui", "web-ui"),
|
|
@@ -196,7 +196,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
196
196
|
localBaseUrl: deployConfig.surfaces?.web?.localBaseUrl ?? null
|
|
197
197
|
},
|
|
198
198
|
secrets: {},
|
|
199
|
-
metadata: {}
|
|
199
|
+
metadata: { bootstrapSystem: "web" }
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
const scope = target.kind === "persistent" ? target.scope : "staging";
|
|
@@ -208,6 +208,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
208
208
|
continue;
|
|
209
209
|
}
|
|
210
210
|
const concreteType = railwayConcreteUnitTypeForServiceKey(serviceKey);
|
|
211
|
+
const serviceBootstrapSystem = serviceKey === "api" ? "api" : "agents";
|
|
211
212
|
const concreteId = add({
|
|
212
213
|
unitId: createTreeseedReconcileUnitId(concreteType, serviceState?.serviceName ?? configuredService.serviceName ?? serviceKey),
|
|
213
214
|
unitType: concreteType,
|
|
@@ -238,7 +239,8 @@ function deriveTreeseedDesiredUnits({
|
|
|
238
239
|
serviceKey,
|
|
239
240
|
scheduleManaged: Array.isArray(configuredService.schedule) && configuredService.schedule.length > 0,
|
|
240
241
|
scheduleBootstrap: false,
|
|
241
|
-
scheduleDeployScopes: ["prod"]
|
|
242
|
+
scheduleDeployScopes: ["prod"],
|
|
243
|
+
bootstrapSystem: serviceBootstrapSystem
|
|
242
244
|
}
|
|
243
245
|
});
|
|
244
246
|
const apiDomain = serviceKey === "api" && target.kind === "persistent" ? resolveConfiguredSurfaceDomain(deployConfig, target, "api") : null;
|
|
@@ -257,7 +259,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
257
259
|
environment: normalizeRailwayEnvironmentName(serviceState?.environment ?? configuredService.railwayEnvironment)
|
|
258
260
|
},
|
|
259
261
|
secrets: {},
|
|
260
|
-
metadata: { surface: "api", serviceKey }
|
|
262
|
+
metadata: { surface: "api", serviceKey, bootstrapSystem: "api" }
|
|
261
263
|
}) : null;
|
|
262
264
|
const apiDnsUnitId = apiDomain ? add({
|
|
263
265
|
unitId: createTreeseedReconcileUnitId("dns-record", `api:${apiDomain}`),
|
|
@@ -274,7 +276,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
274
276
|
serviceKey
|
|
275
277
|
},
|
|
276
278
|
secrets: {},
|
|
277
|
-
metadata: { surface: "api", serviceKey }
|
|
279
|
+
metadata: { surface: "api", serviceKey, bootstrapSystem: "api" }
|
|
278
280
|
}) : null;
|
|
279
281
|
const runtimeUnitType = (() => {
|
|
280
282
|
switch (serviceKey) {
|
|
@@ -309,7 +311,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
309
311
|
publicBaseUrl: serviceState?.publicBaseUrl ?? null
|
|
310
312
|
},
|
|
311
313
|
secrets: {},
|
|
312
|
-
metadata: {}
|
|
314
|
+
metadata: { bootstrapSystem: serviceBootstrapSystem }
|
|
313
315
|
});
|
|
314
316
|
}
|
|
315
317
|
return { deployConfig, legacyState, units };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { TreeseedObservedUnitState, TreeseedReconcilePlan, TreeseedReconcileResult, TreeseedReconcileStateRecord, TreeseedReconcileTarget, TreeseedUnitVerificationResult } from './contracts.ts';
|
|
2
|
-
|
|
2
|
+
import { type TreeseedRunnableBootstrapSystem } from './bootstrap-systems.ts';
|
|
3
|
+
export declare function observeTreeseedUnits({ tenantRoot, target, env, systems, write, }: {
|
|
3
4
|
tenantRoot: string;
|
|
4
5
|
target: TreeseedReconcileTarget;
|
|
5
6
|
env?: NodeJS.ProcessEnv;
|
|
7
|
+
systems?: TreeseedRunnableBootstrapSystem[];
|
|
6
8
|
write?: (line: string) => void;
|
|
7
9
|
}): Promise<{
|
|
8
10
|
units: import("./contracts.ts").TreeseedDesiredUnit[];
|
|
@@ -180,10 +182,11 @@ export declare function observeTreeseedUnits({ tenantRoot, target, env, write, }
|
|
|
180
182
|
};
|
|
181
183
|
};
|
|
182
184
|
}>;
|
|
183
|
-
export declare function planTreeseedReconciliation({ tenantRoot, target, env, write, }: {
|
|
185
|
+
export declare function planTreeseedReconciliation({ tenantRoot, target, env, systems, write, }: {
|
|
184
186
|
tenantRoot: string;
|
|
185
187
|
target: TreeseedReconcileTarget;
|
|
186
188
|
env?: NodeJS.ProcessEnv;
|
|
189
|
+
systems?: TreeseedRunnableBootstrapSystem[];
|
|
187
190
|
write?: (line: string) => void;
|
|
188
191
|
}): Promise<{
|
|
189
192
|
plans: TreeseedReconcilePlan[];
|
|
@@ -362,10 +365,11 @@ export declare function planTreeseedReconciliation({ tenantRoot, target, env, wr
|
|
|
362
365
|
};
|
|
363
366
|
};
|
|
364
367
|
}>;
|
|
365
|
-
export declare function reconcileTreeseedTarget({ tenantRoot, target, env, write, }: {
|
|
368
|
+
export declare function reconcileTreeseedTarget({ tenantRoot, target, env, systems, write, }: {
|
|
366
369
|
tenantRoot: string;
|
|
367
370
|
target: TreeseedReconcileTarget;
|
|
368
371
|
env?: NodeJS.ProcessEnv;
|
|
372
|
+
systems?: TreeseedRunnableBootstrapSystem[];
|
|
369
373
|
write?: (line: string) => void;
|
|
370
374
|
}): Promise<{
|
|
371
375
|
target: TreeseedReconcileTarget;
|
|
@@ -383,10 +387,11 @@ export declare function destroyTreeseedTargetUnits({ tenantRoot, target, env, wr
|
|
|
383
387
|
target: TreeseedReconcileTarget;
|
|
384
388
|
results: TreeseedReconcileResult[];
|
|
385
389
|
}>;
|
|
386
|
-
export declare function collectTreeseedReconcileStatus({ tenantRoot, target, env, }: {
|
|
390
|
+
export declare function collectTreeseedReconcileStatus({ tenantRoot, target, env, systems, }: {
|
|
387
391
|
tenantRoot: string;
|
|
388
392
|
target: TreeseedReconcileTarget;
|
|
389
393
|
env?: NodeJS.ProcessEnv;
|
|
394
|
+
systems?: TreeseedRunnableBootstrapSystem[];
|
|
390
395
|
}): Promise<{
|
|
391
396
|
target: TreeseedReconcileTarget;
|
|
392
397
|
ready: boolean;
|
package/dist/reconcile/engine.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createTreeseedReconcileRegistry } from "./registry.js";
|
|
|
2
2
|
import { deriveTreeseedDesiredUnits } from "./desired-state.js";
|
|
3
3
|
import { ensureTreeseedPersistedUnitState, desiredUnitSpecHash, loadTreeseedReconcileState, updateTreeseedPersistedUnitState, writeTreeseedReconcileState } from "./state.js";
|
|
4
4
|
import { reverseTopologicallySortedUnits, topologicallySortDesiredUnits } from "./units.js";
|
|
5
|
+
import { filterTreeseedDesiredUnitsByBootstrapSystems } from "./bootstrap-systems.js";
|
|
5
6
|
function nowIso() {
|
|
6
7
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
7
8
|
}
|
|
@@ -35,6 +36,33 @@ function createRunContext(tenantRoot, target, launchEnv, write) {
|
|
|
35
36
|
session: /* @__PURE__ */ new Map()
|
|
36
37
|
};
|
|
37
38
|
}
|
|
39
|
+
function dependencyLevels(units) {
|
|
40
|
+
const remaining = new Map(units.map((unit) => [unit.unitId, unit]));
|
|
41
|
+
const completed = /* @__PURE__ */ new Set();
|
|
42
|
+
const levels = [];
|
|
43
|
+
while (remaining.size > 0) {
|
|
44
|
+
const ready = [...remaining.values()].filter(
|
|
45
|
+
(unit) => unit.dependencies.every((dependencyId) => completed.has(dependencyId) || !remaining.has(dependencyId))
|
|
46
|
+
);
|
|
47
|
+
if (ready.length === 0) {
|
|
48
|
+
topologicallySortDesiredUnits(units);
|
|
49
|
+
throw new Error("Treeseed reconcile dependency graph could not be scheduled.");
|
|
50
|
+
}
|
|
51
|
+
for (const unit of ready) {
|
|
52
|
+
remaining.delete(unit.unitId);
|
|
53
|
+
completed.add(unit.unitId);
|
|
54
|
+
}
|
|
55
|
+
levels.push(ready);
|
|
56
|
+
}
|
|
57
|
+
return levels;
|
|
58
|
+
}
|
|
59
|
+
async function runByDependencyLevel(units, action) {
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const level of dependencyLevels(units)) {
|
|
62
|
+
results.push(...await Promise.all(level.map((unit) => action(unit))));
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
38
66
|
function persistResult(reconcileState, previous, result) {
|
|
39
67
|
updateTreeseedPersistedUnitState(reconcileState, {
|
|
40
68
|
...previous,
|
|
@@ -98,14 +126,17 @@ async function observeTreeseedUnits({
|
|
|
98
126
|
tenantRoot,
|
|
99
127
|
target,
|
|
100
128
|
env = process.env,
|
|
129
|
+
systems,
|
|
101
130
|
write
|
|
102
131
|
}) {
|
|
103
|
-
const
|
|
132
|
+
const derived = deriveTreeseedDesiredUnits({ tenantRoot, target });
|
|
133
|
+
const units = filterTreeseedDesiredUnitsByBootstrapSystems(derived.units, systems);
|
|
134
|
+
const deployConfig = derived.deployConfig;
|
|
104
135
|
const registry = createTreeseedReconcileRegistry(deployConfig);
|
|
105
136
|
const reconcileState = loadTreeseedReconcileState(tenantRoot, target);
|
|
106
137
|
const context = createRunContext(tenantRoot, target, env, write);
|
|
107
138
|
const observations = /* @__PURE__ */ new Map();
|
|
108
|
-
|
|
139
|
+
await runByDependencyLevel(topologicallySortDesiredUnits(units), async (unit) => {
|
|
109
140
|
write?.(`Observing ${unit.provider}:${unit.unitType}...`);
|
|
110
141
|
const adapter = registry.get(unit.unitType, unit.provider);
|
|
111
142
|
const persisted = ensureTreeseedPersistedUnitState(reconcileState, unit);
|
|
@@ -120,7 +151,7 @@ async function observeTreeseedUnits({
|
|
|
120
151
|
wrapAdapterFailure("observe", unit.provider, unit.unitType, unit.unitId, error);
|
|
121
152
|
}
|
|
122
153
|
observations.set(unit.unitId, observed);
|
|
123
|
-
}
|
|
154
|
+
});
|
|
124
155
|
return {
|
|
125
156
|
units,
|
|
126
157
|
observations,
|
|
@@ -132,13 +163,14 @@ async function planTreeseedReconciliation({
|
|
|
132
163
|
tenantRoot,
|
|
133
164
|
target,
|
|
134
165
|
env = process.env,
|
|
166
|
+
systems,
|
|
135
167
|
write
|
|
136
168
|
}) {
|
|
137
|
-
const observed = await observeTreeseedUnits({ tenantRoot, target, env, write });
|
|
169
|
+
const observed = await observeTreeseedUnits({ tenantRoot, target, env, systems, write });
|
|
138
170
|
const registry = createTreeseedReconcileRegistry(observed.deployConfig);
|
|
139
171
|
const context = createRunContext(tenantRoot, target, env, write);
|
|
140
172
|
const plans = [];
|
|
141
|
-
|
|
173
|
+
await runByDependencyLevel(topologicallySortDesiredUnits(observed.units), async (unit) => {
|
|
142
174
|
const adapter = registry.get(unit.unitType, unit.provider);
|
|
143
175
|
const persisted = ensureTreeseedPersistedUnitState(observed.state, unit);
|
|
144
176
|
const observation = observed.observations.get(unit.unitId);
|
|
@@ -159,7 +191,7 @@ async function planTreeseedReconciliation({
|
|
|
159
191
|
diff,
|
|
160
192
|
persisted
|
|
161
193
|
});
|
|
162
|
-
}
|
|
194
|
+
});
|
|
163
195
|
return {
|
|
164
196
|
...observed,
|
|
165
197
|
plans
|
|
@@ -169,15 +201,26 @@ async function reconcileTreeseedTarget({
|
|
|
169
201
|
tenantRoot,
|
|
170
202
|
target,
|
|
171
203
|
env = process.env,
|
|
204
|
+
systems,
|
|
172
205
|
write
|
|
173
206
|
}) {
|
|
174
|
-
const planned = await planTreeseedReconciliation({ tenantRoot, target, env, write });
|
|
207
|
+
const planned = await planTreeseedReconciliation({ tenantRoot, target, env, systems, write });
|
|
175
208
|
const registry = createTreeseedReconcileRegistry(planned.deployConfig);
|
|
176
209
|
const context = createRunContext(tenantRoot, target, env, write);
|
|
177
210
|
const results = [];
|
|
178
211
|
const verificationMap = /* @__PURE__ */ new Map();
|
|
179
212
|
context.session.set("treeseed:verification-results", verificationMap);
|
|
180
|
-
|
|
213
|
+
const planByUnitId = new Map(planned.plans.map((plan) => [plan.unit.unitId, plan]));
|
|
214
|
+
let persistChain = Promise.resolve();
|
|
215
|
+
const persistVerifiedResult = async (persisted, verifiedResult) => {
|
|
216
|
+
persistChain = persistChain.then(() => {
|
|
217
|
+
persistResult(planned.state, persisted, verifiedResult);
|
|
218
|
+
writeTreeseedReconcileState(tenantRoot, planned.state);
|
|
219
|
+
});
|
|
220
|
+
await persistChain;
|
|
221
|
+
};
|
|
222
|
+
await runByDependencyLevel(topologicallySortDesiredUnits(planned.units), async (unit) => {
|
|
223
|
+
const plan = planByUnitId.get(unit.unitId);
|
|
181
224
|
write?.(`Reconciling ${plan.unit.provider}:${plan.unit.unitType}...`);
|
|
182
225
|
const adapter = registry.get(plan.unit.unitType, plan.unit.provider);
|
|
183
226
|
const persisted = ensureTreeseedPersistedUnitState(planned.state, plan.unit);
|
|
@@ -232,13 +275,12 @@ async function reconcileTreeseedTarget({
|
|
|
232
275
|
};
|
|
233
276
|
verificationMap.set(plan.unit.unitId, verification);
|
|
234
277
|
if (!verification.verified) {
|
|
235
|
-
|
|
236
|
-
writeTreeseedReconcileState(tenantRoot, planned.state);
|
|
278
|
+
await persistVerifiedResult(persisted, verifiedResult);
|
|
237
279
|
throw new Error(`Treeseed reconcile verification failed for ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.unitId}): ${formatVerificationFailure(verification)}`);
|
|
238
280
|
}
|
|
239
|
-
|
|
281
|
+
await persistVerifiedResult(persisted, verifiedResult);
|
|
240
282
|
results.push(verifiedResult);
|
|
241
|
-
}
|
|
283
|
+
});
|
|
242
284
|
writeTreeseedReconcileState(tenantRoot, planned.state);
|
|
243
285
|
return {
|
|
244
286
|
target,
|
|
@@ -294,9 +336,10 @@ async function destroyTreeseedTargetUnits({
|
|
|
294
336
|
async function collectTreeseedReconcileStatus({
|
|
295
337
|
tenantRoot,
|
|
296
338
|
target,
|
|
297
|
-
env = process.env
|
|
339
|
+
env = process.env,
|
|
340
|
+
systems
|
|
298
341
|
}) {
|
|
299
|
-
const observed = await observeTreeseedUnits({ tenantRoot, target, env });
|
|
342
|
+
const observed = await observeTreeseedUnits({ tenantRoot, target, env, systems });
|
|
300
343
|
const registry = createTreeseedReconcileRegistry(observed.deployConfig);
|
|
301
344
|
const context = createRunContext(tenantRoot, target, env);
|
|
302
345
|
const plans = await Promise.all(observed.units.map(async (unit) => {
|
package/dist/reconcile/index.js
CHANGED
|
@@ -5,6 +5,9 @@ function parseArgs(argv) {
|
|
|
5
5
|
const parsed = {
|
|
6
6
|
scopes: [],
|
|
7
7
|
sync: 'all',
|
|
8
|
+
systems: [],
|
|
9
|
+
skipUnavailable: undefined,
|
|
10
|
+
bootstrapExecution: 'parallel',
|
|
8
11
|
bootstrap: false,
|
|
9
12
|
rotateMachineKey: false,
|
|
10
13
|
};
|
|
@@ -34,6 +37,30 @@ function parseArgs(argv) {
|
|
|
34
37
|
parsed.bootstrap = true;
|
|
35
38
|
continue;
|
|
36
39
|
}
|
|
40
|
+
if (current === '--system') {
|
|
41
|
+
parsed.systems.push(rest.shift() ?? '');
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (current.startsWith('--system=')) {
|
|
45
|
+
parsed.systems.push(current.split('=', 2)[1] ?? '');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (current === '--systems') {
|
|
49
|
+
parsed.systems.push(...String(rest.shift() ?? '').split(','));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (current.startsWith('--systems=')) {
|
|
53
|
+
parsed.systems.push(...String(current.split('=', 2)[1] ?? '').split(','));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (current === '--skip-unavailable') {
|
|
57
|
+
parsed.skipUnavailable = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (current === '--bootstrap-sequential') {
|
|
61
|
+
parsed.bootstrapExecution = 'sequential';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
37
64
|
if (current === '--rotate-machine-key') {
|
|
38
65
|
parsed.rotateMachineKey = true;
|
|
39
66
|
continue;
|
|
@@ -75,6 +102,9 @@ try {
|
|
|
75
102
|
tenantRoot,
|
|
76
103
|
scopes,
|
|
77
104
|
sync: options.sync,
|
|
105
|
+
systems: options.systems.length > 0 ? options.systems : undefined,
|
|
106
|
+
skipUnavailable: options.skipUnavailable,
|
|
107
|
+
bootstrapExecution: options.bootstrapExecution,
|
|
78
108
|
env: process.env,
|
|
79
109
|
});
|
|
80
110
|
const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
@@ -34,8 +34,6 @@ export function resolveWranglerBin() {
|
|
|
34
34
|
export function createProductionBuildEnv(extraEnv = {}) {
|
|
35
35
|
return {
|
|
36
36
|
TREESEED_LOCAL_DEV_MODE: 'cloudflare',
|
|
37
|
-
TREESEED_PUBLIC_FORMS_LOCAL_BYPASS_TURNSTILE: '',
|
|
38
|
-
TREESEED_FORMS_LOCAL_BYPASS_TURNSTILE: '',
|
|
39
37
|
TREESEED_FORMS_LOCAL_BYPASS_CLOUDFLARE_GUARDS: '',
|
|
40
38
|
TREESEED_PUBLIC_DEV_WATCH_RELOAD: '',
|
|
41
39
|
...extraEnv,
|