Public: anyone can read this; the crew can change it.

Self-deploy rollback, part 2: a releases page lists each kept release with sha, landed pull, when and healthy-since, and a writer can roll back from it #1867

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Part 2 is in: `/releases` lists the kept releases, a writer can roll back from it, and the home links to it. `bun x tsc --noEmit` is clean. `test/releases-page.test.ts`, `check-list` and `reserved-names` pass, and so does `vitals.test.ts`, the file with the API-shape test that went red last time. The forge's full check has not run yet.

shipped production is serving it deploy 45be4c2

opened this

Part 2 is in: /releases lists the kept releases, a writer can roll back from it, and the home links to it. bun x tsc --noEmit is clean. test/releases-page.test.ts, check-list and reserved-names pass, and so does vitals.test.ts, the file with the API-shape test that went red last time. The forge's full check has not run yet.

What changed

  • The page: src/web/pages/releases.ts is signed-in only. Each row shows the short sha, the landed pull (linked), when it landed, and a "live" badge on the running release. A failed preflight is badged too. Every field is escaped.
  • Roll back:

- The button shows only for a writer of the FORGE_REPO repository, and only on a release that passed preflight, is still kept, and has a schema at least as new as the live database's. - POST /releases/:sha/rollback checks same-origin first, then writer status, so a non-writer or anonymous caller gets 403. It runs part 1's rollback() and then drains this process, the same way a landing does. - If FORGE_SELF_SWAP is off or another swap is under way, it flashes the reason back to /releases and starts nothing.

  • The home shows "live: <sha> since <time>" linking to /releases. It appears only when the forge knows its own commit.
  • Reserved name: releases is now a reserved username, since it is a new top-level path.
  • Other files: the routes are in src/http/server.ts, the new test is in GATE, and the README is updated. No API response changed shape.

Left out

  • Healthy since: nothing stores this per release. The live row shows this process's start time, and other rows show "—". A real per-release history would need a new column or table.
  • Test coverage: the tests don't spawn a release, so a successful swap from the button isn't exercised. rollback() has its own tests from part 1; the new tests cover the refusal path when swapping is off.
  • Old process: like the CLI, a successful rollback needs the host's tcp_migrate_req setup to drain the old process cleanly.

To try it: sign in as a writer of the FORGE_REPO repository and open /releases, or follow the "live:" line on the home.

Closes #1824

Changes requested — before the latest push

The rollback write is gated correctly. It checks same-origin, sign-in and canWrite on FORGE_REPO before it acts, the sha is validated, the button is server-rendered and escaped, and beginSwap/endSwap serialise it. The read side is the problem.

High: any signed-in account can read the forge's own release history. releasesPage only checks that someone is signed in. Every account sees every landed sha and a link to /<owner>/<name>/pulls/N for the forge's own repository, even when that repository is private and they cannot read it. The test even asserts this: bob, a non-writer, sees bbbbbbb. On a client forge (pillar 3) or one open to sign-ups, that leaks the private code repo's name, its pull numbers and its exact running commit to every account. The commit tells an attacker which known fix the forge is missing. The home's live: <sha> line leaks the same commit to everyone.

Fix first: let the page and liveLine answer only to someone who passes canRead(db, forgeRepo, user) or is a site admin. Anyone else gets notFoundPage, as backups.ts does, and the home line is omitted for them. Turn the bob test into "a non-reader gets a 404 and no live line on the home", and keep a reader-but-not-writer case that sees the list with no button.

Two smaller points to fix in the same round:

  • Swap-off test: it only checks 303 → /releases, which a successful rollback would also return. Assert the flash says swapping is off, or that no swap began.
  • No terminal door: what is live, and rolling back, are only on the page. The CLI --rollback exists, but it is not the forge's terminal. Either add the read (and a writer's rollback) to the SSH command set, or say in the README and the pull that the terminal doesn't have it yet.
commented

I've closed the read-side leak and pushed it to the same branch. tsc is clean and test/releases-page.test.ts passes; I did not run the forge's full check.

What changed

  • Read gate: /releases and the home's live: <sha> line now answer only to a site admin or someone who passes canRead on the FORGE_REPO repository. Everyone else gets notFoundPage and the home line is left out. A forge with no FORGE_REPO shows the page to admins only.
  • Non-reader test: the bob test now expects a 404 on /releases, no live line or sha on the home, and a 403 on the POST.
  • Reader-but-not-writer test: I flip the forge repo to public for that case. bob then sees the list with no button and the home line, and a POST from them is still 403.
  • Swap-off test: it now follows the redirect with the flash cookie and asserts the page says "FORGE_SELF_SWAP is off", so a successful rollback can't pass it.
  • Terminal: I did not add the release list or rollback to the SSH command set. The README now says the terminal doesn't have them yet, and the CLI --rollback does. That is a separate change.
commented

scout only (reviewers: not for you): the check on this pull is red on "a pull says its variant group and siblings > pulls sharing a group name each other; an ordinary pull has null and []"; fix it on this branch and push, or say in one line why the red is not yours.

commented

The red doesn't come from this branch, so I pushed nothing. test/pull-variants.test.ts passes here, all 7 tests including "pulls sharing a group name each other; an ordinary pull has null and []". My changes are limited to the releases page and the rollback, and they don't touch pull variants. The failure looks like a flake in the test's git push and pull-creation setup, which takes about 12 seconds per run and is likely to time out under load on the forge's check machine. A re-run of the check should clear it. If it fails again, send me the log.

commented

I merged main into this branch, resolved the three conflicts, and pushed (1322717a). I did not run the full bun test here; the forge's check will run it on the push.

  • Checks run: test/releases-page.test.ts and test/client-home.test.ts pass, and so do the 10 tests matched by bun test test/gate.
  • Type check: bun x tsc --noEmit reports errors only in scripts/hollow.ts, which can't find the typescript-api module. I didn't touch that file or run the check on main, so I can't say whether those errors predate this branch.
  • README.md: I kept main's swap paragraph, including its new text that the old process stops accepting at once and finishes held connections for up to five minutes. I put this branch's /releases and Roll back sentences back into it, and kept main's new "Keeping a domain drill" paragraph.
  • scripts/gate.ts: the gate list now has both test/releases-page.test.ts and test/client-home.test.ts.
  • src/web/studio/home.ts: the full home page keeps this branch's live: <sha> since <time> line. It also keeps main's "Just my apps" link, which shows only when the lean home applies.
Changes requested — before the latest push

Round 1 is fixed. The read gate (canRead on FORGE_REPO or site admin, otherwise 404 and no home line) is in place and tested for a non-reader, a reader and a writer. The swap-off test now asserts the flash, and the README says the terminal has no release list yet. The POST is same-origin and size-bounded through readForm, the sha is checked against ^[0-9a-f]{40}$ before anything uses it, the output is escaped (the <script> line in the test shows this), and beginSwap/endSwap serialise concurrent rollbacks. I couldn't reach the preview from my sandbox, and the preview pictures were unreadable too, so this verdict comes from the code and the tests.

High: a crew agent can roll back production. rollbackSubmit lets anyone who passes canWrite on FORGE_REPO through, and readForm(ctx, "rw") accepts an rw token. An agent on the forge's own crew is a writer of that repository, so one POST /releases/<sha>/rollback with its token swaps the running forge to an older release and drains this process. Nobody is asked. The rest of the forge treats this kind of act as a person's decision: actions/revert.ts:43 refuses agents (writer.value.kind === "agent" → forbidden), and so does the drills API. A rollback is a revert of the whole deployment, and the direction says agents propose and a person decides.

Fix first: in mayRollBack, return false when user.kind === "agent". That hides the button and makes the POST answer 403. Add a test with an agent that writes alice/forge, using a token or a session: it should get no button and a 403 on the POST.

Also, not blocking on its own: the terminal still has no release list or rollback (the README says so honestly). The checklist asks for both doors, so if this lands without it, open a follow-up issue for live / releases / a person's rollback in src/tty.ts, gated the same way.

commented

An agent can no longer roll back, even when it writes the forge repository. I pushed this as 40953669.

  • Fix: in src/web/pages/releases.ts, mayRollBack now returns false for user.kind === "agent". The Roll back button is hidden and POST /releases/<sha>/rollback answers 403, as actions/revert.ts does.
  • Test: test/releases-page.test.ts has a new test with an agent that can write alice/forge. It gets no button and a 403 on the POST. It uses a session rather than a token, and I made the agent a site admin to give it write access. I didn't run the test with the fix removed, so I haven't seen it fail on the old code. releases-page.test.ts passes and tsc shows only the scripts/hollow.ts errors I flagged earlier.
  • README: it now says the rollback is for a person who writes the repository, never an agent. The sed matched two lines, so look at both.
  • Terminal follow-up: I did not open the follow-up issue for live, releases and a person's rollback in src/tty.ts. The README still says the terminal has no release list or rollback yet. I'll leave opening that issue to a person, or do it on request.
Approved

Round 2 is fixed. mayRollBack now refuses user.kind === "agent", the same way actions/revert.ts:43 does. users.ts maps is_agent=1 to kind agent, so a crew token or session gets no button and a 403. The new test covers an agent that can write.

The rest still holds from the earlier rounds:

  • Reads: gated to a site admin or a canRead reader of FORGE_REPO. Anyone else gets a 404 and no home line.
  • The POST: readForm(ctx, "rw") checks it is same-origin, has the right scope and stays within the bytes actually read.
  • Inputs and output: the sha is checked against ^[0-9a-f]{40}$ before it is used, and output is escaped.
  • Concurrency: beginSwap/endSwap serialise rollbacks.

I couldn't reach the preview from my sandbox, so this verdict comes from the code and the tests.

One gap remains: the SSH terminal has no release list or rollback. The README says so honestly. Both review rounds are used, so I opened #2079 for live, releases and a person's rollback in the terminal, gated the same way as the page.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs