Skip to content

feat(cloudflare): warm prerendered paths before deploy#2481

Merged
james-elicx merged 19 commits into
mainfrom
codex/cloudflare-cdn-warm-prerender
Jul 4, 2026
Merged

feat(cloudflare): warm prerendered paths before deploy#2481
james-elicx merged 19 commits into
mainfrom
codex/cloudflare-cdn-warm-prerender

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • add an opt-in --warm-cdn-cache deploy path that uploads a Worker version, warms prerendered paths through the version preview URL, then deploys that exact version
  • expose a narrow internal prerender-manifest helper for selecting concrete warmup paths
  • guard warmup against fallback shells, 404 documents, missing preview URLs, and stale prerender manifests

Validation

  • vp check packages/cloudflare/src/deploy.ts packages/cloudflare/src/cdn-warm.ts packages/cloudflare/src/version-deploy.ts packages/cloudflare/src/cli.ts packages/cloudflare/src/deploy-help.ts packages/vinext/src/server/prerender-manifest.ts tests/deploy.test.ts tests/deploy-prerender-config.test.ts tests/cloudflare-cdn-warm.test.ts tests/cloudflare-version-deploy.test.ts tests/pregenerated-concrete-paths.test.ts
  • vp test run tests/deploy.test.ts tests/deploy-prerender-config.test.ts tests/cloudflare-cdn-warm.test.ts tests/cloudflare-version-deploy.test.ts tests/pregenerated-concrete-paths.test.ts

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/vinext/@vinext/cloudflare@2481
npm i https://pkg.pr.new/cloudflare/vinext/create-vinext-app@2481
npm i https://pkg.pr.new/cloudflare/vinext@2481

commit: 7ba145a

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

Comment thread packages/cloudflare/src/version-deploy.ts Fixed

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review — warm prerendered paths before deploy

Reviewed the new --warm-cdn-cache path end-to-end (deploy.ts, cdn-warm.ts, version-deploy.ts, prerender-manifest.ts) plus tests. The code is clean, well-factored, and well-tested. A few issues worth resolving before merge, ordered by severity.

1. (Blocking question) Does warming the version preview URL actually populate the production CDN cache?

deployWithCdnWarmup warms paths against upload.previewUrl (the <version-prefix>-<worker>.workers.dev preview URL), then runs versions deploy … @100%. The CDN adapter (packages/cloudflare/src/cache/cdn-adapter.runtime.ts) relies on Cloudflare's edge cache (ctx.cache), which is keyed by request URL including hostname/zone.

The version preview URL lives on the workers.dev zone; a production custom domain (and even the stable <worker>.workers.dev URL) is a different hostname/zone. Cache entries populated against the preview hostname are not guaranteed to be served for production requests. If that's the case, this feature warms a cache namespace that production traffic never reads, and the whole flow is a no-op for the intended benefit.

Please confirm (ideally with a real deploy against a custom domain) that a page warmed via the preview URL is served as a CDN HIT on the production hostname. If it isn't, warming needs to target the production URL after versions deploy (or use the promoted stable URL), not the preview URL. This is the core correctness question for the PR.

2. Only /404 is filtered as an error document — /500 (and /_error) also land in the manifest

isErrorDocumentRoute in prerender-manifest.ts only excludes /404:

function isErrorDocumentRoute(pathname: string, route: PrerenderManifestRoute): boolean {
  return pathname === "/404" || route.route === "/404";
}

But /500 is a normal static page (it is not in BLOCKED_PAGES, which is only _document/_app/_error — see shims/constants.ts:119 and STATIC_STATUS_PAGES = ["/500"] at :169). The Pages Router prerender loop renders it with status: "rendered" (build/prerender.ts:752+ skips only /404), so /500 will be selected and warmed.

A direct GET to /500 returns 200 (so it won't fail strict mode), but warming the error/status page is wasteful and semantically wrong — you're caching the error page at the edge. Suggest extending the filter to cover /500 and /_error as well, mirroring the routePattern !== "/_error" && routePattern !== "/500" carve-outs the server already uses.

3. Warm paths are read twice

deploy() reads readPrerenderWarmPaths(...) at deploy.ts:539 to decide whether to warm, then warmCdnCacheFromPrerender() reads them again at cdn-warm.ts:157. Redundant file/JSON parse and, more importantly, two places that must stay in sync (same options threading). Consider reading once in deploy() and passing the resolved paths into warmCdnCacheFromPrerender (or having deployWithCdnWarmup return early), so the "should we warm?" decision and the actual warm use the exact same list.

4. getWarmPathsFromPrerenderManifest drops includeErrorDocuments

export function getWarmPathsFromPrerenderManifest(
  manifest: PrerenderManifest,
  options?: { includeFallbackShells?: boolean },  // no includeErrorDocuments
): string[] {
  return getPrerenderedConcretePaths(manifest, options);
}

The underlying getPrerenderedConcretePaths supports includeErrorDocuments, but this public wrapper narrows the options type so callers can't pass it. Either forward the full PrerenderedPathSelectionOptions or document why error docs are intentionally non-configurable here.

5. 3xx responses count as a successful warm

warmOnePath uses redirect: "manual" and treats response.status < 400 as ok. A prerendered path that responds with a redirect (e.g. trailing-slash normalization) is recorded as "warmed" even though the actual destination page was never fetched/cached. Probably acceptable, but worth a comment, or follow the redirect once for the target you actually want warmed.

Nits

  • fetchWithTimeout sends User-Agent: vinext-cloudflare-cdn-warm/0 — the /0 version suffix reads like a placeholder; either drop it or wire the real package version.
  • warmOnePath retries network exceptions up to retries but does not distinguish abort-from-timeout vs. other errors; fine, just noting the abort message ("The operation was aborted") is what surfaces in failures.

Items 1 and 2 are the ones I'd want resolved before merge; 3–5 are quality improvements.

@ask-bonk

ask-bonk Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481. Summary of what I found:

Review posted

The --warm-cdn-cache feature is cleanly implemented and well-tested (29 passing tests, clean type/lint check). I flagged 5 issues, 2 of which I'd resolve before merge:

Blocking:

  1. Cache scope mismatch — Warmup targets the version preview URL (<prefix>-worker.workers.dev), but the CDN adapter uses Cloudflare's edge cache keyed by hostname/zone. Cache populated on the preview hostname is not guaranteed to serve production requests on a custom domain, potentially making the feature a no-op for its intended benefit. Needs real-deploy verification.

  2. Only /404 filtered/500 is a normal static page (not in BLOCKED_PAGES) that lands in the manifest as status: "rendered", so it gets warmed. Wasteful/wrong; the filter should also cover /500 and /_error, mirroring existing server carve-outs.

Quality:
3. Warm paths are read twice (deploy.ts:539 and cdn-warm.ts:157) — two sites that must stay in sync.
4. getWarmPathsFromPrerenderManifest narrows its options type and drops includeErrorDocuments.
5. 3xx redirects count as a successful warm without fetching the real target.

Plus two nits (placeholder /0 User-Agent version, abort-error messaging).

I verified concerns against the codebase: confirmed /500 reaches the prerender manifest via build/prerender.ts:752+ and shims/constants.ts, and confirmed the CDN cache is hostname-scoped via cache/cdn-adapter.runtime.ts.

github run

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 09cc1bb against base 454dfbd using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 126.3 KB 126.3 KB ⚫ -0.0%
Client entry size (gzip) vinext 120.3 KB 120.3 KB ⚫ 0.0%
Dev server cold start vinext 2.66 s 2.65 s ⚫ -0.5%
Production build time vinext 3.16 s 3.16 s ⚫ -0.1%
RSC entry closure size (gzip) vinext 97.5 KB 97.5 KB ⚫ +0.0%
Server bundle size (gzip) vinext 164.0 KB 164.0 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for 24126e6)

