From d7794d6472a180f7be5c1b7743fb1db70e4d164e Mon Sep 17 00:00:00 2001 From: James Date: Sat, 22 Aug 2026 03:33:08 +0100 Subject: [PATCH] fix(cloudflare): enforce no-promote warmup contract --- packages/cloudflare/src/deploy.ts | 15 +++++++++++++++ tests/cloudflare-cdn-warm-deploy.test.ts | 12 ++++++++++++ tests/deploy-prerender-config.test.ts | 15 +++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/packages/cloudflare/src/deploy.ts b/packages/cloudflare/src/deploy.ts index 5b7728fcf..a161f4240 100644 --- a/packages/cloudflare/src/deploy.ts +++ b/packages/cloudflare/src/deploy.ts @@ -592,6 +592,16 @@ export async function deployWithCdnWarmup( "Configure that adapter capability or rerun without --warm-cdn-strict.", ); } + if ( + options.warmCdnPromote === false && + paths.length > 0 && + options.expectedBuildId === undefined + ) { + throw new Error( + "CDN warmup cannot skip promotion because the discovered HTML requests cannot be verified. " + + "Configure a CDN adapter that declares build-identity response headers.", + ); + } const upload = runWranglerVersionUpload(root, options); const warmUploadedVersion = ( targetUrl: string, @@ -1116,6 +1126,11 @@ export async function deploy(options: DeployOptions): Promise { warmCdnPromotionDelay: options.warmCdnPromotionDelay, }); } else { + if (options.warmCdnPromote === false) { + throw new Error( + "CDN warmup cannot skip promotion because no build-discovered requests were found to warm.", + ); + } console.log("\n CDN warmup skipped: no build-discovered paths found."); url = await runWranglerDeploy(root, wranglerOptions); } diff --git a/tests/cloudflare-cdn-warm-deploy.test.ts b/tests/cloudflare-cdn-warm-deploy.test.ts index d49a83cdd..14089861c 100644 --- a/tests/cloudflare-cdn-warm-deploy.test.ts +++ b/tests/cloudflare-cdn-warm-deploy.test.ts @@ -724,6 +724,18 @@ describe("Cloudflare CDN warmup deploy flow", () => { expect(fetch).not.toHaveBeenCalled(); }); + it("rejects no-promote HTML warmup without verifiable build identity before upload", async () => { + const { deployWithCdnWarmup } = await import("../packages/cloudflare/src/deploy.js"); + + await expect( + deployWithCdnWarmup(tmpDir, ["/about"], { + warmCdnPromote: false, + }), + ).rejects.toThrow("discovered HTML requests cannot be verified"); + expect(execFileSyncMock).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); + }); + it("applies triggers before post-promotion fallback warmup", async () => { const events: string[] = []; writeFile( diff --git a/tests/deploy-prerender-config.test.ts b/tests/deploy-prerender-config.test.ts index db9f1d46b..50642fabf 100644 --- a/tests/deploy-prerender-config.test.ts +++ b/tests/deploy-prerender-config.test.ts @@ -414,4 +414,19 @@ describe("deploy prerender config wiring", () => { "deploy", ]); }); + + it("rejects no-promote warmup when discovery finds no requests", async () => { + writeApiOnlyProject(); + const { deploy } = await import("../packages/cloudflare/src/deploy.js"); + + await expect( + deploy({ + root: tmpDir, + skipBuild: true, + warmCdnCache: true, + warmCdnPromote: false, + }), + ).rejects.toThrow("no build-discovered requests were found to warm"); + expect(spawn).not.toHaveBeenCalled(); + }); });