commit fb095a538c20297617a8446413f22c193d250bd3
parent befef15c84dca98ff2ec22d57e9ee3f38f06f44e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 6 Oct 2026 13:28:52 -0400
plans: release 18 — slice S4, as shipped (the publish surfaces), and the index's settings signature in What makes the index stale
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
| M | plans/release-18.md | | | 152 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 152 insertions(+), 0 deletions(-)
diff --git a/plans/release-18.md b/plans/release-18.md
@@ -1283,6 +1283,158 @@ deploys: "none"}`); `enqueuePublishRun(paths, plan, {runId?})` and `enqueueStage
`resumeLaneAction("publish")` (`editor/app/operations/actions.ts`) and `isGateHeld(settings, "publish")`; the
policies `sitePublishPolicy(site)`, `sitePublishProblem(site)`, `settings.publish`.
+### Slice S4, as shipped — the publish surfaces: /sites Publish panel, /operations/publish, the ops API (2026-10-06)
+
+Branch `r18/surfaces` off `r18/integration` `85a38e92` (main `edadc712` merged in first, step 0, as `f2fd11a7`; then
+main's own red pin fixed, `85a38e92`), built in the integration worktree `~/Projects/r18-integration` (editor 7101,
+test 7111, export 7110) by the orchestrating Opus session — this session can run git only in its own worktree, so
+the prepared `r18-publish-surfaces` worktree was not used — with two Opus helpers (the build.ts deletion; the
+review). Scratch files `s4-*` in the job's `tmp`. The plan is "Surfaces", the step 1 pre-fix and S3's "Seams".
+
+**What it does.**
+- **The index is judged by the settings it reads** (step 1). `indexSettingsSig` (`publish/inputSig.ts`) signs
+ `socialLinks`, `homepageUrl`, `buildArchives`, `archiveStorage`, `social.x.visibility`,
+ `maxTranscriptPageBytes` and the storage locations' roots; update-index records it as `settingsSig` in the
+ stamp and `needs()` says "the settings the index reads changed" when it differs (an older stamp reads as
+ changed, once). The settings file's mtime is no longer an index input: a pause click, a priority change or a
+ drive auto-pause no longer stales the index. The charts config stays an mtime (its own file).
+- **/sites → Publish** (`sites/components/PublishPanel.tsx`) replaces "Build all sites", the batch panel and the
+ hub's and homepage's build sections: a row per site, then the hub, then the homepage, each with the status's
+ four chips (index | built | deployed | live), its policy and what is next. A site row: **Build**, **Deploy
+ preview** (box "preview branch", starts on `settings.publish.previewBranch`; status "preview problem"),
+ **Deploy production**, **Deploy local** (only with `ARCHILYZER_SITE_OUT`); a private site says it never
+ deploys, a site with no project says so. The hub and homepage rows keep their names ("Build hub", "Deploy
+ hub", "Build homepage", "Deploy homepage", "Deploy after build", an empty preview box = production, testid
+ `homepage-ships`, group "Homepage build" / "Hub build"). Above the rows: **Publish now**, **Build all stale**,
+ the index chip, the lane chip (a link to /operations/publish) and the plan Publish now would run. Each button
+ is one JobLane; the hub's config form stays below as "Hub config".
+- **The Pool**: **Build index** is the update-index stage (no queue control: it is a publish stage); **Build stats
+ dataset is removed** (the stats are part of the index update); normalize and the archives unchanged.
+- **A site's Publish tab**: Build & deploy (exact names kept) = the index update when stale, the build `--force`,
+ the production deploy with `builtAfter`; "Build static export" without "Skip data rebuild"; "Deploy to
+ production" / "Deploy preview"; "Last deployed" is this site's `deployed.json` — production, the last preview,
+ the live-check verdict — and "Built" its `built.json`.
+- **One console per run** (`sites/lib/publishRunStream.ts`): the run's jobs' streams joined in order under `===
+ <stage> (job <id>) ===`; the verdict is the first job not `done`; a job of the run that ends `cancelled`
+ cancels the jobs after it (the console's Cancel is the run's — else it would wait on a stage queued behind
+ something else).
+- **The actions** (`sites/lib/publishActions.ts`, `"use server"`, over `publishCore.ts`): `publishNowAction`,
+ `buildAllStaleAction`, `updateIndexAction`, `buildTargetAction`, `deployTargetAction`,
+ `buildAndDeployTargetAction`, and the lane's `start|stop|drainPublishLaneAction`. Every one enqueues a plan
+ through `enqueuePublishRun` under one run id; a manual build or deploy is forced, and a build carries the index
+ update first when the index is not fresh. A deploy is refused before any job in the deploy stage's own words:
+ steps 1–3 of `runDeployStage` moved unchanged into `resolveDeployRequest` (`publish/deployStage.ts`), which the
+ stage and the surfaces both ask ("no build of X in <dir> — archilyzer publish build X" is one sentence).
+- **/operations/publish** (static route beside `[id]`): the runner pill, Start / Drain / Stop (`Start publish
+ lane` …), the hold (`pause publishing` / `resume publishing`), lane on/held/quiet, a pass due or not and why,
+ last check / next check / last pass / last decision, the runner's job log, the plan a pass would run, and
+ `settings.publish`'s form (`savePublishSettingsAction`: enabled, check/refresh minutes, quiet hours, runner,
+ preview branch — refused with the deploy buttons' sentence —, hub and homepage policies). The page refreshes
+ itself every 5 s. `setLaneHeld` revalidates it and /sites for the publish lane; the operations board links it.
+- **SiteForm**: "Publish policy" (Off / Build / Preview / Production) writes `site.json` `publish.auto`;
+ `writeSite`'s refusal of a deploying policy with no Pages project is the form's error. A save no longer drops
+ the key (the form rebuilt the site without it).
+- **ops API**: `POST /api/ops/publish` `{verb: index | build | deploy | hub | homepage | now | stale, …}` →
+ `{ok, runId, jobs: [{target, kind, jobId, previewUrl?, existing?}], skipped, refused}` (+ `jobId` with one job;
+ every request refused → 400), a key a verb does not take is a 400; `GET` → the publish status. The eight old
+ routes are aliases with their bodies and answers (`build-index` ignores `queueKey`, `build-site` ignores
+ `skipData`; `build-deploy` `all` = every deployable site to production; per site the DEPLOY job is the one
+ reported). Shared adapter code in `api/ops/_publish.ts`. `pnpm ops publish` and `pnpm ops get publish`.
+- **CLI**: `deploy hub` / `deploy homepage` print and run `publish hub|homepage --deploy-only` (new flag) through
+ the deploy stage (S2 review I2). `build.ts` loses `buildSite`, `deploySite`, `buildAll`, `deployHub`,
+ `deployHomepage`, `runDockerBuildAllPhase`, `runDockerDeployAllPhase` and what only they used
+ (`runDeployIntoLog`, `runPagesDeployIntoLog`, `homepageDeployArgs`, the outcome types): 1452 → ~980 lines.
+- **A stage ends "Done"**: `[stage] <kind> <target>: Done — …` / `Done (no-op) — …` (was "done"/"no-op"); the
+ e2e `buildIndex()` helper waits for that line.
+- **e2e seams**: `EXPORT_NEXT_BIN` (`build.ts nextBuildStep`, ENVIRONMENT.md) runs `<bin> build` in place of
+ `pnpm exec next build` for a site's and the hub's build — the test server points it at
+ `e2e/fixtures/bin/fake-next.mjs`, which copies the composed public dir to export/out (compose runs for real;
+ the export app is never rebuilt beside its dev server); `ARCHILYZER_BRANCH=main`; a dummy
+ `CLOUDFLARE_API_TOKEN` (the preflight; the fake never sends it); the fake wrangler's mode sidecar
+ `<exportBuildsDir>/.fake-wrangler-mode.json` `{"authFail": true}`.
+
+**Deviations from the plan** (one sentence each):
+1. Built on a branch of the integration worktree (`r18/surfaces`), not in `r18-publish-surfaces`: this session
+ could run git only in its own worktree.
+2. A manual Build (row, tab, hub, homepage) enqueues the index update first when the index is not fresh — the
+ data phase a build used to run — so "Build & deploy" is two jobs only when the index is fresh.
+3. Manual deploys are forced (the plan's "--force (manual buttons only)"); `POST publish {verb: "deploy"}` forces
+ only with `force: true`.
+4. `EXPORT_NEXT_BIN` and the fake `next` are new: the plan's publish.spec needed a real build, and a real `next
+ build` of the export app inside the editor's e2e would race its dev server.
+5. A cancel of a run cancels its later jobs (publishRunStream) — the plan did not say; without it the hub and
+ homepage rows' consoles never settled.
+6. The /sites "Hub" heading is "Hub config" (the panel's row is "Hub"); the homepage's own section is gone (its
+ row is in the panel).
+7. GET /api/ops/publish answers `{ok: true, …status}` (the status at the top level, so `lane.held` reads directly).
+8. sites-homepage.spec's "run Build index" line is the row's chips now ("no index yet", "update the index first"), as
+ planned.
+9. `writeSite` (e2e helper) passes `audience` through.
+10. Files beyond the slice's list: `videoChoreCards.test.ts` (main's red pin, on integration), `source.test.ts`
+ (the manifest refusal kept a test), two comments in `builtExport.test.ts`.
+
+**Found and fixed on the way.**
+- main was red: `videoChoreCards.test.ts` pinned every import from `./cards` as a chore card, and main's
+ multi-track merge added `TranscriptTracksReader` there (`85a38e92`, on integration).
+- The site form dropped `site.json` `publish` on every save (S3 added the key, the form never knew it).
+
+- The e2e build stage left the worktree's `export/out` linked to a test bundle that resetData then deleted, and a
+ bare `next build` of the export app failed on it (`5a77682e`: the suite's setup and teardown drop such a link).
+
+**Found and left** — in "Follow-ups carried over" above.
+
+| commit | what |
+|---|---|
+| `f2fd11a7` | (integration) merge `main` `edadc712` — multi-track captions, en-track fallback, Wayback, Odysee/BitChute spacing; the changelog keeps both sides |
+| `85a38e92` | (integration) main's red pin: the chore-card test leaves the transcript reader out |
+| `195e54f8` | publish: the index judged by `settingsSig`, not the settings file's mtime (step 1) |
+| `2b533c6c` | publish: `resolveDeployRequest`; a stage ends "Done" |
+| `a30581f7` | editor: the /sites Publish panel, the site Publish tab, `POST|GET /api/ops/publish` and the eight aliases |
+| `e360f7da` | editor: /operations/publish; the site form's publish policy |
+| `ce20b387` | cli: `deploy hub|homepage` → `publish hub|homepage --deploy-only` |
+| `4cb8e81b` | ops: `pnpm ops publish`, `pnpm ops get publish` |
+| `514e9674` | publish: build.ts loses the pre-stage entry points (helper agent) |
+| `fce11b60` | e2e seams (`EXPORT_NEXT_BIN`, the mode sidecar, `ARCHILYZER_BRANCH`), the moved labels' specs |
+| `4813bafd` | e2e: publish.spec, publish-lane.spec, ops-api / jobs / duplicate-shorts |
+| `6882b507` | e2e: alerts past Next's route announcer; the job page's first compile; the changelog fold |
+| `5a77682e` | e2e: setup / teardown drop a test `export/out` link |
+| `6a697701` | the review's fixes (below) |
+
+**Gates** (worktree root; logs `$T/s4-*.log`, `$T/s4f-*.log`): tsc clean at every commit; common **3394 passed**,
+**3395** after the review round (9 tests went with the deleted build.ts code; new: settingsSig 2, `EXPORT_NEXT_BIN`
+1, the source manifest refusal 1, the CLI flags 2); editor unit **142**; `test:scripts` **599 + 3 skipped**, **600 + 2
+skipped** after the round (the ops client's publish test new); mcp
+**292**; export unit **116**; homepage unit **23**; `pnpm --filter editor exec next build` ok (47 s); `pnpm --filter
+export exec next build` ok — first FAILED (`stat export/out` ENOENT: the e2e build stage's link left dangling by
+resetData; fixed by `5a77682e`), then ok over the committed fixture compose linked into the worktree's
+`export/public`; `pnpm --filter homepage run build:nodata` ok (15 s); umtool's capped build ok (19 s, link removed).
+e2e (editor suite, `$T/s4-specs.txt`: publish, publish-lane, ops-api, build, deploy-page, site-scope,
+site-publish-preview, sites-homepage, duplicate-shorts, sites-crud, digest, jobs, lane-runner): first run **104
+passed, 4 failed, 9.6 min** — two spec bugs (Next's route announcer is an alert too), `jobs.spec` "tails its log"
+and `site-scope.spec` "charts is a site's tab" (both pass alone: **5 passed, 0 failed, 46 s**); after the fixes and the
+review round **109 passed, 0 failed, 8.8 min**. The whole suite runs once at the end of the release (step 4).
+
+**Review** (`$T/s4-review.md`, a separate Opus agent, read-only): **SHIP AFTER FIXES**, nine findings, all fixed in
+`6a697701`:
+
+| # | finding | fix |
+|---|---|---|
+| 1 | a console's Cancel cancelled only the run's first job — a no-op once it had ended, so a queued production deploy could not be stopped from the console | `cancelPublishRunAction`: every live stage with the console job's run id, newest first; every publish console uses it (`JobLane` takes `cancelAction`) |
+| 2 | a part another run had queued (`existing`) could be the console's job, be cascaded, and was not waited on | `RunPart.existing`; never the console's job, never cascaded |
+| 3 | publish-lane.spec's queue holder started its clock before the page compiled | taken after the page is up, 45 s |
+| 4 | a preview with no branch name fell through to production | refused; `wantedPlan` throws on it |
+| 5 | `POST publish {build, runner: docker}` skipped the stale index | the index update first, `indexAfter` on the containers' build |
+| 6 | the hub/homepage verbs dropped a preview on a local deploy | refused with the deploy verb's sentence |
+| 7 | `build-site` / `build-deploy` `all` lost the top-level `jobId` | the run's last job; comments and the changelog say so |
+| 8 | `--deploy-only --force` dropped the force; `--deploy --deploy-only` passed | force reaches the deploy; the pair is a usage error |
+| 9 | one target's throw was the whole request's 500 | that target's refusal |
+
+Cleanups taken: `fanOutSiteJobs` deleted; `followRun` refuses an empty run; `enqueueRun` releases its kept branches on a
+throw; the plan's list keys carry the preview; saving the lane on starts its runner; the foreground/background window
+noted in `publishStages.ts`. From the S6 drafts' readings, in the same commit: the charts page no longer points at
+"Build stats dataset"; the source gate's refusal names `archilyzer publish homepage` (only a stage stamps what a
+deploy ships); `build hub` stays a raw, unstamped build and says so (e2e:2origin's `build:hub`); three stale comments.
+
## Rollout
(Steps 1–7 above; "### As it went" is written as the rollout runs.)