Re-reviewed after the follow-up commit 24126e6 ("warm production CDN for uploaded versions"). That commit resolves the substantive issues from the first pass, and switches the design to the correct Cloudflare "smoke-test" pattern (upload version → stage new version at 0% → warm the production URL with Cloudflare-Workers-Version-Overrides → promote to 100%). Verified against the Cloudflare version-overrides docs, which document exactly this flow.

Resolved since the last review:

  • #1 (cache scope) — warmup now targets the production URL (custom domain, else the promoted/stable workers.dev URL) via a 0% staged version + version-override header, instead of the version preview hostname. This is the right fix for the hostname/zone-scoped edge cache.
  • #2 (/500 + /_error)isErrorDocumentRoute now excludes /404, /500, and /_error (prerender-manifest.ts:54). Covered by tests.
  • #3 (double read) — paths are read once in deploy() and threaded into deployWithCdnWarmup(root, warmPaths, …). No more duplicate read.
  • #4 (includeErrorDocuments)getWarmPathsFromPrerenderManifest now forwards the full PrerenderedPathSelectionOptions.
  • #5 (3xx warming)fetchWithTimeout now uses redirect: "follow", so redirect targets get warmed.

Tests pass (304 in the 5 PR-affected files) and vp check is clean. Remaining items below, ordered by severity.

1. (Blocking) --env / --name deploys build the version-override header with the wrong Worker name

buildVersionOverrideHeaders(wranglerConfig?.name, versionId) (deploy.ts:382) keys the header off the top-level name from parseWranglerConfig, which only reads the top-level name field (tpr.ts:191, and extractFromTOML at :273). But the pre-traffic warmup path only runs for --env/preview/--name deploys as well as production, and:

  • With --env staging, Cloudflare deploys the script as <name>-staging by default (or whatever env.staging.name is). The override header still says name="…", so the version override silently does not apply — per the docs, an unmatched/invalid override falls back to routing by traffic percentage. Since the new version is staged at 0%, the warmup requests hit the old 100% version. Warming still returns 200 (old version serves fine), so even --warm-cdn-strict won't catch it — the new version's cache is never warmed, and the feature is a silent no-op for env deploys.
  • --name my-app has the same problem: it sets info.projectName but never reaches parseWranglerConfig, so the header uses the config name, not the actual deployed name.

Fix: resolve the effective Worker name the same way Wrangler does for the target env (top-level name + env suffix, or env.<name>.name), and use options.name when provided. Consider verifying the override actually applied (e.g. version-metadata binding echoed in a response header) before trusting the warm, since a mismatch fails open.

2. (Design limitation, please document) Edge warming is per-colo, not global

The CDN adapter serves from Cloudflare's edge cache (ctx.cache / caches.default), which is per-data-center, not a global store. A warmup run from a single CI runner only reaches the handful of colos that terminate those requests, so --warm-cdn-cache warms a small subset of edge locations, not the whole network. That's still useful (it primes the origin render + the nearest colos) but the naming/help text ("warm prerendered paths through the production URL") implies broader coverage than it delivers. Suggest a one-line caveat in deploy-help.ts and/or the option doc comment so users don't expect a globally-warm cache.

3. (Please verify on a real deploy) Does a version-override request populate the cache that normal traffic reads?

The whole flow assumes a response produced while serving the 0% version (selected via Cloudflare-Workers-Version-Overrides) lands in the same URL-keyed edge cache entry that a subsequent normal request (no override header) will HIT. That's plausible — the override is a routing header, not part of the cache key — but I couldn't find explicit Cloudflare documentation confirming the override header is excluded from the cache key and doesn't force a bypass/Vary. This is the core correctness assumption of the pre-promotion warm; please confirm with a real custom-domain deploy that a path warmed via the 0% override serves as a HIT (or UPDATING) on the production hostname without the header. If it doesn't, the pre-promotion warm is wasted and only the post-promotion fallback path (warmedBeforePromotion === false) actually helps.

Nits

  • ReDoS (CodeQL #239): parseWorkersDevUrl regex /https:\/\/[^\s"'<>]+\.workers\.dev[^\s"'<>]*/ (version-deploy.ts:65) is polynomial-backtracking on inputs like https:// + long non-matching runs. Input is trusted Wrangler stdout, so not exploitable, but it's a trivial fix to make the character class exclude .// boundaries or anchor the host, which would also silence the scanner.
  • getZeroPercentStagingTraffic returns [current, {versionId, percentage:0}] without guarding the case where the freshly-uploaded versionId equals the current 100% version (would produce a v@100% v@0% pair). Unlikely in practice, but a quick equality check would be defensive.
  • User-Agent is now vinext-cloudflare-cdn-warm (dropped the placeholder /0 — good).

Item 1 is the one I'd block on; 2 and 3 are verification/documentation asks before this is relied on in production.

@ask-bonk

ask-bonk Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head (24126e6), which added a follow-up commit not reflected in the original PR context. That commit resolved all 5 issues from the prior review by switching to Cloudflare's documented "smoke-test" pattern (upload → stage new version at 0% → warm the production URL via Cloudflare-Workers-Version-Overrides → promote). Tests (304) pass and vp check is clean.

New finding I'd block on:

  1. --env/--name deploys build the version-override header with the wrong Worker name. buildVersionOverrideHeaders keys off the top-level name from parseWranglerConfig, but Cloudflare deploys env scripts as <name>-<env> (or env.<name>.name), and --name never reaches that parser. When the override key mismatches, Cloudflare falls back to percentage routing — so warmup hits the old 100% version, not the staged 0% version. Even --warm-cdn-strict won't catch it (old version serves 200), making the feature a silent no-op for env deploys.

Verification/documentation asks:
2. Edge cache is per-colo, so warming from one CI runner only primes a few data centers — help text implies broader coverage.
3. Needs a real-deploy confirmation that a version-override request actually populates the URL-keyed cache that normal (no-header) production traffic reads.

Plus nits: the CodeQL-flagged polynomial regex in parseWorkersDevUrl (trusted input, trivially fixable), and a defensive equality guard in getZeroPercentStagingTraffic.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for 7ed95fd)

