Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cloudflare/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async function deployCommand(): Promise<void> {
warmCdnRetries: parsed.warmCdnRetries,
warmCdnStrict: parsed.warmCdnStrict,
warmCdnPromote: parsed.warmCdnPromote,
warmCdnPromotionDelay: parsed.warmCdnPromotionDelay,
warmCdnIncludeFallbacks: parsed.warmCdnIncludeFallbacks,
experimentalTPR: parsed.experimentalTPR,
tprCoverage: parsed.tprCoverage,
Expand Down
2 changes: 2 additions & 0 deletions packages/cloudflare/src/deploy-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export function formatDeployHelp(): string {
--warm-cdn-retries <n> Retries for transient CDN warmup failures (default: 1)
--warm-cdn-strict Fail deploy when any CDN warmup request fails
--warm-cdn-no-promote Leave the warmed Worker version staged at 0% traffic
--warm-cdn-promotion-delay <ms>
Delay before promotion after warmup (default: 15000)
--warm-cdn-include-fallbacks
Also warm PPR fallback-shell placeholder paths
-h, --help Show this help
Expand Down
23 changes: 20 additions & 3 deletions packages/cloudflare/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import { parseWorkerDeploymentUrl } from "./worker-deployment-url.js";
import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
import { buildPrerenderKVPairs, type KVBulkPair } from "./prerender-kv-populate.js";

const CDN_WARM_PROPAGATION_DELAY_MS = 15_000;
export const DEFAULT_CDN_WARM_PROMOTION_DELAY_MS = 15_000;

// ─── Types ───────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -102,6 +102,8 @@ export type DeployOptions = {
warmCdnStrict?: boolean;
/** Promote the warmed Worker version to 100% traffic (default: true) */
warmCdnPromote?: boolean;
/** Delay between successful warmup and promotion in milliseconds */
warmCdnPromotionDelay?: number;
/** Include PPR fallback-shell placeholder paths during CDN warmup */
warmCdnIncludeFallbacks?: boolean;
/** Enable experimental TPR (Traffic-aware Pre-Rendering) */
Expand Down Expand Up @@ -169,6 +171,7 @@ const deployArgOptions = {
"warm-cdn-retries": { type: "string" },
"warm-cdn-strict": { type: "boolean", default: false },
"warm-cdn-no-promote": { type: "boolean", default: false },
"warm-cdn-promotion-delay": { type: "string" },
"warm-cdn-include-fallbacks": { type: "boolean", default: false },
"experimental-tpr": { type: "boolean", default: false },
"tpr-coverage": { type: "string" },
Expand Down Expand Up @@ -217,6 +220,13 @@ export function parseDeployArgs(args: string[]) {
: parseNonNegativeIntegerArg(values["warm-cdn-retries"], "--warm-cdn-retries"),
warmCdnStrict: values["warm-cdn-strict"],
warmCdnPromote: !values["warm-cdn-no-promote"],
warmCdnPromotionDelay:
values["warm-cdn-promotion-delay"] === undefined
? undefined
: parseNonNegativeIntegerArg(
values["warm-cdn-promotion-delay"],
"--warm-cdn-promotion-delay",
),
warmCdnIncludeFallbacks: values["warm-cdn-include-fallbacks"],
experimentalTPR: values["experimental-tpr"],
tprCoverage: parseIntArg("tpr-coverage", values["tpr-coverage"]),
Expand Down Expand Up @@ -558,6 +568,7 @@ export async function deployWithCdnWarmup(
| "warmCdnRetries"
| "warmCdnStrict"
| "warmCdnPromote"
| "warmCdnPromotionDelay"
> &
Pick<CdnWarmOptions, "deploymentId" | "expectedRscBuildId" | "loadingShellPaths" | "rscPaths">,
): Promise<string> {
Expand Down Expand Up @@ -651,8 +662,13 @@ export async function deployWithCdnWarmup(
let deployed: ReturnType<typeof runWranglerVersionDeploy>;
try {
if (warmedBeforePromotion) {
console.log(" CDN warmup: waiting 15 seconds for cache propagation before promotion...");
await delay(CDN_WARM_PROPAGATION_DELAY_MS);
const promotionDelay = options.warmCdnPromotionDelay ?? DEFAULT_CDN_WARM_PROMOTION_DELAY_MS;
if (promotionDelay > 0) {
console.log(
` CDN warmup: waiting ${promotionDelay / 1_000} seconds for cache propagation before promotion...`,
);
await delay(promotionDelay);
}
}
deployed = runWranglerVersionDeploy(
root,
Expand Down Expand Up @@ -1017,6 +1033,7 @@ export async function deploy(options: DeployOptions): Promise<void> {
warmCdnRetries: options.warmCdnRetries,
warmCdnStrict: options.warmCdnStrict,
warmCdnPromote: options.warmCdnPromote,
warmCdnPromotionDelay: options.warmCdnPromotionDelay,
});
} else {
console.log("\n CDN warmup skipped: no build-discovered paths found.");
Expand Down
2 changes: 2 additions & 0 deletions tests/cloudflare-cdn-warm-deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,13 +338,15 @@ describe("Cloudflare CDN warmup deploy flow", () => {
await deployWithCdnWarmup(tmpDir, ["/"], {
env: "staging",
warmCdnConcurrency: 1,
warmCdnPromotionDelay: 2_500,
});

expect(fetch).toHaveBeenCalledWith(new URL("https://staging.example.com/"), expect.any(Object));
const firstInit = vi.mocked(fetch).mock.calls[0]![1] as RequestInit;
expect(new Headers(firstInit.headers).get("Cloudflare-Workers-Version-Overrides")).toBe(
'my-worker-staging-custom="22222222-2222-4222-8222-222222222222"',
);
expect(delayMock).toHaveBeenCalledWith(2_500);
for (const [, args] of execFileSyncMock.mock.calls as Array<[string, string[]]>) {
expect(args).toEqual(expect.arrayContaining(["--env", "staging"]));
}
Expand Down
9 changes: 9 additions & 0 deletions tests/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ describe("parseDeployArgs", () => {
"0",
"--warm-cdn-strict",
"--warm-cdn-no-promote",
"--warm-cdn-promotion-delay=2500",
"--warm-cdn-include-fallbacks",
]);

Expand All @@ -778,20 +779,28 @@ describe("parseDeployArgs", () => {
expect(parsed.warmCdnRetries).toBe(0);
expect(parsed.warmCdnStrict).toBe(true);
expect(parsed.warmCdnPromote).toBe(false);
expect(parsed.warmCdnPromotionDelay).toBe(2500);
expect(parsed.warmCdnIncludeFallbacks).toBe(true);
});

it("promotes warmed Worker versions by default", () => {
expect(parseDeployArgs([]).warmCdnPromote).toBe(true);
});

it("allows the CDN warmup promotion delay to be set to zero", () => {
expect(parseDeployArgs(["--warm-cdn-promotion-delay=0"]).warmCdnPromotionDelay).toBe(0);
});

it("throws for invalid CDN warmup numeric flags", () => {
expect(() => parseDeployArgs(["--warm-cdn-concurrency=0"])).toThrow(
'--warm-cdn-concurrency expects a positive integer, but got "0".',
);
expect(() => parseDeployArgs(["--warm-cdn-retries=-1"])).toThrow(
'--warm-cdn-retries expects a non-negative integer, but got "-1".',
);
expect(() => parseDeployArgs(["--warm-cdn-promotion-delay=-1"])).toThrow(
'--warm-cdn-promotion-delay expects a non-negative integer, but got "-1".',
);
});

it("trims whitespace from --env value", () => {
Expand Down
Loading