@treeseed/sdk 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +46 -0
  3. package/dist/operations/providers/default.js +1 -1
  4. package/dist/operations/services/config-runtime.d.ts +49 -42
  5. package/dist/operations/services/config-runtime.js +465 -142
  6. package/dist/operations/services/deploy.d.ts +298 -0
  7. package/dist/operations/services/deploy.js +381 -137
  8. package/dist/operations/services/git-workflow.d.ts +9 -0
  9. package/dist/operations/services/git-workflow.js +32 -0
  10. package/dist/operations/services/github-api.d.ts +115 -0
  11. package/dist/operations/services/github-api.js +455 -0
  12. package/dist/operations/services/github-automation.d.ts +19 -33
  13. package/dist/operations/services/github-automation.js +44 -131
  14. package/dist/operations/services/key-agent.d.ts +20 -1
  15. package/dist/operations/services/key-agent.js +267 -102
  16. package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
  17. package/dist/operations/services/knowledge-coop-launch.js +26 -12
  18. package/dist/operations/services/project-platform.d.ts +157 -150
  19. package/dist/operations/services/project-platform.js +129 -26
  20. package/dist/operations/services/railway-api.d.ts +244 -0
  21. package/dist/operations/services/railway-api.js +882 -0
  22. package/dist/operations/services/railway-deploy.d.ts +171 -27
  23. package/dist/operations/services/railway-deploy.js +672 -172
  24. package/dist/operations/services/runtime-tools.d.ts +18 -0
  25. package/dist/operations/services/runtime-tools.js +19 -6
  26. package/dist/operations/services/workspace-preflight.js +2 -2
  27. package/dist/platform/contracts.d.ts +7 -0
  28. package/dist/platform/deploy-config.js +23 -0
  29. package/dist/platform/deploy-runtime.d.ts +1 -0
  30. package/dist/platform/deploy-runtime.js +7 -9
  31. package/dist/platform/env.yaml +10 -9
  32. package/dist/platform/environment.js +4 -0
  33. package/dist/platform/plugin.d.ts +6 -0
  34. package/dist/platform/plugins/constants.d.ts +1 -0
  35. package/dist/platform/plugins/constants.js +1 -0
  36. package/dist/platform/plugins/runtime.d.ts +4 -0
  37. package/dist/platform/plugins/runtime.js +8 -1
  38. package/dist/platform/published-content.js +27 -4
  39. package/dist/platform/tenant/runtime-config.js +33 -24
  40. package/dist/plugin-default.d.ts +1 -0
  41. package/dist/plugin-default.js +1 -0
  42. package/dist/reconcile/builtin-adapters.d.ts +3 -0
  43. package/dist/reconcile/builtin-adapters.js +2093 -0
  44. package/dist/reconcile/contracts.d.ts +155 -0
  45. package/dist/reconcile/contracts.js +0 -0
  46. package/dist/reconcile/desired-state.d.ts +179 -0
  47. package/dist/reconcile/desired-state.js +319 -0
  48. package/dist/reconcile/engine.d.ts +405 -0
  49. package/dist/reconcile/engine.js +356 -0
  50. package/dist/reconcile/errors.d.ts +5 -0
  51. package/dist/reconcile/errors.js +13 -0
  52. package/dist/reconcile/index.d.ts +7 -0
  53. package/dist/reconcile/index.js +7 -0
  54. package/dist/reconcile/registry.d.ts +7 -0
  55. package/dist/reconcile/registry.js +64 -0
  56. package/dist/reconcile/state.d.ts +7 -0
  57. package/dist/reconcile/state.js +303 -0
  58. package/dist/reconcile/units.d.ts +6 -0
  59. package/dist/reconcile/units.js +68 -0
  60. package/dist/scripts/config-treeseed.js +27 -19
  61. package/dist/scripts/tenant-deploy.js +35 -14
  62. package/dist/workflow/operations.js +127 -22
  63. package/dist/workflow-support.d.ts +3 -1
  64. package/dist/workflow-support.js +50 -0
  65. package/dist/workflow.d.ts +2 -0
  66. package/package.json +7 -1
@@ -0,0 +1,303 @@
1
+ import { createHash } from "node:crypto";
2
+ import { loadCliDeployConfig } from "../operations/services/runtime-tools.js";
3
+ import { loadDeployState, resolveTreeseedResourceIdentity, writeDeployState } from "../operations/services/deploy.js";
4
+ import { targetKey } from "./units.js";
5
+ function stableHash(value) {
6
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
7
+ }
8
+ function railwayUnitTypeForServiceKey(serviceKey) {
9
+ if (serviceKey === "workdayStart") {
10
+ return "railway-service:workday-start";
11
+ }
12
+ if (serviceKey === "workdayReport") {
13
+ return "railway-service:workday-report";
14
+ }
15
+ return `railway-service:${serviceKey}`;
16
+ }
17
+ function emptyPersistedUnitState(unit) {
18
+ return {
19
+ unitId: unit.unitId,
20
+ unitType: unit.unitType,
21
+ provider: unit.provider,
22
+ identity: unit.identity,
23
+ target: unit.target,
24
+ logicalName: unit.logicalName,
25
+ desiredSpecHash: stableHash(unit.spec),
26
+ lastObservedAt: null,
27
+ lastReconciledAt: null,
28
+ lastVerifiedAt: null,
29
+ lastStatus: "pending",
30
+ lastObservedState: {},
31
+ lastReconciledState: {},
32
+ lastDiff: null,
33
+ lastVerification: null,
34
+ lastAction: null,
35
+ resourceLocators: {},
36
+ warnings: [],
37
+ error: null
38
+ };
39
+ }
40
+ function migrateLegacyDeployStateUnits(legacyState, target) {
41
+ const identity = legacyState.identity ?? resolveTreeseedResourceIdentity({
42
+ slug: legacyState.hosting?.projectId ?? legacyState.runtime?.projectId ?? "project",
43
+ hosting: legacyState.hosting ?? {},
44
+ runtime: legacyState.runtime ?? {},
45
+ cloudflare: {}
46
+ }, target);
47
+ const units = {};
48
+ const queue = legacyState.queues?.agentWork;
49
+ if (queue?.name) {
50
+ units[`queue:${queue.name}`] = {
51
+ unitId: `queue:${queue.name}`,
52
+ unitType: "queue",
53
+ provider: "cloudflare",
54
+ identity,
55
+ target,
56
+ logicalName: queue.name,
57
+ desiredSpecHash: "",
58
+ lastObservedAt: legacyState.readiness?.lastValidatedAt ?? null,
59
+ lastReconciledAt: legacyState.lastDeploymentTimestamp ?? null,
60
+ lastVerifiedAt: legacyState.readiness?.lastValidatedAt ?? null,
61
+ lastStatus: queue.queueId ? "ready" : "pending",
62
+ lastObservedState: {
63
+ name: queue.name,
64
+ dlqName: queue.dlqName ?? null,
65
+ queueId: queue.queueId ?? null,
66
+ dlqId: queue.dlqId ?? null
67
+ },
68
+ lastReconciledState: {
69
+ name: queue.name,
70
+ dlqName: queue.dlqName ?? null,
71
+ queueId: queue.queueId ?? null,
72
+ dlqId: queue.dlqId ?? null
73
+ },
74
+ lastDiff: null,
75
+ lastVerification: null,
76
+ lastAction: null,
77
+ resourceLocators: {
78
+ queueId: queue.queueId ?? null,
79
+ dlqId: queue.dlqId ?? null
80
+ },
81
+ warnings: [],
82
+ error: null
83
+ };
84
+ }
85
+ const db = legacyState.d1Databases?.SITE_DATA_DB;
86
+ if (db?.databaseName) {
87
+ units[`database:${db.databaseName}`] = {
88
+ unitId: `database:${db.databaseName}`,
89
+ unitType: "database",
90
+ provider: "cloudflare",
91
+ identity,
92
+ target,
93
+ logicalName: db.databaseName,
94
+ desiredSpecHash: "",
95
+ lastObservedAt: legacyState.readiness?.lastValidatedAt ?? null,
96
+ lastReconciledAt: legacyState.lastDeploymentTimestamp ?? null,
97
+ lastVerifiedAt: legacyState.readiness?.lastValidatedAt ?? null,
98
+ lastStatus: db.databaseId ? "ready" : "pending",
99
+ lastObservedState: {
100
+ databaseName: db.databaseName,
101
+ databaseId: db.databaseId ?? null,
102
+ previewDatabaseId: db.previewDatabaseId ?? null
103
+ },
104
+ lastReconciledState: {
105
+ databaseName: db.databaseName,
106
+ databaseId: db.databaseId ?? null,
107
+ previewDatabaseId: db.previewDatabaseId ?? null
108
+ },
109
+ lastDiff: null,
110
+ lastVerification: null,
111
+ lastAction: null,
112
+ resourceLocators: {
113
+ databaseId: db.databaseId ?? null,
114
+ previewDatabaseId: db.previewDatabaseId ?? null
115
+ },
116
+ warnings: [],
117
+ error: null
118
+ };
119
+ }
120
+ for (const [binding, namespace] of Object.entries(legacyState.kvNamespaces ?? {})) {
121
+ const record = namespace;
122
+ if (!record?.name) continue;
123
+ const unitType = binding === "FORM_GUARD_KV" ? "kv-form-guard" : "kv-session";
124
+ units[`${unitType}:${record.name}`] = {
125
+ unitId: `${unitType}:${record.name}`,
126
+ unitType,
127
+ provider: "cloudflare",
128
+ identity,
129
+ target,
130
+ logicalName: record.name,
131
+ desiredSpecHash: "",
132
+ lastObservedAt: legacyState.readiness?.lastValidatedAt ?? null,
133
+ lastReconciledAt: legacyState.lastDeploymentTimestamp ?? null,
134
+ lastVerifiedAt: legacyState.readiness?.lastValidatedAt ?? null,
135
+ lastStatus: record.id ? "ready" : "pending",
136
+ lastObservedState: { ...record },
137
+ lastReconciledState: { ...record },
138
+ lastDiff: null,
139
+ lastVerification: null,
140
+ lastAction: null,
141
+ resourceLocators: {
142
+ id: record.id ?? null,
143
+ previewId: record.previewId ?? null
144
+ },
145
+ warnings: [],
146
+ error: null
147
+ };
148
+ }
149
+ if (legacyState.content?.bucketName) {
150
+ units[`content-store:${legacyState.content.bucketName}`] = {
151
+ unitId: `content-store:${legacyState.content.bucketName}`,
152
+ unitType: "content-store",
153
+ provider: "cloudflare",
154
+ identity,
155
+ target,
156
+ logicalName: legacyState.content.bucketName,
157
+ desiredSpecHash: "",
158
+ lastObservedAt: legacyState.readiness?.lastValidatedAt ?? null,
159
+ lastReconciledAt: legacyState.lastDeploymentTimestamp ?? null,
160
+ lastVerifiedAt: legacyState.readiness?.lastValidatedAt ?? null,
161
+ lastStatus: "ready",
162
+ lastObservedState: { ...legacyState.content },
163
+ lastReconciledState: { ...legacyState.content },
164
+ lastDiff: null,
165
+ lastVerification: null,
166
+ lastAction: null,
167
+ resourceLocators: {
168
+ bucketName: legacyState.content.bucketName,
169
+ r2Binding: legacyState.content.r2Binding ?? null
170
+ },
171
+ warnings: [],
172
+ error: null
173
+ };
174
+ }
175
+ if (legacyState.pages?.projectName) {
176
+ units[`pages-project:${legacyState.pages.projectName}`] = {
177
+ unitId: `pages-project:${legacyState.pages.projectName}`,
178
+ unitType: "pages-project",
179
+ provider: "cloudflare",
180
+ identity,
181
+ target,
182
+ logicalName: legacyState.pages.projectName,
183
+ desiredSpecHash: "",
184
+ lastObservedAt: legacyState.readiness?.lastValidatedAt ?? null,
185
+ lastReconciledAt: legacyState.lastDeploymentTimestamp ?? null,
186
+ lastVerifiedAt: legacyState.readiness?.lastValidatedAt ?? null,
187
+ lastStatus: "ready",
188
+ lastObservedState: { ...legacyState.pages },
189
+ lastReconciledState: { ...legacyState.pages },
190
+ lastDiff: null,
191
+ lastVerification: null,
192
+ lastAction: null,
193
+ resourceLocators: {
194
+ projectName: legacyState.pages.projectName,
195
+ url: legacyState.pages.url ?? null
196
+ },
197
+ warnings: [],
198
+ error: null
199
+ };
200
+ }
201
+ if (legacyState.workerName) {
202
+ units[`edge-worker:${legacyState.workerName}`] = {
203
+ unitId: `edge-worker:${legacyState.workerName}`,
204
+ unitType: "edge-worker",
205
+ provider: "cloudflare",
206
+ identity,
207
+ target,
208
+ logicalName: legacyState.workerName,
209
+ desiredSpecHash: "",
210
+ lastObservedAt: legacyState.readiness?.lastValidatedAt ?? null,
211
+ lastReconciledAt: legacyState.lastDeploymentTimestamp ?? null,
212
+ lastVerifiedAt: legacyState.readiness?.lastValidatedAt ?? null,
213
+ lastStatus: legacyState.workerName ? "ready" : "pending",
214
+ lastObservedState: { workerName: legacyState.workerName, lastDeployedUrl: legacyState.lastDeployedUrl ?? null },
215
+ lastReconciledState: { workerName: legacyState.workerName, lastDeployedUrl: legacyState.lastDeployedUrl ?? null },
216
+ lastDiff: null,
217
+ lastVerification: null,
218
+ lastAction: null,
219
+ resourceLocators: {
220
+ workerName: legacyState.workerName,
221
+ url: legacyState.lastDeployedUrl ?? null
222
+ },
223
+ warnings: [],
224
+ error: null
225
+ };
226
+ }
227
+ for (const [serviceKey, service] of Object.entries(legacyState.services ?? {})) {
228
+ const record = service;
229
+ if (!record?.enabled || record.provider !== "railway") {
230
+ continue;
231
+ }
232
+ const unitType = railwayUnitTypeForServiceKey(serviceKey);
233
+ const logicalName = record.serviceName ?? record.serviceId ?? serviceKey;
234
+ units[`${unitType}:${logicalName}`] = {
235
+ unitId: `${unitType}:${logicalName}`,
236
+ unitType,
237
+ provider: "railway",
238
+ identity,
239
+ target,
240
+ logicalName,
241
+ desiredSpecHash: "",
242
+ lastObservedAt: legacyState.readiness?.lastValidatedAt ?? null,
243
+ lastReconciledAt: record.lastDeploymentTimestamp ?? null,
244
+ lastVerifiedAt: legacyState.readiness?.lastValidatedAt ?? null,
245
+ lastStatus: record.initialized ? "ready" : "pending",
246
+ lastObservedState: { ...record },
247
+ lastReconciledState: { ...record },
248
+ lastDiff: null,
249
+ lastVerification: null,
250
+ lastAction: null,
251
+ resourceLocators: {
252
+ projectId: record.projectId ?? null,
253
+ serviceId: record.serviceId ?? null,
254
+ serviceName: record.serviceName ?? null,
255
+ publicBaseUrl: record.publicBaseUrl ?? null
256
+ },
257
+ warnings: [],
258
+ error: null
259
+ };
260
+ }
261
+ return units;
262
+ }
263
+ function loadTreeseedReconcileState(tenantRoot, target) {
264
+ const deployConfig = loadCliDeployConfig(tenantRoot);
265
+ const legacyState = loadDeployState(tenantRoot, deployConfig, { target });
266
+ const persistedUnits = legacyState.units && typeof legacyState.units === "object" ? legacyState.units : migrateLegacyDeployStateUnits(legacyState, target);
267
+ return {
268
+ version: 1,
269
+ target,
270
+ dependencyGraphVersion: legacyState.reconcile?.dependencyGraphVersion ?? 1,
271
+ units: { ...persistedUnits }
272
+ };
273
+ }
274
+ function writeTreeseedReconcileState(tenantRoot, reconcileState) {
275
+ const deployConfig = loadCliDeployConfig(tenantRoot);
276
+ const legacyState = loadDeployState(tenantRoot, deployConfig, { target: reconcileState.target });
277
+ writeDeployState(tenantRoot, {
278
+ ...legacyState,
279
+ reconcile: {
280
+ version: reconcileState.version,
281
+ dependencyGraphVersion: reconcileState.dependencyGraphVersion,
282
+ targetKey: targetKey(reconcileState.target)
283
+ },
284
+ units: reconcileState.units
285
+ }, { target: reconcileState.target });
286
+ }
287
+ function ensureTreeseedPersistedUnitState(reconcileState, unit) {
288
+ return reconcileState.units[unit.unitId] ?? emptyPersistedUnitState(unit);
289
+ }
290
+ function updateTreeseedPersistedUnitState(reconcileState, state) {
291
+ reconcileState.units[state.unitId] = state;
292
+ }
293
+ function desiredUnitSpecHash(unit) {
294
+ return stableHash(unit.spec);
295
+ }
296
+ export {
297
+ desiredUnitSpecHash,
298
+ ensureTreeseedPersistedUnitState,
299
+ loadTreeseedReconcileState,
300
+ migrateLegacyDeployStateUnits,
301
+ updateTreeseedPersistedUnitState,
302
+ writeTreeseedReconcileState
303
+ };
@@ -0,0 +1,6 @@
1
+ import type { TreeseedDesiredUnit, TreeseedReconcileTarget, TreeseedReconcileUnitType } from './contracts.ts';
2
+ export declare const TRESEED_RECONCILE_UNIT_TYPES: TreeseedReconcileUnitType[];
3
+ export declare function targetKey(target: TreeseedReconcileTarget): string;
4
+ export declare function createTreeseedReconcileUnitId(unitType: TreeseedReconcileUnitType, logicalName: string): string;
5
+ export declare function topologicallySortDesiredUnits(units: TreeseedDesiredUnit[]): TreeseedDesiredUnit[];
6
+ export declare function reverseTopologicallySortedUnits(units: TreeseedDesiredUnit[]): TreeseedDesiredUnit[];
@@ -0,0 +1,68 @@
1
+ const TRESEED_RECONCILE_UNIT_TYPES = [
2
+ "web-ui",
3
+ "api-runtime",
4
+ "manager-runtime",
5
+ "worker-runtime",
6
+ "workday-start-runtime",
7
+ "workday-report-runtime",
8
+ "edge-worker",
9
+ "content-store",
10
+ "queue",
11
+ "database",
12
+ "kv-form-guard",
13
+ "kv-session",
14
+ "pages-project",
15
+ "custom-domain:web",
16
+ "custom-domain:api",
17
+ "dns-record",
18
+ "railway-service:api",
19
+ "railway-service:manager",
20
+ "railway-service:worker",
21
+ "railway-service:workday-start",
22
+ "railway-service:workday-report"
23
+ ];
24
+ function targetKey(target) {
25
+ return target.kind === "persistent" ? target.scope : `branch:${target.branchName}`;
26
+ }
27
+ function createTreeseedReconcileUnitId(unitType, logicalName) {
28
+ return `${unitType}:${logicalName}`;
29
+ }
30
+ function topologicallySortDesiredUnits(units) {
31
+ const byId = new Map(units.map((unit) => [unit.unitId, unit]));
32
+ const visiting = /* @__PURE__ */ new Set();
33
+ const visited = /* @__PURE__ */ new Set();
34
+ const output = [];
35
+ const visit = (unit) => {
36
+ if (visited.has(unit.unitId)) {
37
+ return;
38
+ }
39
+ if (visiting.has(unit.unitId)) {
40
+ throw new Error(`Treeseed reconcile dependency cycle detected at ${unit.unitId}.`);
41
+ }
42
+ visiting.add(unit.unitId);
43
+ for (const dependencyId of unit.dependencies) {
44
+ const dependency = byId.get(dependencyId);
45
+ if (!dependency) {
46
+ throw new Error(`Treeseed reconcile dependency ${dependencyId} referenced by ${unit.unitId} is missing.`);
47
+ }
48
+ visit(dependency);
49
+ }
50
+ visiting.delete(unit.unitId);
51
+ visited.add(unit.unitId);
52
+ output.push(unit);
53
+ };
54
+ for (const unit of units) {
55
+ visit(unit);
56
+ }
57
+ return output;
58
+ }
59
+ function reverseTopologicallySortedUnits(units) {
60
+ return [...topologicallySortDesiredUnits(units)].reverse();
61
+ }
62
+ export {
63
+ TRESEED_RECONCILE_UNIT_TYPES,
64
+ createTreeseedReconcileUnitId,
65
+ reverseTopologicallySortedUnits,
66
+ targetKey,
67
+ topologicallySortDesiredUnits
68
+ };
@@ -5,6 +5,7 @@ function parseArgs(argv) {
5
5
  const parsed = {
6
6
  scopes: [],
7
7
  sync: 'all',
8
+ bootstrap: false,
8
9
  rotateMachineKey: false,
9
10
  };
10
11
  const rest = [...argv];
@@ -29,6 +30,10 @@ function parseArgs(argv) {
29
30
  parsed.sync = current.split('=', 2)[1] ?? 'all';
30
31
  continue;
31
32
  }
33
+ if (current === '--bootstrap') {
34
+ parsed.bootstrap = true;
35
+ continue;
36
+ }
32
37
  if (current === '--rotate-machine-key') {
33
38
  parsed.rotateMachineKey = true;
34
39
  continue;
@@ -50,30 +55,34 @@ try {
50
55
  }
51
56
  else {
52
57
  applyTreeseedSafeRepairs(tenantRoot);
53
- const context = collectTreeseedConfigContext({
54
- tenantRoot,
55
- scopes,
56
- env: process.env,
57
- });
58
- const updates = scopes.flatMap((scope) => context.entriesByScope[scope].map((entry) => ({
59
- scope,
60
- entryId: entry.id,
61
- value: entry.effectiveValue,
62
- reused: entry.currentValue.length > 0 || entry.suggestedValue.length > 0,
63
- })));
64
- const applyResult = applyTreeseedConfigValues({ tenantRoot, updates });
65
- const result = finalizeTreeseedConfig({
58
+ const applyResult = options.bootstrap
59
+ ? { updated: [], sharedStorageMigrations: [] }
60
+ : (() => {
61
+ const context = collectTreeseedConfigContext({
62
+ tenantRoot,
63
+ scopes,
64
+ env: process.env,
65
+ });
66
+ const updates = scopes.flatMap((scope) => context.entriesByScope[scope].map((entry) => ({
67
+ scope,
68
+ entryId: entry.id,
69
+ value: entry.effectiveValue,
70
+ reused: entry.currentValue.length > 0 || entry.suggestedValue.length > 0,
71
+ })));
72
+ return applyTreeseedConfigValues({ tenantRoot, updates });
73
+ })();
74
+ const result = await finalizeTreeseedConfig({
66
75
  tenantRoot,
67
76
  scopes,
68
77
  sync: options.sync,
69
78
  env: process.env,
70
79
  });
71
80
  const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
72
- console.log('Treeseed config completed.');
81
+ console.log(options.bootstrap ? 'Treeseed bootstrap completed.' : 'Treeseed config completed.');
73
82
  console.log(`Machine config: ${configPath}`);
74
83
  console.log(`Machine key: ${keyPath}`);
75
84
  console.log(`Updated values: ${applyResult.updated.length}`);
76
- console.log(`Initialized environments: ${result.initialized.length}`);
85
+ console.log(`Reconciled environments: ${result.reconciled.length}`);
77
86
  for (const scope of scopes) {
78
87
  const readiness = result.readinessByScope?.[scope];
79
88
  if (!readiness)
@@ -84,10 +93,9 @@ try {
84
93
  if (result.synced.github) {
85
94
  console.log(`GitHub sync: ${result.synced.github.secrets.length} secrets, ${result.synced.github.variables.length} variables (${result.synced.github.repository})`);
86
95
  }
87
- if (result.synced.cloudflare) {
88
- console.log(`Cloudflare sync: ${result.synced.cloudflare.secrets.length} secrets, ${result.synced.cloudflare.varsManagedByWranglerConfig.length} vars via Wrangler config`);
89
- }
90
96
  }
91
97
  }
92
- finally {
98
+ catch (error) {
99
+ console.error(error instanceof Error ? error.message : String(error));
100
+ process.exitCode = 1;
93
101
  }
@@ -99,14 +99,29 @@ function runNodeScript(scriptPath, scriptArgs = [], env = {}) {
99
99
  process.exit(result.status ?? 1);
100
100
  }
101
101
  }
102
+ function isTransientWranglerFailure(result) {
103
+ const output = [result.stderr, result.stdout]
104
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
105
+ .join('\n');
106
+ return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|connectivity issue|internal error/i.test(output);
107
+ }
108
+ function sleepSync(milliseconds) {
109
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
110
+ }
102
111
  function runWranglerDeploy(configPath) {
103
- const result = spawnSync(process.execPath, [resolveWranglerBin(), 'deploy', '--config', configPath], {
104
- stdio: 'inherit',
105
- cwd: tenantRoot,
106
- env: { ...process.env },
107
- });
108
- if (result.status !== 0) {
109
- process.exit(result.status ?? 1);
112
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
113
+ const result = spawnSync(process.execPath, [resolveWranglerBin(), 'deploy', '--config', configPath], {
114
+ stdio: 'inherit',
115
+ cwd: tenantRoot,
116
+ env: { ...process.env },
117
+ });
118
+ if (result.status === 0) {
119
+ return;
120
+ }
121
+ if (attempt === 3 || !isTransientWranglerFailure(result)) {
122
+ process.exit(result.status ?? 1);
123
+ }
124
+ sleepSync(2000 * attempt);
110
125
  }
111
126
  }
112
127
  function runWranglerPagesDeploy(projectName, branchName, outputDir = 'dist') {
@@ -121,13 +136,19 @@ function runWranglerPagesDeploy(projectName, branchName, outputDir = 'dist') {
121
136
  if (branchName) {
122
137
  args.push('--branch', branchName);
123
138
  }
124
- const result = spawnSync(process.execPath, args, {
125
- stdio: 'inherit',
126
- cwd: tenantRoot,
127
- env: { ...process.env },
128
- });
129
- if (result.status !== 0) {
130
- process.exit(result.status ?? 1);
139
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
140
+ const result = spawnSync(process.execPath, args, {
141
+ stdio: 'inherit',
142
+ cwd: tenantRoot,
143
+ env: { ...process.env },
144
+ });
145
+ if (result.status === 0) {
146
+ return;
147
+ }
148
+ if (attempt === 3 || !isTransientWranglerFailure(result)) {
149
+ process.exit(result.status ?? 1);
150
+ }
151
+ sleepSync(2000 * attempt);
131
152
  }
132
153
  }
133
154
  async function main() {