Re-reviewed after the new follow-up commit 7ed95fd ("resolve Worker names for CDN warmup"), which landed on top of the 24126e6 head my previous review covered. This commit is a targeted fix for the one blocking issue from last time, and it holds up.

Resolved since the last review

#1 (blocking) — wrong Worker name in the version-override header for --env / --name. Fixed by resolveWorkerNameForVersionOverride (deploy.ts:503) and threaded through deployWithCdnWarmupbuildVersionOverrideHeaders (deploy.ts:391–392). I verified the resolution logic against Wrangler's own config normalization in workers-sdk (validation-helpers.ts inheritableInWranglerEnvironments / appendEnvName / notAllowedInNamedServiceEnvironment), and the three branches match Wrangler exactly:

Case Wrangler deployed name vinext resolves
--name X X X
legacy env, env.X.name set env.X.name verbatim env.X.name
legacy env, no per-env name <name>-<env> `${name}-${env}`
service env (legacy_env:false) top-level name (per-env name is a hard error in Wrangler) config.name

Covered end-to-end by tests/cloudflare-cdn-warm-deploy.test.ts (production, --env staging with custom env name + env custom domain, explicit --name) and unit-tested in tests/deploy.test.ts resolveWorkerNameForVersionOverride.

Nit — getZeroPercentStagingTraffic equality guard. Now guards the "uploaded version already deployed at 100%" case (deploy.ts:491–493), falling back to the post-promotion warm instead of staging a v@100% v@0% self-pair. Tested.

Nit — CodeQL #239 (polynomial regex). parseWorkersDevUrl was rewritten as a manual whitespace tokenizer + new URL() validation (workers-dev-url.ts), dropping the backtracking regex entirely. Regression-tested with a 2048-char input (cloudflare-version-deploy.test.ts:124).

All 324 tests in the affected files pass and vp check is clean on the changed files.

Remaining (verification/documentation, not blockers)

A. (Please verify on a real deploy) Does a version-override request populate the cache normal traffic reads? This is still the core unproven assumption, unchanged by this commit. The CDN adapter (cache/cdn-adapter.runtime.ts) is a pure edge-cache design: origin get() returns null, set() is a no-op, and caching happens entirely via CDN-Cache-Control on the response, keyed by URL. The pre-promotion warm sends Cloudflare-Workers-Version-Overrides to route to the 0% version; the flow assumes the response it produces lands in the same URL-keyed edge entry a later header-less request will HIT. That's plausible (override is a routing header, not part of the cache key), but I still couldn't find Cloudflare docs confirming the header is excluded from the cache key and doesn't force a bypass/Vary. Please confirm with a real custom-domain deploy that a path warmed via the 0% override serves as HIT/UPDATING on the production hostname without the header. If not, only the post-promotion fallback (warmedBeforePromotion === false) actually helps.

B. (Documented — good) Per-colo warming. The help text now carries the caveat that warmup only primes the data centers the warm run reaches (deploy-help.ts:46–47). Matches the edge-cache reality.

Nits (non-blocking)

  • parseWranglerVersionUploadOutput uses a recursive findStringByKey over the JSON for ["id","version_id","versionId"] and ["preview_url","previewUrl","url"] (version-deploy.ts:44–54). A recursive first-match could latch onto an unrelated nested id/url if Wrangler's --json shape changes; the parseVersionId UUID-regex fallback covers version IDs, but a stray nested url could beat the real preview_url. Low risk given current output shape — worth anchoring to the known top-level keys if you want to be defensive.
  • Strict-mode failure between staging and promotion leaves the deployment in the staged old@100% / new@0% state with triggers not applied. Production is unaffected (old version still serves), so this is a safe failure, but a one-line note in the thrown error that the staged version was left in place (and how to clean it up) would help operators.

Net: the blocking issue from the previous pass is genuinely fixed and matches Wrangler's naming semantics. The only thing gating "rely on this in production" is the real-deploy confirmation in A.

@ask-bonk

ask-bonk Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head 7ed95fd — a new follow-up commit ("resolve Worker names for CDN warmup") that landed on top of the 24126e6 head the prior ask-bonk review covered. It's a targeted fix for the one remaining blocking issue.

The blocking issue from last review is genuinely fixed. The wrong-Worker-name bug for --env/--name deploys is resolved by resolveWorkerNameForVersionOverride. I verified the resolution logic against Wrangler's own config normalization in workers-sdk source (via a research subagent), and all four branches match Wrangler exactly:

  • --name XX
  • legacy env + per-env name → verbatim
  • legacy env, no per-env name → <name>-<env>
  • service env (legacy_env:false) → top-level name (per-env name is a hard error in Wrangler, so falling back to top-level is correct)

I initially suspected the legacyEnv === false branch ignored per-env names incorrectly, but the research confirmed service environments genuinely always use the top-level name — the code is right.

Also resolved: the getZeroPercentStagingTraffic self-pair equality guard, and the CodeQL #239 ReDoS regex (rewritten as a tokenizer, regression-tested with a 2048-char input).

Verification: 324 tests pass in the affected files, vp check clean.

Remaining (non-blocking): the core unproven assumption — that a version-override (0%-staged) request actually populates the URL-keyed edge cache that later header-less production traffic reads — still needs a real custom-domain deploy to confirm. Per-colo warming is now documented in the help text. Two nits: the recursive JSON key search in parseWranglerVersionUploadOutput could latch onto a nested url/id, and strict-mode failure leaves the deployment in a staged (but safe) state worth noting in the error.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for e158661)

Re-reviewed after the new follow-up commit e158661 ("harden CDN warmup diagnostics"), which landed on top of the 7ed95fd head my previous review covered. It's a focused commit that closes out the two non-blocking nits from last pass — nothing new to block on.

Resolved since the last review

Nit A — recursive JSON key search could latch onto a nested id/url. Fixed. findStringByKey (arbitrary-depth recursion) is replaced by findStringInRecord anchored to a fixed set of top-level containers: the root object, .version, and .result (version-deploy.ts:64–80). A stray diagnostics.id / diagnostics.url can no longer beat the real version_id / preview_url. Covered by the new test tests/cloudflare-version-deploy.test.ts:141 ("does not treat unrelated nested JSON IDs and URLs as upload metadata"), and the existing .version-nested case (:145) still passes, so the legitimate one-level nesting Wrangler uses is preserved. The parseVersionId / parseWorkersDevUrl regex fallbacks remain as a backstop.

Nit B — strict-mode failure between staging and promotion leaves a staged version. Fixed. Both pre-promotion strict paths now attach an operator-facing cleanup note:

  • the strict warm failure wraps the underlying error via withStagedVersionCleanupNote and preserves it as cause (deploy.ts:404–406, 542–547);
  • the "no production URL/Worker name" strict case appends getStagedVersionCleanupNote() (deploy.ts:408–413).

The note correctly explains the version may remain at 0% with the previous version still serving 100%, and how to promote/clean it up. I confirmed the placement is right: both wrapped sites run before the 100% promotion (line 421), so the staged-at-0% description is accurate. The post-promotion fallback warm (warmedBeforePromotion === false, line 429) intentionally does not get this note, which is correct — by that point the version is already live at 100%, so there's nothing staged to clean up. Tested by tests/cloudflare-cdn-warm-deploy.test.ts:225 ("explains staged version cleanup when strict pre-promotion warmup fails").

