@treeseed/sdk 0.6.1 → 0.6.3
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 +303 -127
- package/dist/operations/services/github-api.d.ts +34 -0
- package/dist/operations/services/github-api.js +187 -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 +319 -15
- 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 +71 -96
- package/dist/platform/environment.d.ts +4 -0
- package/dist/platform/environment.js +54 -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
|
@@ -12,16 +12,18 @@ import {
|
|
|
12
12
|
signEditorialPreviewToken
|
|
13
13
|
} from "../../platform/published-content.js";
|
|
14
14
|
import { createPublishedContentPipeline } from "../../platform/published-content-pipeline.js";
|
|
15
|
-
import { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../reconcile/index.js";
|
|
15
|
+
import { collectTreeseedReconcileStatus, reconcileTreeseedTarget, resolveTreeseedBootstrapSelection } from "../../reconcile/index.js";
|
|
16
16
|
import { loadTreeseedManifest } from "../../platform/tenant-config.js";
|
|
17
17
|
import { applyTreeseedEnvironmentToProcess } from "./config-runtime.js";
|
|
18
18
|
import {
|
|
19
|
+
assertDeploymentInitialized,
|
|
19
20
|
createPersistentDeployTarget,
|
|
20
21
|
deployTargetLabel,
|
|
21
22
|
ensureGeneratedWranglerConfig,
|
|
22
23
|
finalizeDeploymentState,
|
|
23
24
|
loadDeployState,
|
|
24
25
|
purgePublishedContentCaches,
|
|
26
|
+
resolveConfiguredCloudflareAccountId,
|
|
25
27
|
resolveConfiguredSurfaceBaseUrl,
|
|
26
28
|
writeDeployState
|
|
27
29
|
} from "./deploy.js";
|
|
@@ -36,6 +38,9 @@ import {
|
|
|
36
38
|
} from "./railway-deploy.js";
|
|
37
39
|
import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "./runtime-tools.js";
|
|
38
40
|
import { CloudflareQueuePullClient, CloudflareQueuePushClient } from "../../remote.js";
|
|
41
|
+
import { runPrefixedCommand, runTreeseedBootstrapDag, sleep, writeTreeseedBootstrapLine } from "./bootstrap-runner.js";
|
|
42
|
+
import { runTenantDeployPreflight } from "./save-deploy-preflight.js";
|
|
43
|
+
const PROJECT_PLATFORM_BOOTSTRAP_SYSTEMS = ["data", "web", "api", "agents"];
|
|
39
44
|
function stableHash(value) {
|
|
40
45
|
return createHash("sha256").update(value).digest("hex");
|
|
41
46
|
}
|
|
@@ -86,6 +91,30 @@ function runNodeScript(tenantRoot, scriptName, scriptArgs = []) {
|
|
|
86
91
|
throw new Error(`${scriptName} failed.`);
|
|
87
92
|
}
|
|
88
93
|
}
|
|
94
|
+
function resolveProjectPlatformBootstrapSystems(options, siteConfig = loadCliDeployConfig(options.tenantRoot)) {
|
|
95
|
+
if (options.bootstrapSystems && options.bootstrapSystems.length > 0) {
|
|
96
|
+
return [...options.bootstrapSystems];
|
|
97
|
+
}
|
|
98
|
+
const selection = resolveTreeseedBootstrapSelection({
|
|
99
|
+
deployConfig: siteConfig,
|
|
100
|
+
env: { ...process.env, ...options.env ?? {} },
|
|
101
|
+
systems: PROJECT_PLATFORM_BOOTSTRAP_SYSTEMS,
|
|
102
|
+
skipUnavailable: true
|
|
103
|
+
});
|
|
104
|
+
for (const skipped of selection.skipped) {
|
|
105
|
+
writeTreeseedBootstrapLine(
|
|
106
|
+
options.write,
|
|
107
|
+
{
|
|
108
|
+
scope: options.scope,
|
|
109
|
+
system: skipped.system,
|
|
110
|
+
task: "availability",
|
|
111
|
+
stage: "skip"
|
|
112
|
+
},
|
|
113
|
+
skipped.reason
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return selection.runnable.filter((system) => system !== "github");
|
|
117
|
+
}
|
|
89
118
|
function runWrangler(tenantRoot, args, extraEnv = {}, options = {}) {
|
|
90
119
|
const result = spawnSync(process.execPath, [resolveWranglerBin(), ...args], {
|
|
91
120
|
cwd: tenantRoot,
|
|
@@ -98,6 +127,165 @@ function runWrangler(tenantRoot, args, extraEnv = {}, options = {}) {
|
|
|
98
127
|
}
|
|
99
128
|
return result;
|
|
100
129
|
}
|
|
130
|
+
function isTransientWranglerOutput(output) {
|
|
131
|
+
return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|connectivity issue|internal error|aborted/i.test(output);
|
|
132
|
+
}
|
|
133
|
+
async function runPrefixedWranglerWithRetry(tenantRoot, args, {
|
|
134
|
+
env = {},
|
|
135
|
+
write,
|
|
136
|
+
prefix
|
|
137
|
+
}) {
|
|
138
|
+
let lastOutput = "";
|
|
139
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
140
|
+
const result = await runPrefixedCommand(process.execPath, [resolveWranglerBin(), ...args], {
|
|
141
|
+
cwd: tenantRoot,
|
|
142
|
+
env,
|
|
143
|
+
write,
|
|
144
|
+
prefix
|
|
145
|
+
});
|
|
146
|
+
if (result.status === 0) {
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
lastOutput = [result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join("\n");
|
|
150
|
+
if (attempt === 3 || !isTransientWranglerOutput(lastOutput)) {
|
|
151
|
+
throw new Error(lastOutput || `wrangler ${args.join(" ")} failed`);
|
|
152
|
+
}
|
|
153
|
+
writeTreeseedBootstrapLine(
|
|
154
|
+
write,
|
|
155
|
+
{ ...prefix, stage: "retry" },
|
|
156
|
+
`Wrangler command hit a transient failure; retrying in ${2 * attempt}s...`,
|
|
157
|
+
"stderr"
|
|
158
|
+
);
|
|
159
|
+
await sleep(2e3 * attempt);
|
|
160
|
+
}
|
|
161
|
+
throw new Error(lastOutput || `wrangler ${args.join(" ")} failed`);
|
|
162
|
+
}
|
|
163
|
+
function prepareTenantCloudflareDeploy({
|
|
164
|
+
tenantRoot,
|
|
165
|
+
scope,
|
|
166
|
+
target: explicitTarget,
|
|
167
|
+
dryRun,
|
|
168
|
+
write,
|
|
169
|
+
env = process.env
|
|
170
|
+
}) {
|
|
171
|
+
const target = explicitTarget ?? createPersistentDeployTarget(scope === "local" ? "staging" : scope);
|
|
172
|
+
if (scope !== "local") {
|
|
173
|
+
assertDeploymentInitialized(tenantRoot, { target });
|
|
174
|
+
runTenantDeployPreflight({ cwd: tenantRoot, scope });
|
|
175
|
+
}
|
|
176
|
+
const { wranglerPath, deployConfig, state } = ensureGeneratedWranglerConfig(tenantRoot, { target });
|
|
177
|
+
const deployState = loadDeployState(tenantRoot, deployConfig, { target });
|
|
178
|
+
const pagesProjectName = target.kind === "persistent" ? deployState.pages?.projectName ?? null : null;
|
|
179
|
+
const pagesBranchName = target.kind === "persistent" ? target.scope === "prod" ? deployState.pages?.productionBranch ?? "main" : deployState.pages?.stagingBranch ?? "staging" : null;
|
|
180
|
+
return {
|
|
181
|
+
tenantRoot,
|
|
182
|
+
scope,
|
|
183
|
+
target,
|
|
184
|
+
dryRun,
|
|
185
|
+
wranglerPath,
|
|
186
|
+
databaseName: state.d1Databases.SITE_DATA_DB.databaseName,
|
|
187
|
+
pagesProjectName,
|
|
188
|
+
pagesBranchName,
|
|
189
|
+
env: {
|
|
190
|
+
...process.env,
|
|
191
|
+
...env,
|
|
192
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
193
|
+
},
|
|
194
|
+
write
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function runTenantDataMigration(context) {
|
|
198
|
+
if (context.dryRun) {
|
|
199
|
+
writeTreeseedBootstrapLine(context.write, {
|
|
200
|
+
scope: context.scope,
|
|
201
|
+
system: "data",
|
|
202
|
+
task: "d1-migrate",
|
|
203
|
+
stage: "deploy"
|
|
204
|
+
}, `Dry run: would apply remote migrations for ${context.databaseName}.`);
|
|
205
|
+
return { databaseName: context.databaseName, dryRun: true };
|
|
206
|
+
}
|
|
207
|
+
await runPrefixedWranglerWithRetry(context.tenantRoot, [
|
|
208
|
+
"d1",
|
|
209
|
+
"migrations",
|
|
210
|
+
"apply",
|
|
211
|
+
context.databaseName,
|
|
212
|
+
"--remote",
|
|
213
|
+
"--config",
|
|
214
|
+
context.wranglerPath
|
|
215
|
+
], {
|
|
216
|
+
env: context.env,
|
|
217
|
+
write: context.write,
|
|
218
|
+
prefix: {
|
|
219
|
+
scope: context.scope,
|
|
220
|
+
system: "data",
|
|
221
|
+
task: "d1-migrate",
|
|
222
|
+
stage: "deploy"
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return { databaseName: context.databaseName, dryRun: false };
|
|
226
|
+
}
|
|
227
|
+
async function runTenantWebBuild(context) {
|
|
228
|
+
const prefix = {
|
|
229
|
+
scope: context.scope,
|
|
230
|
+
system: "web",
|
|
231
|
+
task: "build",
|
|
232
|
+
stage: "deploy"
|
|
233
|
+
};
|
|
234
|
+
if (context.dryRun) {
|
|
235
|
+
writeTreeseedBootstrapLine(context.write, prefix, "Dry run: skipped tenant build.");
|
|
236
|
+
return { dryRun: true };
|
|
237
|
+
}
|
|
238
|
+
const result = await runPrefixedCommand(process.execPath, [packageScriptPath("tenant-build")], {
|
|
239
|
+
cwd: context.tenantRoot,
|
|
240
|
+
env: context.env,
|
|
241
|
+
write: context.write,
|
|
242
|
+
prefix
|
|
243
|
+
});
|
|
244
|
+
if (result.status !== 0) {
|
|
245
|
+
throw new Error("tenant-build failed.");
|
|
246
|
+
}
|
|
247
|
+
return { dryRun: false };
|
|
248
|
+
}
|
|
249
|
+
async function runTenantWebPublish(context) {
|
|
250
|
+
const prefix = {
|
|
251
|
+
scope: context.scope,
|
|
252
|
+
system: "web",
|
|
253
|
+
task: "publish",
|
|
254
|
+
stage: "deploy"
|
|
255
|
+
};
|
|
256
|
+
if (context.dryRun) {
|
|
257
|
+
if (context.pagesProjectName) {
|
|
258
|
+
writeTreeseedBootstrapLine(context.write, prefix, `Dry run: would deploy ${deployTargetLabel(context.target)} to Pages project ${context.pagesProjectName} from ${resolve(context.tenantRoot, "dist")}.`);
|
|
259
|
+
} else {
|
|
260
|
+
writeTreeseedBootstrapLine(context.write, prefix, `Dry run: would deploy ${deployTargetLabel(context.target)} with generated Wrangler config at ${resolve(context.wranglerPath)}.`);
|
|
261
|
+
}
|
|
262
|
+
return { dryRun: true };
|
|
263
|
+
}
|
|
264
|
+
if (context.pagesProjectName) {
|
|
265
|
+
const args = [
|
|
266
|
+
"pages",
|
|
267
|
+
"deploy",
|
|
268
|
+
resolve(context.tenantRoot, "dist"),
|
|
269
|
+
"--project-name",
|
|
270
|
+
context.pagesProjectName
|
|
271
|
+
];
|
|
272
|
+
if (context.pagesBranchName) {
|
|
273
|
+
args.push("--branch", context.pagesBranchName);
|
|
274
|
+
}
|
|
275
|
+
await runPrefixedWranglerWithRetry(context.tenantRoot, args, {
|
|
276
|
+
env: context.env,
|
|
277
|
+
write: context.write,
|
|
278
|
+
prefix
|
|
279
|
+
});
|
|
280
|
+
} else {
|
|
281
|
+
await runPrefixedWranglerWithRetry(context.tenantRoot, ["deploy", "--config", context.wranglerPath], {
|
|
282
|
+
env: context.env,
|
|
283
|
+
write: context.write,
|
|
284
|
+
prefix
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return { dryRun: false };
|
|
288
|
+
}
|
|
101
289
|
function inferContentType(filePath) {
|
|
102
290
|
const extension = extname(filePath).toLowerCase();
|
|
103
291
|
if (extension === ".json") return "application/json";
|
|
@@ -655,18 +843,24 @@ async function provisionProjectPlatform(options) {
|
|
|
655
843
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
656
844
|
const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
|
|
657
845
|
const siteConfig = loadCliDeployConfig(options.tenantRoot);
|
|
846
|
+
const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options, siteConfig);
|
|
847
|
+
const selectedSystems = new Set(bootstrapSystems);
|
|
848
|
+
const env = { ...process.env, ...options.env ?? {} };
|
|
658
849
|
const summary = await reconcileTreeseedTarget({
|
|
659
850
|
tenantRoot: options.tenantRoot,
|
|
660
851
|
target,
|
|
661
|
-
env
|
|
852
|
+
env,
|
|
853
|
+
systems: bootstrapSystems
|
|
662
854
|
});
|
|
663
855
|
const verification = await collectTreeseedReconcileStatus({
|
|
664
856
|
tenantRoot: options.tenantRoot,
|
|
665
857
|
target,
|
|
666
|
-
env
|
|
858
|
+
env,
|
|
859
|
+
systems: bootstrapSystems
|
|
667
860
|
});
|
|
668
861
|
ensureGeneratedWranglerConfig(options.tenantRoot, { target });
|
|
669
|
-
const
|
|
862
|
+
const shouldValidateRailway = selectedSystems.has("api") || selectedSystems.has("agents");
|
|
863
|
+
const railwayValidation = shouldValidateRailway ? options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env }) : { services: [] };
|
|
670
864
|
const railwaySchedules = [];
|
|
671
865
|
const railwayScheduleVerification = {
|
|
672
866
|
ok: true,
|
|
@@ -806,6 +1000,11 @@ async function deployProjectPlatform(options) {
|
|
|
806
1000
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
807
1001
|
const commitSha = currentCommit(options.tenantRoot);
|
|
808
1002
|
const branchName = currentRef(options.tenantRoot);
|
|
1003
|
+
const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options);
|
|
1004
|
+
const selectedSystems = new Set(bootstrapSystems);
|
|
1005
|
+
const execution = options.bootstrapExecution ?? "parallel";
|
|
1006
|
+
const write = options.write;
|
|
1007
|
+
const env = { ...process.env, ...options.env ?? {} };
|
|
809
1008
|
await reportDeployment(reporter, {
|
|
810
1009
|
environment: options.scope,
|
|
811
1010
|
deploymentKind: "code",
|
|
@@ -816,21 +1015,122 @@ async function deployProjectPlatform(options) {
|
|
|
816
1015
|
metadata: { scope: options.scope }
|
|
817
1016
|
});
|
|
818
1017
|
if (!options.skipProvision) {
|
|
819
|
-
await provisionProjectPlatform({ ...options, reporter });
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (options.scope
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1018
|
+
await provisionProjectPlatform({ ...options, reporter, bootstrapSystems });
|
|
1019
|
+
}
|
|
1020
|
+
const nodes = [];
|
|
1021
|
+
let cloudflareContext = null;
|
|
1022
|
+
if (options.scope === "local" && selectedSystems.has("web")) {
|
|
1023
|
+
nodes.push({
|
|
1024
|
+
id: "web:build",
|
|
1025
|
+
run: () => runTenantWebBuild({
|
|
1026
|
+
tenantRoot: options.tenantRoot,
|
|
1027
|
+
scope: "local",
|
|
1028
|
+
dryRun: options.dryRun,
|
|
1029
|
+
env,
|
|
1030
|
+
write
|
|
1031
|
+
})
|
|
1032
|
+
});
|
|
1033
|
+
} else if (selectedSystems.has("data") || selectedSystems.has("web")) {
|
|
1034
|
+
cloudflareContext = prepareTenantCloudflareDeploy({
|
|
1035
|
+
tenantRoot: options.tenantRoot,
|
|
1036
|
+
scope: options.scope,
|
|
1037
|
+
dryRun: options.dryRun,
|
|
1038
|
+
write,
|
|
1039
|
+
env
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (cloudflareContext && selectedSystems.has("data")) {
|
|
1043
|
+
const context = cloudflareContext;
|
|
1044
|
+
nodes.push({
|
|
1045
|
+
id: "data:d1-migrate",
|
|
1046
|
+
run: () => runTenantDataMigration(context)
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
if (cloudflareContext && selectedSystems.has("web")) {
|
|
1050
|
+
const context = cloudflareContext;
|
|
1051
|
+
nodes.push({
|
|
1052
|
+
id: "web:build",
|
|
1053
|
+
run: () => runTenantWebBuild(context)
|
|
1054
|
+
});
|
|
1055
|
+
nodes.push({
|
|
1056
|
+
id: "web:publish",
|
|
1057
|
+
dependencies: ["web:build", ...selectedSystems.has("data") ? ["data:d1-migrate"] : []],
|
|
1058
|
+
run: () => runTenantWebPublish(context)
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const serviceResultsByKey = /* @__PURE__ */ new Map();
|
|
1062
|
+
let selectedRailwayServiceKeys = [];
|
|
1063
|
+
if (options.scope !== "local" && (selectedSystems.has("api") || selectedSystems.has("agents"))) {
|
|
1064
|
+
const validation = validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env });
|
|
1065
|
+
const selectedServices = validation.services.filter(
|
|
1066
|
+
(service) => service.key === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")
|
|
1067
|
+
);
|
|
1068
|
+
for (const service of selectedServices) {
|
|
1069
|
+
const system = service.key === "api" ? "api" : "agents";
|
|
1070
|
+
const nodeId = `${system}:${service.key}-railway-deploy`;
|
|
1071
|
+
selectedRailwayServiceKeys.push(service.key);
|
|
1072
|
+
nodes.push({
|
|
1073
|
+
id: nodeId,
|
|
1074
|
+
dependencies: selectedSystems.has("data") ? ["data:d1-migrate"] : [],
|
|
1075
|
+
run: async () => {
|
|
1076
|
+
const result = await deployRailwayService(options.tenantRoot, service, {
|
|
1077
|
+
dryRun: options.dryRun,
|
|
1078
|
+
write,
|
|
1079
|
+
env,
|
|
1080
|
+
prefix: {
|
|
1081
|
+
scope: options.scope,
|
|
1082
|
+
system,
|
|
1083
|
+
task: `${service.key}-railway-deploy`,
|
|
1084
|
+
stage: "deploy"
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
serviceResultsByKey.set(service.key, result);
|
|
1088
|
+
return result;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
827
1091
|
}
|
|
828
|
-
|
|
829
|
-
|
|
1092
|
+
}
|
|
1093
|
+
let railwaySchedules = [];
|
|
1094
|
+
let railwayScheduleVerification = { ok: true, checks: [], skipped: true, reason: !selectedSystems.has("agents") ? "agents_not_selected" : options.scope !== "prod" ? "prod_only" : "dry_run" };
|
|
1095
|
+
if (options.scope === "prod" && selectedSystems.has("agents")) {
|
|
1096
|
+
const agentDeployNodeIds = nodes.filter((node) => node.id.startsWith("agents:") && node.id.endsWith("-railway-deploy")).map((node) => node.id);
|
|
1097
|
+
nodes.push({
|
|
1098
|
+
id: "agents:schedules",
|
|
1099
|
+
dependencies: agentDeployNodeIds,
|
|
1100
|
+
run: async () => {
|
|
1101
|
+
writeTreeseedBootstrapLine(write, {
|
|
1102
|
+
scope: options.scope,
|
|
1103
|
+
system: "agents",
|
|
1104
|
+
task: "schedules",
|
|
1105
|
+
stage: "deploy"
|
|
1106
|
+
}, "Reconciling Railway schedules...");
|
|
1107
|
+
railwaySchedules = await ensureRailwayScheduledJobs(options.tenantRoot, options.scope, { dryRun: options.dryRun, env });
|
|
1108
|
+
railwayScheduleVerification = !options.dryRun ? await verifyRailwayScheduledJobs(options.tenantRoot, options.scope) : { ok: true, checks: railwaySchedules, skipped: true, reason: "dry_run" };
|
|
1109
|
+
return {
|
|
1110
|
+
service: "railway-schedules",
|
|
1111
|
+
status: railwayScheduleVerification.ok ? "verified" : "failed",
|
|
1112
|
+
command: "railway schedules reconcile",
|
|
1113
|
+
cwd: options.tenantRoot,
|
|
1114
|
+
publicBaseUrl: null,
|
|
1115
|
+
schedules: railwaySchedules,
|
|
1116
|
+
scheduleVerification: railwayScheduleVerification
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
await runTreeseedBootstrapDag({ nodes, execution });
|
|
1122
|
+
const serviceResults = selectedRailwayServiceKeys.map((serviceKey) => serviceResultsByKey.get(serviceKey)).filter(Boolean);
|
|
1123
|
+
if (options.scope !== "local" && !options.dryRun && (selectedSystems.has("web") || serviceResults.length > 0)) {
|
|
830
1124
|
finalizeDeploymentState(options.tenantRoot, {
|
|
831
1125
|
target: createPersistentDeployTarget(options.scope),
|
|
832
1126
|
serviceResults
|
|
833
1127
|
});
|
|
1128
|
+
}
|
|
1129
|
+
if (options.scope !== "prod" || !selectedSystems.has("agents")) {
|
|
1130
|
+
railwaySchedules = [];
|
|
1131
|
+
railwayScheduleVerification = { ok: true, checks: railwaySchedules, skipped: true, reason: !selectedSystems.has("agents") ? "agents_not_selected" : options.scope !== "prod" ? "prod_only" : "dry_run" };
|
|
1132
|
+
}
|
|
1133
|
+
if (selectedSystems.has("agents")) {
|
|
834
1134
|
serviceResults.push({
|
|
835
1135
|
service: "railway-schedules",
|
|
836
1136
|
status: railwayScheduleVerification.ok ? "verified" : "failed",
|
|
@@ -851,7 +1151,7 @@ async function deployProjectPlatform(options) {
|
|
|
851
1151
|
triggeredByType: "project_runner",
|
|
852
1152
|
metadata: {
|
|
853
1153
|
scope: options.scope,
|
|
854
|
-
railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key),
|
|
1154
|
+
railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key).filter((serviceKey) => serviceKey === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")),
|
|
855
1155
|
monitor
|
|
856
1156
|
},
|
|
857
1157
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -980,9 +1280,13 @@ export {
|
|
|
980
1280
|
deployProjectPlatform,
|
|
981
1281
|
inferEnvironmentFromBranch,
|
|
982
1282
|
monitorProjectPlatform,
|
|
1283
|
+
prepareTenantCloudflareDeploy,
|
|
983
1284
|
provisionProjectPlatform,
|
|
984
1285
|
publishProjectContent,
|
|
985
1286
|
resolveScope,
|
|
986
1287
|
runProjectPlatformAction,
|
|
1288
|
+
runTenantDataMigration,
|
|
1289
|
+
runTenantWebBuild,
|
|
1290
|
+
runTenantWebPublish,
|
|
987
1291
|
syncControlPlaneState
|
|
988
1292
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TreeseedBootstrapTaskPrefix, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
|
|
1
2
|
export declare function isUsableRailwayToken(value: any): boolean;
|
|
2
3
|
export declare function resolveRailwayAuthToken(env?: NodeJS.ProcessEnv): string;
|
|
3
4
|
export declare function buildRailwayCommandEnv(env?: NodeJS.ProcessEnv): {
|
|
@@ -269,8 +270,11 @@ export declare function planRailwayServiceDeploy(service: any): {
|
|
|
269
270
|
args: any[];
|
|
270
271
|
cwd: any;
|
|
271
272
|
};
|
|
272
|
-
export declare function deployRailwayService(tenantRoot: any, service: any, { dryRun }?: {
|
|
273
|
-
dryRun?: boolean
|
|
273
|
+
export declare function deployRailwayService(tenantRoot: any, service: any, { dryRun, write, prefix, env, }?: {
|
|
274
|
+
dryRun?: boolean;
|
|
275
|
+
write?: TreeseedBootstrapWriter;
|
|
276
|
+
prefix?: TreeseedBootstrapTaskPrefix;
|
|
277
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
274
278
|
}): Promise<{
|
|
275
279
|
service: any;
|
|
276
280
|
status: string;
|
|
@@ -3,6 +3,7 @@ import { relative, resolve } from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { loadCliDeployConfig } from "./runtime-tools.js";
|
|
5
5
|
import { createPersistentDeployTarget, resolveTreeseedResourceIdentity } from "./deploy.js";
|
|
6
|
+
import { runPrefixedCommand, sleep } from "./bootstrap-runner.js";
|
|
6
7
|
import {
|
|
7
8
|
ensureRailwayEnvironment,
|
|
8
9
|
ensureRailwayProject,
|
|
@@ -900,7 +901,12 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
|
|
|
900
901
|
env
|
|
901
902
|
});
|
|
902
903
|
}
|
|
903
|
-
async function deployRailwayService(tenantRoot, service, {
|
|
904
|
+
async function deployRailwayService(tenantRoot, service, {
|
|
905
|
+
dryRun = false,
|
|
906
|
+
write,
|
|
907
|
+
prefix,
|
|
908
|
+
env = process.env
|
|
909
|
+
} = {}) {
|
|
904
910
|
const plan = planRailwayServiceDeploy(service);
|
|
905
911
|
if (dryRun) {
|
|
906
912
|
return {
|
|
@@ -911,11 +917,19 @@ async function deployRailwayService(tenantRoot, service, { dryRun = false } = {}
|
|
|
911
917
|
publicBaseUrl: service.publicBaseUrl
|
|
912
918
|
};
|
|
913
919
|
}
|
|
920
|
+
const taskPrefix = prefix ?? {
|
|
921
|
+
scope: normalizeScope(service.scope ?? service.railwayEnvironment ?? "railway"),
|
|
922
|
+
system: service.key === "api" ? "api" : "agents",
|
|
923
|
+
task: `${service.key}-railway-deploy`,
|
|
924
|
+
stage: "deploy"
|
|
925
|
+
};
|
|
926
|
+
const commandEnv = buildRailwayCommandEnv({ ...process.env, ...env });
|
|
914
927
|
if (service.buildCommand) {
|
|
915
|
-
const buildResult =
|
|
928
|
+
const buildResult = await runPrefixedCommand("bash", ["-lc", service.buildCommand], {
|
|
916
929
|
cwd: service.rootDir,
|
|
917
|
-
|
|
918
|
-
|
|
930
|
+
env: commandEnv,
|
|
931
|
+
write,
|
|
932
|
+
prefix: { ...taskPrefix, stage: "build" }
|
|
919
933
|
});
|
|
920
934
|
if (buildResult.status !== 0) {
|
|
921
935
|
throw new Error(`Railway ${service.key} build command failed.`);
|
|
@@ -923,17 +937,12 @@ async function deployRailwayService(tenantRoot, service, { dryRun = false } = {}
|
|
|
923
937
|
}
|
|
924
938
|
let lastFailure = null;
|
|
925
939
|
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
926
|
-
const result =
|
|
940
|
+
const result = await runPrefixedCommand(plan.command, plan.args, {
|
|
927
941
|
cwd: service.rootDir,
|
|
928
|
-
|
|
929
|
-
|
|
942
|
+
env: commandEnv,
|
|
943
|
+
write,
|
|
944
|
+
prefix: taskPrefix
|
|
930
945
|
});
|
|
931
|
-
if (result.stdout) {
|
|
932
|
-
process.stdout.write(result.stdout);
|
|
933
|
-
}
|
|
934
|
-
if (result.stderr) {
|
|
935
|
-
process.stderr.write(result.stderr);
|
|
936
|
-
}
|
|
937
946
|
if (result.status === 0) {
|
|
938
947
|
lastFailure = null;
|
|
939
948
|
break;
|
|
@@ -943,16 +952,15 @@ async function deployRailwayService(tenantRoot, service, { dryRun = false } = {}
|
|
|
943
952
|
throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${plan.args.join(" ")} failed`);
|
|
944
953
|
}
|
|
945
954
|
const backoffMs = 5e3 * attempt;
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
);
|
|
949
|
-
sleepSync(backoffMs);
|
|
955
|
+
const warning = `Railway deploy for ${service.serviceName ?? service.serviceId ?? service.key} hit a transient failure; retrying in ${Math.round(backoffMs / 1e3)}s...`;
|
|
956
|
+
write ? write(`[${taskPrefix.scope}][${taskPrefix.system}][${taskPrefix.task}][retry] ${warning}`, "stderr") : console.warn(warning);
|
|
957
|
+
await sleep(backoffMs);
|
|
950
958
|
}
|
|
951
959
|
if (lastFailure) {
|
|
952
960
|
throw new Error(lastFailure.stderr?.trim() || lastFailure.stdout?.trim() || `railway ${plan.args.join(" ")} failed`);
|
|
953
961
|
}
|
|
954
962
|
const runtimeConfiguration = await syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, service, {
|
|
955
|
-
env:
|
|
963
|
+
env: commandEnv
|
|
956
964
|
});
|
|
957
965
|
return {
|
|
958
966
|
service: service.key,
|
|
@@ -23,8 +23,6 @@ export declare function loadPackageJson(root?: string): any;
|
|
|
23
23
|
export declare function isWorkspaceRoot(root?: string): boolean;
|
|
24
24
|
export declare function createProductionBuildEnv(extraEnv?: {}): {
|
|
25
25
|
TREESEED_LOCAL_DEV_MODE: string;
|
|
26
|
-
TREESEED_PUBLIC_FORMS_LOCAL_BYPASS_TURNSTILE: string;
|
|
27
|
-
TREESEED_FORMS_LOCAL_BYPASS_TURNSTILE: string;
|
|
28
26
|
TREESEED_FORMS_LOCAL_BYPASS_CLOUDFLARE_GUARDS: string;
|
|
29
27
|
TREESEED_PUBLIC_DEV_WATCH_RELOAD: string;
|
|
30
28
|
};
|
|
@@ -331,8 +331,6 @@ function isWorkspaceRoot(root = process.cwd()) {
|
|
|
331
331
|
function createProductionBuildEnv(extraEnv = {}) {
|
|
332
332
|
return {
|
|
333
333
|
TREESEED_LOCAL_DEV_MODE: "cloudflare",
|
|
334
|
-
TREESEED_PUBLIC_FORMS_LOCAL_BYPASS_TURNSTILE: "",
|
|
335
|
-
TREESEED_FORMS_LOCAL_BYPASS_TURNSTILE: "",
|
|
336
334
|
TREESEED_FORMS_LOCAL_BYPASS_CLOUDFLARE_GUARDS: "",
|
|
337
335
|
TREESEED_PUBLIC_DEV_WATCH_RELOAD: "",
|
|
338
336
|
...extraEnv
|