@treeseed/sdk 0.4.8 → 0.4.10

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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/control-plane-client.d.ts +45 -0
  3. package/dist/control-plane-client.js +229 -0
  4. package/dist/control-plane.d.ts +94 -0
  5. package/dist/control-plane.js +125 -0
  6. package/dist/d1-store.d.ts +56 -1
  7. package/dist/d1-store.js +132 -0
  8. package/dist/dispatch.d.ts +4 -0
  9. package/dist/dispatch.js +180 -0
  10. package/dist/index.d.ts +14 -2
  11. package/dist/index.js +94 -4
  12. package/dist/operations/services/config-runtime.d.ts +10 -0
  13. package/dist/operations/services/config-runtime.js +62 -4
  14. package/dist/operations/services/deploy.d.ts +95 -3
  15. package/dist/operations/services/deploy.js +351 -10
  16. package/dist/operations/services/github-automation.d.ts +37 -1
  17. package/dist/operations/services/github-automation.js +71 -14
  18. package/dist/operations/services/project-platform.d.ts +835 -0
  19. package/dist/operations/services/project-platform.js +782 -0
  20. package/dist/operations/services/railway-deploy.d.ts +113 -18
  21. package/dist/operations/services/railway-deploy.js +357 -8
  22. package/dist/operations/services/runtime-tools.d.ts +25 -1
  23. package/dist/operations/services/runtime-tools.js +66 -5
  24. package/dist/operations/services/template-registry.d.ts +1 -1
  25. package/dist/operations/services/template-registry.js +17 -3
  26. package/dist/platform/books-data.d.ts +3 -4
  27. package/dist/platform/books-data.js +30 -4
  28. package/dist/platform/contracts.d.ts +56 -4
  29. package/dist/platform/deploy-config.js +109 -4
  30. package/dist/platform/deploy-runtime.d.ts +2 -0
  31. package/dist/platform/deploy-runtime.js +9 -1
  32. package/dist/platform/env.yaml +677 -0
  33. package/dist/platform/environment.js +57 -2
  34. package/dist/platform/plugin.d.ts +8 -0
  35. package/dist/platform/plugins/constants.d.ts +2 -0
  36. package/dist/platform/plugins/constants.js +2 -0
  37. package/dist/platform/plugins/runtime.d.ts +2 -0
  38. package/dist/platform/plugins/runtime.js +9 -1
  39. package/dist/platform/plugins.d.ts +1 -1
  40. package/dist/platform/plugins.js +4 -0
  41. package/dist/platform/published-content-pipeline.d.ts +84 -0
  42. package/dist/platform/published-content-pipeline.js +543 -0
  43. package/dist/platform/published-content.d.ts +223 -0
  44. package/dist/platform/published-content.js +588 -0
  45. package/dist/platform/tenant/runtime-config.d.ts +1 -1
  46. package/dist/platform/tenant/runtime-config.js +34 -1
  47. package/dist/platform/tenant-config.d.ts +2 -1
  48. package/dist/platform/tenant-config.js +17 -1
  49. package/dist/platform/utils/site-config-schema.js +104 -0
  50. package/dist/plugin-default.d.ts +2 -0
  51. package/dist/plugin-default.js +2 -0
  52. package/dist/remote.d.ts +65 -9
  53. package/dist/remote.js +104 -28
  54. package/dist/scripts/check-build-warnings.js +50 -0
  55. package/dist/scripts/config-treeseed.js +7 -0
  56. package/dist/scripts/tenant-workflow-action.js +71 -0
  57. package/dist/sdk-dispatch.d.ts +12 -0
  58. package/dist/sdk-dispatch.js +142 -0
  59. package/dist/sdk-types.d.ts +579 -7
  60. package/dist/sdk-types.js +53 -1
  61. package/dist/sdk.d.ts +17 -1
  62. package/dist/sdk.js +109 -0
  63. package/dist/stores/operational-store.d.ts +22 -2
  64. package/dist/stores/operational-store.js +235 -0
  65. package/dist/template-catalog.js +8 -1
  66. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +20 -0
  67. package/dist/types/cloudflare.d.ts +23 -0
  68. package/dist/workflow/operations.d.ts +12 -3
  69. package/dist/workflow/policy.d.ts +1 -1
  70. package/dist/workflow-state.js +2 -1
  71. package/package.json +7 -2
  72. package/templates/github/deploy.workflow.yml +442 -0
  73. package/templates/github/hosted-project.workflow.yml +77 -0