Verification

  • vp check clean on all changed source files.
  • 326 tests pass across the affected files (tests/deploy.test.ts, tests/cloudflare-cdn-warm.test.ts, tests/cloudflare-version-deploy.test.ts, tests/cloudflare-cdn-warm-deploy.test.ts, tests/pregenerated-concrete-paths.test.ts, tests/deploy-prerender-config.test.ts).

Remaining (unchanged — verification, not a blocker)

A. (Please verify on a real deploy) Does a version-override request populate the cache normal traffic reads? This is still the one open item, unchanged by this commit. The CDN adapter (cache/cdn-adapter.runtime.ts) is a pure edge-cache design keyed by URL, and the pre-promotion warm sends Cloudflare-Workers-Version-Overrides to route to the 0% version. The flow assumes the response produced under that override lands in the same URL-keyed edge entry a later header-less request will HIT. That's plausible (the override is a routing header, not part of the cache key), but I still can't find Cloudflare docs confirming the header is excluded from the cache key and doesn't force a bypass/Vary. Please confirm with a real custom-domain deploy that a path warmed via the 0% override serves as HIT/UPDATING on the production hostname without the header. If it doesn't, only the post-promotion fallback path actually helps. Everything else about the design is sound and well-tested.

Net: no blocking issues. The two remaining nits from the last pass are cleanly resolved and tested. The only thing gating "rely on this in production" is the real-deploy confirmation in A.

@ask-bonk

ask-bonk Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head e158661 — a new follow-up commit ("harden CDN warmup diagnostics") on top of the 7ed95fd head my last review covered. It's a focused cleanup of the two non-blocking nits from the previous pass, and both are genuinely fixed.

Resolved since last review:

  • Recursive JSON key searchfindStringByKey (arbitrary-depth recursion) is replaced by findStringInRecord anchored to the root object, .version, and .result. A nested diagnostics.id/diagnostics.url can no longer beat the real version_id/preview_url. New test at cloudflare-version-deploy.test.ts:141 covers the carve-out; the legitimate one-level .version nesting still parses.
  • Staged-version cleanup on strict failure — both pre-promotion strict paths now attach an operator-facing note explaining the version may remain staged at 0% and how to promote/clean up. I verified placement: both wrapped sites run before the 100% promotion, and the post-promotion fallback correctly omits the note (version is already live). Tested at cloudflare-cdn-warm-deploy.test.ts:225.

Verification: vp check clean; 326 tests pass across the 6 affected files.

No blocking issues. The only remaining item — unchanged by this commit — is the real-deploy confirmation that a version-override (0%-staged) request actually populates the URL-keyed edge cache that later header-less production traffic reads. That's a verification ask, not a code defect; the design and tests are sound.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for d8a1060f)

Re-reviewed after the new follow-up commit d8a1060f ("harden CDN warmup deploy flow"), which landed on top of the e158661 head my previous review covered. It makes two changes, both of which check out.

Resolved / new since last review

1. Trigger ordering (deploy.ts). runWranglerTriggersDeploy was previously called unconditionally at the very end (after promotion). It's now hoisted into an idempotent applyTriggers() closure and invoked before warmup in the staged branch and before the post-promotion fallback warm. This is the right fix: for a first-ever deploy the custom domain/route isn't bound until triggers deploy runs, so warming https://<custom-domain>/… would 404 (or fall through) unless triggers are applied first. I traced every exit path:

  • Staged branch (single 100% version): stage(old@100% new@0%)applyTriggers() (wrapped, throws withStagedVersionCleanupNote on failure) → warm via Cloudflare-Workers-Version-Overridespromote(new@100%). Triggers bind the domain to the deployment (percentage split), so production keeps serving old@100% between trigger-apply and promotion — safe.
  • Fallback branch (warmedBeforePromotion === false): promote(new@100%)applyTriggers() (no-op if already applied) → warm. Correct — by this point the new version is already live.
  • The triggersApplied guard makes the second applyTriggers() a genuine no-op, so triggers run exactly once.

Verified by the two new/updated tests in cloudflare-cdn-warm-deploy.test.ts that assert the exact event order (stage → triggers → fetch → promote for the staged path; promote → triggers → fetch for the fallback path) and that a trigger failure after staging surfaces the "may remain staged at 0%" cleanup note without issuing any warm fetches.

2. JSONC trailing-comma handling (tpr.ts). parseWranglerConfig now runs stripJsonTrailingCommas(stripJsonComments(content)) before JSON.parse. Wrangler treats wrangler.jsonc/wrangler.json as JSONC (comments and trailing commas), so the prior comments-only pass would throw on a valid config with trailing commas and silently continue to the next candidate file (returning null → wrong/absent Worker name/custom-domain resolution, which directly feeds the version-override header and warmup target). I exercised the new stripper against the tricky cases — trailing-comma-like sequences inside strings ("x,}"), comment-like text inside strings ("/* not a comment */",), escaped quotes before a trailing comma ("he said \"hi\"",), nested trailing commas, and a comment immediately preceding a trailing comma — and all parse correctly. The two-pass string-state tracking is consistent between the two functions.

Verification

  • vp check clean on deploy.ts, tpr.ts, version-deploy.ts, cdn-warm.ts.
  • 329 tests pass across the affected files (deploy.test.ts 280, cloudflare-cdn-warm-deploy.test.ts, cloudflare-version-deploy.test.ts, cloudflare-cdn-warm.test.ts, pregenerated-concrete-paths.test.ts, deploy-prerender-config.test.ts).

Nits (non-blocking)

  • applyTriggers() in the fallback branch isn't wrapped with the cleanup note (deploy.ts:439). Minor asymmetry: in the staged branch a trigger failure is annotated with withStagedVersionCleanupNote, but in the fallback branch promote(new@100%) has already succeeded when applyTriggers() runs, so a trigger failure there leaves the new version live at 100% with stale routes/cron and a bare error. Not a staged-version situation (so the existing note wouldn't apply verbatim), but a one-line hint that promotion succeeded and only trigger application failed would help operators know the code is already serving and only routes/cron need reapplying.
  • JSONC parsing is now a hand-rolled two-pass (comments, then trailing commas). It's correct and matches the pre-existing stripJsonComments style, so no change requested — just flagging that these two passes must stay in sync (same string/escape state machine) if either is touched later.

Remaining (unchanged — verification, not a blocker)

Does a version-override (0%-staged) request populate the URL-keyed edge cache that later header-less production traffic reads? Still the one open correctness assumption, untouched by this commit. The CDN adapter (cache/cdn-adapter.runtime.ts) is a pure edge-cache design keyed by URL; the pre-promotion warm sends Cloudflare-Workers-Version-Overrides to route to the 0% version and assumes the resulting response lands in the same URL-keyed entry a later no-header request will HIT. Plausible (the override is a routing header, not part of the cache key), but please confirm on a real custom-domain deploy that a path warmed via the 0% override serves as HIT/UPDATING on the production hostname without the header. If not, only the post-promotion fallback path actually helps.

