@treeseed/sdk 0.12.38 → 0.12.40

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.
@@ -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 webTarget2 = resolvePublicWebCacheTarget(deployConfig);
1253
- return Boolean(webTarget2?.host && !webTarget2.host.endsWith(".workers.dev") && !webTarget2.host.endsWith(".pages.dev"));
1252
+ const webTarget = resolvePublicWebCacheTarget(deployConfig);
1253
+ return Boolean(webTarget?.host && !webTarget.host.endsWith(".workers.dev") && !webTarget.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 ?? "";
@@ -1568,10 +1568,10 @@ function reconcileCloudflareCacheRulesForTarget(role, deployConfig, state, cache
1568
1568
  }
1569
1569
  function reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, target, { planOnly = false, env: providedEnv } = {}) {
1570
1570
  if (!shouldManageCloudflareWebCacheRules(deployConfig, target)) {
1571
- const webTarget3 = resolvePublicWebCacheTarget(deployConfig);
1572
- const contentTarget3 = resolvePublicContentCacheTarget(deployConfig);
1573
- state.webCache.webHost = webTarget3?.host ?? null;
1574
- state.webCache.contentHost = contentTarget3?.host ?? null;
1571
+ const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
1572
+ const contentTarget2 = resolvePublicContentCacheTarget(deployConfig);
1573
+ state.webCache.webHost = webTarget2?.host ?? null;
1574
+ state.webCache.contentHost = contentTarget2?.host ?? null;
1575
1575
  state.webCache.rulesManaged = false;
1576
1576
  state.webCache.lastError = null;
1577
1577
  return { managed: false, skipped: true, reason: "unsupported_target_or_host" };
@@ -1586,15 +1586,15 @@ function reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, targe
1586
1586
  state.webCache.lastError = "CLOUDFLARE_API_TOKEN is required to manage Cloudflare Cache Rules.";
1587
1587
  return { managed: false, skipped: true, reason: "missing_api_token" };
1588
1588
  }
1589
- const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
1590
- const contentTarget2 = resolvePublicContentCacheTarget(deployConfig);
1589
+ const webTarget = resolvePublicWebCacheTarget(deployConfig);
1590
+ const contentTarget = resolvePublicContentCacheTarget(deployConfig);
1591
1591
  try {
1592
1592
  const results = [];
1593
- if (webTarget2?.host) {
1594
- results.push(reconcileCloudflareCacheRulesForTarget("web", deployConfig, state, webTarget2, env, { planOnly }));
1593
+ if (webTarget?.host) {
1594
+ results.push(reconcileCloudflareCacheRulesForTarget("web", deployConfig, state, webTarget, env, { planOnly }));
1595
1595
  }
1596
- if (contentTarget2?.host) {
1597
- results.push(reconcileCloudflareCacheRulesForTarget("content", deployConfig, state, contentTarget2, env, { planOnly }));
1596
+ if (contentTarget?.host) {
1597
+ results.push(reconcileCloudflareCacheRulesForTarget("content", deployConfig, state, contentTarget, env, { planOnly }));
1598
1598
  }
1599
1599
  state.webCache.rulesManaged = true;
1600
1600
  state.webCache.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1700,7 +1700,7 @@ function recordCachePurgeResult(targetState, results, error = null) {
1700
1700
  return;
1701
1701
  }
1702
1702
  targetState.lastPurgedAt = (/* @__PURE__ */ new Date()).toISOString();
1703
- targetState.purgeCount = Array.isArray(results) ? results.reduce((sum, result) => sum + (result?.count ?? 0), 0) : 0;
1703
+ targetState.purgeCount = Array.isArray(results) ? results.reduce((sum, result) => sum + (typeof result?.count === "number" ? result.count : 0), 0) : 0;
1704
1704
  targetState.lastError = null;
1705
1705
  }
1706
1706
  function resolveCloudflareCachePurgeEnv(options = {}) {
@@ -1728,7 +1728,8 @@ function purgeSourcePageCaches(tenantRoot, options = {}) {
1728
1728
  const results = purgeCloudflareCacheByUrls(urls, deployConfig, {
1729
1729
  env
1730
1730
  });
1731
- const allResults = target.scope === "prod" ? purgeCloudflareCacheEverythingByHosts([webTarget.host, contentTarget?.host], deployConfig, { env }) : [];
1731
+ const hosts = [...new Set(urls.map((url) => safeUrl(url)?.hostname).filter(Boolean))];
1732
+ const allResults = target.scope === "prod" ? purgeCloudflareCacheEverythingByHosts(hosts, deployConfig, { env }) : [];
1732
1733
  assertCloudflareCachePurgeSucceeded([...results, ...allResults]);
1733
1734
  recordCachePurgeResult(state.webCache.deployPurge, [...results, ...allResults]);
1734
1735
  writeDeployState(tenantRoot, state, { target });
@@ -1192,6 +1192,20 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1192
1192
  };
1193
1193
  };
1194
1194
  productionWebVerification: Record<string, unknown> | null;
1195
+ productionFinalGuarantees: {
1196
+ ok: true;
1197
+ environment: string;
1198
+ runId: string;
1199
+ outputRoot: string;
1200
+ counts: {
1201
+ planned: number;
1202
+ passed: number;
1203
+ failed: number;
1204
+ skipped: number;
1205
+ blocked: number;
1206
+ releaseBlockingFailures: number;
1207
+ };
1208
+ };
1195
1209
  backMerge: {
1196
1210
  packages: {
1197
1211
  status: string;
@@ -762,6 +762,54 @@ ${diagnostics.join("\n") || `See ${report.outputRoot}`}`, {
762
762
  };
763
763
  });
764
764
  }
765
+ async function runReleaseProductionGuarantees(root, helpers, operation, sceneArtifacts) {
766
+ const environment = "prod";
767
+ const env = {
768
+ ...helpers.context.env,
769
+ ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
770
+ };
771
+ env.TREESEED_ACCEPTANCE_SERVICE_ID ??= env.TREESEED_API_WEB_SERVICE_ID ?? env.TREESEED_WEB_SERVICE_ID;
772
+ env.TREESEED_ACCEPTANCE_SERVICE_SECRET ??= env.TREESEED_API_WEB_SERVICE_SECRET ?? env.TREESEED_WEB_SERVICE_SECRET;
773
+ if (!env.TREESEED_ACCEPTANCE_SERVICE_ID || !env.TREESEED_ACCEPTANCE_SERVICE_SECRET) {
774
+ workflowError(operation, "release_gate_failed", "Final production release guarantees cannot run because production acceptance service credentials are missing.", {
775
+ details: {
776
+ environment,
777
+ missing: [
778
+ !env.TREESEED_ACCEPTANCE_SERVICE_ID ? "TREESEED_ACCEPTANCE_SERVICE_ID" : null,
779
+ !env.TREESEED_ACCEPTANCE_SERVICE_SECRET ? "TREESEED_ACCEPTANCE_SERVICE_SECRET" : null
780
+ ].filter((value) => Boolean(value))
781
+ }
782
+ });
783
+ }
784
+ helpers.write(`[${operation}][workflow] Running final production release guarantees against the fully deployed production environment.`);
785
+ return await withContextEnv(env, async () => {
786
+ const report = await runTreeseedGuarantees({
787
+ workspaceRoot: root,
788
+ environment,
789
+ evidenceTarget: "release",
790
+ sceneArtifacts
791
+ });
792
+ if (!report.ok) {
793
+ const diagnostics = report.diagnostics.filter((entry) => entry.severity === "error").slice(0, 20).map((entry) => `${entry.code}: ${entry.message}${entry.sourcePath ? ` (${entry.sourcePath})` : ""}`);
794
+ const failedGuarantees = report.results.filter((entry) => entry.status === "failed" || entry.status === "blocked").slice(0, 20).map((entry) => `${entry.id}: ${entry.status}`);
795
+ workflowError(operation, "release_gate_failed", [
796
+ "Final production release guarantees failed after production deployment.",
797
+ ...failedGuarantees,
798
+ ...diagnostics,
799
+ failedGuarantees.length === 0 && diagnostics.length === 0 ? `See ${report.outputRoot}` : null
800
+ ].filter((line) => Boolean(line)).join("\n"), {
801
+ details: { environment, outputRoot: report.outputRoot, counts: report.counts, diagnostics: report.diagnostics }
802
+ });
803
+ }
804
+ return {
805
+ ok: report.ok,
806
+ environment: report.environment,
807
+ runId: report.runId,
808
+ outputRoot: report.outputRoot,
809
+ counts: report.counts
810
+ };
811
+ });
812
+ }
765
813
  function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
766
814
  const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
767
815
  const releaseRecord = stringRecord(rootRelease) ?? {};
@@ -5697,6 +5745,7 @@ ${blockers.join("\n")}`, {
5697
5745
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5698
5746
  { id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5699
5747
  { id: "production-web-live-verification", description: "Run production web live verification after root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5748
+ { id: "production-final-guarantees", description: "Run final production release guarantees after all production deploys", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5700
5749
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5701
5750
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
5702
5751
  ],
@@ -5875,6 +5924,7 @@ ${rendered}`);
5875
5924
  onProgress: (line, stream) => helpers.write(line, stream)
5876
5925
  }).then((workflowGates) => ({ workflowGates })));
5877
5926
  const productionWebVerification = await executeJournalStep(root, workflowRun.runId, "production-web-live-verification", () => runReleaseWebLiveVerification(root, "prod", helpers, "release"));
5927
+ const productionFinalGuarantees = await executeJournalStep(root, workflowRun.runId, "production-final-guarantees", () => runReleaseProductionGuarantees(root, helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
5878
5928
  const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
5879
5929
  const packageBackMerges = selectedPackageNames.map((name) => packageRepoByName.get(name)).filter((pkg) => Boolean(pkg)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
5880
5930
  subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
@@ -5905,6 +5955,7 @@ ${rendered}`);
5905
5955
  productionHosting,
5906
5956
  productionApiGuarantees,
5907
5957
  productionWebVerification,
5958
+ productionFinalGuarantees,
5908
5959
  backMerge,
5909
5960
  workspaceLinks,
5910
5961
  releasedCommit: String(rootRelease.commit.commitSha ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.38",
3
+ "version": "0.12.40",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {