@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.
Files changed (33) hide show
  1. package/dist/operations/services/bootstrap-runner.d.ts +33 -0
  2. package/dist/operations/services/bootstrap-runner.js +136 -0
  3. package/dist/operations/services/config-runtime.d.ts +27 -8
  4. package/dist/operations/services/config-runtime.js +303 -127
  5. package/dist/operations/services/github-api.d.ts +34 -0
  6. package/dist/operations/services/github-api.js +187 -4
  7. package/dist/operations/services/github-automation.d.ts +30 -0
  8. package/dist/operations/services/github-automation.js +107 -1
  9. package/dist/operations/services/project-platform.d.ts +38 -2
  10. package/dist/operations/services/project-platform.js +319 -15
  11. package/dist/operations/services/railway-deploy.d.ts +6 -2
  12. package/dist/operations/services/railway-deploy.js +26 -18
  13. package/dist/operations/services/runtime-tools.d.ts +0 -2
  14. package/dist/operations/services/runtime-tools.js +0 -2
  15. package/dist/platform/env.yaml +71 -96
  16. package/dist/platform/environment.d.ts +4 -0
  17. package/dist/platform/environment.js +54 -0
  18. package/dist/reconcile/bootstrap-systems.d.ts +32 -0
  19. package/dist/reconcile/bootstrap-systems.js +175 -0
  20. package/dist/reconcile/builtin-adapters.js +1 -9
  21. package/dist/reconcile/desired-state.js +16 -14
  22. package/dist/reconcile/engine.d.ts +9 -4
  23. package/dist/reconcile/engine.js +57 -14
  24. package/dist/reconcile/index.d.ts +1 -0
  25. package/dist/reconcile/index.js +1 -0
  26. package/dist/scripts/config-treeseed.js +30 -0
  27. package/dist/scripts/package-tools.js +0 -2
  28. package/dist/scripts/tenant-deploy.js +16 -36
  29. package/dist/scripts/test-cloudflare-local.js +0 -2
  30. package/dist/workflow/operations.js +23 -4
  31. package/dist/workflow.d.ts +5 -0
  32. package/package.json +1 -1
  33. 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: process.env
852
+ env,
853
+ systems: bootstrapSystems
662
854
  });
663
855
  const verification = await collectTreeseedReconcileStatus({
664
856
  tenantRoot: options.tenantRoot,
665
857
  target,
666
- env: process.env
858
+ env,
859
+ systems: bootstrapSystems
667
860
  });
668
861
  ensureGeneratedWranglerConfig(options.tenantRoot, { target });
669
- const railwayValidation = options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope);
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
- runNodeScript(options.tenantRoot, "tenant-deploy", ["--environment", options.scope, ...options.dryRun ? ["--dry-run"] : []]);
822
- const serviceResults = [];
823
- if (options.scope !== "local") {
824
- const validation = validateRailwayDeployPrerequisites(options.tenantRoot, options.scope);
825
- for (const service of validation.services) {
826
- serviceResults.push(await deployRailwayService(options.tenantRoot, service, { dryRun: options.dryRun }));
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
- const railwaySchedules = options.scope === "prod" ? await ensureRailwayScheduledJobs(options.tenantRoot, options.scope, { dryRun: options.dryRun }) : [];
829
- const railwayScheduleVerification = options.scope === "prod" && !options.dryRun ? await verifyRailwayScheduledJobs(options.tenantRoot, options.scope) : { ok: true, checks: railwaySchedules, skipped: options.scope !== "prod" ? true : options.dryRun, reason: options.scope !== "prod" ? "prod_only" : "dry_run" };
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 | undefined;
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, { dryRun = false } = {}) {
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 = spawnSync("bash", ["-lc", service.buildCommand], {
928
+ const buildResult = await runPrefixedCommand("bash", ["-lc", service.buildCommand], {
916
929
  cwd: service.rootDir,
917
- stdio: "inherit",
918
- env: { ...process.env }
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 = runRailway(plan.args, {
940
+ const result = await runPrefixedCommand(plan.command, plan.args, {
927
941
  cwd: service.rootDir,
928
- capture: true,
929
- allowFailure: true
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
- console.warn(
947
- `Railway deploy for ${service.serviceName ?? service.serviceId ?? service.key} hit a transient failure; retrying in ${Math.round(backoffMs / 1e3)}s...`
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: buildRailwayCommandEnv(process.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