Net: no blocking issues. The trigger-ordering hoist is correct and the JSONC trailing-comma fix closes a real config-parse gap that would have broken Worker-name/custom-domain resolution for common wrangler.jsonc files. The only thing gating "rely on this in production" remains the real-deploy cache confirmation above.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head d8a1060f ("harden CDN warmup deploy flow") — a new follow-up commit that landed on top of the e158661 head the prior review covered. It makes two changes, both of which I verified are correct.

Resolved / new since last review:

  1. Trigger orderingrunWranglerTriggersDeploy was previously called unconditionally at the end (after promotion). It's now hoisted into an idempotent applyTriggers() closure and run before warmup in the staged branch (and before the fallback warm). This is the right fix: on a first deploy the custom domain isn't bound until triggers deploy runs, so warming it would fail. I traced all exit paths — production keeps serving old@100% between trigger-apply and promotion (safe), and the triggersApplied guard ensures triggers run exactly once. Covered by new tests asserting exact event ordering.

  2. JSONC trailing-comma strippingparseWranglerConfig now strips trailing commas before JSON.parse. Wrangler treats configs as JSONC (comments and trailing commas), so the prior comments-only pass would throw on valid configs and silently return null, breaking Worker-name/custom-domain resolution that feeds the override header. I tested the stripper against tricky cases (comma/comment-like text inside strings, escaped quotes, nested trailing commas) — all correct.

Verification: vp check clean; 329 tests pass across the affected files.

No blocking issues. Two nits: (1) applyTriggers() in the post-promotion fallback branch isn't annotated on failure — a trigger failure there leaves the new version live with stale routes and a bare error; (2) the JSONC parsing is now a hand-rolled two-pass that must stay in sync. The only remaining item is the unchanged real-deploy confirmation that a 0%-staged version-override request populates the URL-keyed edge cache normal traffic reads.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for 3d3e3cd)

Re-reviewed after the new follow-up commit 3d3e3cd ("clarify CDN warmup trigger failures"), which landed on top of the d8a1060f head the prior review covered. It closes out the two non-blocking nits from last pass, and both check out.

Resolved since the last review

Nit 1 — fallback-branch trigger failure was unannotated (deploy.ts:439). Fixed. The post-promotion applyTriggers() in the warmedBeforePromotion === false branch is now wrapped and rethrown via withPromotedVersionTriggerNote (deploy.ts:438–443, 572–580). I traced every fallback entry point (stagingTraffic === null, or staged-but-no-URL/headers-and-not-strict) — in all of them runWranglerVersionDeploy(…@100%) at deploy.ts:433 has already run before applyTriggers(), so the "may already be promoted to 100%, but triggers/routes may not be updated" wording is accurate (arguably it's definitely promoted, not just "may", but that's a harmless conservative phrasing). The note preserves the original error as cause. Covered by the new test cloudflare-cdn-warm-deploy.test.ts:374 ("explains promoted version state when fallback trigger deployment fails"), which drives the fallback path via a non-single-100% status, promotes, then fails triggers, and asserts both the note and that no warm fetch fired.

Nit 2 — hand-rolled two-pass JSONC parsing had to stay in sync. Fixed by merging the two passes. stripJsonComments + stripJsonTrailingCommas are collapsed into a single stripJsonCommentsAndTrailingCommas (tpr.ts:126) with a small isJsonTrailingComma lookahead helper (tpr.ts:199). This removes the duplicate string/escape state machine — there's now one comment/string tracker, and the trailing-comma decision re-uses a comment-aware lookahead so a comma followed by a comment-then-}/] is still correctly stripped. I re-ran the tricky cases against the merged implementation (comma-like/comment-like text inside strings, escaped quotes before a trailing comma, nested trailing commas, a comment between a comma and its closing bracket, and a comma followed by a comment then a real key which must be preserved) — all parse correctly.

Verification

  • vp check clean on deploy.ts, tpr.ts, cdn-warm.ts, version-deploy.ts.
  • 330 tests pass across the six affected files (deploy.test.ts, cloudflare-cdn-warm-deploy.test.ts, cloudflare-version-deploy.test.ts, cloudflare-cdn-warm.test.ts, pregenerated-concrete-paths.test.ts, deploy-prerender-config.test.ts).

Remaining (unchanged — verification, not a blocker)

Does a version-override (0%-staged) request populate the URL-keyed edge cache that later header-less production traffic reads? Still the one open correctness assumption, untouched by this commit. The CDN adapter (cache/cdn-adapter.runtime.ts) is a pure edge-cache design keyed by URL; the pre-promotion warm sends Cloudflare-Workers-Version-Overrides to route to the 0% version and assumes the resulting response lands in the same URL-keyed entry a later no-header request will HIT. Plausible (the override is a routing header, not part of the cache key), but please confirm on a real custom-domain deploy that a path warmed via the 0% override serves as HIT/UPDATING on the production hostname without the header. If not, only the post-promotion fallback path actually helps.

Net: no blocking issues. Both nits from the previous pass are cleanly resolved, the JSONC merge removes a genuine dual-maintenance hazard, and the new fallback trigger annotation gives operators an accurate recovery hint. The only thing gating "rely on this in production" remains the real-deploy cache confirmation above.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head 3d3e3cd ("clarify CDN warmup trigger failures") — a new follow-up commit on top of the d8a1060f head the prior review covered. It closes out the two non-blocking nits from the last pass, and both check out.

Resolved since last review:

  1. Fallback-branch trigger failure now annotated. The post-promotion applyTriggers() (deploy.ts:439) is wrapped/rethrown via withPromotedVersionTriggerNote. I traced every fallback entry point — promotion at deploy.ts:433 always runs before the trigger apply, so the "may already be promoted to 100%, but triggers/routes may not be updated" wording is accurate. Covered by a new test that drives the fallback path, promotes, fails triggers, and asserts both the note and that no warm fetch fired.

  2. JSONC two-pass merged into one. stripJsonComments + stripJsonTrailingCommas collapsed into a single stripJsonCommentsAndTrailingCommas with an isJsonTrailingComma lookahead, removing the duplicate string/escape state machine. I re-ran the tricky edge cases (comment/comma-like text in strings, escaped quotes, nested trailing commas, comment between comma and closing bracket, comma-then-comment-then-real-key) — all correct.

Verification: vp check clean; 330 tests pass across the six affected files.

No blocking issues. The only remaining item is the unchanged real-deploy confirmation that a 0%-staged version-override request populates the URL-keyed edge cache that later header-less production traffic reads — a verification ask, not a code defect. The design and tests are sound.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head a89176e ("emit warm paths for skip-build deploys"), the follow-up on top of the 4c56a37 merge my last pass covered. It's a targeted fix for the blocking --skip-build regression and confirms the intentional "don't emit on plain builds" contract.

