@treeseed/sdk 0.12.36 → 0.12.38
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/hosting/builtins.js +69 -0
- package/dist/operations/services/deploy.d.ts +9 -2
- package/dist/operations/services/deploy.js +48 -25
- package/dist/operations/services/hosted-service-checks.js +5 -3
- package/dist/operations/services/repository-save-orchestrator.js +1 -1
- package/dist/workflow/operations.js +16 -0
- package/package.json +1 -1
package/dist/hosting/builtins.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { dirname, resolve } from "node:path";
|
|
3
4
|
import { resolveTreeseedLaunchEnvironment } from "../operations/services/config-runtime.js";
|
|
4
5
|
import { cloudflareApiRequest, resolveCloudflareZoneIdForHost, resolveConfiguredCloudflareAccountId, runWrangler } from "../operations/services/deploy.js";
|
|
@@ -161,6 +162,62 @@ function cloudflarePagesDomain(input) {
|
|
|
161
162
|
function cloudflarePagesDeploymentUrl(projectName, branchName, environment) {
|
|
162
163
|
return environment === "prod" ? `https://${projectName}.pages.dev` : `https://${branchName}.${projectName}.pages.dev`;
|
|
163
164
|
}
|
|
165
|
+
function probeCloudflarePagesPublicUrl(url) {
|
|
166
|
+
if (!url) {
|
|
167
|
+
return { ok: false, status: null, finalUrl: null, headers: {}, error: "missing_url" };
|
|
168
|
+
}
|
|
169
|
+
const script = `
|
|
170
|
+
const url = process.argv[1];
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(url, {
|
|
173
|
+
redirect: 'follow',
|
|
174
|
+
headers: {
|
|
175
|
+
'user-agent': 'treeseed-hosting-verifier/1.0',
|
|
176
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
177
|
+
},
|
|
178
|
+
signal: AbortSignal.timeout(15000),
|
|
179
|
+
});
|
|
180
|
+
const headers = {};
|
|
181
|
+
for (const key of ['cf-cache-status', 'age', 'server', 'cache-control', 'content-type']) {
|
|
182
|
+
const value = response.headers.get(key);
|
|
183
|
+
if (value) headers[key] = value;
|
|
184
|
+
}
|
|
185
|
+
process.stdout.write(JSON.stringify({
|
|
186
|
+
ok: response.ok,
|
|
187
|
+
status: response.status,
|
|
188
|
+
finalUrl: response.url,
|
|
189
|
+
headers,
|
|
190
|
+
error: null,
|
|
191
|
+
}));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
process.stdout.write(JSON.stringify({
|
|
194
|
+
ok: false,
|
|
195
|
+
status: null,
|
|
196
|
+
finalUrl: url,
|
|
197
|
+
headers: {},
|
|
198
|
+
error: error instanceof Error ? error.message : String(error),
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
const result = spawnSync(process.execPath, ["--input-type=module", "-e", script, url], {
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
timeout: 2e4
|
|
205
|
+
});
|
|
206
|
+
if (result.error) {
|
|
207
|
+
return { ok: false, status: null, finalUrl: url, headers: {}, error: result.error.message };
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(result.stdout || "{}");
|
|
211
|
+
} catch {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
status: null,
|
|
215
|
+
finalUrl: url,
|
|
216
|
+
headers: {},
|
|
217
|
+
error: result.stderr?.trim() || result.stdout?.trim() || "invalid_probe_output"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
164
221
|
function cloudflarePagesDnsTarget(projectName, branchName, environment) {
|
|
165
222
|
return environment === "prod" ? `${projectName}.pages.dev` : `${branchName}.${projectName}.pages.dev`;
|
|
166
223
|
}
|
|
@@ -337,6 +394,8 @@ function createCloudflareHostAdapter() {
|
|
|
337
394
|
const observedDomain = observedState.observedDomain ?? null;
|
|
338
395
|
const observedDnsRecord = observedState.observedDnsRecord ?? null;
|
|
339
396
|
const observedDeployment = observedState.observedDeployment ?? null;
|
|
397
|
+
const publicUrl = domain ? `https://${domain}` : projectName && branchName ? cloudflarePagesDeploymentUrl(projectName, branchName, input.environment) : null;
|
|
398
|
+
const publicProbe = input.environment === "prod" ? probeCloudflarePagesPublicUrl(publicUrl) : null;
|
|
340
399
|
const checks = [
|
|
341
400
|
{
|
|
342
401
|
key: "pages-project.exists",
|
|
@@ -376,6 +435,16 @@ function createCloudflareHostAdapter() {
|
|
|
376
435
|
issues: !domain || observedDnsRecord ? [] : [`Cloudflare DNS record ${domain} -> ${projectName && branchName ? cloudflarePagesDnsTarget(projectName, branchName, input.environment) : "(unset)"} is missing.`]
|
|
377
436
|
}
|
|
378
437
|
];
|
|
438
|
+
if (input.environment === "prod") {
|
|
439
|
+
checks.push({
|
|
440
|
+
key: "pages-public-url.ok",
|
|
441
|
+
label: "Cloudflare Pages public URL responds successfully",
|
|
442
|
+
ok: publicProbe?.ok === true,
|
|
443
|
+
expected: { url: publicUrl, ok: true },
|
|
444
|
+
observed: publicProbe,
|
|
445
|
+
issues: publicProbe?.ok === true ? [] : [`Cloudflare Pages public URL ${publicUrl ?? "(unset)"} did not return a successful status.`]
|
|
446
|
+
});
|
|
447
|
+
}
|
|
379
448
|
return {
|
|
380
449
|
unitId: input.unit.id,
|
|
381
450
|
status: checks.every((check) => check.ok) ? "ready" : "pending",
|
|
@@ -300,11 +300,17 @@ export declare function purgeSourcePageCaches(tenantRoot: any, options?: {}): {
|
|
|
300
300
|
results: never[];
|
|
301
301
|
} | {
|
|
302
302
|
urls: (string | null)[];
|
|
303
|
-
results: {
|
|
303
|
+
results: ({
|
|
304
304
|
zoneId: any;
|
|
305
305
|
count: number;
|
|
306
306
|
success: boolean;
|
|
307
|
-
|
|
307
|
+
errors: any;
|
|
308
|
+
} | {
|
|
309
|
+
zoneId: unknown;
|
|
310
|
+
count: string;
|
|
311
|
+
success: boolean;
|
|
312
|
+
errors: any;
|
|
313
|
+
})[];
|
|
308
314
|
skipped?: undefined;
|
|
309
315
|
reason?: undefined;
|
|
310
316
|
};
|
|
@@ -319,6 +325,7 @@ export declare function purgePublishedContentCaches(tenantRoot: any, urls: any,
|
|
|
319
325
|
zoneId: any;
|
|
320
326
|
count: number;
|
|
321
327
|
success: boolean;
|
|
328
|
+
errors: any;
|
|
322
329
|
}[];
|
|
323
330
|
skipped?: undefined;
|
|
324
331
|
reason?: undefined;
|
|
@@ -1249,8 +1249,8 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
|
|
|
1249
1249
|
if ((deployConfig.surfaces?.web?.provider ?? deployConfig.providers?.deploy) !== "cloudflare") {
|
|
1250
1250
|
return false;
|
|
1251
1251
|
}
|
|
1252
|
-
const
|
|
1253
|
-
return Boolean(
|
|
1252
|
+
const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
|
|
1253
|
+
return Boolean(webTarget2?.host && !webTarget2.host.endsWith(".workers.dev") && !webTarget2.host.endsWith(".pages.dev"));
|
|
1254
1254
|
}
|
|
1255
1255
|
function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
|
|
1256
1256
|
const token = env?.TREESEED_CLOUDFLARE_API_TOKEN ?? env?.CLOUDFLARE_API_TOKEN ?? process.env.TREESEED_CLOUDFLARE_API_TOKEN ?? "";
|
|
@@ -1476,7 +1476,7 @@ function buildTreeseedManagedCloudflareCacheRules(deployConfig, cacheTarget, kin
|
|
|
1476
1476
|
];
|
|
1477
1477
|
if (sourcePathExpression) {
|
|
1478
1478
|
rules.push({
|
|
1479
|
-
description: "treeseed-managed:
|
|
1479
|
+
description: "treeseed-managed: bypass source html routes",
|
|
1480
1480
|
expression: joinCloudflareAndExpression([
|
|
1481
1481
|
hostExpression,
|
|
1482
1482
|
pathExpression,
|
|
@@ -1485,15 +1485,7 @@ function buildTreeseedManagedCloudflareCacheRules(deployConfig, cacheTarget, kin
|
|
|
1485
1485
|
]),
|
|
1486
1486
|
action: "set_cache_settings",
|
|
1487
1487
|
action_parameters: {
|
|
1488
|
-
cache:
|
|
1489
|
-
edge_ttl: {
|
|
1490
|
-
mode: "override_origin",
|
|
1491
|
-
default: policy.sourcePages.edgeTtlSeconds
|
|
1492
|
-
},
|
|
1493
|
-
browser_ttl: {
|
|
1494
|
-
mode: "override_origin",
|
|
1495
|
-
default: policy.sourcePages.browserTtlSeconds
|
|
1496
|
-
}
|
|
1488
|
+
cache: false
|
|
1497
1489
|
},
|
|
1498
1490
|
enabled: true
|
|
1499
1491
|
});
|
|
@@ -1576,10 +1568,10 @@ function reconcileCloudflareCacheRulesForTarget(role, deployConfig, state, cache
|
|
|
1576
1568
|
}
|
|
1577
1569
|
function reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, target, { planOnly = false, env: providedEnv } = {}) {
|
|
1578
1570
|
if (!shouldManageCloudflareWebCacheRules(deployConfig, target)) {
|
|
1579
|
-
const
|
|
1580
|
-
const
|
|
1581
|
-
state.webCache.webHost =
|
|
1582
|
-
state.webCache.contentHost =
|
|
1571
|
+
const webTarget3 = resolvePublicWebCacheTarget(deployConfig);
|
|
1572
|
+
const contentTarget3 = resolvePublicContentCacheTarget(deployConfig);
|
|
1573
|
+
state.webCache.webHost = webTarget3?.host ?? null;
|
|
1574
|
+
state.webCache.contentHost = contentTarget3?.host ?? null;
|
|
1583
1575
|
state.webCache.rulesManaged = false;
|
|
1584
1576
|
state.webCache.lastError = null;
|
|
1585
1577
|
return { managed: false, skipped: true, reason: "unsupported_target_or_host" };
|
|
@@ -1594,15 +1586,15 @@ function reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, targe
|
|
|
1594
1586
|
state.webCache.lastError = "CLOUDFLARE_API_TOKEN is required to manage Cloudflare Cache Rules.";
|
|
1595
1587
|
return { managed: false, skipped: true, reason: "missing_api_token" };
|
|
1596
1588
|
}
|
|
1597
|
-
const
|
|
1598
|
-
const
|
|
1589
|
+
const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
|
|
1590
|
+
const contentTarget2 = resolvePublicContentCacheTarget(deployConfig);
|
|
1599
1591
|
try {
|
|
1600
1592
|
const results = [];
|
|
1601
|
-
if (
|
|
1602
|
-
results.push(reconcileCloudflareCacheRulesForTarget("web", deployConfig, state,
|
|
1593
|
+
if (webTarget2?.host) {
|
|
1594
|
+
results.push(reconcileCloudflareCacheRulesForTarget("web", deployConfig, state, webTarget2, env, { planOnly }));
|
|
1603
1595
|
}
|
|
1604
|
-
if (
|
|
1605
|
-
results.push(reconcileCloudflareCacheRulesForTarget("content", deployConfig, state,
|
|
1596
|
+
if (contentTarget2?.host) {
|
|
1597
|
+
results.push(reconcileCloudflareCacheRulesForTarget("content", deployConfig, state, contentTarget2, env, { planOnly }));
|
|
1606
1598
|
}
|
|
1607
1599
|
state.webCache.rulesManaged = true;
|
|
1608
1600
|
state.webCache.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1643,13 +1635,41 @@ function purgeCloudflareCacheByUrls(urls, deployConfig, { env } = {}) {
|
|
|
1643
1635
|
body: { files: [...new Set(files)] },
|
|
1644
1636
|
env
|
|
1645
1637
|
});
|
|
1638
|
+
const errors = Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).filter(Boolean) : [];
|
|
1646
1639
|
return {
|
|
1647
1640
|
zoneId,
|
|
1648
1641
|
count: [...new Set(files)].length,
|
|
1649
|
-
success: payload?.success === true
|
|
1642
|
+
success: payload?.success === true,
|
|
1643
|
+
errors
|
|
1650
1644
|
};
|
|
1651
1645
|
});
|
|
1652
1646
|
}
|
|
1647
|
+
function purgeCloudflareCacheEverythingByHosts(hosts, deployConfig, { env } = {}) {
|
|
1648
|
+
const zoneIds = [...new Set((hosts ?? []).map((host) => typeof host === "string" ? host.trim() : "").filter(Boolean).map((host) => resolveCloudflareZoneIdForHost(deployConfig, host, env)).filter(Boolean))];
|
|
1649
|
+
return zoneIds.map((zoneId) => {
|
|
1650
|
+
const payload = cloudflareApiRequest(`/zones/${zoneId}/purge_cache`, {
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
body: { purge_everything: true },
|
|
1653
|
+
env
|
|
1654
|
+
});
|
|
1655
|
+
const errors = Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).filter(Boolean) : [];
|
|
1656
|
+
return {
|
|
1657
|
+
zoneId,
|
|
1658
|
+
count: "everything",
|
|
1659
|
+
success: payload?.success === true,
|
|
1660
|
+
errors
|
|
1661
|
+
};
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
function assertCloudflareCachePurgeSucceeded(results) {
|
|
1665
|
+
const failures = (results ?? []).filter((result) => result?.success !== true);
|
|
1666
|
+
if (failures.length === 0) return;
|
|
1667
|
+
const detail = failures.map((result) => {
|
|
1668
|
+
const errors = Array.isArray(result?.errors) && result.errors.length > 0 ? `: ${result.errors.join("; ")}` : "";
|
|
1669
|
+
return `${result?.zoneId ?? "unknown-zone"} (${result?.count ?? 0} urls)${errors}`;
|
|
1670
|
+
}).join(", ");
|
|
1671
|
+
throw new Error(`Cloudflare cache purge did not succeed for ${detail}.`);
|
|
1672
|
+
}
|
|
1653
1673
|
function queueName(entry) {
|
|
1654
1674
|
return entry?.queue_name ?? entry?.queueName ?? entry?.name ?? null;
|
|
1655
1675
|
}
|
|
@@ -1708,9 +1728,11 @@ function purgeSourcePageCaches(tenantRoot, options = {}) {
|
|
|
1708
1728
|
const results = purgeCloudflareCacheByUrls(urls, deployConfig, {
|
|
1709
1729
|
env
|
|
1710
1730
|
});
|
|
1711
|
-
|
|
1731
|
+
const allResults = target.scope === "prod" ? purgeCloudflareCacheEverythingByHosts([webTarget.host, contentTarget?.host], deployConfig, { env }) : [];
|
|
1732
|
+
assertCloudflareCachePurgeSucceeded([...results, ...allResults]);
|
|
1733
|
+
recordCachePurgeResult(state.webCache.deployPurge, [...results, ...allResults]);
|
|
1712
1734
|
writeDeployState(tenantRoot, state, { target });
|
|
1713
|
-
return { urls, results };
|
|
1735
|
+
return { urls, results: [...results, ...allResults] };
|
|
1714
1736
|
} catch (error) {
|
|
1715
1737
|
recordCachePurgeResult(state.webCache.deployPurge, [], error);
|
|
1716
1738
|
writeDeployState(tenantRoot, state, { target });
|
|
@@ -1736,6 +1758,7 @@ function purgePublishedContentCaches(tenantRoot, urls, options = {}) {
|
|
|
1736
1758
|
const results = purgeCloudflareCacheByUrls(urls, deployConfig, {
|
|
1737
1759
|
env
|
|
1738
1760
|
});
|
|
1761
|
+
assertCloudflareCachePurgeSucceeded(results);
|
|
1739
1762
|
recordCachePurgeResult(state.webCache.contentPurge, results);
|
|
1740
1763
|
writeDeployState(tenantRoot, state, { target });
|
|
1741
1764
|
return { urls, results };
|
|
@@ -79,12 +79,14 @@ function variableObserved(observed, key) {
|
|
|
79
79
|
function valuePresence(values, observed, key, serviceTarget) {
|
|
80
80
|
const providerPresent = variableObserved(observed, key);
|
|
81
81
|
const machinePresent = hasConfiguredValue(values, key);
|
|
82
|
+
const providerObserved = Boolean(observed);
|
|
83
|
+
const present = providerObserved ? providerPresent : machinePresent;
|
|
82
84
|
return {
|
|
83
85
|
key,
|
|
84
86
|
serviceTarget,
|
|
85
|
-
present
|
|
86
|
-
source: providerPresent ? "provider" : machinePresent ? "machine-config" : "missing",
|
|
87
|
-
observation:
|
|
87
|
+
present,
|
|
88
|
+
source: providerPresent ? "provider" : !providerObserved && machinePresent ? "machine-config" : "missing",
|
|
89
|
+
observation: providerObserved ? "provider-live" : "not-observed"
|
|
88
90
|
};
|
|
89
91
|
}
|
|
90
92
|
function serviceTypeFor(key) {
|
|
@@ -973,7 +973,7 @@ async function validateRepositoryLockfile(node, options) {
|
|
|
973
973
|
return { status: "skipped", command: commandText, issues: [], error: "disabled" };
|
|
974
974
|
}
|
|
975
975
|
try {
|
|
976
|
-
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs:
|
|
976
|
+
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs: 6e5, emitOutputOnSuccess: false });
|
|
977
977
|
const packageCount = npmLockfilePackageCount(node.path);
|
|
978
978
|
const countText = packageCount === null ? "package-lock entries" : `${packageCount} package${packageCount === 1 ? "" : "s"}`;
|
|
979
979
|
emitProgress(options, node, "lockfile", `Lockfile validation passed: ${countText} checked, 0 issues.`);
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
createPersistentDeployTarget,
|
|
45
45
|
destroyTreeseedEnvironmentResources,
|
|
46
46
|
loadDeployState,
|
|
47
|
+
purgeSourcePageCaches,
|
|
47
48
|
recordHostedDeploymentState,
|
|
48
49
|
resolveConfiguredSurfaceDomain,
|
|
49
50
|
validateDestroyPrerequisites
|
|
@@ -646,6 +647,21 @@ async function runReleaseWebLiveVerification(root, environment, helpers, operati
|
|
|
646
647
|
...helpers.context.env,
|
|
647
648
|
...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
|
|
648
649
|
};
|
|
650
|
+
let purge;
|
|
651
|
+
try {
|
|
652
|
+
purge = purgeSourcePageCaches(root, { target: environment, env });
|
|
653
|
+
} catch (error) {
|
|
654
|
+
workflowError(operation, "hosted_live_verification_failed", `Production web cache purge failed before root live verification:
|
|
655
|
+
${error instanceof Error ? error.message : String(error)}`, {
|
|
656
|
+
details: { environment }
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
if (purge?.skipped) {
|
|
660
|
+
workflowError(operation, "hosted_live_verification_failed", `Production web cache purge was skipped before root live verification: ${purge.reason ?? "unknown reason"}`, {
|
|
661
|
+
details: { environment, purge }
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
helpers.write(`[${operation}][cloudflare] purged production source page cache for ${purge?.urls?.length ?? 0} urls before web live verification.`, "stderr");
|
|
649
665
|
const live = await collectTreeseedLiveHostedServiceChecks({
|
|
650
666
|
tenantRoot: root,
|
|
651
667
|
target: environment,
|