@treeseed/sdk 0.4.13 → 0.5.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.
- package/dist/control-plane-client.d.ts +60 -1
- package/dist/control-plane-client.js +59 -0
- package/dist/control-plane.d.ts +1 -1
- package/dist/control-plane.js +11 -4
- package/dist/d1-store.d.ts +58 -0
- package/dist/d1-store.js +64 -0
- package/dist/dispatch.js +6 -0
- package/dist/graph/schema.js +4 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +32 -0
- package/dist/knowledge-coop.d.ts +223 -0
- package/dist/knowledge-coop.js +82 -0
- package/dist/model-registry.js +79 -0
- package/dist/operations/providers/default.js +126 -7
- package/dist/operations/services/config-runtime.d.ts +102 -24
- package/dist/operations/services/config-runtime.js +896 -160
- package/dist/operations/services/deploy.d.ts +223 -15
- package/dist/operations/services/deploy.js +626 -55
- package/dist/operations/services/github-automation.d.ts +60 -0
- package/dist/operations/services/github-automation.js +138 -0
- package/dist/operations/services/key-agent.d.ts +118 -0
- package/dist/operations/services/key-agent.js +476 -0
- package/dist/operations/services/knowledge-coop-launch.d.ts +90 -0
- package/dist/operations/services/knowledge-coop-launch.js +753 -0
- package/dist/operations/services/knowledge-coop-packaging.d.ts +59 -0
- package/dist/operations/services/knowledge-coop-packaging.js +234 -0
- package/dist/operations/services/local-dev.d.ts +0 -1
- package/dist/operations/services/local-dev.js +1 -14
- package/dist/operations/services/project-platform.d.ts +42 -182
- package/dist/operations/services/project-platform.js +162 -59
- package/dist/operations/services/railway-deploy.d.ts +1 -0
- package/dist/operations/services/railway-deploy.js +31 -13
- package/dist/operations/services/runtime-tools.d.ts +52 -5
- package/dist/operations/services/runtime-tools.js +186 -26
- package/dist/operations/services/watch-dev.js +2 -4
- package/dist/operations/services/workspace-preflight.d.ts +4 -4
- package/dist/operations/services/workspace-preflight.js +22 -20
- package/dist/operations-registry.js +7 -2
- package/dist/platform/contracts.d.ts +39 -3
- package/dist/platform/deploy-config.d.ts +12 -1
- package/dist/platform/deploy-config.js +214 -15
- package/dist/platform/deploy-runtime.d.ts +1 -0
- package/dist/platform/deploy-runtime.js +10 -2
- package/dist/platform/env.yaml +93 -61
- package/dist/platform/environment.d.ts +13 -2
- package/dist/platform/environment.js +90 -20
- package/dist/platform/plugins/constants.d.ts +1 -0
- package/dist/platform/plugins/constants.js +7 -6
- package/dist/platform/tenant/runtime-config.js +8 -1
- package/dist/platform/tenant-config.js +4 -0
- package/dist/platform/utils/site-config-schema.js +18 -0
- package/dist/plugin-default.js +2 -2
- package/dist/scripts/key-agent.js +165 -0
- package/dist/scripts/tenant-build.js +4 -1
- package/dist/scripts/tenant-check.js +4 -1
- package/dist/scripts/tenant-deploy.js +43 -4
- package/dist/scripts/tenant-dev.js +0 -1
- package/dist/sdk-types.d.ts +2 -2
- package/dist/sdk-types.js +2 -0
- package/dist/sdk.d.ts +13 -0
- package/dist/sdk.js +40 -0
- package/dist/stores/knowledge-coop-store.d.ts +56 -0
- package/dist/stores/knowledge-coop-store.js +482 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +6 -2
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +4 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +25 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +22 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +11 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +17 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +17 -10
- package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +69 -7
- package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +1 -0
- package/dist/workflow/operations.d.ts +98 -0
- package/dist/workflow/operations.js +229 -7
- package/dist/workflow-state.d.ts +54 -2
- package/dist/workflow-state.js +170 -24
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +32 -2
- package/dist/workflow.d.ts +29 -0
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +11 -1
- package/dist/scripts/sync-dev-vars.js +0 -6
|
@@ -3,14 +3,14 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
3
3
|
import { dirname, relative, resolve } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { deriveCloudflareWorkerName } from "../../platform/deploy-config.js";
|
|
6
|
+
import { deriveCloudflareWorkerName, resolveTreeseedWebCachePolicy } from "../../platform/deploy-config.js";
|
|
7
7
|
import { loadCliDeployConfig, resolveWranglerBin } from "./runtime-tools.js";
|
|
8
8
|
const DEFAULT_COMPATIBILITY_DATE = "2026-04-05";
|
|
9
9
|
const DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
|
|
10
10
|
const GENERATED_ROOT = ".treeseed/generated";
|
|
11
11
|
const STATE_ROOT = ".treeseed/state";
|
|
12
12
|
const PERSISTENT_SCOPES = /* @__PURE__ */ new Set(["local", "staging", "prod"]);
|
|
13
|
-
const MANAGED_SERVICE_KEYS = ["api", "
|
|
13
|
+
const MANAGED_SERVICE_KEYS = ["api", "manager", "worker", "workdayStart", "workdayReport"];
|
|
14
14
|
const TRESEED_ENVELOPE_SCHEMA_GENERATION = "runtime-envelopes-v1";
|
|
15
15
|
const TRESEED_MIGRATION_WAVE_ID = "0005_runtime_envelopes";
|
|
16
16
|
const TRESEED_SUPPORTED_PAYLOAD_RANGE = { min: 1, max: 1 };
|
|
@@ -121,6 +121,18 @@ function targetWorkerName(deployConfig, target) {
|
|
|
121
121
|
}
|
|
122
122
|
return `${baseName}-${sanitizeSegment(target.branchName)}`;
|
|
123
123
|
}
|
|
124
|
+
function targetScopedResourceName(baseName, target) {
|
|
125
|
+
if (!baseName) {
|
|
126
|
+
return baseName;
|
|
127
|
+
}
|
|
128
|
+
if (target.kind === "persistent") {
|
|
129
|
+
if (target.scope === "prod") {
|
|
130
|
+
return baseName;
|
|
131
|
+
}
|
|
132
|
+
return `${baseName}-${target.scope}`;
|
|
133
|
+
}
|
|
134
|
+
return `${baseName}-${sanitizeSegment(target.branchName)}`;
|
|
135
|
+
}
|
|
124
136
|
function targetWorkersDevUrl(workerName) {
|
|
125
137
|
return `https://${workerName}.workers.dev`;
|
|
126
138
|
}
|
|
@@ -130,32 +142,38 @@ function relativeFromGeneratedRoot(targetPath, generatedRoot) {
|
|
|
130
142
|
function buildPublicVars(deployConfig) {
|
|
131
143
|
const contentRuntimeProvider = deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay";
|
|
132
144
|
const contentPublishProvider = deployConfig.providers?.content?.publish ?? contentRuntimeProvider;
|
|
133
|
-
const
|
|
145
|
+
const contentServingMode = envOrNull("TREESEED_CONTENT_SERVING_MODE") ?? deployConfig.providers?.content?.serving ?? "local_collections";
|
|
146
|
+
const contentDefaultTeamId = resolveConfiguredHostedTeamId(deployConfig);
|
|
134
147
|
const contentManifestKeyTemplate = deployConfig.cloudflare.r2?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json";
|
|
135
148
|
const contentPreviewRootTemplate = deployConfig.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews";
|
|
136
149
|
const contentManifestKey = contentManifestKeyTemplate.replaceAll("{teamId}", contentDefaultTeamId);
|
|
137
|
-
const
|
|
150
|
+
const managedRuntime = deployConfig.runtime?.mode === "treeseed_managed";
|
|
138
151
|
const workerRailway = deployConfig.services?.worker?.railway ?? {};
|
|
152
|
+
const webCachePolicy = resolveTreeseedWebCachePolicy(deployConfig);
|
|
139
153
|
return {
|
|
140
154
|
TREESEED_HOSTING_KIND: deployConfig.hosting?.kind ?? "self_hosted_project",
|
|
141
155
|
TREESEED_HOSTING_REGISTRATION: deployConfig.hosting?.registration ?? "none",
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
TREESEED_HUB_MODE: deployConfig.hub?.mode ?? "treeseed_hosted",
|
|
157
|
+
TREESEED_RUNTIME_MODE: deployConfig.runtime?.mode ?? "none",
|
|
158
|
+
TREESEED_RUNTIME_REGISTRATION: deployConfig.runtime?.registration ?? "none",
|
|
159
|
+
TREESEED_MARKET_API_BASE_URL: resolveConfiguredMarketBaseUrl(deployConfig),
|
|
160
|
+
TREESEED_HOSTING_TEAM_ID: contentDefaultTeamId,
|
|
161
|
+
TREESEED_PROJECT_ID: resolveConfiguredProjectId(deployConfig),
|
|
145
162
|
TREESEED_AGENT_EXECUTION_PROVIDER: deployConfig.providers?.agents?.execution ?? "stub",
|
|
146
163
|
TREESEED_AGENT_REPOSITORY_PROVIDER: deployConfig.providers?.agents?.repository ?? "stub",
|
|
147
164
|
TREESEED_AGENT_VERIFICATION_PROVIDER: deployConfig.providers?.agents?.verification ?? "stub",
|
|
148
165
|
TREESEED_CONTENT_RUNTIME_PROVIDER: contentRuntimeProvider,
|
|
149
166
|
TREESEED_CONTENT_PUBLISH_PROVIDER: contentPublishProvider,
|
|
167
|
+
TREESEED_CONTENT_SERVING_MODE: contentServingMode,
|
|
150
168
|
TREESEED_CONTENT_DEFAULT_TEAM_ID: contentDefaultTeamId,
|
|
151
169
|
TREESEED_CONTENT_MANIFEST_KEY: contentManifestKey,
|
|
152
170
|
TREESEED_CONTENT_MANIFEST_KEY_TEMPLATE: contentManifestKeyTemplate,
|
|
153
171
|
TREESEED_CONTENT_PREVIEW_ROOT_TEMPLATE: contentPreviewRootTemplate,
|
|
154
172
|
TREESEED_EDITORIAL_PREVIEW_ROOT: contentPreviewRootTemplate.replaceAll("{teamId}", contentDefaultTeamId),
|
|
155
173
|
TREESEED_EDITORIAL_PREVIEW_TTL_HOURS: String(deployConfig.cloudflare.r2?.previewTtlHours ?? 168),
|
|
156
|
-
TREESEED_CONTENT_BUCKET_NAME: deployConfig
|
|
157
|
-
TREESEED_CONTENT_PUBLIC_BASE_URL: deployConfig
|
|
158
|
-
TREESEED_WORKER_POOL_SCALER: envOrNull("TREESEED_WORKER_POOL_SCALER") ?? (
|
|
174
|
+
TREESEED_CONTENT_BUCKET_NAME: resolveConfiguredContentBucketName(deployConfig),
|
|
175
|
+
TREESEED_CONTENT_PUBLIC_BASE_URL: resolveConfiguredContentPublicBaseUrl(deployConfig),
|
|
176
|
+
TREESEED_WORKER_POOL_SCALER: envOrNull("TREESEED_WORKER_POOL_SCALER") ?? (managedRuntime ? "railway" : ""),
|
|
159
177
|
TREESEED_WORKDAY_TIMEZONE: envOrNull("TREESEED_WORKDAY_TIMEZONE") ?? "",
|
|
160
178
|
TREESEED_WORKDAY_WINDOWS_JSON: envOrNull("TREESEED_WORKDAY_WINDOWS_JSON") ?? "",
|
|
161
179
|
TREESEED_WORKDAY_TASK_CREDIT_BUDGET: envOrNull("TREESEED_WORKDAY_TASK_CREDIT_BUDGET") ?? "",
|
|
@@ -170,7 +188,19 @@ function buildPublicVars(deployConfig) {
|
|
|
170
188
|
TREESEED_RAILWAY_PROJECT_ID: envOrNull("TREESEED_RAILWAY_PROJECT_ID") ?? workerRailway.projectId ?? "",
|
|
171
189
|
TREESEED_RAILWAY_ENVIRONMENT_ID: envOrNull("TREESEED_RAILWAY_ENVIRONMENT_ID") ?? "",
|
|
172
190
|
TREESEED_RAILWAY_WORKER_SERVICE_ID: envOrNull("TREESEED_RAILWAY_WORKER_SERVICE_ID") ?? workerRailway.serviceId ?? "",
|
|
173
|
-
TREESEED_PUBLIC_TURNSTILE_SITE_KEY: envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY") ?? ""
|
|
191
|
+
TREESEED_PUBLIC_TURNSTILE_SITE_KEY: envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY") ?? "",
|
|
192
|
+
TREESEED_WEB_CACHE_SOURCE_BROWSER_TTL_SECONDS: String(webCachePolicy.sourcePages.browserTtlSeconds),
|
|
193
|
+
TREESEED_WEB_CACHE_SOURCE_EDGE_TTL_SECONDS: String(webCachePolicy.sourcePages.edgeTtlSeconds),
|
|
194
|
+
TREESEED_WEB_CACHE_SOURCE_STALE_WHILE_REVALIDATE_SECONDS: String(webCachePolicy.sourcePages.staleWhileRevalidateSeconds),
|
|
195
|
+
TREESEED_WEB_CACHE_SOURCE_STALE_IF_ERROR_SECONDS: String(webCachePolicy.sourcePages.staleIfErrorSeconds),
|
|
196
|
+
TREESEED_WEB_CACHE_CONTENT_BROWSER_TTL_SECONDS: String(webCachePolicy.contentPages.browserTtlSeconds),
|
|
197
|
+
TREESEED_WEB_CACHE_CONTENT_EDGE_TTL_SECONDS: String(webCachePolicy.contentPages.edgeTtlSeconds),
|
|
198
|
+
TREESEED_WEB_CACHE_CONTENT_STALE_WHILE_REVALIDATE_SECONDS: String(webCachePolicy.contentPages.staleWhileRevalidateSeconds),
|
|
199
|
+
TREESEED_WEB_CACHE_CONTENT_STALE_IF_ERROR_SECONDS: String(webCachePolicy.contentPages.staleIfErrorSeconds),
|
|
200
|
+
TREESEED_WEB_CACHE_R2_BROWSER_TTL_SECONDS: String(webCachePolicy.r2PublishedObjects.browserTtlSeconds),
|
|
201
|
+
TREESEED_WEB_CACHE_R2_EDGE_TTL_SECONDS: String(webCachePolicy.r2PublishedObjects.edgeTtlSeconds),
|
|
202
|
+
TREESEED_WEB_CACHE_R2_STALE_WHILE_REVALIDATE_SECONDS: String(webCachePolicy.r2PublishedObjects.staleWhileRevalidateSeconds),
|
|
203
|
+
TREESEED_WEB_CACHE_R2_STALE_IF_ERROR_SECONDS: String(webCachePolicy.r2PublishedObjects.staleIfErrorSeconds)
|
|
174
204
|
};
|
|
175
205
|
}
|
|
176
206
|
function buildSecretMap(deployConfig, state) {
|
|
@@ -193,7 +223,7 @@ function defaultStateFromConfig(deployConfig, target) {
|
|
|
193
223
|
const suffix = target.kind === "persistent" ? target.scope : sanitizeSegment(target.branchName);
|
|
194
224
|
const contentManifestKeyTemplate = deployConfig.cloudflare.r2?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json";
|
|
195
225
|
const contentPreviewRootTemplate = deployConfig.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews";
|
|
196
|
-
const contentDefaultTeamId = deployConfig
|
|
226
|
+
const contentDefaultTeamId = resolveConfiguredHostedTeamId(deployConfig);
|
|
197
227
|
const contentManifestKey = contentManifestKeyTemplate.replaceAll("{teamId}", contentDefaultTeamId);
|
|
198
228
|
return {
|
|
199
229
|
version: 2,
|
|
@@ -221,15 +251,15 @@ function defaultStateFromConfig(deployConfig, target) {
|
|
|
221
251
|
},
|
|
222
252
|
queues: {
|
|
223
253
|
agentWork: {
|
|
224
|
-
name: deployConfig.cloudflare.queueName ?? "agent-work",
|
|
225
|
-
dlqName: deployConfig.cloudflare.dlqName ?? "agent-work-dlq",
|
|
254
|
+
name: targetScopedResourceName(deployConfig.cloudflare.queueName ?? "agent-work", target),
|
|
255
|
+
dlqName: targetScopedResourceName(deployConfig.cloudflare.dlqName ?? "agent-work-dlq", target),
|
|
226
256
|
binding: deployConfig.cloudflare.queueBinding ?? "AGENT_WORK_QUEUE",
|
|
227
257
|
queueId: null,
|
|
228
258
|
dlqId: null
|
|
229
259
|
}
|
|
230
260
|
},
|
|
231
261
|
pages: {
|
|
232
|
-
projectName: target.kind === "persistent" ? target.scope === "prod" ? deployConfig
|
|
262
|
+
projectName: target.kind === "persistent" ? target.scope === "prod" ? resolveConfiguredPagesProjectName(deployConfig) : resolveConfiguredPagesPreviewProjectName(deployConfig) : `${resolveConfiguredPagesProjectName(deployConfig)}-preview`,
|
|
233
263
|
productionBranch: deployConfig.cloudflare.pages?.productionBranch ?? "main",
|
|
234
264
|
stagingBranch: deployConfig.cloudflare.pages?.stagingBranch ?? "staging",
|
|
235
265
|
buildOutputDir: deployConfig.cloudflare.pages?.buildOutputDir ?? "dist",
|
|
@@ -239,9 +269,9 @@ function defaultStateFromConfig(deployConfig, target) {
|
|
|
239
269
|
runtimeProvider: deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay",
|
|
240
270
|
publishProvider: deployConfig.providers?.content?.publish ?? deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay",
|
|
241
271
|
defaultTeamId: contentDefaultTeamId,
|
|
242
|
-
r2Binding: deployConfig
|
|
243
|
-
bucketName: deployConfig
|
|
244
|
-
publicBaseUrl: deployConfig
|
|
272
|
+
r2Binding: resolveConfiguredContentBucketBinding(deployConfig),
|
|
273
|
+
bucketName: resolveConfiguredContentBucketName(deployConfig),
|
|
274
|
+
publicBaseUrl: resolveConfiguredContentPublicBaseUrl(deployConfig) || null,
|
|
245
275
|
manifestKeyTemplate: contentManifestKeyTemplate,
|
|
246
276
|
previewRootTemplate: contentPreviewRootTemplate,
|
|
247
277
|
previewTtlHours: deployConfig.cloudflare.r2?.previewTtlHours ?? 168,
|
|
@@ -252,12 +282,45 @@ function defaultStateFromConfig(deployConfig, target) {
|
|
|
252
282
|
hosting: {
|
|
253
283
|
kind: deployConfig.hosting?.kind ?? "self_hosted_project",
|
|
254
284
|
registration: deployConfig.hosting?.registration ?? "none",
|
|
255
|
-
marketBaseUrl: deployConfig
|
|
256
|
-
teamId:
|
|
257
|
-
projectId: deployConfig
|
|
285
|
+
marketBaseUrl: resolveConfiguredMarketBaseUrl(deployConfig) || null,
|
|
286
|
+
teamId: contentDefaultTeamId,
|
|
287
|
+
projectId: resolveConfiguredProjectId(deployConfig)
|
|
288
|
+
},
|
|
289
|
+
hub: {
|
|
290
|
+
mode: deployConfig.hub?.mode ?? "treeseed_hosted"
|
|
291
|
+
},
|
|
292
|
+
runtime: {
|
|
293
|
+
mode: deployConfig.runtime?.mode ?? "none",
|
|
294
|
+
registration: deployConfig.runtime?.registration ?? "none",
|
|
295
|
+
marketBaseUrl: resolveConfiguredMarketBaseUrl(deployConfig) || null,
|
|
296
|
+
teamId: contentDefaultTeamId,
|
|
297
|
+
projectId: resolveConfiguredProjectId(deployConfig)
|
|
298
|
+
},
|
|
299
|
+
webCache: {
|
|
300
|
+
webHost: null,
|
|
301
|
+
contentHost: null,
|
|
302
|
+
webZoneId: null,
|
|
303
|
+
contentZoneId: null,
|
|
304
|
+
webRulesetId: null,
|
|
305
|
+
contentRulesetId: null,
|
|
306
|
+
rulesManaged: false,
|
|
307
|
+
lastSyncedAt: null,
|
|
308
|
+
lastVerifiedAt: null,
|
|
309
|
+
lastError: null,
|
|
310
|
+
deployPurge: {
|
|
311
|
+
lastPurgedAt: null,
|
|
312
|
+
purgeCount: 0,
|
|
313
|
+
lastError: null
|
|
314
|
+
},
|
|
315
|
+
contentPurge: {
|
|
316
|
+
lastPurgedAt: null,
|
|
317
|
+
purgeCount: 0,
|
|
318
|
+
lastError: null
|
|
319
|
+
}
|
|
258
320
|
},
|
|
259
321
|
generatedSecrets: {},
|
|
260
322
|
readiness: {
|
|
323
|
+
phase: "pending",
|
|
261
324
|
configured: false,
|
|
262
325
|
provisioned: false,
|
|
263
326
|
deployable: false,
|
|
@@ -265,6 +328,8 @@ function defaultStateFromConfig(deployConfig, target) {
|
|
|
265
328
|
initializedAt: null,
|
|
266
329
|
lastValidatedAt: null,
|
|
267
330
|
lastConfigFingerprint: null,
|
|
331
|
+
blockers: [],
|
|
332
|
+
warnings: [],
|
|
268
333
|
lastValidationSummary: null
|
|
269
334
|
},
|
|
270
335
|
lastDeployedUrl: target.kind === "branch" ? targetWorkersDevUrl(workerName) : null,
|
|
@@ -385,6 +450,48 @@ function loadDeployState(tenantRoot, deployConfig, options = {}) {
|
|
|
385
450
|
teamId: defaults.hosting?.teamId ?? persisted.hosting?.teamId ?? deployConfig.slug,
|
|
386
451
|
projectId: defaults.hosting?.projectId ?? persisted.hosting?.projectId ?? deployConfig.slug
|
|
387
452
|
},
|
|
453
|
+
hub: {
|
|
454
|
+
...defaults.hub ?? {},
|
|
455
|
+
...persisted.hub ?? {},
|
|
456
|
+
mode: defaults.hub?.mode ?? persisted.hub?.mode ?? "treeseed_hosted"
|
|
457
|
+
},
|
|
458
|
+
runtime: {
|
|
459
|
+
...defaults.runtime ?? {},
|
|
460
|
+
...persisted.runtime ?? {},
|
|
461
|
+
mode: defaults.runtime?.mode ?? persisted.runtime?.mode ?? "none",
|
|
462
|
+
registration: defaults.runtime?.registration ?? persisted.runtime?.registration ?? "none",
|
|
463
|
+
marketBaseUrl: defaults.runtime?.marketBaseUrl ?? persisted.runtime?.marketBaseUrl ?? null,
|
|
464
|
+
teamId: defaults.runtime?.teamId ?? persisted.runtime?.teamId ?? deployConfig.slug,
|
|
465
|
+
projectId: defaults.runtime?.projectId ?? persisted.runtime?.projectId ?? deployConfig.slug
|
|
466
|
+
},
|
|
467
|
+
webCache: {
|
|
468
|
+
...defaults.webCache ?? {},
|
|
469
|
+
...persisted.webCache ?? {},
|
|
470
|
+
webHost: persisted.webCache?.webHost ?? persisted.webCache?.publicHost ?? defaults.webCache?.webHost ?? null,
|
|
471
|
+
contentHost: persisted.webCache?.contentHost ?? defaults.webCache?.contentHost ?? null,
|
|
472
|
+
webZoneId: persisted.webCache?.webZoneId ?? persisted.webCache?.zoneId ?? defaults.webCache?.webZoneId ?? null,
|
|
473
|
+
contentZoneId: persisted.webCache?.contentZoneId ?? defaults.webCache?.contentZoneId ?? null,
|
|
474
|
+
webRulesetId: persisted.webCache?.webRulesetId ?? persisted.webCache?.rulesetId ?? defaults.webCache?.webRulesetId ?? null,
|
|
475
|
+
contentRulesetId: persisted.webCache?.contentRulesetId ?? defaults.webCache?.contentRulesetId ?? null,
|
|
476
|
+
rulesManaged: persisted.webCache?.rulesManaged ?? defaults.webCache?.rulesManaged ?? false,
|
|
477
|
+
lastSyncedAt: persisted.webCache?.lastSyncedAt ?? defaults.webCache?.lastSyncedAt ?? null,
|
|
478
|
+
lastVerifiedAt: persisted.webCache?.lastVerifiedAt ?? defaults.webCache?.lastVerifiedAt ?? null,
|
|
479
|
+
lastError: persisted.webCache?.lastError ?? defaults.webCache?.lastError ?? null,
|
|
480
|
+
deployPurge: {
|
|
481
|
+
...defaults.webCache?.deployPurge ?? {},
|
|
482
|
+
...persisted.webCache?.deployPurge ?? {},
|
|
483
|
+
lastPurgedAt: persisted.webCache?.deployPurge?.lastPurgedAt ?? defaults.webCache?.deployPurge?.lastPurgedAt ?? null,
|
|
484
|
+
purgeCount: persisted.webCache?.deployPurge?.purgeCount ?? defaults.webCache?.deployPurge?.purgeCount ?? 0,
|
|
485
|
+
lastError: persisted.webCache?.deployPurge?.lastError ?? defaults.webCache?.deployPurge?.lastError ?? null
|
|
486
|
+
},
|
|
487
|
+
contentPurge: {
|
|
488
|
+
...defaults.webCache?.contentPurge ?? {},
|
|
489
|
+
...persisted.webCache?.contentPurge ?? {},
|
|
490
|
+
lastPurgedAt: persisted.webCache?.contentPurge?.lastPurgedAt ?? defaults.webCache?.contentPurge?.lastPurgedAt ?? null,
|
|
491
|
+
purgeCount: persisted.webCache?.contentPurge?.purgeCount ?? defaults.webCache?.contentPurge?.purgeCount ?? 0,
|
|
492
|
+
lastError: persisted.webCache?.contentPurge?.lastError ?? defaults.webCache?.contentPurge?.lastError ?? null
|
|
493
|
+
}
|
|
494
|
+
},
|
|
388
495
|
pages: {
|
|
389
496
|
...defaults.pages ?? {},
|
|
390
497
|
...persisted.pages ?? {},
|
|
@@ -402,12 +509,14 @@ function loadDeployState(tenantRoot, deployConfig, options = {}) {
|
|
|
402
509
|
MANAGED_SERVICE_KEYS.map((serviceKey) => {
|
|
403
510
|
const defaultService = defaults.services?.[serviceKey] ?? {};
|
|
404
511
|
const persistedService = persisted.services?.[serviceKey] ?? {};
|
|
512
|
+
const effectiveDeploymentTimestamp = persistedService.lastDeploymentTimestamp ?? defaultService.lastDeploymentTimestamp ?? null;
|
|
405
513
|
return [
|
|
406
514
|
serviceKey,
|
|
407
515
|
{
|
|
408
516
|
...defaultService,
|
|
409
517
|
...persistedService,
|
|
410
518
|
enabled: defaultService.enabled,
|
|
519
|
+
initialized: persistedService.initialized === true && Boolean(effectiveDeploymentTimestamp),
|
|
411
520
|
provider: defaultService.provider,
|
|
412
521
|
projectId: defaultService.projectId ?? persistedService.projectId ?? null,
|
|
413
522
|
projectName: defaultService.projectName ?? persistedService.projectName ?? null,
|
|
@@ -418,6 +527,7 @@ function loadDeployState(tenantRoot, deployConfig, options = {}) {
|
|
|
418
527
|
environment: defaultService.environment ?? persistedService.environment ?? null,
|
|
419
528
|
schedule: defaultService.schedule ?? persistedService.schedule ?? null,
|
|
420
529
|
publicBaseUrl: defaultService.publicBaseUrl ?? persistedService.publicBaseUrl ?? null,
|
|
530
|
+
lastDeploymentTimestamp: effectiveDeploymentTimestamp,
|
|
421
531
|
lastDeployedUrl: persistedService.lastDeployedUrl ?? defaultService.publicBaseUrl ?? null,
|
|
422
532
|
lastScheduleSyncAt: persistedService.lastScheduleSyncAt ?? defaultService.lastScheduleSyncAt ?? null
|
|
423
533
|
}
|
|
@@ -453,8 +563,8 @@ function buildWranglerConfigContents(tenantRoot, deployConfig, state, options =
|
|
|
453
563
|
const migrationsDir = relativeFromGeneratedRoot(resolve(tenantRoot, "migrations"), generatedRoot);
|
|
454
564
|
const vars = buildPublicVars(deployConfig);
|
|
455
565
|
const r2Config = deployConfig.cloudflare.r2;
|
|
456
|
-
const r2Binding =
|
|
457
|
-
const r2BucketName =
|
|
566
|
+
const r2Binding = resolveConfiguredContentBucketBinding(deployConfig);
|
|
567
|
+
const r2BucketName = resolveConfiguredContentBucketName(deployConfig);
|
|
458
568
|
return [
|
|
459
569
|
`name = ${renderTomlString(workerName)}`,
|
|
460
570
|
`compatibility_date = ${renderTomlString(DEFAULT_COMPATIBILITY_DATE)}`,
|
|
@@ -545,6 +655,10 @@ function parseWranglerJsonOutput(result, label) {
|
|
|
545
655
|
}
|
|
546
656
|
return JSON.parse(source);
|
|
547
657
|
}
|
|
658
|
+
function isWranglerAlreadyExistsError(error, matchers) {
|
|
659
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
660
|
+
return matchers.some((matcher) => matcher.test(message));
|
|
661
|
+
}
|
|
548
662
|
function listKvNamespaces(tenantRoot, env) {
|
|
549
663
|
const result = runWrangler(["kv", "namespace", "list"], {
|
|
550
664
|
cwd: tenantRoot,
|
|
@@ -595,19 +709,318 @@ function isPlaceholderResourceId(value) {
|
|
|
595
709
|
return value.startsWith("local-") || value.startsWith("dryrun-") || value.endsWith("-id") || value.endsWith("-preview-id");
|
|
596
710
|
}
|
|
597
711
|
function buildProvisioningSummary(deployConfig, state, target) {
|
|
712
|
+
const webCachePolicy = resolveTreeseedWebCachePolicy(deployConfig);
|
|
598
713
|
return {
|
|
599
714
|
target: deployTargetLabel(target),
|
|
600
715
|
workerName: state.workerName ?? targetWorkerName(deployConfig, target),
|
|
601
716
|
siteUrl: target.kind === "branch" ? targetWorkersDevUrl(state.workerName) : deployConfig.siteUrl,
|
|
602
|
-
accountId: deployConfig
|
|
717
|
+
accountId: resolveConfiguredCloudflareAccountId(deployConfig),
|
|
603
718
|
pages: state.pages ?? null,
|
|
604
719
|
formGuardKv: state.kvNamespaces.FORM_GUARD_KV,
|
|
605
720
|
sessionKv: state.kvNamespaces.SESSION,
|
|
606
721
|
siteDataDb: state.d1Databases.SITE_DATA_DB,
|
|
607
722
|
queue: state.queues?.agentWork ?? null,
|
|
608
|
-
content: state.content ?? null
|
|
723
|
+
content: state.content ?? null,
|
|
724
|
+
webCache: {
|
|
725
|
+
webHost: state.webCache?.webHost ?? null,
|
|
726
|
+
contentHost: state.webCache?.contentHost ?? null,
|
|
727
|
+
rulesManaged: state.webCache?.rulesManaged === true,
|
|
728
|
+
lastSyncedAt: state.webCache?.lastSyncedAt ?? null,
|
|
729
|
+
lastError: state.webCache?.lastError ?? null,
|
|
730
|
+
policy: webCachePolicy,
|
|
731
|
+
deployPurge: state.webCache?.deployPurge ?? null,
|
|
732
|
+
contentPurge: state.webCache?.contentPurge ?? null
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function safeUrl(value) {
|
|
737
|
+
try {
|
|
738
|
+
return new URL(value);
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function normalizePathPrefix(pathname) {
|
|
744
|
+
const normalized = String(pathname ?? "").replace(/\/+$/u, "");
|
|
745
|
+
if (!normalized || normalized === "/") {
|
|
746
|
+
return "";
|
|
747
|
+
}
|
|
748
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
749
|
+
}
|
|
750
|
+
function resolvePublicWebCacheTarget(deployConfig) {
|
|
751
|
+
const parsed = safeUrl(deployConfig.surfaces?.web?.publicBaseUrl ?? deployConfig.siteUrl);
|
|
752
|
+
if (!parsed) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
host: parsed.hostname,
|
|
757
|
+
pathPrefix: normalizePathPrefix(parsed.pathname)
|
|
609
758
|
};
|
|
610
759
|
}
|
|
760
|
+
function resolvePublicContentCacheTarget(deployConfig) {
|
|
761
|
+
const parsed = safeUrl(resolveConfiguredContentPublicBaseUrl(deployConfig));
|
|
762
|
+
if (!parsed) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
host: parsed.hostname,
|
|
767
|
+
pathPrefix: normalizePathPrefix(parsed.pathname)
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function shouldManageCloudflareWebCacheRules(deployConfig, target) {
|
|
771
|
+
if (target.kind !== "persistent" || target.scope !== "prod") {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
if ((deployConfig.surfaces?.web?.provider ?? deployConfig.providers?.deploy) !== "cloudflare") {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
const webTarget = resolvePublicWebCacheTarget(deployConfig);
|
|
778
|
+
return Boolean(webTarget?.host && !webTarget.host.endsWith(".workers.dev") && !webTarget.host.endsWith(".pages.dev"));
|
|
779
|
+
}
|
|
780
|
+
function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
|
|
781
|
+
const response = spawnSync(
|
|
782
|
+
process.execPath,
|
|
783
|
+
[
|
|
784
|
+
"--input-type=module",
|
|
785
|
+
"-e",
|
|
786
|
+
`import { readFileSync } from 'node:fs';
|
|
787
|
+
const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
|
|
788
|
+
const response = await fetch(input.url, {
|
|
789
|
+
method: input.method,
|
|
790
|
+
headers: {
|
|
791
|
+
authorization: 'Bearer ' + input.token,
|
|
792
|
+
'content-type': 'application/json',
|
|
793
|
+
},
|
|
794
|
+
body: input.body ? JSON.stringify(input.body) : undefined,
|
|
795
|
+
});
|
|
796
|
+
const payload = await response.json().catch(async () => ({ success: false, errors: [{ message: await response.text() }] }));
|
|
797
|
+
process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`
|
|
798
|
+
],
|
|
799
|
+
{
|
|
800
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
801
|
+
encoding: "utf8",
|
|
802
|
+
env: { ...process.env, ...env ?? {} },
|
|
803
|
+
input: JSON.stringify({
|
|
804
|
+
url: `https://api.cloudflare.com/client/v4${path}`,
|
|
805
|
+
method,
|
|
806
|
+
body,
|
|
807
|
+
token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
|
|
808
|
+
})
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
if (response.status !== 0 && !allowFailure) {
|
|
812
|
+
throw new Error(response.stderr?.trim() || `Cloudflare API request failed: ${method} ${path}`);
|
|
813
|
+
}
|
|
814
|
+
const parsed = JSON.parse(response.stdout?.trim() || '{"ok":false,"payload":{"success":false,"errors":[{"message":"empty response"}]}}');
|
|
815
|
+
if (!parsed.ok && !allowFailure) {
|
|
816
|
+
const details = Array.isArray(parsed.payload?.errors) ? parsed.payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "unknown error";
|
|
817
|
+
throw new Error(details || `Cloudflare API request failed: ${method} ${path}`);
|
|
818
|
+
}
|
|
819
|
+
return parsed.payload;
|
|
820
|
+
}
|
|
821
|
+
function resolveCloudflareZoneIdForHost(deployConfig, host, env) {
|
|
822
|
+
if (deployConfig.cloudflare.zoneId) {
|
|
823
|
+
return deployConfig.cloudflare.zoneId;
|
|
824
|
+
}
|
|
825
|
+
const result = cloudflareApiRequest(`/zones?name=${encodeURIComponent(host)}`, { env, allowFailure: true });
|
|
826
|
+
const exact = Array.isArray(result?.result) ? result.result.find((zone) => zone?.name === host) : null;
|
|
827
|
+
if (exact?.id) {
|
|
828
|
+
return exact.id;
|
|
829
|
+
}
|
|
830
|
+
const fallback = cloudflareApiRequest("/zones", { env, allowFailure: true });
|
|
831
|
+
const zones = Array.isArray(fallback?.result) ? fallback.result : [];
|
|
832
|
+
const matched = zones.filter((zone) => typeof zone?.name === "string" && (host === zone.name || host.endsWith(`.${zone.name}`))).sort((left, right) => String(right.name).length - String(left.name).length)[0];
|
|
833
|
+
return matched?.id ?? null;
|
|
834
|
+
}
|
|
835
|
+
function listCloudflareZoneRulesets(zoneId, env) {
|
|
836
|
+
const result = cloudflareApiRequest(`/zones/${zoneId}/rulesets`, { env, allowFailure: true });
|
|
837
|
+
return Array.isArray(result?.result) ? result.result : [];
|
|
838
|
+
}
|
|
839
|
+
function buildTreeseedManagedCloudflareCacheRules(deployConfig, cacheTarget, kind) {
|
|
840
|
+
if (!cacheTarget?.host) {
|
|
841
|
+
return [];
|
|
842
|
+
}
|
|
843
|
+
const policy = resolveTreeseedWebCachePolicy(deployConfig);
|
|
844
|
+
const cachePolicy = kind === "web" ? policy.contentPages : policy.r2PublishedObjects;
|
|
845
|
+
const hostExpression = `(http.host eq "${cacheTarget.host}")`;
|
|
846
|
+
const pathExpression = cacheTarget.pathPrefix ? `(starts_with(http.request.uri.path, "${cacheTarget.pathPrefix}/") or (http.request.uri.path eq "${cacheTarget.pathPrefix}"))` : "true";
|
|
847
|
+
if (kind === "content") {
|
|
848
|
+
return [
|
|
849
|
+
{
|
|
850
|
+
description: "treeseed-managed: cache public r2 objects",
|
|
851
|
+
expression: `((${hostExpression}) and (${pathExpression}) and (http.request.method in {"GET" "HEAD"}))`,
|
|
852
|
+
action: "set_cache_settings",
|
|
853
|
+
action_parameters: {
|
|
854
|
+
cache: true,
|
|
855
|
+
edge_ttl: {
|
|
856
|
+
mode: "override_origin",
|
|
857
|
+
default: cachePolicy.edgeTtlSeconds
|
|
858
|
+
},
|
|
859
|
+
browser_ttl: {
|
|
860
|
+
mode: "override_origin",
|
|
861
|
+
default: cachePolicy.browserTtlSeconds
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
enabled: true
|
|
865
|
+
}
|
|
866
|
+
];
|
|
867
|
+
}
|
|
868
|
+
const sourcePaths = policy.sourcePages.paths.map((path) => path === "/" ? "/" : path.replace(/\/+$/u, ""));
|
|
869
|
+
const sourcePathExpression = sourcePaths.length > 0 ? `(${sourcePaths.map((path) => `(http.request.uri.path eq "${path}")`).join(" or ")})` : "false";
|
|
870
|
+
const notSourcePathExpression = sourcePaths.length > 0 ? `not ${sourcePathExpression}` : "true";
|
|
871
|
+
return [
|
|
872
|
+
{
|
|
873
|
+
description: "treeseed-managed: bypass preview and dynamic routes",
|
|
874
|
+
expression: `((${hostExpression}) and ((starts_with(http.request.uri.path, "/api/")) or (http.request.uri.path eq "/api") or (starts_with(http.request.uri.path, "/auth")) or (starts_with(http.request.uri.path, "/admin")) or (starts_with(http.request.uri.path, "/app")) or (starts_with(http.request.uri.path, "/internal")) or (http.request.uri.query contains "preview=") or (http.cookie contains "treeseed-content-preview=")))`,
|
|
875
|
+
action: "set_cache_settings",
|
|
876
|
+
action_parameters: {
|
|
877
|
+
cache: false
|
|
878
|
+
},
|
|
879
|
+
enabled: true
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
description: "treeseed-managed: cache source html routes",
|
|
883
|
+
expression: `((${hostExpression}) and (${pathExpression}) and (${sourcePathExpression}) and (http.request.method in {"GET" "HEAD"}))`,
|
|
884
|
+
action: "set_cache_settings",
|
|
885
|
+
action_parameters: {
|
|
886
|
+
cache: true,
|
|
887
|
+
edge_ttl: {
|
|
888
|
+
mode: "override_origin",
|
|
889
|
+
default: policy.sourcePages.edgeTtlSeconds
|
|
890
|
+
},
|
|
891
|
+
browser_ttl: {
|
|
892
|
+
mode: "override_origin",
|
|
893
|
+
default: policy.sourcePages.browserTtlSeconds
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
enabled: true
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
description: "treeseed-managed: cache content html routes",
|
|
900
|
+
expression: `((${hostExpression}) and (${pathExpression}) and (${notSourcePathExpression}) and (http.request.method in {"GET" "HEAD"}) and (http.request.uri.path.extension eq "") and not (starts_with(http.request.uri.path, "/api/")) and not (http.request.uri.path eq "/api") and not (starts_with(http.request.uri.path, "/auth")) and not (starts_with(http.request.uri.path, "/admin")) and not (starts_with(http.request.uri.path, "/app")) and not (starts_with(http.request.uri.path, "/internal")) and not (http.request.uri.query contains "preview=") and not (http.cookie contains "treeseed-content-preview="))`,
|
|
901
|
+
action: "set_cache_settings",
|
|
902
|
+
action_parameters: {
|
|
903
|
+
cache: true,
|
|
904
|
+
edge_ttl: {
|
|
905
|
+
mode: "override_origin",
|
|
906
|
+
default: cachePolicy.edgeTtlSeconds
|
|
907
|
+
},
|
|
908
|
+
browser_ttl: {
|
|
909
|
+
mode: "override_origin",
|
|
910
|
+
default: cachePolicy.browserTtlSeconds
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
enabled: true
|
|
914
|
+
}
|
|
915
|
+
];
|
|
916
|
+
}
|
|
917
|
+
function reconcileCloudflareCacheRulesForTarget(role, deployConfig, state, cacheTarget, env, { dryRun = false } = {}) {
|
|
918
|
+
const roleKey = role === "web" ? "Web" : "Content";
|
|
919
|
+
if (!cacheTarget?.host) {
|
|
920
|
+
return { managed: false, skipped: true, reason: "missing_host" };
|
|
921
|
+
}
|
|
922
|
+
const zoneId = resolveCloudflareZoneIdForHost(deployConfig, cacheTarget.host, env);
|
|
923
|
+
if (!zoneId) {
|
|
924
|
+
return { managed: false, skipped: true, reason: "zone_unresolved" };
|
|
925
|
+
}
|
|
926
|
+
const desiredRules = buildTreeseedManagedCloudflareCacheRules(deployConfig, cacheTarget, role);
|
|
927
|
+
state.webCache[role === "web" ? "webHost" : "contentHost"] = cacheTarget.host;
|
|
928
|
+
state.webCache[role === "web" ? "webZoneId" : "contentZoneId"] = zoneId;
|
|
929
|
+
if (dryRun) {
|
|
930
|
+
return { managed: true, dryRun: true, zoneId, host: cacheTarget.host, rules: desiredRules };
|
|
931
|
+
}
|
|
932
|
+
const rulesets = listCloudflareZoneRulesets(zoneId, env);
|
|
933
|
+
const existing = rulesets.find((ruleset) => ruleset?.phase === "http_request_cache_settings") ?? null;
|
|
934
|
+
const prefix = `treeseed-managed:${roleKey.toLowerCase()}:`;
|
|
935
|
+
const unmanagedRules = Array.isArray(existing?.rules) ? existing.rules.filter((rule) => typeof rule?.description !== "string" || !rule.description.startsWith(prefix)) : [];
|
|
936
|
+
const rules = [
|
|
937
|
+
...unmanagedRules,
|
|
938
|
+
...desiredRules.map((rule) => ({ ...rule, description: `${prefix} ${rule.description}` }))
|
|
939
|
+
];
|
|
940
|
+
const payload = existing ? cloudflareApiRequest(`/zones/${zoneId}/rulesets/${existing.id}`, {
|
|
941
|
+
method: "PUT",
|
|
942
|
+
body: { rules },
|
|
943
|
+
env
|
|
944
|
+
}) : cloudflareApiRequest(`/zones/${zoneId}/rulesets`, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
body: {
|
|
947
|
+
name: `Treeseed Managed ${roleKey} Cache Rules`,
|
|
948
|
+
kind: "zone",
|
|
949
|
+
phase: "http_request_cache_settings",
|
|
950
|
+
rules
|
|
951
|
+
},
|
|
952
|
+
env
|
|
953
|
+
});
|
|
954
|
+
const rulesetId = payload?.result?.id ?? existing?.id ?? null;
|
|
955
|
+
state.webCache[role === "web" ? "webRulesetId" : "contentRulesetId"] = rulesetId;
|
|
956
|
+
return { managed: true, zoneId, host: cacheTarget.host, rulesetId };
|
|
957
|
+
}
|
|
958
|
+
function reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, target, { dryRun = false } = {}) {
|
|
959
|
+
if (!shouldManageCloudflareWebCacheRules(deployConfig, target)) {
|
|
960
|
+
const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
|
|
961
|
+
const contentTarget2 = resolvePublicContentCacheTarget(deployConfig);
|
|
962
|
+
state.webCache.webHost = webTarget2?.host ?? null;
|
|
963
|
+
state.webCache.contentHost = contentTarget2?.host ?? null;
|
|
964
|
+
state.webCache.rulesManaged = false;
|
|
965
|
+
state.webCache.lastError = null;
|
|
966
|
+
return { managed: false, skipped: true, reason: "unsupported_target_or_host" };
|
|
967
|
+
}
|
|
968
|
+
const env = {
|
|
969
|
+
CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN ?? ""
|
|
970
|
+
};
|
|
971
|
+
if (!env.CLOUDFLARE_API_TOKEN) {
|
|
972
|
+
state.webCache.webHost = resolvePublicWebCacheTarget(deployConfig)?.host ?? null;
|
|
973
|
+
state.webCache.contentHost = resolvePublicContentCacheTarget(deployConfig)?.host ?? null;
|
|
974
|
+
state.webCache.rulesManaged = false;
|
|
975
|
+
state.webCache.lastError = "CLOUDFLARE_API_TOKEN is required to manage Cloudflare Cache Rules.";
|
|
976
|
+
return { managed: false, skipped: true, reason: "missing_api_token" };
|
|
977
|
+
}
|
|
978
|
+
const webTarget = resolvePublicWebCacheTarget(deployConfig);
|
|
979
|
+
const contentTarget = resolvePublicContentCacheTarget(deployConfig);
|
|
980
|
+
const results = [];
|
|
981
|
+
if (webTarget?.host) {
|
|
982
|
+
results.push(reconcileCloudflareCacheRulesForTarget("web", deployConfig, state, webTarget, env, { dryRun }));
|
|
983
|
+
}
|
|
984
|
+
if (contentTarget?.host) {
|
|
985
|
+
results.push(reconcileCloudflareCacheRulesForTarget("content", deployConfig, state, contentTarget, env, { dryRun }));
|
|
986
|
+
}
|
|
987
|
+
state.webCache.rulesManaged = true;
|
|
988
|
+
state.webCache.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
989
|
+
state.webCache.lastError = null;
|
|
990
|
+
return { managed: true, results };
|
|
991
|
+
}
|
|
992
|
+
function purgeCloudflareCacheByUrls(urls, deployConfig, { env } = {}) {
|
|
993
|
+
const uniqueUrls = [...new Set((urls ?? []).filter(Boolean))];
|
|
994
|
+
if (uniqueUrls.length === 0) {
|
|
995
|
+
return [];
|
|
996
|
+
}
|
|
997
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
998
|
+
for (const urlValue of uniqueUrls) {
|
|
999
|
+
const parsed = safeUrl(urlValue);
|
|
1000
|
+
if (!parsed) {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
const zoneId = resolveCloudflareZoneIdForHost(deployConfig, parsed.hostname, env);
|
|
1004
|
+
if (!zoneId) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const current = grouped.get(zoneId) ?? [];
|
|
1008
|
+
current.push(parsed.toString());
|
|
1009
|
+
grouped.set(zoneId, current);
|
|
1010
|
+
}
|
|
1011
|
+
return [...grouped.entries()].map(([zoneId, files]) => {
|
|
1012
|
+
const payload = cloudflareApiRequest(`/zones/${zoneId}/purge_cache`, {
|
|
1013
|
+
method: "POST",
|
|
1014
|
+
body: { files: [...new Set(files)] },
|
|
1015
|
+
env
|
|
1016
|
+
});
|
|
1017
|
+
return {
|
|
1018
|
+
zoneId,
|
|
1019
|
+
count: [...new Set(files)].length,
|
|
1020
|
+
success: payload?.success === true
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
611
1024
|
function queueName(entry) {
|
|
612
1025
|
return entry?.queue_name ?? entry?.queueName ?? entry?.name ?? null;
|
|
613
1026
|
}
|
|
@@ -619,12 +1032,106 @@ function hasProvisionedCloudflareResources(state) {
|
|
|
619
1032
|
state?.pages?.projectName && state?.pages?.url && state?.d1Databases?.SITE_DATA_DB?.databaseId && state?.kvNamespaces?.FORM_GUARD_KV?.id && state?.kvNamespaces?.SESSION?.id && state?.queues?.agentWork?.name && state?.content?.bucketName
|
|
620
1033
|
);
|
|
621
1034
|
}
|
|
1035
|
+
function absoluteUrlForPath(baseUrl, path) {
|
|
1036
|
+
const parsed = safeUrl(baseUrl);
|
|
1037
|
+
if (!parsed) {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
const normalizedPath = String(path ?? "").startsWith("/") ? String(path) : `/${String(path ?? "")}`;
|
|
1041
|
+
return new URL(normalizedPath, parsed).toString();
|
|
1042
|
+
}
|
|
1043
|
+
function resolveSourcePagePurgeUrls(deployConfig) {
|
|
1044
|
+
const webBaseUrl = deployConfig.surfaces?.web?.publicBaseUrl ?? deployConfig.siteUrl;
|
|
1045
|
+
const paths = resolveTreeseedWebCachePolicy(deployConfig).sourcePages.paths;
|
|
1046
|
+
return paths.map((path) => absoluteUrlForPath(webBaseUrl, path)).filter(Boolean);
|
|
1047
|
+
}
|
|
1048
|
+
function recordCachePurgeResult(targetState, results, error = null) {
|
|
1049
|
+
if (error) {
|
|
1050
|
+
targetState.lastError = error instanceof Error ? error.message : String(error);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
targetState.lastPurgedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1054
|
+
targetState.purgeCount = Array.isArray(results) ? results.reduce((sum, result) => sum + (result?.count ?? 0), 0) : 0;
|
|
1055
|
+
targetState.lastError = null;
|
|
1056
|
+
}
|
|
1057
|
+
function purgeSourcePageCaches(tenantRoot, options = {}) {
|
|
1058
|
+
const target = normalizeTarget(options.scope ?? options.target ?? "prod");
|
|
1059
|
+
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
1060
|
+
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
1061
|
+
const urls = resolveSourcePagePurgeUrls(deployConfig);
|
|
1062
|
+
if ((options.dryRun ?? false) || urls.length === 0 || !process.env.CLOUDFLARE_API_TOKEN) {
|
|
1063
|
+
recordCachePurgeResult(state.webCache.deployPurge, urls.map((url) => ({ count: url ? 1 : 0 })));
|
|
1064
|
+
writeDeployState(tenantRoot, state, { target });
|
|
1065
|
+
return { skipped: options.dryRun ?? false, urls, results: [] };
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const results = purgeCloudflareCacheByUrls(urls, deployConfig, {
|
|
1069
|
+
env: { CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN }
|
|
1070
|
+
});
|
|
1071
|
+
recordCachePurgeResult(state.webCache.deployPurge, results);
|
|
1072
|
+
writeDeployState(tenantRoot, state, { target });
|
|
1073
|
+
return { urls, results };
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
recordCachePurgeResult(state.webCache.deployPurge, [], error);
|
|
1076
|
+
writeDeployState(tenantRoot, state, { target });
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function purgePublishedContentCaches(tenantRoot, urls, options = {}) {
|
|
1081
|
+
const target = normalizeTarget(options.scope ?? options.target ?? "prod");
|
|
1082
|
+
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
1083
|
+
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
1084
|
+
if ((options.dryRun ?? false) || !urls?.length || !process.env.CLOUDFLARE_API_TOKEN) {
|
|
1085
|
+
recordCachePurgeResult(state.webCache.contentPurge, (urls ?? []).map((url) => ({ count: url ? 1 : 0 })));
|
|
1086
|
+
writeDeployState(tenantRoot, state, { target });
|
|
1087
|
+
return { skipped: options.dryRun ?? false, urls: urls ?? [], results: [] };
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
const results = purgeCloudflareCacheByUrls(urls, deployConfig, {
|
|
1091
|
+
env: { CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN }
|
|
1092
|
+
});
|
|
1093
|
+
recordCachePurgeResult(state.webCache.contentPurge, results);
|
|
1094
|
+
writeDeployState(tenantRoot, state, { target });
|
|
1095
|
+
return { urls, results };
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
recordCachePurgeResult(state.webCache.contentPurge, [], error);
|
|
1098
|
+
writeDeployState(tenantRoot, state, { target });
|
|
1099
|
+
throw error;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
622
1102
|
function buildDestroySummary(deployConfig, state, target) {
|
|
623
1103
|
return buildProvisioningSummary(deployConfig, state, target);
|
|
624
1104
|
}
|
|
625
1105
|
function isPlaceholderAccountId(value) {
|
|
626
1106
|
return !value || value === "replace-with-cloudflare-account-id";
|
|
627
1107
|
}
|
|
1108
|
+
function resolveConfiguredCloudflareAccountId(deployConfig) {
|
|
1109
|
+
return envOrNull("CLOUDFLARE_ACCOUNT_ID") ?? deployConfig.cloudflare.accountId;
|
|
1110
|
+
}
|
|
1111
|
+
function resolveConfiguredMarketBaseUrl(deployConfig) {
|
|
1112
|
+
return envOrNull("TREESEED_MARKET_API_BASE_URL") ?? deployConfig.runtime?.marketBaseUrl ?? deployConfig.hosting?.marketBaseUrl ?? "";
|
|
1113
|
+
}
|
|
1114
|
+
function resolveConfiguredHostedTeamId(deployConfig) {
|
|
1115
|
+
return envOrNull("TREESEED_HOSTING_TEAM_ID") ?? deployConfig.runtime?.teamId ?? deployConfig.hosting?.teamId ?? deployConfig.slug;
|
|
1116
|
+
}
|
|
1117
|
+
function resolveConfiguredProjectId(deployConfig) {
|
|
1118
|
+
return envOrNull("TREESEED_PROJECT_ID") ?? deployConfig.runtime?.projectId ?? deployConfig.hosting?.projectId ?? deployConfig.slug;
|
|
1119
|
+
}
|
|
1120
|
+
function resolveConfiguredPagesProjectName(deployConfig) {
|
|
1121
|
+
return envOrNull("TREESEED_CLOUDFLARE_PAGES_PROJECT_NAME") ?? deployConfig.cloudflare.pages?.projectName ?? deployConfig.slug;
|
|
1122
|
+
}
|
|
1123
|
+
function resolveConfiguredPagesPreviewProjectName(deployConfig) {
|
|
1124
|
+
return envOrNull("TREESEED_CLOUDFLARE_PAGES_PREVIEW_PROJECT_NAME") ?? deployConfig.cloudflare.pages?.previewProjectName ?? `${resolveConfiguredPagesProjectName(deployConfig)}-staging`;
|
|
1125
|
+
}
|
|
1126
|
+
function resolveConfiguredContentBucketBinding(deployConfig) {
|
|
1127
|
+
return envOrNull("TREESEED_CONTENT_BUCKET_BINDING") ?? deployConfig.cloudflare.r2?.binding ?? "TREESEED_CONTENT_BUCKET";
|
|
1128
|
+
}
|
|
1129
|
+
function resolveConfiguredContentBucketName(deployConfig) {
|
|
1130
|
+
return envOrNull("TREESEED_CONTENT_BUCKET_NAME") ?? deployConfig.cloudflare.r2?.bucketName ?? `${deployConfig.slug}-content`;
|
|
1131
|
+
}
|
|
1132
|
+
function resolveConfiguredContentPublicBaseUrl(deployConfig) {
|
|
1133
|
+
return envOrNull("TREESEED_CONTENT_PUBLIC_BASE_URL") ?? deployConfig.cloudflare.r2?.publicBaseUrl ?? "";
|
|
1134
|
+
}
|
|
628
1135
|
function missingTurnstileRequirements() {
|
|
629
1136
|
const issues = [];
|
|
630
1137
|
if (!envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY")) {
|
|
@@ -638,8 +1145,8 @@ function missingTurnstileRequirements() {
|
|
|
638
1145
|
function missingContentRuntimeRequirements(deployConfig) {
|
|
639
1146
|
const issues = [];
|
|
640
1147
|
if (deployConfig.providers?.content?.runtime === "team_scoped_r2_overlay") {
|
|
641
|
-
if (!deployConfig
|
|
642
|
-
issues.push("Set
|
|
1148
|
+
if (!resolveConfiguredContentBucketName(deployConfig)) {
|
|
1149
|
+
issues.push("Set TREESEED_CONTENT_BUCKET_NAME before deploying team-scoped hosted content.");
|
|
643
1150
|
}
|
|
644
1151
|
if (!envOrNull("TREESEED_EDITORIAL_PREVIEW_SECRET")) {
|
|
645
1152
|
issues.push("Set TREESEED_EDITORIAL_PREVIEW_SECRET before deploying team-scoped hosted content.");
|
|
@@ -654,7 +1161,7 @@ function collectMissingDeployInputs(tenantRoot) {
|
|
|
654
1161
|
missing.push({
|
|
655
1162
|
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
656
1163
|
label: "Cloudflare account ID",
|
|
657
|
-
message:
|
|
1164
|
+
message: "Cloudflare account ID is missing. Set CLOUDFLARE_ACCOUNT_ID with treeseed config or provide it now."
|
|
658
1165
|
});
|
|
659
1166
|
}
|
|
660
1167
|
if (!envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY")) {
|
|
@@ -712,7 +1219,7 @@ function validateDeployPrerequisites(tenantRoot, { requireRemote = true } = {})
|
|
|
712
1219
|
const issues = [];
|
|
713
1220
|
if (isPlaceholderAccountId(deployConfig.cloudflare.accountId)) {
|
|
714
1221
|
issues.push(
|
|
715
|
-
|
|
1222
|
+
"Set CLOUDFLARE_ACCOUNT_ID with treeseed config or export it before deploying."
|
|
716
1223
|
);
|
|
717
1224
|
}
|
|
718
1225
|
if (requireRemote) {
|
|
@@ -740,7 +1247,7 @@ function validateDestroyPrerequisites(tenantRoot, { requireRemote = true } = {})
|
|
|
740
1247
|
const issues = [];
|
|
741
1248
|
if (requireRemote && isPlaceholderAccountId(deployConfig.cloudflare.accountId)) {
|
|
742
1249
|
issues.push(
|
|
743
|
-
|
|
1250
|
+
"Set CLOUDFLARE_ACCOUNT_ID with treeseed config or export it before destroying infrastructure."
|
|
744
1251
|
);
|
|
745
1252
|
}
|
|
746
1253
|
if (requireRemote) {
|
|
@@ -859,7 +1366,7 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
|
|
|
859
1366
|
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
860
1367
|
state.workerName = targetWorkerName(deployConfig, target);
|
|
861
1368
|
const env = {
|
|
862
|
-
CLOUDFLARE_ACCOUNT_ID: deployConfig
|
|
1369
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
863
1370
|
};
|
|
864
1371
|
const dryRun = options.dryRun ?? false;
|
|
865
1372
|
const force = options.force ?? false;
|
|
@@ -919,7 +1426,7 @@ function provisionCloudflareResources(tenantRoot, options = {}) {
|
|
|
919
1426
|
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
920
1427
|
state.workerName = targetWorkerName(deployConfig, target);
|
|
921
1428
|
const env = {
|
|
922
|
-
CLOUDFLARE_ACCOUNT_ID: deployConfig
|
|
1429
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
923
1430
|
};
|
|
924
1431
|
const dryRun = options.dryRun ?? false;
|
|
925
1432
|
const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
|
|
@@ -987,10 +1494,11 @@ function provisionCloudflareResources(tenantRoot, options = {}) {
|
|
|
987
1494
|
if (!current?.name) {
|
|
988
1495
|
return;
|
|
989
1496
|
}
|
|
990
|
-
|
|
1497
|
+
let refreshedQueues = queues;
|
|
1498
|
+
const exists = refreshedQueues.find((entry) => queueName(entry) === current.name);
|
|
991
1499
|
if (exists) {
|
|
992
1500
|
current.queueId = queueId(exists);
|
|
993
|
-
const currentDlq = current.dlqName ?
|
|
1501
|
+
const currentDlq = current.dlqName ? refreshedQueues.find((entry) => queueName(entry) === current.dlqName) : null;
|
|
994
1502
|
current.dlqId = queueId(currentDlq);
|
|
995
1503
|
return;
|
|
996
1504
|
}
|
|
@@ -999,22 +1507,41 @@ function provisionCloudflareResources(tenantRoot, options = {}) {
|
|
|
999
1507
|
current.dlqId = current.dlqName ? `dryrun-${current.dlqName}` : null;
|
|
1000
1508
|
return;
|
|
1001
1509
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
capture: true,
|
|
1005
|
-
env
|
|
1006
|
-
});
|
|
1007
|
-
if (current.dlqName && !queues.find((entry) => queueName(entry) === current.dlqName)) {
|
|
1008
|
-
runWrangler(["queues", "create", current.dlqName], {
|
|
1510
|
+
try {
|
|
1511
|
+
runWrangler(["queues", "create", current.name], {
|
|
1009
1512
|
cwd: tenantRoot,
|
|
1010
1513
|
capture: true,
|
|
1011
1514
|
env
|
|
1012
1515
|
});
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (!isWranglerAlreadyExistsError(error, [/Queue name .* is already taken/i, /\[code:\s*11009\]/i])) {
|
|
1518
|
+
throw error;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
refreshedQueues = listQueues(tenantRoot, env);
|
|
1522
|
+
if (current.dlqName && !refreshedQueues.find((entry) => queueName(entry) === current.dlqName)) {
|
|
1523
|
+
try {
|
|
1524
|
+
runWrangler(["queues", "create", current.dlqName], {
|
|
1525
|
+
cwd: tenantRoot,
|
|
1526
|
+
capture: true,
|
|
1527
|
+
env
|
|
1528
|
+
});
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
if (!isWranglerAlreadyExistsError(error, [/Queue name .* is already taken/i, /\[code:\s*11009\]/i])) {
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
refreshedQueues = listQueues(tenantRoot, env);
|
|
1536
|
+
const created = refreshedQueues.find((entry) => queueName(entry) === current.name);
|
|
1537
|
+
if (!created) {
|
|
1538
|
+
throw new Error(`Unable to resolve Cloudflare queue ${current.name} after reconciliation.`);
|
|
1013
1539
|
}
|
|
1014
|
-
const refreshed = listQueues(tenantRoot, env);
|
|
1015
|
-
const created = refreshed.find((entry) => queueName(entry) === current.name);
|
|
1016
1540
|
current.queueId = queueId(created);
|
|
1017
|
-
const createdDlq = current.dlqName ?
|
|
1541
|
+
const createdDlq = current.dlqName ? refreshedQueues.find((entry) => queueName(entry) === current.dlqName) : null;
|
|
1542
|
+
if (current.dlqName && !createdDlq) {
|
|
1543
|
+
throw new Error(`Unable to resolve Cloudflare dead-letter queue ${current.dlqName} after reconciliation.`);
|
|
1544
|
+
}
|
|
1018
1545
|
current.dlqId = queueId(createdDlq);
|
|
1019
1546
|
};
|
|
1020
1547
|
const ensureR2Bucket = () => {
|
|
@@ -1022,18 +1549,29 @@ function provisionCloudflareResources(tenantRoot, options = {}) {
|
|
|
1022
1549
|
if (!bucketName) {
|
|
1023
1550
|
return;
|
|
1024
1551
|
}
|
|
1025
|
-
|
|
1552
|
+
let refreshedBuckets = buckets;
|
|
1553
|
+
const exists = refreshedBuckets.find((entry) => entry?.name === bucketName);
|
|
1026
1554
|
if (exists) {
|
|
1027
1555
|
return;
|
|
1028
1556
|
}
|
|
1029
1557
|
if (dryRun) {
|
|
1030
1558
|
return;
|
|
1031
1559
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1560
|
+
try {
|
|
1561
|
+
runWrangler(["r2", "bucket", "create", bucketName], {
|
|
1562
|
+
cwd: tenantRoot,
|
|
1563
|
+
capture: true,
|
|
1564
|
+
env
|
|
1565
|
+
});
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
if (!isWranglerAlreadyExistsError(error, [/bucket you tried to create already exists, and you own it/i, /\[code:\s*10004\]/i])) {
|
|
1568
|
+
throw error;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
refreshedBuckets = listR2Buckets(tenantRoot, env);
|
|
1572
|
+
if (!refreshedBuckets.find((entry) => entry?.name === bucketName)) {
|
|
1573
|
+
throw new Error(`Unable to resolve Cloudflare R2 bucket ${bucketName} after reconciliation.`);
|
|
1574
|
+
}
|
|
1037
1575
|
};
|
|
1038
1576
|
const ensurePagesProject = () => {
|
|
1039
1577
|
const current = state.pages;
|
|
@@ -1069,12 +1607,16 @@ function provisionCloudflareResources(tenantRoot, options = {}) {
|
|
|
1069
1607
|
ensureQueue();
|
|
1070
1608
|
ensureR2Bucket();
|
|
1071
1609
|
ensurePagesProject();
|
|
1610
|
+
reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, target, { dryRun });
|
|
1072
1611
|
state.readiness.configured = true;
|
|
1073
1612
|
state.readiness.provisioned = hasProvisionedCloudflareResources(state);
|
|
1074
1613
|
state.readiness.deployable = state.readiness.provisioned === true;
|
|
1614
|
+
state.readiness.phase = state.readiness.provisioned === true ? "provisioned" : "config_complete";
|
|
1075
1615
|
state.readiness.initialized = true;
|
|
1076
1616
|
state.readiness.initializedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1077
1617
|
state.readiness.lastValidatedAt = state.readiness.initializedAt;
|
|
1618
|
+
state.readiness.blockers = [];
|
|
1619
|
+
state.readiness.warnings = [];
|
|
1078
1620
|
state.readiness.lastValidationSummary = {
|
|
1079
1621
|
cloudflare: state.readiness.provisioned === true ? "ready" : "incomplete",
|
|
1080
1622
|
railway: "configured"
|
|
@@ -1087,7 +1629,7 @@ function syncCloudflareSecrets(tenantRoot, options = {}) {
|
|
|
1087
1629
|
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
1088
1630
|
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
1089
1631
|
const env = {
|
|
1090
|
-
CLOUDFLARE_ACCOUNT_ID: deployConfig
|
|
1632
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
1091
1633
|
};
|
|
1092
1634
|
const secrets = buildSecretMap(deployConfig, state);
|
|
1093
1635
|
const synced = [];
|
|
@@ -1100,7 +1642,8 @@ function syncCloudflareSecrets(tenantRoot, options = {}) {
|
|
|
1100
1642
|
if (dryRun) {
|
|
1101
1643
|
continue;
|
|
1102
1644
|
}
|
|
1103
|
-
const
|
|
1645
|
+
const command = state.pages?.projectName && target.kind === "persistent" ? [resolveWranglerBin(), "pages", "secret", "put", key, "--project-name", state.pages.projectName] : [resolveWranglerBin(), "secret", "put", key, "--config", resolveGeneratedWranglerPath(tenantRoot, { target })];
|
|
1646
|
+
const result = spawnSync(process.execPath, command, {
|
|
1104
1647
|
cwd: tenantRoot,
|
|
1105
1648
|
input: `${value}
|
|
1106
1649
|
`,
|
|
@@ -1125,7 +1668,7 @@ function verifyProvisionedCloudflareResources(tenantRoot, options = {}) {
|
|
|
1125
1668
|
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
1126
1669
|
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
1127
1670
|
const env = {
|
|
1128
|
-
CLOUDFLARE_ACCOUNT_ID: deployConfig
|
|
1671
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
1129
1672
|
};
|
|
1130
1673
|
const dryRun = options.dryRun ?? false;
|
|
1131
1674
|
const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
|
|
@@ -1140,13 +1683,17 @@ function verifyProvisionedCloudflareResources(tenantRoot, options = {}) {
|
|
|
1140
1683
|
d1: Boolean(state.d1Databases?.SITE_DATA_DB?.databaseName && d1Databases.find((entry) => entry?.name === state.d1Databases.SITE_DATA_DB.databaseName)),
|
|
1141
1684
|
queue: Boolean(state.queues?.agentWork?.name && queues.find((entry) => queueName(entry) === state.queues.agentWork.name)),
|
|
1142
1685
|
dlq: !state.queues?.agentWork?.dlqName || Boolean(queues.find((entry) => queueName(entry) === state.queues.agentWork.dlqName)),
|
|
1143
|
-
r2: Boolean(state.content?.bucketName && buckets.find((entry) => entry?.name === state.content.bucketName))
|
|
1686
|
+
r2: Boolean(state.content?.bucketName && buckets.find((entry) => entry?.name === state.content.bucketName)),
|
|
1687
|
+
webCache: !shouldManageCloudflareWebCacheRules(deployConfig, target) || state.webCache?.rulesManaged === true
|
|
1144
1688
|
};
|
|
1145
1689
|
const ok = dryRun ? true : Object.values(checks).every(Boolean);
|
|
1146
1690
|
state.readiness.configured = true;
|
|
1147
1691
|
state.readiness.provisioned = ok;
|
|
1148
1692
|
state.readiness.deployable = ok;
|
|
1693
|
+
state.readiness.phase = ok ? "provisioned" : "config_complete";
|
|
1149
1694
|
state.readiness.lastValidatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1695
|
+
state.readiness.blockers = [];
|
|
1696
|
+
state.readiness.warnings = [];
|
|
1150
1697
|
state.readiness.lastValidationSummary = checks;
|
|
1151
1698
|
const liveQueue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
|
|
1152
1699
|
if (state.queues?.agentWork) {
|
|
@@ -1158,6 +1705,15 @@ function verifyProvisionedCloudflareResources(tenantRoot, options = {}) {
|
|
|
1158
1705
|
if (state.pages && livePages?.subdomain) {
|
|
1159
1706
|
state.pages.url = `https://${livePages.subdomain}`;
|
|
1160
1707
|
}
|
|
1708
|
+
if (!dryRun) {
|
|
1709
|
+
try {
|
|
1710
|
+
reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, target, { dryRun: false });
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
state.webCache.rulesManaged = false;
|
|
1713
|
+
state.webCache.lastError = error instanceof Error ? error.message : String(error);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
state.webCache.lastVerifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1161
1717
|
writeDeployState(tenantRoot, state, { target });
|
|
1162
1718
|
return {
|
|
1163
1719
|
ok,
|
|
@@ -1176,7 +1732,7 @@ function runRemoteD1Migrations(tenantRoot, options = {}) {
|
|
|
1176
1732
|
["d1", "migrations", "apply", state.d1Databases.SITE_DATA_DB.databaseName, "--remote", "--config", wranglerPath],
|
|
1177
1733
|
{
|
|
1178
1734
|
cwd: tenantRoot,
|
|
1179
|
-
env: { CLOUDFLARE_ACCOUNT_ID: deployConfig
|
|
1735
|
+
env: { CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig) }
|
|
1180
1736
|
}
|
|
1181
1737
|
);
|
|
1182
1738
|
return { databaseName: state.d1Databases.SITE_DATA_DB.databaseName, dryRun: false };
|
|
@@ -1190,9 +1746,12 @@ function markDeploymentInitialized(tenantRoot, options = {}) {
|
|
|
1190
1746
|
state.readiness.configured = true;
|
|
1191
1747
|
state.readiness.provisioned = hasProvisionedCloudflareResources(state);
|
|
1192
1748
|
state.readiness.deployable = state.readiness.provisioned === true;
|
|
1749
|
+
state.readiness.phase = state.readiness.provisioned === true ? "provisioned" : "config_complete";
|
|
1193
1750
|
state.readiness.initializedAt = state.readiness.initializedAt ?? timestamp;
|
|
1194
1751
|
state.readiness.lastValidatedAt = timestamp;
|
|
1195
1752
|
state.readiness.lastConfigFingerprint = state.lastManifestFingerprint ?? state.readiness.lastConfigFingerprint;
|
|
1753
|
+
state.readiness.blockers = [];
|
|
1754
|
+
state.readiness.warnings = [];
|
|
1196
1755
|
writeDeployState(tenantRoot, state, { target });
|
|
1197
1756
|
return state;
|
|
1198
1757
|
}
|
|
@@ -1252,7 +1811,10 @@ function finalizeDeploymentState(tenantRoot, options = {}) {
|
|
|
1252
1811
|
state.readiness.configured = true;
|
|
1253
1812
|
state.readiness.provisioned = hasProvisionedCloudflareResources(state);
|
|
1254
1813
|
state.readiness.deployable = state.readiness.provisioned === true;
|
|
1814
|
+
state.readiness.phase = state.readiness.provisioned === true ? "provisioned" : "config_complete";
|
|
1255
1815
|
state.readiness.lastValidatedAt = state.lastDeploymentTimestamp;
|
|
1816
|
+
state.readiness.blockers = [];
|
|
1817
|
+
state.readiness.warnings = [];
|
|
1256
1818
|
for (const result of options.serviceResults ?? []) {
|
|
1257
1819
|
if (!result?.service || !state.services?.[result.service]) {
|
|
1258
1820
|
continue;
|
|
@@ -1263,6 +1825,13 @@ function finalizeDeploymentState(tenantRoot, options = {}) {
|
|
|
1263
1825
|
state.services[result.service].lastDeploymentCommand = result.command ?? null;
|
|
1264
1826
|
}
|
|
1265
1827
|
writeDeployState(tenantRoot, state, { target });
|
|
1828
|
+
if (target.kind === "persistent") {
|
|
1829
|
+
try {
|
|
1830
|
+
purgeSourcePageCaches(tenantRoot, { target });
|
|
1831
|
+
} catch {
|
|
1832
|
+
}
|
|
1833
|
+
return loadDeployState(tenantRoot, deployConfig, { target });
|
|
1834
|
+
}
|
|
1266
1835
|
return state;
|
|
1267
1836
|
}
|
|
1268
1837
|
function printDeploySummary(summary) {
|
|
@@ -1311,6 +1880,8 @@ export {
|
|
|
1311
1880
|
printDestroySummary,
|
|
1312
1881
|
promptForMissingDeployInputs,
|
|
1313
1882
|
provisionCloudflareResources,
|
|
1883
|
+
purgePublishedContentCaches,
|
|
1884
|
+
purgeSourcePageCaches,
|
|
1314
1885
|
resolveGeneratedWranglerPath,
|
|
1315
1886
|
runRemoteD1Migrations,
|
|
1316
1887
|
scopeFromTarget,
|