Blocking issue from last pass is genuinely fixed. The emitPrerenderPathManifest call is decoupled from the if (!options.skipBuild) build block and hoisted into its own if (shouldEmitPrerenderPathManifest) gate (deploy.ts:822–844). I verified it works against a pre-existing build without a fresh one:

  • The emitted manifest's buildId is read from the existing dist/server/BUILD_ID, and the consumer re-reads the same file — so the buildId guard always matches within a deploy (no stale false-negative).
  • Missing prior build degrades safely (null buildId → [], or throws under --warm-cdn-strict), no crash.
  • Covered by the new test at deploy-prerender-config.test.ts:288.

Contract confirmed sound. Plain vite build / vinext build emits no manifest (CLI emits only inside if (prerenderDecision), no index.ts post-hook), asserted by app-router-production-build.test.ts:136. Pre-warm deploys are the trigger — a coherent, tested design decision.

One minor code nit (non-blocking): the refactor now gates routeRootConfig loading behind shouldLoadRouteRootConfig, so in the --prerender-all-only (no --warm-cdn-cache) path it's dropped to null. For apps with a custom route root, discovery would scan default dirs and write an inaccurate manifest — but that manifest is never consumed in that exact path (warmup only reads under --warm-cdn-cache, where the config always loads), so impact is low. Simplest fix: load it whenever shouldEmitPrerenderPathManifest.

Verification: vp check clean; 340 tests pass across the affected files. The only remaining item is the unchanged real-deploy confirmation that a 0%-staged version-override request populates the URL-keyed edge cache normal traffic reads.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Addressed the remaining nit from the last Bonk pass: Cloudflare deploy now emits the supplemental path manifest for warm deploys and for non-skip explicit prerender builds only. That means route-root metadata is always loaded when the manifest is emitted, while --skip-build --prerender-all keeps the existing contract of not loading Vite config just for a supplemental manifest.

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for 6d1df98)

Re-reviewed after the new follow-up commit 6d1df98 ("avoid stale skip-build path manifests"), which lands on top of the a89176e head my previous review covered. It's a tight, correct fix for the exact nit I raised last pass (the dropped routeRootConfig), plus a deliberate tightening of the manifest-emit gate.

The change (deploy.ts)

Two edits:

  1. shouldEmitPrerenderPathManifest narrowed from warmCdnCache || prerenderDecision to warmCdnCache || (!skipBuild && prerenderDecision).
  2. routeRootConfig is now loaded unconditionally whenever the manifest is emitted (the shouldLoadRouteRootConfig gate is gone).

Resolved since the last review — nit #1 (dropped routeRootConfig)

Fixed correctly. Previously, in the --prerender-all-only (no --warm-cdn-cache) path, shouldLoadRouteRootConfig was false, so routeRootConfig was passed as null and discovery could scan the wrong dirs for a custom route root. Now routeRootConfig always loads when a manifest is emitted, so the manifest is accurate whenever it's actually written. This matches the PR description's stated intent: "route-root metadata is always loaded when the manifest is emitted."

The emit-gate tightening checks out

I traced the full truth table across warmCdnCache / skipBuild / prerenderDecision:

warmCdnCache skipBuild prerenderDecision emits manifest? loads config? prerender runs?
T F any yes yes if prerenderDecision
T T any yes yes if prerenderDecision
F F T yes yes yes
F F F no no no
F T T no no yes
F T F no no no
  • --skip-build --warm-cdn-cache (rows 1–2, warm) still emits + loads config — the a89176e skip-build fix I verified last pass is preserved. Confirmed by deploy-prerender-config.test.ts:288.
  • --skip-build --prerender-all (row 5) no longer emits. I confirmed this is a genuinely safe no-op, not a regression: vinext-prerender-paths.json is consumed only by readPrerenderWarmPaths, which is called only under if (options.warmCdnCache) (deploy.ts:898). With --skip-build and no --warm-cdn-cache, the manifest was never read — so previously it wrote a manifest from a stale dist that nothing consumed, and now it correctly skips that work (and skips loading Vite config for it). That's exactly the "existing contract" described in the PR body, and it matches the commit title. Step 6a's runPrerender gate (line 849) is untouched, so prerender still runs in that case as before.

Verification

  • vp check packages/cloudflare/src/deploy.ts clean (format + lint + types).
  • shouldLoadVinextPrerenderConfig (line 810) is still live at line 811 after removing the shouldLoadRouteRootConfig reference — no dead variable.
  • Tests pass: deploy-prerender-config.test.ts + deploy.test.ts + cloudflare-cdn-warm.test.ts (316), app-router-production-build.test.ts + prerender-paths.test.ts + cloudflare-cdn-warm-deploy.test.ts (24).

Note (optional, non-blocking)

There's no test asserting the new narrowed behavior — i.e. that --skip-build --prerender-all (without --warm-cdn-cache) now does not emit vinext-prerender-paths.json. The existing suite covers --skip-build --warm-cdn-cache (emits) and plain build (doesn't emit), but not this specific row that the commit changed. A one-line assertion would lock in the intended contract and guard against a future merge silently widening the gate again (the same class of accidental regression the 4c56a37 merge introduced). Not required for merge.

Remaining (unchanged — verification, not a blocker)

Does a version-override (0%-staged) request populate the URL-keyed edge cache that later header-less production traffic reads? Still the one open correctness assumption, untouched by this commit. Please confirm on a real custom-domain deploy that a path warmed via the 0% override serves as HIT/UPDATING on the production hostname without the header.

Net: no blocking issues. The dropped-routeRootConfig nit is cleanly fixed, and the emit-gate tightening is a correct, deliberate contract (skip-build only emits when warming, which is the only path that consumes the manifest). The only suggestion is an optional test for the newly-narrowed no-emit row. The real-deploy cache confirmation remains the gate for relying on this in production.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head 6d1df98 ("avoid stale skip-build path manifests"), the follow-up on top of the a89176e head my last pass covered. It's a targeted fix for the one nit I raised last time, plus a deliberate tightening of the manifest-emit gate.

Resolved — the dropped routeRootConfig nit. routeRootConfig now loads unconditionally whenever the manifest is emitted, so a custom route root is respected in every emit path (fixing the --prerender-all-only divergence).

Emit-gate tightening verified correct. I traced the full warmCdnCache/skipBuild/prerenderDecision truth table:

  • --skip-build --warm-cdn-cache still emits + loads config (the a89176e skip-build fix is preserved, tested at deploy-prerender-config.test.ts:288).
  • --skip-build --prerender-all (no warm) now skips emit — a genuinely safe no-op, since vinext-prerender-paths.json is only ever read under if (options.warmCdnCache) (deploy.ts:898). Previously it wrote a stale, unconsumed manifest; now it correctly skips that work and the Vite-config load, matching the stated contract.

Verification: vp check clean; 316 + 24 tests pass across the affected suites; no dead shouldLoadVinextPrerenderConfig variable.

