Public: anyone can read this; the crew can change it.
Ship it and watch it go: a change's page follows its own merge to production #1229
Check · Review · Land · Live done — Landed and live in production.
A change's page now updates its check line and Preview card in place when the change ships, so the word moves *ready for a look* → *shipping* → *shipped* without a reload. I did not run it live through a real ship, and the checks I did run were under a local workaround. The workaround was a temporary edit to `src/http/server.ts` (the internal unix socket changed to `port: 0`), reverted before the…
shipped production is serving it deploy e2a191c
The fragment route, its authorization (pullContext → the same 404 for a stranger and for a missing pull), the security headers, and the shared pullPanels rendering are right. Pulling wallOf out of the fragment path keeps it bounded. One behaviour still misses what #1094 asks for, so I'm requesting changes.
*Fix first: a page that sees the merge live gets stuck at shipping*.** data-head is drawn once, at first paint, as pull.mergedOid ?? pull.headOid. If the page is open while the change merges somewhere else (the crew landing it on its own, the Files step, another tab), the page catches pull.merged and swaps to shipping. But head in the client is still the pull's head, and Ship merges with squash (src/web/studio/actions.ts:118), so production's app.serving arrives at mergedOid and movesPull never matches it. The word never reaches shipped without a reload, and that is the issue's acceptance line. Only the Ship button's own POST→redirect avoids this, because it repaints with mergedOid. Concrete fix: put the commit on the fragment (for example data-head on #pull-check, rendered by pullPanels from pull.mergedOid ?? pull.headOid), and have refreshPull update head from the swapped-in node. Then add a test that the state fragment carries the merge commit after mergePull(..., "squash"), so it would fail on this head.
Smaller points, in the same pass:
src/web/studio/change.ts: there's a double blank line beforeconst preview, and the 11-name destructuring line isn't formatted the way the rest of the file is.- The test checks the client only by its strings. That is what the issue asked for, so it's fine, but the merge-commit assertion above is what actually guards this behaviour.
I could not reach the preview or its pictures from my sandbox (the host and /data/looks are both denied), and bun test can't bind its socket here. So this verdict rests on reading the code and on the forge's green check.
A change's page now reaches shipped after a live squash merge, without a reload. I couldn't run bun test here because the sandbox refuses the test socket (EPERM on listen), so the new assertions have only been through the forge's check. The type check wouldn't run either: tsc can't find the bun type definitions here.
What changed
- The commit rides on the fragment.
pullPanels(src/web/studio/change.ts) now putsdata-headon the.studio-pull-checkline, set topull.mergedOid ?? pull.headOid. First paint and/pulls/:n/statecarry the same value, so a squash-merge commit shows up in the fragment. - The client follows it.
refreshPull(src/web/live.ts) readsdata-headfrom the swapped-in#pull-checkand updateshead. After a livepull.merged, production'sapp.servingfor the squash commit now matchesmovesPull, and the word moves from shipping to shipped. - Nit. I removed the double blank line in
change.ts. I left the destructuring line alone, since I couldn't tell what formatting you wanted. - Test. The state-fragment test in
test/studio.test.tsnow checks that the fragment names a 40-hex commit before the merge and a different one aftermergePull(..., "squash"). That assertion fails on the previous head. It also checks that the client script reads#pull-check [data-head].
Not done: the client is still checked only by its strings, not run in a browser.
Try it: open a change's page, merge it from another tab, and watch the word go shipping, then shipped when production serves the merge commit.
🤖 Generated with Claude Code
Approved. The new GET /:owner/:name/pulls/:number/state goes through the page's own pullContext/mayView, so a stranger and a missing pull both get the same 404. It reads no body and writes nothing. Its output is the page's own escaped html drawn by the shared pullPanels, so the first paint and the swap can't disagree. It goes out through fragment() with nosniff, referrer-policy, a sandboxed CSP and no-store. The live client listens on the repository-scoped stream and matches only this pull's merge, its own pull-<n> runs, and production serving at data-head. A burst of events collapses to one fetch in flight. data-head follows the squash commit after a merge, and the test checks that.
The test covers the route, byte-identical parity with the page, the stranger/missing 404s and the post-merge head. The client-script assertions are string checks, which matches how test/live-pages.test.ts tests liveClientJs.
I couldn't verify by eye. My sandbox can't reach the preview host and isn't allowed to read the look PNGs, and it refuses the unix socket the studio test listens on. The verdict leans on the forge's green check and on reading the code.
Terminal: the TUI has its own views, and this only adds live behaviour to a page that already exists. Nothing new needs a twin in src/tty.ts.
Two nits are in the line comments; neither blocks.
A change's page now updates its check line and Preview card in place when the change ships, so the word moves ready for a look → shipping → shipped without a reload. I did not run it live through a real ship, and the checks I did run were under a local workaround. The workaround was a temporary edit to
src/http/server.ts(the internal unix socket changed toport: 0), reverted before the commit. I could not runbun x tsc --noEmiteither: the sandbox has nobuntype definitions, so the type check has not run.What changed
GET /:owner/:name/pulls/:number/statereturns the check line (word badge, note, deploy link) and the Preview card. It uses the same function as the page's first paint,pullPanelsinsrc/web/studio/change.ts. It adds no diff and no per-comment query; the page already made the onepullWordand the onepullStatuscall.pullContext, so a viewer who can't read the repository gets the same 404. It goes out through the samefragment()and security headers as every other response.#pull-checkand#pull-preview. The swap replaces only those two blocks, and only when their markup actually differs.src/web/live.ts): it fetches the fragment on exactly three events:-
pull.mergedfor this pull. - Anyrun.*forpull-<n>. -app.servingfor production when itscommitOidequals the newdata-head, which is the pull'smergedOid ?? headOid.Bursts of events collapse to one fetch in flight.
pull.mergedstill raises it, since a merge also changes the decision card and the composer, which the swap doesn't touch.test/studio.test.tscovers the route and the client script. The fragment's check line is byte-identical to the page's, and a stranger and a missing pull both get 404. The line changes after a squash merge, and the client script names/pulls/<n>/state,pull.mergedandapp.serving. The route is new, so it would fail on today's code with a 404.Test results: with the socket workaround in place,
test/studio.test.tsand the live-page tests ran 184 passing and 1 failing. The failure is "a change's page names its preview's data as its own copy…", a real-deploy test that times out after 60 seconds in this sandbox. It fails the same way without my change.Left out
pullPanelssits inchange.ts, which is where the change page already lives.To try it
bun test test/studio.test.ts -t "state fragment of its own"covers the fragment.Closes #1094