@treeseed/sdk 0.6.0 → 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 +24 -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
|
@@ -16,12 +16,14 @@ import { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../r
|
|
|
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,8 @@ 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";
|
|
39
43
|
function stableHash(value) {
|
|
40
44
|
return createHash("sha256").update(value).digest("hex");
|
|
41
45
|
}
|
|
@@ -98,6 +102,165 @@ function runWrangler(tenantRoot, args, extraEnv = {}, options = {}) {
|
|
|
98
102
|
}
|
|
99
103
|
return result;
|
|
100
104
|
}
|
|
105
|
+
function isTransientWranglerOutput(output) {
|
|
106
|
+
return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|connectivity issue|internal error|aborted/i.test(output);
|
|
107
|
+
}
|
|
108
|
+
async function runPrefixedWranglerWithRetry(tenantRoot, args, {
|
|
109
|
+
env = {},
|
|
110
|
+
write,
|
|
111
|
+
prefix
|
|
112
|
+
}) {
|
|
113
|
+
let lastOutput = "";
|
|
114
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
115
|
+
const result = await runPrefixedCommand(process.execPath, [resolveWranglerBin(), ...args], {
|
|
116
|
+
cwd: tenantRoot,
|
|
117
|
+
env,
|
|
118
|
+
write,
|
|
119
|
+
prefix
|
|
120
|
+
});
|
|
121
|
+
if (result.status === 0) {
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
lastOutput = [result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join("\n");
|
|
125
|
+
if (attempt === 3 || !isTransientWranglerOutput(lastOutput)) {
|
|
126
|
+
throw new Error(lastOutput || `wrangler ${args.join(" ")} failed`);
|
|
127
|
+
}
|
|
128
|
+
writeTreeseedBootstrapLine(
|
|
129
|
+
write,
|
|
130
|
+
{ ...prefix, stage: "retry" },
|
|
131
|
+
`Wrangler command hit a transient failure; retrying in ${2 * attempt}s...`,
|
|
132
|
+
"stderr"
|
|
133
|
+
);
|
|
134
|
+
await sleep(2e3 * attempt);
|
|
135
|
+
}
|
|
136
|
+
throw new Error(lastOutput || `wrangler ${args.join(" ")} failed`);
|
|
137
|
+
}
|
|
138
|
+
function prepareTenantCloudflareDeploy({
|
|
139
|
+
tenantRoot,
|
|
140
|
+
scope,
|
|
141
|
+
target: explicitTarget,
|
|
142
|
+
dryRun,
|
|
143
|
+
write,
|
|
144
|
+
env = process.env
|
|
145
|
+
}) {
|
|
146
|
+
const target = explicitTarget ?? createPersistentDeployTarget(scope === "local" ? "staging" : scope);
|
|
147
|
+
if (scope !== "local") {
|
|
148
|
+
assertDeploymentInitialized(tenantRoot, { target });
|
|
149
|
+
runTenantDeployPreflight({ cwd: tenantRoot, scope });
|
|
150
|
+
}
|
|
151
|
+
const { wranglerPath, deployConfig, state } = ensureGeneratedWranglerConfig(tenantRoot, { target });
|
|
152
|
+
const deployState = loadDeployState(tenantRoot, deployConfig, { target });
|
|
153
|
+
const pagesProjectName = target.kind === "persistent" ? deployState.pages?.projectName ?? null : null;
|
|
154
|
+
const pagesBranchName = target.kind === "persistent" ? target.scope === "prod" ? deployState.pages?.productionBranch ?? "main" : deployState.pages?.stagingBranch ?? "staging" : null;
|
|
155
|
+
return {
|
|
156
|
+
tenantRoot,
|
|
157
|
+
scope,
|
|
158
|
+
target,
|
|
159
|
+
dryRun,
|
|
160
|
+
wranglerPath,
|
|
161
|
+
databaseName: state.d1Databases.SITE_DATA_DB.databaseName,
|
|
162
|
+
pagesProjectName,
|
|
163
|
+
pagesBranchName,
|
|
164
|
+
env: {
|
|
165
|
+
...process.env,
|
|
166
|
+
...env,
|
|
167
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
168
|
+
},
|
|
169
|
+
write
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function runTenantDataMigration(context) {
|
|
173
|
+
if (context.dryRun) {
|
|
174
|
+
writeTreeseedBootstrapLine(context.write, {
|
|
175
|
+
scope: context.scope,
|
|
176
|
+
system: "data",
|
|
177
|
+
task: "d1-migrate",
|
|
178
|
+
stage: "deploy"
|
|
179
|
+
}, `Dry run: would apply remote migrations for ${context.databaseName}.`);
|
|
180
|
+
return { databaseName: context.databaseName, dryRun: true };
|
|
181
|
+
}
|
|
182
|
+
await runPrefixedWranglerWithRetry(context.tenantRoot, [
|
|
183
|
+
"d1",
|
|
184
|
+
"migrations",
|
|
185
|
+
"apply",
|
|
186
|
+
context.databaseName,
|
|
187
|
+
"--remote",
|
|
188
|
+
"--config",
|
|
189
|
+
context.wranglerPath
|
|
190
|
+
], {
|
|
191
|
+
env: context.env,
|
|
192
|
+
write: context.write,
|
|
193
|
+
prefix: {
|
|
194
|
+
scope: context.scope,
|
|
195
|
+
system: "data",
|
|
196
|
+
task: "d1-migrate",
|
|
197
|
+
stage: "deploy"
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return { databaseName: context.databaseName, dryRun: false };
|
|
201
|
+
}
|
|
202
|
+
async function runTenantWebBuild(context) {
|
|
203
|
+
const prefix = {
|
|
204
|
+
scope: context.scope,
|
|
205
|
+
system: "web",
|
|
206
|
+
task: "build",
|
|
207
|
+
stage: "deploy"
|
|
208
|
+
};
|
|
209
|
+
if (context.dryRun) {
|
|
210
|
+
writeTreeseedBootstrapLine(context.write, prefix, "Dry run: skipped tenant build.");
|
|
211
|
+
return { dryRun: true };
|
|
212
|
+
}
|
|
213
|
+
const result = await runPrefixedCommand(process.execPath, [packageScriptPath("tenant-build")], {
|
|
214
|
+
cwd: context.tenantRoot,
|
|
215
|
+
env: context.env,
|
|
216
|
+
write: context.write,
|
|
217
|
+
prefix
|
|
218
|
+
});
|
|
219
|
+
if (result.status !== 0) {
|
|
220
|
+
throw new Error("tenant-build failed.");
|
|
221
|
+
}
|
|
222
|
+
return { dryRun: false };
|
|
223
|
+
}
|
|
224
|
+
async function runTenantWebPublish(context) {
|
|
225
|
+
const prefix = {
|
|
226
|
+
scope: context.scope,
|
|
227
|
+
system: "web",
|
|
228
|
+
task: "publish",
|
|
229
|
+
stage: "deploy"
|
|
230
|
+
};
|
|
231
|
+
if (context.dryRun) {
|
|
232
|
+
if (context.pagesProjectName) {
|
|
233
|
+
writeTreeseedBootstrapLine(context.write, prefix, `Dry run: would deploy ${deployTargetLabel(context.target)} to Pages project ${context.pagesProjectName} from ${resolve(context.tenantRoot, "dist")}.`);
|
|
234
|
+
} else {
|
|
235
|
+
writeTreeseedBootstrapLine(context.write, prefix, `Dry run: would deploy ${deployTargetLabel(context.target)} with generated Wrangler config at ${resolve(context.wranglerPath)}.`);
|
|
236
|
+
}
|
|
237
|
+
return { dryRun: true };
|
|
238
|
+
}
|
|
239
|
+
if (context.pagesProjectName) {
|
|
240
|
+
const args = [
|
|
241
|
+
"pages",
|
|
242
|
+
"deploy",
|
|
243
|
+
resolve(context.tenantRoot, "dist"),
|
|
244
|
+
"--project-name",
|
|
245
|
+
context.pagesProjectName
|
|
246
|
+
];
|
|
247
|
+
if (context.pagesBranchName) {
|
|
248
|
+
args.push("--branch", context.pagesBranchName);
|
|
249
|
+
}
|
|
250
|
+
await runPrefixedWranglerWithRetry(context.tenantRoot, args, {
|
|
251
|
+
env: context.env,
|
|
252
|
+
write: context.write,
|
|
253
|
+
prefix
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
await runPrefixedWranglerWithRetry(context.tenantRoot, ["deploy", "--config", context.wranglerPath], {
|
|
257
|
+
env: context.env,
|
|
258
|
+
write: context.write,
|
|
259
|
+
prefix
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return { dryRun: false };
|
|
263
|
+
}
|
|
101
264
|
function inferContentType(filePath) {
|
|
102
265
|
const extension = extname(filePath).toLowerCase();
|
|
103
266
|
if (extension === ".json") return "application/json";
|
|
@@ -806,6 +969,10 @@ async function deployProjectPlatform(options) {
|
|
|
806
969
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
807
970
|
const commitSha = currentCommit(options.tenantRoot);
|
|
808
971
|
const branchName = currentRef(options.tenantRoot);
|
|
972
|
+
const selectedSystems = new Set(options.bootstrapSystems ?? ["data", "web", "api", "agents"]);
|
|
973
|
+
const execution = options.bootstrapExecution ?? "parallel";
|
|
974
|
+
const write = options.write;
|
|
975
|
+
const env = { ...process.env, ...options.env ?? {} };
|
|
809
976
|
await reportDeployment(reporter, {
|
|
810
977
|
environment: options.scope,
|
|
811
978
|
deploymentKind: "code",
|
|
@@ -818,19 +985,120 @@ async function deployProjectPlatform(options) {
|
|
|
818
985
|
if (!options.skipProvision) {
|
|
819
986
|
await provisionProjectPlatform({ ...options, reporter });
|
|
820
987
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (options.scope
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
988
|
+
const nodes = [];
|
|
989
|
+
let cloudflareContext = null;
|
|
990
|
+
if (options.scope === "local" && selectedSystems.has("web")) {
|
|
991
|
+
nodes.push({
|
|
992
|
+
id: "web:build",
|
|
993
|
+
run: () => runTenantWebBuild({
|
|
994
|
+
tenantRoot: options.tenantRoot,
|
|
995
|
+
scope: "local",
|
|
996
|
+
dryRun: options.dryRun,
|
|
997
|
+
env,
|
|
998
|
+
write
|
|
999
|
+
})
|
|
1000
|
+
});
|
|
1001
|
+
} else if (selectedSystems.has("data") || selectedSystems.has("web")) {
|
|
1002
|
+
cloudflareContext = prepareTenantCloudflareDeploy({
|
|
1003
|
+
tenantRoot: options.tenantRoot,
|
|
1004
|
+
scope: options.scope,
|
|
1005
|
+
dryRun: options.dryRun,
|
|
1006
|
+
write,
|
|
1007
|
+
env
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
if (cloudflareContext && selectedSystems.has("data")) {
|
|
1011
|
+
const context = cloudflareContext;
|
|
1012
|
+
nodes.push({
|
|
1013
|
+
id: "data:d1-migrate",
|
|
1014
|
+
run: () => runTenantDataMigration(context)
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (cloudflareContext && selectedSystems.has("web")) {
|
|
1018
|
+
const context = cloudflareContext;
|
|
1019
|
+
nodes.push({
|
|
1020
|
+
id: "web:build",
|
|
1021
|
+
run: () => runTenantWebBuild(context)
|
|
1022
|
+
});
|
|
1023
|
+
nodes.push({
|
|
1024
|
+
id: "web:publish",
|
|
1025
|
+
dependencies: ["web:build", ...selectedSystems.has("data") ? ["data:d1-migrate"] : []],
|
|
1026
|
+
run: () => runTenantWebPublish(context)
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
const serviceResultsByKey = /* @__PURE__ */ new Map();
|
|
1030
|
+
let selectedRailwayServiceKeys = [];
|
|
1031
|
+
if (options.scope !== "local" && (selectedSystems.has("api") || selectedSystems.has("agents"))) {
|
|
1032
|
+
const validation = validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env });
|
|
1033
|
+
const selectedServices = validation.services.filter(
|
|
1034
|
+
(service) => service.key === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")
|
|
1035
|
+
);
|
|
1036
|
+
for (const service of selectedServices) {
|
|
1037
|
+
const system = service.key === "api" ? "api" : "agents";
|
|
1038
|
+
const nodeId = `${system}:${service.key}-railway-deploy`;
|
|
1039
|
+
selectedRailwayServiceKeys.push(service.key);
|
|
1040
|
+
nodes.push({
|
|
1041
|
+
id: nodeId,
|
|
1042
|
+
dependencies: selectedSystems.has("data") ? ["data:d1-migrate"] : [],
|
|
1043
|
+
run: async () => {
|
|
1044
|
+
const result = await deployRailwayService(options.tenantRoot, service, {
|
|
1045
|
+
dryRun: options.dryRun,
|
|
1046
|
+
write,
|
|
1047
|
+
env,
|
|
1048
|
+
prefix: {
|
|
1049
|
+
scope: options.scope,
|
|
1050
|
+
system,
|
|
1051
|
+
task: `${service.key}-railway-deploy`,
|
|
1052
|
+
stage: "deploy"
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
serviceResultsByKey.set(service.key, result);
|
|
1056
|
+
return result;
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
827
1059
|
}
|
|
828
|
-
|
|
829
|
-
|
|
1060
|
+
}
|
|
1061
|
+
let railwaySchedules = [];
|
|
1062
|
+
let railwayScheduleVerification = { ok: true, checks: [], skipped: true, reason: !selectedSystems.has("agents") ? "agents_not_selected" : options.scope !== "prod" ? "prod_only" : "dry_run" };
|
|
1063
|
+
if (options.scope === "prod" && selectedSystems.has("agents")) {
|
|
1064
|
+
const agentDeployNodeIds = nodes.filter((node) => node.id.startsWith("agents:") && node.id.endsWith("-railway-deploy")).map((node) => node.id);
|
|
1065
|
+
nodes.push({
|
|
1066
|
+
id: "agents:schedules",
|
|
1067
|
+
dependencies: agentDeployNodeIds,
|
|
1068
|
+
run: async () => {
|
|
1069
|
+
writeTreeseedBootstrapLine(write, {
|
|
1070
|
+
scope: options.scope,
|
|
1071
|
+
system: "agents",
|
|
1072
|
+
task: "schedules",
|
|
1073
|
+
stage: "deploy"
|
|
1074
|
+
}, "Reconciling Railway schedules...");
|
|
1075
|
+
railwaySchedules = await ensureRailwayScheduledJobs(options.tenantRoot, options.scope, { dryRun: options.dryRun, env });
|
|
1076
|
+
railwayScheduleVerification = !options.dryRun ? await verifyRailwayScheduledJobs(options.tenantRoot, options.scope) : { ok: true, checks: railwaySchedules, skipped: true, reason: "dry_run" };
|
|
1077
|
+
return {
|
|
1078
|
+
service: "railway-schedules",
|
|
1079
|
+
status: railwayScheduleVerification.ok ? "verified" : "failed",
|
|
1080
|
+
command: "railway schedules reconcile",
|
|
1081
|
+
cwd: options.tenantRoot,
|
|
1082
|
+
publicBaseUrl: null,
|
|
1083
|
+
schedules: railwaySchedules,
|
|
1084
|
+
scheduleVerification: railwayScheduleVerification
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
await runTreeseedBootstrapDag({ nodes, execution });
|
|
1090
|
+
const serviceResults = selectedRailwayServiceKeys.map((serviceKey) => serviceResultsByKey.get(serviceKey)).filter(Boolean);
|
|
1091
|
+
if (options.scope !== "local" && !options.dryRun && (selectedSystems.has("web") || serviceResults.length > 0)) {
|
|
830
1092
|
finalizeDeploymentState(options.tenantRoot, {
|
|
831
1093
|
target: createPersistentDeployTarget(options.scope),
|
|
832
1094
|
serviceResults
|
|
833
1095
|
});
|
|
1096
|
+
}
|
|
1097
|
+
if (options.scope !== "prod" || !selectedSystems.has("agents")) {
|
|
1098
|
+
railwaySchedules = [];
|
|
1099
|
+
railwayScheduleVerification = { ok: true, checks: railwaySchedules, skipped: true, reason: !selectedSystems.has("agents") ? "agents_not_selected" : options.scope !== "prod" ? "prod_only" : "dry_run" };
|
|
1100
|
+
}
|
|
1101
|
+
if (selectedSystems.has("agents")) {
|
|
834
1102
|
serviceResults.push({
|
|
835
1103
|
service: "railway-schedules",
|
|
836
1104
|
status: railwayScheduleVerification.ok ? "verified" : "failed",
|
|
@@ -851,7 +1119,7 @@ async function deployProjectPlatform(options) {
|
|
|
851
1119
|
triggeredByType: "project_runner",
|
|
852
1120
|
metadata: {
|
|
853
1121
|
scope: options.scope,
|
|
854
|
-
railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key),
|
|
1122
|
+
railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key).filter((serviceKey) => serviceKey === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")),
|
|
855
1123
|
monitor
|
|
856
1124
|
},
|
|
857
1125
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -980,9 +1248,13 @@ export {
|
|
|
980
1248
|
deployProjectPlatform,
|
|
981
1249
|
inferEnvironmentFromBranch,
|
|
982
1250
|
monitorProjectPlatform,
|
|
1251
|
+
prepareTenantCloudflareDeploy,
|
|
983
1252
|
provisionProjectPlatform,
|
|
984
1253
|
publishProjectContent,
|
|
985
1254
|
resolveScope,
|
|
986
1255
|
runProjectPlatformAction,
|
|
1256
|
+
runTenantDataMigration,
|
|
1257
|
+
runTenantWebBuild,
|
|
1258
|
+
runTenantWebPublish,
|
|
987
1259
|
syncControlPlaneState
|
|
988
1260
|
};
|
|
@@ -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
|