@@ -150,18 +150,55 @@ jobs:
150150 # Last in the job on purpose: the artifact then only exists for a commit
151151 # that cleared every gate above it. And only on a push to `main`, because
152152 # that is the only event that deploys, so a pull request pays nothing.
153+ # TEMPORARY (#261 round 3, reverted in this branch before review): the
154+ # `pull_request` clause below is the only difference from the shipped
155+ # step. `--skipNextBuild` has never once executed with the incremental
156+ # cache configured, because this step is push-only; its first run would
157+ # otherwise be the merge commit. If that path does not produce
158+ # `.open-next/cache`, the Worker deploys with the cache CONFIGURED and
159+ # EMPTY, every lookup misses, `dynamicParams = false` refuses the
160+ # on-demand render, and every page 404s — the outage this PR diagnoses,
161+ # reproduced by its own fix.
153162 - name : Package the Worker from the build this job tested
154- if : github.event_name == 'push' && github.ref == 'refs/heads/main'
163+ if : >-
164+ (github.event_name == 'push' && github.ref == 'refs/heads/main')
165+ || github.event_name == 'pull_request'
155166 working-directory : apps/docs
156167 run : pnpm exec opennextjs-cloudflare build --skipNextBuild
157168
169+ # TEMPORARY (#261 round 3, reverted with the clause above).
170+ - name : TEMP — the cache the deploy depends on, as CI's own build makes it
171+ if : github.event_name == 'pull_request'
172+ working-directory : apps/docs
173+ shell : bash
174+ run : |
175+ set -euo pipefail
176+ test -d .open-next/cache || { echo "::error::.open-next/cache absent after --skipNextBuild"; exit 1; }
177+ echo "cache entries : $(find .open-next/cache -type f | wc -l)"
178+ echo "cache bytes : $(du -sb .open-next/cache | cut -f1)"
179+ node -e '
180+ const fs=require("fs"),path=require("path");
181+ const bid=fs.readFileSync(".open-next/assets/BUILD_ID","utf8").trim();
182+ const m=JSON.parse(fs.readFileSync(".open-next/server-functions/default/apps/docs/.next/prerender-manifest.json","utf8"));
183+ const routes=Object.keys(m.routes||{});
184+ const root=path.join(".open-next/cache",bid);
185+ const missing=routes.filter(r=>!fs.existsSync(path.join(root,(r==="/"?"/index":r).slice(1)+".cache")));
186+ console.log("prerendered routes:",routes.length);
187+ console.log("missing entries :",missing.length);
188+ if(missing.length){console.error("::error::"+missing.length+" prerendered route(s) have no cache entry");process.exit(1);}
189+ '
190+
158191 # `include-hidden-files` is load-bearing, not tidiness: the compiled
159192 # OpenNext config the deploy reads lives at `.open-next/.build/`, and
160193 # upload-artifact excludes dotted paths by default. Without it the
161194 # download succeeds, the deploy exits 1 on a missing config, and the
162195 # cause is three directories away from the message.
196+ # TEMPORARY (#261 round 3, reverted in this branch): `pull_request` added
197+ # so the artifact really round-trips, rather than being reasoned about.
163198 - name : Upload the Worker bundle
164- if : github.event_name == 'push' && github.ref == 'refs/heads/main'
199+ if : >-
200+ (github.event_name == 'push' && github.ref == 'refs/heads/main')
201+ || github.event_name == 'pull_request'
165202 uses : actions/upload-artifact@v7
166203 with :
167204 name : docs-worker
@@ -170,6 +207,82 @@ jobs:
170207 if-no-files-found : error
171208 retention-days : 3
172209
210+ # ==========================================================================
211+ # TEMPORARY JOB (#261 round 3). Reverted in this branch before review.
212+ #
213+ # It runs the deploy job's own inputs on the artifact CI actually produced:
214+ # download it, then call `opennextjs-cloudflare populateCache local` — the
215+ # exact step `opennextjs-cloudflare deploy` performs before `wrangler deploy`,
216+ # and for the static-assets cache a pure filesystem copy needing no
217+ # credentials. Then `wrangler deploy --dry-run` weighs what would be uploaded.
218+ #
219+ # This exists because "probably fine on a path that has never run" is the
220+ # reasoning that cost this repo a production outage on 2026-09-04.
221+ # ==========================================================================
222+ verify-deploy-inputs :
223+ name : TEMP — the artifact the deploy would publish
224+ needs : [build]
225+ if : github.event_name == 'pull_request'
226+ runs-on : ubuntu-latest
227+ permissions :
228+ contents : read
229+ steps :
230+ - uses : actions/checkout@v7
231+ - uses : pnpm/action-setup@v6
232+ - uses : actions/setup-node@v7
233+ with :
234+ node-version : 22
235+ cache : pnpm
236+ - run : pnpm install --frozen-lockfile
237+
238+ # Byte-for-byte what deploy-docs.yml does.
239+ - name : Download the Worker CI built and tested
240+ uses : actions/download-artifact@v8
241+ with :
242+ name : docs-worker
243+ path : apps/docs/.open-next
244+
245+ - name : The cache survived the artifact round-trip
246+ working-directory : apps/docs
247+ shell : bash
248+ run : |
249+ set -euo pipefail
250+ test -d .open-next/cache || { echo "::error::.open-next/cache did NOT survive the artifact"; exit 1; }
251+ test -f .open-next/.build/open-next.config.mjs || { echo "::error::compiled config did not survive"; exit 1; }
252+ echo "cache entries after download : $(find .open-next/cache -type f | wc -l)"
253+ echo "assets before populate : $(find .open-next/assets -type f | wc -l)"
254+
255+ - name : populateCache — the step the deploy runs before wrangler
256+ working-directory : apps/docs
257+ shell : bash
258+ run : |
259+ set -euo pipefail
260+ pnpm exec opennextjs-cloudflare populateCache local
261+ echo "assets after populate : $(find .open-next/assets -type f | wc -l)"
262+ echo "cache entries in assets : $(find .open-next/assets/cdn-cgi/_next_cache -type f | wc -l)"
263+ node -e '
264+ const fs=require("fs"),path=require("path");
265+ const bid=fs.readFileSync(".open-next/assets/BUILD_ID","utf8").trim();
266+ const m=JSON.parse(fs.readFileSync(".open-next/server-functions/default/apps/docs/.next/prerender-manifest.json","utf8"));
267+ const routes=Object.keys(m.routes||{});
268+ const root=path.join(".open-next/assets/cdn-cgi/_next_cache",bid);
269+ const missing=routes.filter(r=>!fs.existsSync(path.join(root,(r==="/"?"/index":r).slice(1)+".cache")));
270+ console.log("prerendered routes :",routes.length);
271+ console.log("servable from static assets :",routes.length-missing.length);
272+ console.log("missing :",missing.length);
273+ if(missing.length){console.error("::error::"+missing.length+" route(s) would 404 in production");process.exit(1);}
274+ '
275+
276+ # The number #262 needs, measured in CI on the bundle wrangler uploads
277+ # rather than scaled from a local ratio. Cloudflare rejects over 65536 KiB.
278+ - name : Weigh the bundle wrangler would upload
279+ working-directory : apps/docs
280+ shell : bash
281+ run : |
282+ set -euo pipefail
283+ pnpm exec wrangler deploy --dry-run --outdir "$RUNNER_TEMP/dryrun" 2>&1 | tee "$RUNNER_TEMP/dryrun.log"
284+ grep -E 'Total Upload|Read [0-9]+ files' "$RUNNER_TEMP/dryrun.log" | tee -a "$GITHUB_STEP_SUMMARY"
285+
173286 # Defect 1 of #269: `deploy-docs.yml` used to hang off `push: branches:
174287 # [main]` exactly as this workflow does, so the two ran in PARALLEL and a
175288 # commit that failed any gate above still deployed. There was no `needs:` and
0 commit comments