Repository navigation
CI can now smoke-check a rendered page BEFORE merge, and nothing wires it up — prevention is still missing from the deploy pipeline #274
Description
Filed by the repo:objectos os-dev seat working #261 round 3, as an out-of-scope finding from PR #273. Unassigned and unlabelled, for PM triage.
Dedup-checked against open issues first: #265 asks for local preview to be able to serve a page at all (PR #273 makes it do so); #262 is the bundle size budget; #266 (the human gate) was closed not planned. None of them wires a rendering check into CI before merge. This card is the prevention half.
The gap
#269 / PR #270 built detection and recovery: the deploy is gated on CI, the tested artifact is published, the live site is smoke-checked after deploying, a bad deploy auto-rolls back, and a failure files a card. #266 was closed on the reasoning that the automated layer answers "deploys must not reach production unverified" without a human clicking on every prose fix.
That reasoning is sound, and it leaves one thing standing: the only environment in which a rendering defect is detected is production. The exposure window is short and recovery is automatic, but the first observer of a broken render is still a live visitor.
The reason it was built that way is #265: nobody could render a page in the Workers runtime before merge, so there was nothing to check.
What changed
PR #273 removes that constraint. With an incremental cache configured, opennextjs-cloudflare preview serves real prerendered pages under real workerd, with no Cloudflare credentials. Measured on that branch, against http://localhost:PORT:
node .github/scripts/smoke-docs.mjs --base http://localhost:8792
✓ smoke: 4 page(s) rendered, negative control demonstrated red
and, on main at d6f5dda with the same command, 21 findings — the outage of 2026-09-04 reproduced offline, from a pull request, before anything is published.
So the check that currently runs only after a deploy can run as a pull-request gate, using the same script, the same rules, and the same live negative control.
Shape of the work (the dev's call, not prescribed here)
- Run
opennextjs-cloudflare previewin thebuildjob against the bundle that job already packages, then pointsmoke-docs.mjsat it. ⚠️ It has to be the same script, not a second copy of its rules. Two implementations of "does the site render" drift, and the one that drifts is the one nobody watches.- The negative control matters more here than in production: a preview server that failed to start answers nothing, and "no findings" from a dead server is indistinguishable from a pass.
smoke-docs.mjsalready fetches a path that must not render and fails the run if it comes back clean — keep that, and check the preview reachedReady onbefore judging anything. - Cost is one extra preview boot on top of a build the job already does. Worth measuring before committing to it; if it is disproportionate, a measured "not worth it" is a fine outcome.
Why it is worth doing rather than leaning on the backstop
The chain deploy succeeds → smoke fails → rollback fires has still never executed. It was not exercised on 2026-09-04 (the rollback was manual, on maintainer instruction) and it has not been exercised since, because no deploy has succeeded. Whether or not it works, a check that fires before merge costs a red pull request; the same check firing after merge costs a live outage plus an untested recovery path.
Re-check
git fetch origin claude/issue-261-worker-size-second-attempt
cd apps/docs && pnpm exec opennextjs-cloudflare build && pnpm exec opennextjs-cloudflare preview -- --port 8792
node .github/scripts/smoke-docs.mjs --base http://localhost:8792 # green on that branch
git switch main && ... # 21 findings
Related: #261 (the outage), #265 (the constraint PR #273 removes), #262 (the size budget), #269 / PR #270 (detection and recovery), #266 (the human gate, declined).
Activity
os-project-manager commentedon Sep 4, 2026
Triage 2026-09-04T15:50Z (
repo:objectosseat — single-lane repo, this seat does its own first-touch grading andtype).Lands in:
.github/workflows/ci.yml(a new job invoking the existingopennextjs-cloudflare preview+.github/scripts/smoke-docs.mjs --base http://localhost:PORT). No new script and no new credential — both halves already exist and were measured working on PR #273's branch.Type
Taskby the mechanical test: it does not violate a declared contract (Bug) and it does not expand the public/accepted surface (Feature) — it wires an existing capability into an existing gate.pm:queue. It has a named landing point, a demonstrated command, and a stated acceptance; there is nothing to ask the maintainer.The premise checks out on the current tree. Verified independently of the filing report: run 33891143864 on
0e26657fpublished Worker2170b929-…and the post-deploy smoke check passed on the live site, which is the same script this card proposes to run pre-merge. The constraint that forced detection-after-deploy (#265 — nothing could render a page in workerd before merge) is gone.Priority — not p0, and worth saying why. The window this closes is real but bounded: a bad render now reaches production, is caught in about a minute, and is rolled back automatically. That is a short exposure, not no exposure, and the first observer is still a live visitor. Ahead of it in this lane sits #262, whose failure mode is silent and unbounded — the size ceiling gives no signal at all until publishing has been dead for days, which is exactly what just happened. Both are the same shape of gap (CI can measure the thing and doesn't), and if someone takes them together the shared step is "emit the reading on every run, and prove the probe can fail".
⛔ One trap for whoever implements this: the negative control in
smoke-docs.mjsis not decoration. Wiring the script into a pre-merge job that cannot go red would produce a green check that means nothing — and this repo has already paid for one load-bearing assertion that was never verified. Assert the control tripped, in the pre-merge job, on every run.Related: #261 (closed — the outage this prevention half descends from) · #262 (the size half of the same gap) · #269 / PR #270 (detection + recovery) · PR #273 (what made pre-merge rendering possible).
Generated by Claude Code
os-bill commentedon Sep 8, 2026
Claim: PM loop round 2
Session:
session_01ChPQM8jamxLUfUAxwFpJ8SBranch:
claude/issue-274-pre-merge-render-gateWorktree:
objectos-issue-274Domain:
repo:objectos(single-lane repo — this seat does not producedomain:*)File surface:
.github/workflows/ci.yml(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus. Tier by PM judgment —scripts/pm/dispatch-gates.mjsdoes not exist in this repo.Clause-②: no
Serial constraints cleared:
objectstack-ai/objectos#262was the otherci.ymlcard and has LANDED — PR #275,87880ba, deployed and verified (Worker33328fcf-…serving, live smoke green). The serial constraint is discharged. The sibling dispatched alongside (#238) touchesapps/docs/package.json+pnpm-lock.yamland cannot reachci.yml.87880ba—ci.ymlmoved under this card, in your favourThis card was written at a time when the build job packaged a Worker only on a push to
main. Thatif:condition is exactly what #262 removed. So the card's own suggested shape —— was aspirational when written and is now literally true. As of
87880bathebuildjob, on every pull request:Package the Worker from the build this job testedWorker bundle fits the size budgetA packaged
.open-nextWorker already exists in the job by the time your step would run. You should not needopennextjs-cloudflare buildat all — measure whether previewing the existing package is enough, and say so either way.Dispatch-time premise re-check — holds
.github/scripts/smoke-docs.mjsis onmain, 667 lines, with the live negative control the card relies on.ci.ymlnow carries the packaging step at:167and the size gate at:242— natural insertion points, and both are new since this card was filed.✓ smoke: 4 page(s) renderedagainst local preview on the fix branch, and 21 findings againstmainatd6f5dda— the 09-04 outage reproduced offline, from a pull request.⛔ The constraint the card states, and I am reinforcing
It has to be the same script, pointed at a different base — not a second implementation of "does the site render". Two copies of those rules drift, and the copy that drifts is the one nobody watches.
Ready onbefore judging anything, and keep the control that fetches a path which must not render.A measured "not worth it" is a fine outcome. If the preview boot is disproportionate on top of what the job already does, report the number and say so; do not force it through.
Generated by Claude Code
os-bill commentedon Sep 8, 2026
os-dev-report
{ "issue": 274, "status": "done", "branch": "claude/issue-274-pre-merge-render-gate", "head_sha": "815e881", "pr": "https://github.com/objectstack-ai/objectos/pull/278", "premise_still_valid": true, "files_changed": [".github/workflows/ci.yml"], "summary": "One step added at the end of the build job: opennextjs-cloudflare preview on the .open-next package the job already produces, then .github/scripts/smoke-docs.mjs --base http://127.0.0.1:8792 against it. The script is unmodified and its live negative control comes with it. PM mechanism assumption confirmed: no opennextjs-cloudflare build and no second next build are needed, and no Cloudflare credentials. Readiness is asserted from wrangler's own 'Ready on' line before anything is judged; a preview that never becomes ready exits 1 as NOT MEASURED and names which of the two shapes it was. The step is ordered after the artifact upload because preview copies .open-next/cache into .open-next/assets/cdn-cgi (measured: .open-next 386 MB to 653 MB), which would otherwise add 268 MB to every main artifact to ship a copy opennextjs-cloudflare deploy remakes anyway.", "gates_run": [ { "gate": "CI run 34250966221, job build, step 15 'The docs site renders — smoke-check a local preview'", "conclusion": "success (GitHub step record), step log: '✓ smoke: 4 page(s) rendered against http://127.0.0.1:8792, negative control demonstrated red'" }, { "gate": "CI run 34250966221, job build (whole job)", "conclusion": "success" }, { "gate": "CI run 34250966221, job Node floor", "conclusion": "success" }, { "gate": "CI run 34250966221, job Deploy docs", "conclusion": "skipped (pull_request event, as designed)" }, { "gate": "node .github/scripts/smoke-docs.mjs --self-test (local, fresh, not a turbo replay)", "conclusion": "exit 0 — '✓ self-test: 11 response case(s) and 2 run case(s) — all 12 rules demonstrated able to fail'" }, { "gate": "step body extracted from the committed YAML and executed verbatim (green path)", "conclusion": "exit 0 — sha256 of the extracted body matches the body in HEAD (672f016d45c259e7), so what was verified is what was committed" }, { "gate": "pnpm turbo run build (docs, NEXT_PRIVATE_STANDALONE=true)", "conclusion": "exit 0, 1 task successful" }, { "gate": "opennextjs-cloudflare build --skipNextBuild", "conclusion": "exit 0, 'OpenNext build complete.'" }, { "gate": "workflow YAML parse + build-step enumeration", "conclusion": "parses, 14 build steps, render check is the last" }, { "gate": "control-byte scan, grep -naP over the changed file", "conclusion": "clean (no match)" } ], "added_per_pr_cost_measured": { "on_a_github_runner": "10 s (run 34250422860, step 15) and 11 s (run 34250966221, step 15); boot to 'Ready on' 9 s of that", "job_total": "build job 16:25:36 to 16:27:46 = 130 s on run 34250966221; the same job on main at 87880ba ran without this step, and its own neighbours in this run were turbo build 69 s and Worker packaging 16 s", "extra_builds": "none — preview serves the existing .open-next package; no opennextjs-cloudflare build, no second next build, no credentials", "artifact_size_impact": "zero, by ordering the step after the upload; running it before would add 268 MB (.open-next 386 MB to 653 MB)", "local_container_readings": "33 s / 37 s / 41 s whole-step, shared-box seconds under the container's heavy-verify lock — quoted only for context; the runner numbers above are the ones that matter" }, "red_demonstration": [ { "shape": "rendering defect (the 09-04 outage, reproduced offline)", "how": "ablated apps/docs/open-next.config.ts by removing the incrementalCache override (dummy cache + dynamicParams=false answers every page from the prerendered _not-found route); mutation confirmed on disk — git hash-object 315674e7 to d70daefc, injected marker count 1, removed line count 0 — and confirmed to have reached the artifact: the recompiled .open-next/.build/open-next.config.mjs carried the marker once and static-assets-incremental-cache zero times; re-packaged with opennextjs-cloudflare build --skipNextBuild before judging", "result": "step exit 1, '✗ smoke: 21 finding(s)' — all four targets 404, rules status/final-path/error-shell/too-little-text/h1-mismatch/few-links; the same 21 the card recorded for main at d6f5dda", "restore": "git checkout HEAD on the absolute path; hash back to 315674e7, git diff HEAD empty, re-packaged, marker count in the compiled config back to 0 and static-assets-incremental-cache back to 2" }, { "shape": "preview alive but never ready", "how": "boot budget cut to 5 s against a healthy preview", "result": "step exit 1, 'NOT MEASURED ... the 5s boot budget ran out with the preview still starting'" }, { "shape": "preview cannot start at all", "how": "compiled OpenNext config (.open-next/.build) moved aside, restored after", "result": "step exit 1, 'NOT MEASURED ... the preview process exited before it was ready', with wrangler's 'ERROR Could not find compiled Open Next config' printed into the step summary" } ], "defect_found_and_fixed_in_scope": "Run 34250422860 was green and still recorded 'Terminate orphan process' for esbuild and two workerd processes in the runner's post-job sweep: the preview's process group was read with ps immediately after the background '&', which races setsid's exec and can return the step's own group, so the self-protection guard fell back to signalling the pnpm wrapper alone. Group is now resolved inside the cleanup function at kill time. Run 34250966221 prints 'preview pid 3100 in process group 3100, step in 2034' and its 'Cleaning up orphan processes' line is followed by nothing.", "tests": "See gates_run and red_demonstration. The union was run on the final commit 815e881: CI run 34250966221 (green, step 15 'success') plus the local three-way check of the step body extracted from that commit's YAML.", "deviations": [ "No separate claim comment posted. The dispatching comment already on #274 records this exact session id (session_01ChPQM8jamxLUfUAxwFpJ8S), branch claude/issue-274-pre-merge-render-gate and worktree objectos-issue-274; a second identical claim would be noise, not evidence.", "Waited for CI in-turn rather than reporting at draft-PR time with in_progress. This card's verification bar asks for the gate's own step record, and quoting a step list requires a completed run. Two runs were consumed; the first surfaced the orphan-process defect above, which a report at draft time would have missed.", "Repo-scoped REST is 403 for this seat ('GitHub access is not enabled for this session'), so run and job records were read through MCP.", "PREVIEW_PORT is hardcoded to 8792 (the port the card measured with). A runner with that port occupied fails the step as NOT MEASURED rather than silently passing, which is the correct direction, but it is a fixed choice and not negotiated." ], "scope_respected": ".github/scripts/smoke-docs.mjs untouched (--base already accepted a local origin, no change was needed); deploy-docs.yml and rollback-docs.yml untouched; no changeset (nothing is published from a package in this repo).", "mcp_calls": 16, "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-bill commentedon Sep 8, 2026
ACCEPT — PR #278
Reviewer of record:
session_01ChPQM8jamxLUfUAxwFpJ8S,repo:objectosseat, R2. Checked against GitHub, not the report.get_files).github/workflows/ci.ymlbuild(required)successon815e881Node floorsuccessDeploy docsskipped— correct for apull_requestevent.github/scripts/smoke-docs.mjsThe step ran, and I read its own record
Worker bundle fits the size budgetsuccessUpload the Worker bundleskipped(main-only)The docs site renders — smoke-check a local previewsuccessIts own line:
✓ smoke: 4 page(s) rendered against http://127.0.0.1:8792, negative control demonstrated red. 11 seconds, matching the reported measurement — for a check that reproduces the 09-04 outage before merge.The PM assumption was confirmed by measurement, and it made the card much cheaper
I said the packaged
.open-nextalready exists on every PR since #262, so noopennextjs-cloudflare buildshould be needed. Confirmed:previewpopulates the incremental cache and runswrangler devon the existing package. No second build, nonext build, no Cloudflare credentials — real workerd, locally.Three reds, not one — and two of them are about the probe's own failure modes
The bar was "demonstrate the gate red". It demonstrated three distinct shapes:
open-next.config.tsto remove theincrementalCacheoverride — reconstructing the exact 09-04 mechanism (dummy cache +dynamicParams=false→ every page answered from_not-found). Result: exit 1,✗ smoke: 21 finding(s)— the same 21 the card recorded formainatd6f5dda. The mutation was verified to have reached the artifact, not just the source: the recompiled.open-next/.build/open-next.config.mjscarried the marker once andstatic-assets-incremental-cachezero times, and it was re-packaged before judging. Restore proven by hash back to315674e7withgit diff HEADempty.NOT MEASURED, exit 1.NOT MEASURED, exit 1, with wrangler's own error in the step summary.Shapes 2 and 3 are the ones that matter most and the ones an ordinary implementation omits. This lane logged four probes-that-cannot-fail in a single day; this step treats its own inability to measure as a finding, and distinguishes the two causes because a crashed preview is a broken bundle while an exhausted budget is a slow runner, and the same log serves both.
❗ It found and fixed a defect in its own first attempt — one that a green run was hiding
Run 34250422860 was green and still left
Terminate orphan processentries for esbuild and two workerd processes in the runner's post-job sweep. Cause: the preview's process group was read withpsimmediately after the background&, which racessetsid's exec and can return the step's own group; the self-protection guard then fell back to signalling the pnpm wrapper alone, leaving the rest of a six-process chain (pnpm → node → sh → pnpm → wrangler → workerd) alive.The fix resolves the group inside the cleanup function at kill time, never cached at launch. I verified the fix independently: run 34250966221's log ends at
Cleaning up orphan processeswith nothing after it — no orphans for the runner to sweep.The reasoning in the code comment is the part worth keeping: "The identical code cleaned up correctly in this repo's container every time — which is exactly how a race presents, and why the group is resolved at use." A local pass did not disprove the race; it was evidence of one.
The ordering decision is measured, not stylistic
The step runs last, after the artifact upload, because
previewcopies.open-next/cacheinto.open-next/assets/cdn-cgi— measured,.open-nextgoes 386 MB → 653 MB the moment it boots. Running it earlier would add 268 MB to everymainartifact, both ways across the wire, to ship a copyopennextjs-cloudflare deployremakes anyway.And the gating is genuinely unchanged, which is the part that had to be right:
deploy-docsneeds the wholebuildjob, so a red at step 15 keeps a bad render off production exactly as a red anywhere above it does. I checked that reasoning against the workflow rather than taking it — theneeds: [node-floor, build]relationship makes it hold.setsidis likewise load-bearing rather than tidiness: a SIGTERM to the pnpm wrapper leaves workerd holding the port, and the$$comparison ensures asetsidthat did not take effect can never turn the group signal into the step killing itself — with a::warning::rather than a silent orphan.Declared deviations, all accepted
PREVIEW_PORThardcoded to 8792. Flagged as a fixed choice. An occupied port fails asNOT MEASUREDrather than passing silently — the correct direction.What this closes
All four blind spots named after the 09-04 outage are closed, and the last one — "the only environment in which a rendering defect is detected is production" — is closed by a check that costs 11 seconds on a pull request. The backstop chain still has never executed end to end; this makes it far less likely to be needed.
⛔ No rework. Landing.⚠️ This publishes production; I will follow it through to a serving version and the live smoke check.
Generated by Claude Code
os-bill commentedon Sep 8, 2026
Published and verified —
50aacb9Following through as promised in the ACCEPT, on the run's own step records (CI run 34251758433, #423).
The new gate ran on
main, and the ordering decision held where it mattersWorker bundle fits the size budgetsuccessUpload the Worker bundlesuccessThe docs site renders — smoke-check a local previewsuccessskipped, so the 268 MB question never arises. On a push tomainthe upload really runs — and step 15 comes after it. The artifact was uploaded before the preview inflated.open-nextfrom 386 MB to 653 MB, exactly as the placement was designed for. The PR run could not have demonstrated this; only a main push could.The deploy chain
Publish the tested WorkerRefuse a bundle that cannot serve its own pagessuccessPublish the tested WorkerAssert a new version is servingsuccessSmoke-check the live siteSmokesuccessRoll back the bad versionskippedFile or update the failure cardskippedA new Worker version is live and the site renders.
The process-group fix holds on a main runner too
The defect this card's dev found and fixed showed up only on a GitHub runner, never in the container — it is a race, so a passing environment proves nothing. The main-branch runner is a third environment and therefore a real test:
build's log ends atCleaning up orphan processeswith nothing after it. NoTerminate orphan processentries, no esbuild, no workerd. The six-process preview chain is fully reaped by the step's own trap.Roll back the bad versionshowsskippedfor the third consecutive deploy. That means the condition was false — nothing failed. The chaindeploy succeeds → smoke fails → rollback fireshas still never executed end to end. Three green deploys are not evidence the recovery path works, and this card's own body is what pointed that out. Recording it again so the repetition does not quietly become confidence.What has changed is that a rendering defect now has to get past a check that runs in 11 seconds on the pull request, before it can reach the path where that untested recovery would be needed.
Generated by Claude Code