From ae692e12126e760fd49bae92226d2cdf4a0b37da Mon Sep 17 00:00:00 2001 From: James Date: Sat, 22 Aug 2026 02:47:06 +0100 Subject: [PATCH] fix(cloudflare): preserve staged warmup guarantees --- packages/cloudflare/src/deploy.ts | 18 ++--- packages/cloudflare/src/version-deploy.ts | 8 ++- .../cloudflare/src/worker-deployment-url.ts | 6 +- tests/cloudflare-cdn-warm-deploy.test.ts | 65 +++++++++++++++++++ tests/cloudflare-version-deploy.test.ts | 1 + tests/deploy.test.ts | 8 +++ 6 files changed, 91 insertions(+), 15 deletions(-) diff --git a/packages/cloudflare/src/deploy.ts b/packages/cloudflare/src/deploy.ts index bd10cbe1a..84beedd70 100644 --- a/packages/cloudflare/src/deploy.ts +++ b/packages/cloudflare/src/deploy.ts @@ -619,7 +619,7 @@ export async function deployWithCdnWarmup( }); const wranglerConfig = parseWranglerConfig(root, options.config); - const deploymentStatus = readWranglerDeploymentStatus(root, options); + const deploymentStatus = runWranglerDeploymentStatus(root, options); const stagingTraffic = getZeroPercentStagingTraffic(deploymentStatus, upload.versionId); const canVerifyStagedHtml = options.expectedBuildId !== undefined; if (!canVerifyStagedHtml && paths.length > 0) { @@ -652,7 +652,10 @@ export async function deployWithCdnWarmup( } const targetUrl = resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? staged.deployedUrl; - const workerName = resolveWorkerNameForVersionOverride(wranglerConfig, options); + const workerName = + options.name ?? + upload.workerName ?? + resolveWorkerNameForVersionOverride(wranglerConfig, options); const headers = buildVersionOverrideHeaders(workerName, upload.versionId); if (targetUrl && headers) { try { @@ -782,17 +785,6 @@ export function resolveCdnWarmupTargetUrl( return deployedUrl; } -function readWranglerDeploymentStatus( - root: string, - options: Pick, -): WranglerDeploymentStatus | null { - try { - return runWranglerDeploymentStatus(root, options); - } catch { - return null; - } -} - export function getZeroPercentStagingTraffic( deployment: WranglerDeploymentStatus | null, versionId: string, diff --git a/packages/cloudflare/src/version-deploy.ts b/packages/cloudflare/src/version-deploy.ts index fe7adff36..3202e58df 100644 --- a/packages/cloudflare/src/version-deploy.ts +++ b/packages/cloudflare/src/version-deploy.ts @@ -13,6 +13,7 @@ export { parseWorkersDevUrl } from "./workers-dev-url.js"; export type WranglerVersionUploadResult = { versionId: string; previewUrl: string | null; + workerName: string | null; output: string; }; @@ -90,16 +91,21 @@ export function parseVersionId(output: string): string | null { return output.match(new RegExp(`\\b${versionIdPattern}\\b`))?.[0] ?? null; } +export function parseUploadedWorkerName(output: string): string | null { + return output.match(/^\s*Uploaded\s+(\S+)\s+\(\d+(?:\.\d+)?\s+sec\)\s*$/im)?.[1] ?? null; +} + export function parseWranglerVersionUploadOutput(output: string): WranglerVersionUploadResult { const parsed = parseJsonObject(output); const versionId = findVersionIdInUploadJson(parsed) ?? parseVersionId(output); const previewUrl = findPreviewUrlInUploadJson(parsed) ?? parseWorkersDevUrl(output); + const workerName = parseUploadedWorkerName(output); if (!versionId) { throw new Error("Could not detect Worker version ID from `wrangler versions upload` output."); } - return { versionId, previewUrl, output }; + return { versionId, previewUrl, workerName, output }; } export function buildWranglerVersionUploadArgs( diff --git a/packages/cloudflare/src/worker-deployment-url.ts b/packages/cloudflare/src/worker-deployment-url.ts index ff4788ed3..39a1d1252 100644 --- a/packages/cloudflare/src/worker-deployment-url.ts +++ b/packages/cloudflare/src/worker-deployment-url.ts @@ -10,7 +10,11 @@ export function parseWorkerDeploymentUrl(output: string): string | null { * canonical deployment URLs for general CLI reporting. */ export function parseCdnWarmupDeploymentUrl(output: string): string | null { - return parseWorkerDeploymentUrl(output) ?? parseCatchAllWorkerRouteOrigin(output); + return ( + parseCustomDomainUrl(output) ?? + parseCatchAllWorkerRouteOrigin(output) ?? + parseWorkersDevUrl(output) + ); } function parseCatchAllWorkerRouteOrigin(output: string): string | null { diff --git a/tests/cloudflare-cdn-warm-deploy.test.ts b/tests/cloudflare-cdn-warm-deploy.test.ts index 119a28e3b..9e429d915 100644 --- a/tests/cloudflare-cdn-warm-deploy.test.ts +++ b/tests/cloudflare-cdn-warm-deploy.test.ts @@ -92,6 +92,29 @@ describe("Cloudflare CDN warmup deploy flow", () => { expect(hasCdnWarmRequests({ loadingShellPaths: [], paths: [], rscPaths: [] })).toBe(false); }); + it("does not promote when deployment status cannot be read", async () => { + writeFile("wrangler.jsonc", JSON.stringify({ name: "my-worker" })); + execFileSyncMock.mockImplementation((_file: string, args: string[]) => { + if (args.includes("upload")) { + return "Uploaded my-worker (1.23 sec)\nWorker Version ID: 22222222-2222-4222-8222-222222222222\n"; + } + if (args.includes("status")) { + throw new Error("deployment status unavailable"); + } + throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`); + }); + const { deployWithCdnWarmup } = await import("../packages/cloudflare/src/deploy.js"); + + await expect( + deployWithCdnWarmup(tmpDir, ["/"], { + expectedBuildId: "app-build-a", + warmCdnStrict: true, + }), + ).rejects.toThrow("deployment status unavailable"); + expect(execFileSyncMock).toHaveBeenCalledTimes(2); + expect(fetch).not.toHaveBeenCalled(); + }); + it("warms the production custom domain through a 0% staged version override", async () => { const events: string[] = []; delayMock.mockImplementation(async (milliseconds: number) => { @@ -838,6 +861,48 @@ describe("Cloudflare CDN warmup deploy flow", () => { } }); + it("uses Wrangler's uploaded Worker name for a TOML config", async () => { + writeFile( + "wrangler.toml", + ["name = 'toml-worker'", "workers_dev = false", "route = 'app.example.com/*'"].join("\n"), + ); + execFileSyncMock.mockImplementation((_file: string, args: string[]) => { + if (args.includes("upload")) { + return [ + "Uploaded toml-worker (1.23 sec)", + "Worker Version ID: 22222222-2222-4222-8222-222222222222", + ].join("\n"); + } + if (args.includes("status")) { + return JSON.stringify({ + versions: [{ version_id: "11111111-1111-4111-8111-111111111111", percentage: 100 }], + }); + } + if (args.includes("deploy") && args.includes("22222222-2222-4222-8222-222222222222@0%")) { + return "Staged version\n"; + } + if (args.includes("deploy") && args.includes("22222222-2222-4222-8222-222222222222@100%")) { + return "Promoted version\n"; + } + if (args.includes("triggers")) { + return "Triggers deployed\n app.example.com/*\n"; + } + throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`); + }); + const { deployWithCdnWarmup } = await import("../packages/cloudflare/src/deploy.js"); + + await deployWithCdnWarmup(tmpDir, ["/"], { + expectedBuildId: "app-build-a", + warmCdnConcurrency: 1, + warmCdnPromotionDelay: 0, + }); + + const headers = new Headers(vi.mocked(fetch).mock.calls[0]?.[1]?.headers); + expect(headers.get("Cloudflare-Workers-Version-Overrides")).toBe( + 'toml-worker="22222222-2222-4222-8222-222222222222"', + ); + }); + it("explains staged version cleanup when strict pre-promotion warmup fails", async () => { writeFile( "wrangler.jsonc", diff --git a/tests/cloudflare-version-deploy.test.ts b/tests/cloudflare-version-deploy.test.ts index ccd79c3e2..bf5d726b2 100644 --- a/tests/cloudflare-version-deploy.test.ts +++ b/tests/cloudflare-version-deploy.test.ts @@ -218,6 +218,7 @@ describe("Cloudflare Wrangler version deployment helpers", () => { expect(parseWranglerVersionUploadOutput(output)).toMatchObject({ versionId: "7283300a-90b0-45d6-ba08-7c4b76797f38", + workerName: "rsc-prewarm-32362416100", previewUrl: "https://7283300a-rsc-prewarm-32362416100.vinext.workers.dev", }); }); diff --git a/tests/deploy.test.ts b/tests/deploy.test.ts index dbc4a00c9..10d72749a 100644 --- a/tests/deploy.test.ts +++ b/tests/deploy.test.ts @@ -572,6 +572,14 @@ describe("parseWorkerDeploymentUrl", () => { expect(parseCdnWarmupDeploymentUrl(`Deployed app triggers\n ${route}\n`)).toBe(expected); }); + it("prefers a concrete Worker route over a workers.dev fallback", () => { + expect( + parseCdnWarmupDeploymentUrl( + "Deployed app triggers\n https://app.account.workers.dev\n app.example.com/*\n", + ), + ).toBe("https://app.example.com"); + }); + it.each(["*.example.com/*", "app.example.com/api/*", "app.example.com/"])( "rejects a non-canonical Worker route for CDN warmup: %s", (route) => {