No blocking issues. One optional suggestion: add a test asserting the newly-narrowed no-emit row (--skip-build --prerender-all without --warm-cdn-cache writes no manifest) to lock in the contract against future merge regressions. The only remaining item is the longstanding real-deploy confirmation that a 0%-staged version-override request populates the URL-keyed edge cache normal traffic reads.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Added the optional contract-lock test from the last Bonk pass: --skip-build --prerender-all without --warm-cdn-cache now explicitly asserts that no supplemental warmup path manifest is written.

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

Fixed the workers-cache deploy issue from the attached real run: when fallback warmup is used and wrangler versions deploy does not print a production URL, we now parse the URL from wrangler triggers deploy and warm that target instead of skipping with “no production URL could be inferred”. Added a regression matching that mixed 100%/0% deployment state.

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

Updated the deploy logs so the version flow distinguishes staging at 0%, fallback promotion, and final warmed promotion. The real workers-cache run should now read as staging -> triggers -> warm -> promote instead of repeating the same “Deploying warmed” label.

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for e0f2e3b)

Re-reviewed after three new follow-up commits on top of the 3d3e3cd/6d1df98 heads prior passes covered:

  • 7436104 ("lock skip-build prerender manifest gate") — the optional contract-lock test I suggested last pass.
  • 796fb4f ("warm fallback deploy URL from triggers") — the workers-cache real-run fix.
  • e0f2e3b ("clarify CDN warmup deploy logs") — distinct staging/promotion log labels.

All changes since the last review are confined to deploy.ts and version-deploy.ts (plus tests). vp check is clean and 328 tests pass across the four affected suites.

Resolved / new since last review

1. Fallback warmup now recovers a target URL from triggers deploy (the workers-cache fix). This is the real bug behind the attached run. When the current deployment is not a single 100% version (e.g. a mixed old@100% / other@0% state), getZeroPercentStagingTraffic returns null, so the flow takes the post-promotion fallback branch. In that branch runWranglerVersionDeploy(…@100%) frequently prints no workers.dev URL, so deployed.deployedUrl was null and warmup bailed with "no production URL could be inferred" (or failed under --warm-cdn-strict).

The fix captures the URL from wrangler triggers deploy output (runWranglerTriggersDeploy now returns { deployedUrl, output }) into triggersDeployedUrl, and both warm sites fall back to it: staged.deployedUrl ?? triggersDeployedUrl (staged branch, deploy.ts:551) and deployed.deployedUrl ?? triggersDeployedUrl (fallback branch, deploy.ts:598). I traced ordering on every path:

  • Fallback branch: promote(@100%)applyTriggers() (populates triggersDeployedUrl) → resolveCdnWarmupTargetUrl(…, deployed.deployedUrl ?? triggersDeployedUrl). The trigger URL is populated before it's read. ✓
  • Staged branch: stage(0%)applyTriggers() → target resolution. Same ordering. ✓
  • resolveCdnWarmupTargetUrl still prefers a configured customDomain first, so triggersDeployedUrl is only a last-resort host and can't override a custom-domain warm target. ✓
  • The final return value adds triggersDeployedUrl to the fallback chain so the "Deployed to:" line reflects the real URL. ✓

Covered by the new test cloudflare-cdn-warm-deploy.test.ts:258 ("uses the triggers deploy URL for post-promotion fallback warmup") which reproduces the mixed-version status, a URL-less promote, and asserts the exact upload → status → promote → triggers → fetch order against the triggers-parsed URL.

2. Distinct deploy log labels. runWranglerVersionDeploy takes a phase param ("stage" | "promote-warmed" | "promote-uploaded") so the version flow now reads Staging uploaded Worker version at 0%… → … → Promoting warmed Worker version… instead of repeating "Deploying warmed Worker version". This directly addresses the confusing repeated label in the workers-cache run. Unit-tested at cloudflare-version-deploy.test.ts:97.

3. Contract-lock test landed. deploy-prerender-config.test.ts:210 now asserts --skip-build --prerender-all (without --warm-cdn-cache) writes no vinext-prerender-paths.json, locking the narrowed emit gate from 6d1df98 against a future merge silently re-widening it — exactly the class of regression the 4c56a37 merge introduced.

Verification

  • vp check packages/cloudflare/src/deploy.ts packages/cloudflare/src/version-deploy.ts — clean (format, lint, types).
  • All callers of the two changed signatures updated: runWranglerTriggersDeploy (now returns a result) has one caller (applyTriggers); runWranglerVersionDeploy's new phase param precedes execute, and all three source calls + three test calls pass args in the correct positions.
  • 328 tests pass across cloudflare-cdn-warm-deploy, cloudflare-version-deploy, deploy, deploy-prerender-config.

Nits (non-blocking)

  • parseWorkersDevUrl on triggers deploy output returns the first workers.dev token. For the fallback URL this is fine (it's only used when there's no custom domain and no promote URL), but if a future Wrangler triggers deploy output prints multiple workers.dev URLs (e.g. a stale route plus the live one), the first-match could pick the wrong host. Low risk given current output shape and the custom-domain-first preference in resolveCdnWarmupTargetUrl; noting for awareness.
  • The staged-branch warm still reads staged.deployedUrl ?? triggersDeployedUrl, but in the staged branch the 0%-staged version deploy typically doesn't print a live URL either, so triggersDeployedUrl is the effective source there too — the two branches now share the same recovery logic, which is good, just implemented at two call sites that must stay in sync.

Remaining (unchanged — verification, not a blocker)