@@ -0,0 +1,782 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, extname, join, resolve } from "node:path";
6
+ import {
7
+ createControlPlaneReporter
8
+ } from "../../control-plane.js";
9
+ import {
10
+ resolvePublishedContentPreviewTtlHours,
11
+ resolveTeamScopedContentLocator,
12
+ signEditorialPreviewToken
13
+ } from "../../platform/published-content.js";
14
+ import { createPublishedContentPipeline } from "../../platform/published-content-pipeline.js";
15
+ import { loadTreeseedManifest } from "../../platform/tenant-config.js";
16
+ import { applyTreeseedEnvironmentToProcess, syncTreeseedRailwayEnvironment } from "./config-runtime.js";
17
+ import {
18
+ createPersistentDeployTarget,
19
+ deployTargetLabel,
20
+ ensureGeneratedWranglerConfig,
21
+ finalizeDeploymentState,
22
+ loadDeployState,
23
+ provisionCloudflareResources,
24
+ syncCloudflareSecrets,
25
+ verifyProvisionedCloudflareResources,
26
+ writeDeployState
27
+ } from "./deploy.js";
28
+ import { currentManagedBranch, PRODUCTION_BRANCH, STAGING_BRANCH } from "./git-workflow.js";
29
+ import {
30
+ configuredRailwayServices,
31
+ deployRailwayService,
32
+ ensureRailwayScheduledJobs,
33
+ validateRailwayDeployPrerequisites,
34
+ validateRailwayServiceConfiguration,
35
+ verifyRailwayScheduledJobs
36
+ } from "./railway-deploy.js";
37
+ import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "./runtime-tools.js";
38
+ import { CloudflareQueuePullClient, CloudflareQueuePushClient } from "../../remote.js";
39
+ function stableHash(value) {
40
+ return createHash("sha256").update(value).digest("hex");
41
+ }
42
+ function inferEnvironmentFromBranch(tenantRoot) {
43
+ const branch = currentManagedBranch(tenantRoot);
44
+ if (branch === STAGING_BRANCH) {
45
+ return "staging";
46
+ }
47
+ if (branch === PRODUCTION_BRANCH) {
48
+ return "prod";
49
+ }
50
+ return "staging";
51
+ }
52
+ function resolveScope(environment) {
53
+ const scope = environment ?? (process.env.CI ? inferEnvironmentFromBranch(process.cwd()) : "local");
54
+ if (!["local", "staging", "prod"].includes(scope)) {
55
+ throw new Error(`Unsupported environment "${scope}". Expected local, staging, or prod.`);
56
+ }
57
+ return scope;
58
+ }
59
+ function currentCommit(tenantRoot) {
60
+ const result = spawnSync("git", ["rev-parse", "HEAD"], {
61
+ cwd: tenantRoot,
62
+ stdio: "pipe",
63
+ encoding: "utf8"
64
+ });
65
+ return result.status === 0 ? result.stdout.trim() : null;
66
+ }
67
+ function currentRef(tenantRoot) {
68
+ const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
69
+ cwd: tenantRoot,
70
+ stdio: "pipe",
71
+ encoding: "utf8"
72
+ });
73
+ return result.status === 0 ? result.stdout.trim() : null;
74
+ }
75
+ function sanitizeSegment(value, fallback) {
76
+ const normalized = String(value ?? "").trim().replaceAll(/[\\/]+/g, "-").replaceAll(/[^a-zA-Z0-9._-]+/g, "-").replaceAll(/-+/g, "-").replaceAll(/^-|-$/g, "");
77
+ return normalized || fallback;
78
+ }
79
+ function runNodeScript(tenantRoot, scriptName, scriptArgs = []) {
80
+ const result = spawnSync(process.execPath, [packageScriptPath(scriptName), ...scriptArgs], {
81
+ cwd: tenantRoot,
82
+ stdio: "inherit",
83
+ env: { ...process.env }
84
+ });
85
+ if (result.status !== 0) {
86
+ throw new Error(`${scriptName} failed.`);
87
+ }
88
+ }
89
+ function runWrangler(tenantRoot, args, extraEnv = {}, options = {}) {
90
+ const result = spawnSync(process.execPath, [resolveWranglerBin(), ...args], {
91
+ cwd: tenantRoot,
92
+ stdio: options.capture ? "pipe" : "inherit",
93
+ encoding: options.capture ? "utf8" : void 0,
94
+ env: { ...process.env, ...extraEnv }
95
+ });
96
+ if (result.status !== 0 && !options.allowFailure) {
97
+ throw new Error(`wrangler ${args.join(" ")} failed`);
98
+ }
99
+ return result;
100
+ }
101
+ function inferContentType(filePath) {
102
+ const extension = extname(filePath).toLowerCase();
103
+ if (extension === ".json") return "application/json";
104
+ if (extension === ".md") return "text/markdown; charset=utf-8";
105
+ if (extension === ".mdx") return "text/mdx; charset=utf-8";
106
+ return "application/octet-stream";
107
+ }
108
+ function writeTempFile(root, name, body) {
109
+ const filePath = resolve(root, name);
110
+ writeFileSync(filePath, body);
111
+ return filePath;
112
+ }
113
+ function toBuffer(body) {
114
+ if (typeof body === "string") {
115
+ return Buffer.from(body);
116
+ }
117
+ if (body instanceof ArrayBuffer) {
118
+ return Buffer.from(body);
119
+ }
120
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
121
+ }
122
+ function readR2JsonObject(tenantRoot, bucketName, objectKey, wranglerPath, wranglerEnv) {
123
+ const tempRoot = mkdtempSync(join(tmpdir(), "treeseed-r2-read-"));
124
+ const filePath = resolve(tempRoot, basename(objectKey) || "payload.json");
125
+ try {
126
+ const result = runWrangler(tenantRoot, [
127
+ "r2",
128
+ "object",
129
+ "get",
130
+ `${bucketName}/${objectKey}`,
131
+ "--config",
132
+ wranglerPath,
133
+ "--remote",
134
+ "--file",
135
+ filePath
136
+ ], wranglerEnv, { allowFailure: true });
137
+ if (result.status !== 0 || !statSafe(filePath)) {
138
+ return null;
139
+ }
140
+ return JSON.parse(readFileSync(filePath, "utf8"));
141
+ } finally {
142
+ rmSync(tempRoot, { recursive: true, force: true });
143
+ }
144
+ }
145
+ function statSafe(filePath) {
146
+ try {
147
+ return statSync(filePath);
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ async function reportDeployment(reporter, input) {
153
+ await reporter.reportDeployment(input);
154
+ }
155
+ function resolveReporter(tenantRoot, explicit) {
156
+ if (explicit) {
157
+ return explicit;
158
+ }
159
+ const deployConfig = loadCliDeployConfig(tenantRoot);
160
+ return createControlPlaneReporter({ deployConfig });
161
+ }
162
+ function uploadObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, pointer, filePath) {
163
+ runWrangler(tenantRoot, [
164
+ "r2",
165
+ "object",
166
+ "put",
167
+ `${bucketName}/${pointer.objectKey}`,
168
+ "--config",
169
+ wranglerPath,
170
+ "--remote",
171
+ "--force",
172
+ "--file",
173
+ filePath,
174
+ "--content-type",
175
+ pointer.contentType ?? inferContentType(filePath)
176
+ ], wranglerEnv);
177
+ }
178
+ function objectFileName(pointer) {
179
+ const ext = extname(pointer.objectKey) || ".json";
180
+ return `${pointer.sha256}${ext}`;
181
+ }
182
+ async function fetchJson(url, init = {}) {
183
+ const response = await fetch(url, init);
184
+ const body = await response.json().catch(() => null);
185
+ return { response, body };
186
+ }
187
+ async function probeHttp(url) {
188
+ try {
189
+ const response = await fetch(url, {
190
+ headers: { accept: "application/json,text/html;q=0.9,*/*;q=0.8" }
191
+ });
192
+ return {
193
+ ok: response.ok,
194
+ status: response.status,
195
+ url
196
+ };
197
+ } catch (error) {
198
+ return {
199
+ ok: false,
200
+ status: null,
201
+ url,
202
+ error: error instanceof Error ? error.message : String(error)
203
+ };
204
+ }
205
+ }
206
+ async function probeRunnerHealth(siteConfig, environment) {
207
+ const baseUrl = String(process.env.TREESEED_MARKET_API_BASE_URL ?? siteConfig.hosting?.marketBaseUrl ?? "").trim();
208
+ const projectId = String(process.env.TREESEED_PROJECT_ID ?? siteConfig.hosting?.projectId ?? "").trim();
209
+ const runnerToken = String(process.env.TREESEED_PROJECT_RUNNER_TOKEN ?? "").trim();
210
+ if (!baseUrl || !projectId || !runnerToken || environment === "local") {
211
+ return { ok: true, skipped: true, reason: "runner_health_unconfigured" };
212
+ }
213
+ try {
214
+ let lastResult = null;
215
+ for (let attempt = 0; attempt < 10; attempt += 1) {
216
+ const { response, body } = await fetchJson(
217
+ `${baseUrl.replace(/\/+$/u, "")}/v1/projects/${encodeURIComponent(projectId)}/runner/health?environment=${encodeURIComponent(environment)}`,
218
+ {
219
+ headers: {
220
+ accept: "application/json",
221
+ authorization: `Bearer ${runnerToken}`
222
+ }
223
+ }
224
+ );
225
+ const pools = Array.isArray(body?.payload?.pools) ? body.payload.pools : [];
226
+ const heartbeatPresent = pools.some((entry) => entry?.latestRegistration);
227
+ lastResult = {
228
+ ok: response.ok && body?.ok === true && heartbeatPresent,
229
+ status: response.status,
230
+ payload: body?.payload ?? null,
231
+ heartbeatPresent,
232
+ attempt: attempt + 1
233
+ };
234
+ if (lastResult.ok) {
235
+ return lastResult;
236
+ }
237
+ await new Promise((resolve2) => setTimeout(resolve2, 3e3));
238
+ }
239
+ return lastResult ?? { ok: false, reason: "runner_health_unavailable" };
240
+ } catch (error) {
241
+ return {
242
+ ok: false,
243
+ error: error instanceof Error ? error.message : String(error)
244
+ };
245
+ }
246
+ }
247
+ function queueClientConfig(siteConfig, state) {
248
+ const accountId = siteConfig.cloudflare.accountId;
249
+ const queueId = state.queues?.agentWork?.queueId;
250
+ const token = process.env.TREESEED_QUEUE_PUSH_TOKEN?.trim() || process.env.TREESEED_QUEUE_PULL_TOKEN?.trim() || process.env.CLOUDFLARE_API_TOKEN?.trim() || "";
251
+ if (!accountId || !queueId || !token) {
252
+ return null;
253
+ }
254
+ return {
255
+ accountId,
256
+ queueId,
257
+ token,
258
+ apiBaseUrl: process.env.TREESEED_QUEUE_API_BASE_URL?.trim() || void 0
259
+ };
260
+ }
261
+ async function probeQueue(siteConfig, state) {
262
+ const config = queueClientConfig(siteConfig, state);
263
+ if (!config) {
264
+ return { ok: false, skipped: true, reason: "queue_probe_unconfigured" };
265
+ }
266
+ const pushClient = new CloudflareQueuePushClient(config);
267
+ const pullClient = new CloudflareQueuePullClient(config);
268
+ const messageId = `health-${Date.now()}`;
269
+ await pushClient.enqueue({
270
+ message: {
271
+ messageId,
272
+ taskId: messageId,
273
+ workDayId: "health-check",
274
+ agentId: "health-check",
275
+ taskType: "health_check",
276
+ idempotencyKey: messageId,
277
+ payloadRef: "health",
278
+ graphVersion: null,
279
+ budgetHint: 0
280
+ }
281
+ });
282
+ const pulled = await pullClient.pull({
283
+ batchSize: 1,
284
+ visibilityTimeoutMs: 1e4
285
+ });
286
+ const message = pulled.messages.find((entry) => entry.body?.messageId === messageId) ?? pulled.messages[0] ?? null;
287
+ if (!message) {
288
+ return { ok: false, reason: "queue_pull_empty" };
289
+ }
290
+ await pullClient.ack([message.leaseId]);
291
+ return {
292
+ ok: true,
293
+ messageId,
294
+ attempts: message.attempts
295
+ };
296
+ }
297
+ function r2HealthKey(state) {
298
+ return `${state.content?.manifestKey?.replace(/\/common\.json$/u, "") ?? "health"}/healthchecks/${Date.now()}.json`;
299
+ }
300
+ function deleteR2Object(tenantRoot, bucketName, objectKey, wranglerPath, wranglerEnv) {
301
+ runWrangler(tenantRoot, [
302
+ "r2",
303
+ "object",
304
+ "delete",
305
+ `${bucketName}/${objectKey}`,
306
+ "--config",
307
+ wranglerPath,
308
+ "--remote"
309
+ ], wranglerEnv, { allowFailure: true });
310
+ }
311
+ function probeR2(tenantRoot, siteConfig, state, target) {
312
+ const bucketName = state.content?.bucketName;
313
+ if (!bucketName) {
314
+ return { ok: false, skipped: true, reason: "r2_unconfigured" };
315
+ }
316
+ const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
317
+ const wranglerEnv = { CLOUDFLARE_ACCOUNT_ID: siteConfig.cloudflare.accountId };
318
+ const tempRoot = mkdtempSync(join(tmpdir(), "treeseed-r2-health-"));
319
+ const objectKey = r2HealthKey(state);
320
+ try {
321
+ const payload = JSON.stringify({ ok: true, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
322
+ const writeFile = writeTempFile(tempRoot, "probe.json", payload);
323
+ runWrangler(tenantRoot, [
324
+ "r2",
325
+ "object",
326
+ "put",
327
+ `${bucketName}/${objectKey}`,
328
+ "--config",
329
+ wranglerPath,
330
+ "--remote",
331
+ "--force",
332
+ "--file",
333
+ writeFile,
334
+ "--content-type",
335
+ "application/json"
336
+ ], wranglerEnv);
337
+ const readBack = readR2JsonObject(tenantRoot, bucketName, objectKey, wranglerPath, wranglerEnv);
338
+ return {
339
+ ok: Boolean(readBack?.ok),
340
+ objectKey
341
+ };
342
+ } finally {
343
+ deleteR2Object(tenantRoot, bucketName, objectKey, wranglerPath, wranglerEnv);
344
+ rmSync(tempRoot, { recursive: true, force: true });
345
+ }
346
+ }
347
+ function probeScaleConfiguration(siteConfig, state) {
348
+ const worker = state.services?.worker ?? {};
349
+ const scalerKind = String(process.env.TREESEED_WORKER_POOL_SCALER ?? "").trim();
350
+ return {
351
+ ok: Boolean(
352
+ (scalerKind === "railway" || siteConfig.services?.worker?.provider === "railway") && (worker.serviceId || process.env.TREESEED_RAILWAY_WORKER_SERVICE_ID) && (process.env.TREESEED_RAILWAY_ENVIRONMENT_ID || process.env.TREESEED_RAILWAY_PROJECT_ID || worker.projectId)
353
+ ),
354
+ mocked: true,
355
+ serviceId: worker.serviceId ?? null
356
+ };
357
+ }
358
+ async function publishContent(options, reporter) {
359
+ const siteConfig = loadCliDeployConfig(options.tenantRoot);
360
+ const tenantConfig = loadTreeseedManifest(resolve(options.tenantRoot, "src", "manifest.yaml"));
361
+ const teamId = siteConfig.hosting?.teamId ?? siteConfig.slug;
362
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
363
+ const commitSha = currentCommit(options.tenantRoot);
364
+ const branchName = currentRef(options.tenantRoot);
365
+ const previewId = options.previewId ?? `staging-${sanitizeSegment(branchName, "preview")}-${sanitizeSegment(commitSha?.slice(0, 12), "latest")}`;
366
+ const locator = resolveTeamScopedContentLocator(siteConfig, teamId);
367
+ const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
368
+ const { wranglerPath } = ensureGeneratedWranglerConfig(options.tenantRoot, { target });
369
+ const wranglerEnv = { CLOUDFLARE_ACCOUNT_ID: siteConfig.cloudflare.accountId };
370
+ const bucketName = siteConfig.cloudflare.r2?.bucketName;
371
+ if (!bucketName) {
372
+ throw new Error("Treeseed content publish requires cloudflare.r2.bucketName in treeseed.site.yaml.");
373
+ }
374
+ const previousManifest = readR2JsonObject(
375
+ options.tenantRoot,
376
+ bucketName,
377
+ locator.manifestKey,
378
+ wranglerPath,
379
+ wranglerEnv
380
+ );
381
+ const pipeline = createPublishedContentPipeline({
382
+ projectRoot: options.tenantRoot,
383
+ siteConfig,
384
+ tenantConfig,
385
+ teamId,
386
+ generatedAt: timestamp,
387
+ sourceCommit: commitSha,
388
+ sourceRef: branchName,
389
+ previewId
390
+ });
391
+ const built = options.scope === "staging" ? await pipeline.buildEditorialOverlay({ previousManifest, previewId }) : await pipeline.buildProductionRevision({ previousManifest });
392
+ const tempRoot = mkdtempSync(join(tmpdir(), "treeseed-content-publish-"));
393
+ try {
394
+ if (!options.dryRun) {
395
+ for (const object of built.objects) {
396
+ const filePath = writeTempFile(tempRoot, objectFileName(object.pointer), toBuffer(object.body));
397
+ uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, object.pointer, filePath);
398
+ }
399
+ if ("overlay" in built) {
400
+ const overlayFile = writeTempFile(tempRoot, "overlay.json", Buffer.from(JSON.stringify(built.overlay, null, 2)));
401
+ uploadObject(
402
+ options.tenantRoot,
403
+ wranglerPath,
404
+ wranglerEnv,
405
+ bucketName,
406
+ {
407
+ objectKey: built.overlay.locator?.overlayKey ?? `${locator.previewRoot}/${previewId}/overlay.json`,
408
+ sha256: stableHash(readFileSync(overlayFile)),
409
+ size: statSync(overlayFile).size,
410
+ contentType: "application/json"
411
+ },
412
+ overlayFile
413
+ );
414
+ } else {
415
+ const manifestFile = writeTempFile(tempRoot, "manifest.json", Buffer.from(JSON.stringify(built.manifest, null, 2)));
416
+ const snapshotKey = locator.manifestKey.replace(/\/common\.json$/u, `/manifests/${built.manifest.revision}.json`);
417
+ uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
418
+ objectKey: snapshotKey,
419
+ sha256: stableHash(readFileSync(manifestFile)),
420
+ size: statSync(manifestFile).size,
421
+ contentType: "application/json"
422
+ }, manifestFile);
423
+ uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
424
+ objectKey: locator.manifestKey,
425
+ sha256: stableHash(readFileSync(manifestFile)),
426
+ size: statSync(manifestFile).size,
427
+ contentType: "application/json"
428
+ }, manifestFile);
429
+ }
430
+ }
431
+ const state = loadDeployState(options.tenantRoot, siteConfig, { target });
432
+ state.content.lastPublishedManifestRevision = "overlay" in built ? built.overlay.previewId : built.manifest.revision;
433
+ state.content.lastPublishedManifestSha256 = stableHash(
434
+ JSON.stringify("overlay" in built ? built.overlay : built.manifest)
435
+ );
436
+ writeDeployState(options.tenantRoot, state, { target });
437
+ const previewToken = options.scope === "staging" && process.env.TREESEED_EDITORIAL_PREVIEW_SECRET ? signEditorialPreviewToken({
438
+ teamId,
439
+ previewId,
440
+ expiresAt: "overlay" in built ? built.overlay.expiresAt ?? new Date(Date.now() + resolvePublishedContentPreviewTtlHours(siteConfig) * 60 * 60 * 1e3).toISOString() : new Date(Date.now() + resolvePublishedContentPreviewTtlHours(siteConfig) * 60 * 60 * 1e3).toISOString()
441
+ }, process.env.TREESEED_EDITORIAL_PREVIEW_SECRET) : null;
442
+ const previewBaseUrl = state.pages?.url ?? siteConfig.siteUrl;
443
+ const previewUrl = previewToken ? `${previewBaseUrl}?preview=${encodeURIComponent(previewToken)}` : null;
444
+ await reportDeployment(reporter, {
445
+ environment: options.scope,
446
+ deploymentKind: "content",
447
+ status: "success",
448
+ sourceRef: branchName,
449
+ commitSha,
450
+ triggeredByType: "project_runner",
451
+ metadata: {
452
+ mode: options.scope === "staging" ? "editorial_overlay" : "production",
453
+ revision: "overlay" in built ? built.overlay.previewId : built.manifest.revision,
454
+ previewId: options.scope === "staging" ? previewId : null,
455
+ previewUrl,
456
+ entries: ("overlay" in built ? built.overlay.entries : built.manifest.entries).length,
457
+ artifacts: ("overlay" in built ? built.overlay.artifacts : built.manifest.artifacts)?.length ?? 0,
458
+ catalog: built.catalog.length
459
+ },
460
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
461
+ });
462
+ return {
463
+ ok: true,
464
+ scope: options.scope,
465
+ mode: options.scope === "staging" ? "editorial_overlay" : "production",
466
+ revision: "overlay" in built ? built.overlay.previewId : built.manifest.revision,
467
+ previewId: options.scope === "staging" ? previewId : null,
468
+ previewUrl,
469
+ target: deployTargetLabel(target)
470
+ };
471
+ } finally {
472
+ rmSync(tempRoot, { recursive: true, force: true });
473
+ }
474
+ }
475
+ async function provisionProjectPlatform(options) {
476
+ const reporter = resolveReporter(options.tenantRoot, options.reporter);
477
+ const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
478
+ const siteConfig = loadCliDeployConfig(options.tenantRoot);
479
+ const summary = provisionCloudflareResources(options.tenantRoot, { dryRun: options.dryRun, target });
480
+ const verification = verifyProvisionedCloudflareResources(options.tenantRoot, { dryRun: options.dryRun, target });
481
+ ensureGeneratedWranglerConfig(options.tenantRoot, { target });
482
+ const syncedSecrets = syncCloudflareSecrets(options.tenantRoot, { dryRun: options.dryRun, target });
483
+ const syncedRailway = options.scope === "local" ? [] : syncTreeseedRailwayEnvironment({ tenantRoot: options.tenantRoot, scope: options.scope, dryRun: options.dryRun });
484
+ const railwayValidation = options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope);
485
+ const railwaySchedules = options.scope === "local" ? [] : await ensureRailwayScheduledJobs(options.tenantRoot, options.scope, { dryRun: options.dryRun });
486
+ const railwayScheduleVerification = options.scope === "local" || options.dryRun ? { ok: true, checks: railwaySchedules } : await verifyRailwayScheduledJobs(options.tenantRoot, options.scope);
487
+ const state = loadDeployState(options.tenantRoot, siteConfig, { target });
488
+ await reporter.reportEnvironment({
489
+ environment: options.scope,
490
+ deploymentProfile: siteConfig.hosting?.kind ?? "self_hosted_project",
491
+ baseUrl: state.lastDeployedUrl,
492
+ cloudflareAccountId: siteConfig.cloudflare.accountId,
493
+ pagesProjectName: state.pages?.projectName ?? null,
494
+ workerName: state.workerName,
495
+ r2BucketName: state.content?.bucketName ?? null,
496
+ d1DatabaseName: state.d1Databases?.SITE_DATA_DB?.databaseName ?? null,
497
+ queueName: state.queues?.agentWork?.name ?? null,
498
+ railwayProjectName: railwayValidation.services[0]?.projectName ?? null,
499
+ metadata: {
500
+ target: deployTargetLabel(target),
501
+ previewEnabled: state.previewEnabled ?? false,
502
+ readiness: state.readiness
503
+ }
504
+ });
505
+ const resourceReports = [
506
+ {
507
+ environment: options.scope,
508
+ provider: "cloudflare",
509
+ resourceKind: "pages",
510
+ logicalName: state.pages?.projectName ?? "pages",
511
+ locator: state.pages?.url ?? null,
512
+ metadata: state.pages ?? {}
513
+ },
514
+ {
515
+ environment: options.scope,
516
+ provider: "cloudflare",
517
+ resourceKind: "worker",
518
+ logicalName: state.workerName,
519
+ locator: state.lastDeployedUrl ?? null,
520
+ metadata: { workerName: state.workerName }
521
+ },
522
+ {
523
+ environment: options.scope,
524
+ provider: "cloudflare",
525
+ resourceKind: "r2",
526
+ logicalName: state.content?.bucketName ?? "content",
527
+ locator: state.content?.manifestKey ?? null,
528
+ metadata: state.content ?? {}
529
+ },
530
+ {
531
+ environment: options.scope,
532
+ provider: "cloudflare",
533
+ resourceKind: "d1",
534
+ logicalName: state.d1Databases?.SITE_DATA_DB?.databaseName ?? "site-data",
535
+ locator: state.d1Databases?.SITE_DATA_DB?.databaseId ?? null,
536
+ metadata: state.d1Databases?.SITE_DATA_DB ?? {}
537
+ },
538
+ {
539
+ environment: options.scope,
540
+ provider: "cloudflare",
541
+ resourceKind: "queue",
542
+ logicalName: state.queues?.agentWork?.name ?? "agent-work",
543
+ locator: state.queues?.agentWork?.binding ?? null,
544
+ metadata: state.queues?.agentWork ?? {}
545
+ }
546
+ ];
547
+ for (const resource of resourceReports) {
548
+ await reporter.reportResource(resource);
549
+ }
550
+ for (const service of railwayValidation.services) {
551
+ await reporter.reportResource({
552
+ environment: options.scope,
553
+ provider: "railway",
554
+ resourceKind: service.serviceId ? "railway_service" : "railway_project",
555
+ logicalName: service.key,
556
+ locator: service.serviceName ?? service.serviceId ?? service.projectName ?? service.projectId ?? null,
557
+ metadata: service
558
+ });
559
+ }
560
+ for (const schedule of railwaySchedules) {
561
+ const serviceState = state.services?.[schedule.service];
562
+ if (serviceState) {
563
+ serviceState.lastScheduleSyncAt = (/* @__PURE__ */ new Date()).toISOString();
564
+ }
565
+ state.railwaySchedules[schedule.logicalName] = {
566
+ ...state.railwaySchedules[schedule.logicalName] ?? {},
567
+ ...schedule,
568
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
569
+ };
570
+ await reporter.reportResource({
571
+ environment: options.scope,
572
+ provider: "railway",
573
+ resourceKind: "railway_schedule",
574
+ logicalName: schedule.logicalName,
575
+ locator: schedule.id ?? schedule.expression,
576
+ metadata: schedule
577
+ });
578
+ }
579
+ writeDeployState(options.tenantRoot, state, { target });
580
+ await reportDeployment(reporter, {
581
+ environment: options.scope,
582
+ deploymentKind: "provision",
583
+ status: "success",
584
+ sourceRef: currentRef(options.tenantRoot),
585
+ commitSha: currentCommit(options.tenantRoot),
586
+ triggeredByType: "project_runner",
587
+ metadata: {
588
+ target: deployTargetLabel(target),
589
+ summary,
590
+ verification,
591
+ syncedSecrets,
592
+ syncedRailway,
593
+ railwayServices: railwayValidation.services.map((service) => service.key),
594
+ railwaySchedules,
595
+ railwayScheduleVerification
596
+ },
597
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
598
+ });
599
+ return {
600
+ ok: true,
601
+ scope: options.scope,
602
+ target: deployTargetLabel(target),
603
+ summary,
604
+ verification,
605
+ railway: {
606
+ services: railwayValidation.services.map((service) => service.key),
607
+ schedules: railwaySchedules,
608
+ verification: railwayScheduleVerification
609
+ }
610
+ };
611
+ }
612
+ async function deployProjectPlatform(options) {
613
+ const reporter = resolveReporter(options.tenantRoot, options.reporter);
614
+ const commitSha = currentCommit(options.tenantRoot);
615
+ const branchName = currentRef(options.tenantRoot);
616
+ await reportDeployment(reporter, {
617
+ environment: options.scope,
618
+ deploymentKind: "code",
619
+ status: "running",
620
+ sourceRef: branchName,
621
+ commitSha,
622
+ triggeredByType: "project_runner",
623
+ metadata: { scope: options.scope }
624
+ });
625
+ await provisionProjectPlatform({ ...options, reporter });
626
+ runNodeScript(options.tenantRoot, "tenant-deploy", ["--environment", options.scope, ...options.dryRun ? ["--dry-run"] : []]);
627
+ const serviceResults = [];
628
+ if (options.scope !== "local") {
629
+ const validation = validateRailwayDeployPrerequisites(options.tenantRoot, options.scope);
630
+ for (const service of validation.services) {
631
+ serviceResults.push(deployRailwayService(options.tenantRoot, service, { dryRun: options.dryRun }));
632
+ }
633
+ finalizeDeploymentState(options.tenantRoot, {
634
+ target: createPersistentDeployTarget(options.scope),
635
+ serviceResults
636
+ });
637
+ }
638
+ const monitor = await monitorProjectPlatform({ ...options, reporter });
639
+ await reportDeployment(reporter, {
640
+ environment: options.scope,
641
+ deploymentKind: "code",
642
+ status: "success",
643
+ sourceRef: branchName,
644
+ commitSha,
645
+ triggeredByType: "project_runner",
646
+ metadata: {
647
+ scope: options.scope,
648
+ railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key),
649
+ monitor
650
+ },
651
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
652
+ });
653
+ return {
654
+ ok: true,
655
+ scope: options.scope,
656
+ monitor,
657
+ serviceResults
658
+ };
659
+ }
660
+ async function publishProjectContent(options) {
661
+ const reporter = resolveReporter(options.tenantRoot, options.reporter);
662
+ return publishContent(options, reporter);
663
+ }
664
+ async function monitorProjectPlatform(options) {
665
+ const reporter = resolveReporter(options.tenantRoot, options.reporter);
666
+ const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
667
+ const siteConfig = loadCliDeployConfig(options.tenantRoot);
668
+ const state = loadDeployState(options.tenantRoot, siteConfig, { target });
669
+ const apiBaseUrl = siteConfig.services?.api?.environments?.[options.scope]?.baseUrl ?? siteConfig.services?.api?.publicBaseUrl ?? state.services?.api?.lastDeployedUrl ?? null;
670
+ const checks = {
671
+ pages: await probeHttp(state.pages?.url ?? siteConfig.siteUrl),
672
+ apiHealth: apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/healthz`) : { ok: false, skipped: true, reason: "api_url_unconfigured" },
673
+ apiReady: apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/readyz`) : { ok: false, skipped: true, reason: "api_url_unconfigured" },
674
+ d1Health: apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/healthz/deep`) : { ok: false, skipped: true, reason: "api_url_unconfigured" },
675
+ agentHealth: apiBaseUrl ? await probeHttp(`${String(apiBaseUrl).replace(/\/+$/u, "")}/agent/healthz`) : { ok: false, skipped: true, reason: "api_url_unconfigured" },
676
+ r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : probeR2(options.tenantRoot, siteConfig, state, target),
677
+ queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : probeQueue(siteConfig, state),
678
+ runner: probeRunnerHealth(siteConfig, options.scope),
679
+ scaleProbe: probeScaleConfiguration(siteConfig, state),
680
+ readiness: state.readiness
681
+ };
682
+ const resolvedChecks = {
683
+ ...checks,
684
+ r2: await checks.r2,
685
+ queue: await checks.queue,
686
+ runner: await checks.runner
687
+ };
688
+ const ok = [
689
+ resolvedChecks.pages,
690
+ resolvedChecks.apiHealth,
691
+ resolvedChecks.apiReady,
692
+ resolvedChecks.d1Health,
693
+ resolvedChecks.agentHealth,
694
+ resolvedChecks.r2,
695
+ resolvedChecks.queue,
696
+ resolvedChecks.runner,
697
+ resolvedChecks.scaleProbe
698
+ ].every((check) => check?.ok === true || check?.skipped === true);
699
+ if (!ok) {
700
+ throw new Error(`Treeseed monitor failed for ${options.scope}.`);
701
+ }
702
+ await reportDeployment(reporter, {
703
+ environment: options.scope,
704
+ deploymentKind: "mixed",
705
+ status: "success",
706
+ sourceRef: currentRef(options.tenantRoot),
707
+ commitSha: currentCommit(options.tenantRoot),
708
+ triggeredByType: "project_runner",
709
+ metadata: {
710
+ mode: "monitor",
711
+ target: deployTargetLabel(target),
712
+ checks: resolvedChecks
713
+ },
714
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
715
+ });
716
+ return {
717
+ ok,
718
+ target: deployTargetLabel(target),
719
+ checks: resolvedChecks
720
+ };
721
+ }
722
+ async function syncControlPlaneState(options) {
723
+ const reporter = resolveReporter(options.tenantRoot, options.reporter);
724
+ const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
725
+ const siteConfig = loadCliDeployConfig(options.tenantRoot);
726
+ const state = loadDeployState(options.tenantRoot, siteConfig, { target });
727
+ await reporter.reportEnvironment({
728
+ environment: options.scope,
729
+ deploymentProfile: siteConfig.hosting?.kind ?? "self_hosted_project",
730
+ baseUrl: state.lastDeployedUrl,
731
+ cloudflareAccountId: siteConfig.cloudflare.accountId,
732
+ pagesProjectName: state.pages?.projectName ?? null,
733
+ workerName: state.workerName,
734
+ r2BucketName: state.content?.bucketName ?? null,
735
+ d1DatabaseName: state.d1Databases?.SITE_DATA_DB?.databaseName ?? null,
736
+ queueName: state.queues?.agentWork?.name ?? null,
737
+ railwayProjectName: state.services.api?.provider === "railway" ? state.services.api?.lastDeployedUrl ?? null : null,
738
+ metadata: { target: deployTargetLabel(target) }
739
+ });
740
+ }
741
+ async function runProjectPlatformAction(action, options) {
742
+ applyTreeseedEnvironmentToProcess({ tenantRoot: options.tenantRoot, scope: options.scope, override: true });
743
+ const reporter = resolveReporter(options.tenantRoot, options.reporter);
744
+ try {
745
+ switch (action) {
746
+ case "provision":
747
+ return await provisionProjectPlatform({ ...options, reporter });
748
+ case "deploy_code":
749
+ return await deployProjectPlatform({ ...options, reporter });
750
+ case "publish_content":
751
+ return await publishProjectContent({ ...options, reporter });
752
+ case "monitor":
753
+ return await monitorProjectPlatform({ ...options, reporter });
754
+ default:
755
+ throw new Error(`Unsupported workflow action "${action}".`);
756
+ }
757
+ } catch (error) {
758
+ await reportDeployment(reporter, {
759
+ environment: options.scope,
760
+ deploymentKind: action === "provision" ? "provision" : action === "publish_content" ? "content" : action === "deploy_code" ? "code" : "mixed",
761
+ status: "failed",
762
+ sourceRef: currentRef(options.tenantRoot),
763
+ commitSha: currentCommit(options.tenantRoot),
764
+ triggeredByType: "project_runner",
765
+ metadata: {
766
+ message: error instanceof Error ? error.message : String(error)
767
+ },
768
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
769
+ }).catch(() => void 0);
770
+ throw error;
771
+ }
772
+ }
773
+ export {
774
+ deployProjectPlatform,
775
+ inferEnvironmentFromBranch,
776
+ monitorProjectPlatform,
777
+ provisionProjectPlatform,
778
+ publishProjectContent,
779
+ resolveScope,
780
+ runProjectPlatformAction,
781
+ syncControlPlaneState
782
+ };