Does a version-override (0%-staged) request populate the URL-keyed edge cache that later header-less production traffic reads? Still the one open correctness assumption, untouched by these commits. The pre-promotion warm sends Cloudflare-Workers-Version-Overrides to route to the 0% version and assumes the resulting response lands in the same URL-keyed edge entry a later no-header request will HIT. Please confirm on a real custom-domain deploy that a path warmed via the 0% override serves as HIT/UPDATING on the production hostname without the header. (Note: the fallback path fixed in this pass warms after promotion, so it doesn't depend on this assumption — only the pre-promotion staged warm does.)

Net: no blocking issues. The 796fb4f fallback-URL fix resolves the real workers-cache deploy failure with correct ordering and a matching regression test; the log-label and contract-lock changes are clean. The only production gate remains the real-deploy edge-cache confirmation for the pre-promotion staged-warm path.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head e0f2e3b, which added three follow-up commits since the last 3d3e3cd/6d1df98 heads prior passes covered:

Resolved / new since last review:

  1. Fallback warmup now recovers a URL from triggers deploy (796fb4f) — this is the real fix for the attached workers-cache run. When the deployment isn't a single 100% version, the flow takes the post-promotion fallback branch, where versions deploy @100% often prints no URL, so warmup bailed with "no production URL could be inferred." runWranglerTriggersDeploy now returns its parsed URL, and both warm sites fall back to triggersDeployedUrl. I traced ordering on every path — applyTriggers() always runs before the target is read, and resolveCdnWarmupTargetUrl still prefers a configured custom domain first. Covered by a new regression test asserting exact upload → status → promote → triggers → fetch order.

  2. Distinct deploy log labels (e0f2e3b) — a phase param makes the flow read staging → promotion instead of repeating "Deploying warmed Worker version," fixing the confusing duplicate labels in the real run.

  3. Contract-lock test landed (7436104) — the optional test I suggested last pass, locking the narrowed skip-build emit gate against future merge re-widening.

Verification: vp check clean; 328 tests pass across the four affected suites; all callers of the two changed signatures updated correctly.

No blocking issues. Two nits (first-match parseWorkersDevUrl on triggers output; two-site fallback logic that must stay in sync). The only remaining item is the longstanding real-deploy confirmation that a 0%-staged version-override request populates the URL-keyed edge cache normal traffic reads — and notably, the newly-fixed fallback path warms after promotion, so it doesn't even depend on that assumption.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk re-review — warm prerendered paths before deploy (updated for 09cc1bb)

Re-reviewed after five new follow-up commits on top of the e0f2e3b head prior passes covered:

  • be4635e ("offer CDN warmup deploy script"), 800b80d ("update wrangler configs"), 6c66db4 ("default CDN warmup to yes") — add the --warm-cdn-cache init/create-app option and default it to Yes for Workers Cache projects.
  • 101bcc6 ("explain first warm deploy failure") — detect the "Worker does not yet exist" upload error and rewrite it into an actionable message.
  • 09cc1bb ("keep deploy errors concise") — the deploy CLI now prints error.message instead of the full error object.

All changes are confined to init/create-app plumbing plus version-deploy.ts / cli.ts error handling. vp check is clean on all six changed source files, and 170 tests pass across the four affected suites.

What I verified

1. Init / create-app wiring is correct.

  • --warm-cdn-cache / --no-warm-cdn-cache parse via the new generalized parseBooleanArg (shared with --prerender), tested for the =maybe error path.
  • The prompt slots in after prerender and only when cdnCache === "workers-cache"; --warm-cdn-cache with a non-Workers-Cache CDN throws --warm-cdn-cache requires --cdn-cache=workers-cache (tested).
  • The default-Yes fallback logic (options.cloudflare?.warmCdnCache ?? options.cloudflare?.cdnCache === "workers-cache") parses correctly — === binds tighter than ??, so it's warmCdnCache ?? (cdnCache === "workers-cache"). The init.ts script writer and the create-vinext-app template use the identical expression, so the two paths stay in parity.
  • Existing data-cache fixtures don't accidentally flip to warm; verified against the untouched cloudflareInitOptions fixture.

2. First-deploy error rewrite is sound and degrades safely. isMissingWorkerVersionUploadError runs errorText(error) over message + stdout/stderr/output (correctly filtering the null-first output array) and only rewrites the upload failure, which is the very first wrangler call — so nothing is staged/promoted yet, and there's no cleanup concern. If the Cloudflare API wording ever changes, detection just falls through to the raw error (safe). Unit-tested at cloudflare-version-deploy.test.ts:134.

Nits (non-blocking)

A. 09cc1bb (error.message only) drops stdout diagnostics for buffered version-deploy failures. The plain (non-warmup) deploy path streams wrangler stdout/stderr live via spawn (deploy.ts:481–489), so it's unaffected. But every warmup wrangler call (versions upload/deploy, deployments status, triggers deploy) goes through runWranglerCommand, which uses buffered execFileSync and only prints captured output on success — on failure it throws before the print. I confirmed execFileSync's error.message appends stderr but not stdout, so wrangler diagnostics it prints to stdout are now silently dropped in the concise CLI path. For a warmup deploy that fails on, say, a config/auth issue surfaced on stdout, the operator sees only Command failed: node …/wrangler … + stderr. The PR already has an errorText helper that reads stdout — consider surfacing captured stdout on failure in runWranglerCommand (print before rethrow) so the concise CLI message doesn't lose it.

B. Default-Yes warmup guarantees a first-deploy failure for brand-new Workers. resolveInitWarmCdnCache defaults to true in agent/non-interactive environments, so a fresh vinext init --platform=cloudflare scaffolds deploy:vinext = "vinext-cloudflare deploy --warm-cdn-cache". On a never-deployed Worker the first deploy will always hit the "Worker does not yet exist" path. That's exactly what 101bcc6 mitigates (clear "run once without --warm-cdn-cache first" message), so it's handled — but it's worth a one-line note in the generated README/help that the first deploy of a new Worker should omit the flag, rather than relying on users hitting the error and reading the remediation.

Remaining (unchanged — verification, not a blocker)

Does a version-override (0%-staged) request populate the URL-keyed edge cache that later header-less production traffic reads? Still the one open correctness assumption, untouched by these commits. Please confirm on a real custom-domain deploy that a path warmed via the 0% override serves as HIT/UPDATING on the production hostname without the header. (The post-promotion fallback path warms after promotion, so it doesn't depend on this assumption — only the pre-promotion staged warm does.)

Net: no blocking issues. The init/create-app wiring is clean, well-tested, and parity-consistent across both scaffolders, and the first-deploy error rewrite is a genuine UX improvement. The two nits are (A) restoring stdout in the concise error path for buffered warmup commands, and (B) a doc note about the first deploy of a new Worker. The real-deploy edge-cache confirmation remains the only gate for relying on the pre-promotion warm in production.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2481.

Summary

I re-reviewed against the current head 09cc1bb, which added five new commits since the e0f2e3b head prior passes covered — all init/create-app plumbing plus deploy error handling, not the core warmup flow.

What I verified as correct:

  • Init/create-app wiring — the new --warm-cdn-cache flag, the Workers-Cache-only prompt (slotted after prerender), the --warm-cdn-cache requires --cdn-cache=workers-cache guard, and the default-Yes fallback. I checked the ?? / === operator precedence and confirmed init.ts and create-vinext-app use the identical expression, so they stay in parity. 170 tests pass; vp check clean.
  • First-deploy error rewrite (101bcc6) — detection reads message+stdout+stderr+output correctly, only rewrites the very-first upload call (nothing staged to clean up), and degrades safely if the API wording changes.

Two non-blocking nits I flagged:

  1. 09cc1bb drops stdout diagnostics. The concise CLI (error.message) plus buffered execFileSync in runWranglerCommand means wrangler output printed to stdout is silently lost on warmup-command failures (stderr is preserved by execFileSync's message). The plain deploy path streams live, so it's unaffected. Suggested printing captured stdout before rethrow.
  2. Default-Yes warmup guarantees a first-deploy failure on a never-deployed Worker — mitigated by the new error message, but worth a README/help note.

No blocking issues. The only remaining item is the longstanding real-deploy confirmation that a 0%-staged version-override request populates the URL-keyed edge cache normal traffic reads.

github run

@james-elicx james-elicx marked this pull request as ready for review July 4, 2026 16:08
@james-elicx james-elicx enabled auto-merge (squash) July 4, 2026 16:08
@james-elicx james-elicx merged commit eba7046 into main Jul 4, 2026
64 checks passed
@james-elicx james-elicx deleted the codex/cloudflare-cdn-warm-prerender branch July 4, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants