Batch 4C pt6: the validated paired-release install + live-cutover runbook - #197
Conversation
…derers Increment 6 of Batch 4C. The install/cutover runbook (deploy/INSTALL-paired- release.md) predated the renderers, #3, and #196; its step 5 was hand-crafted sed. Brought current with everything that landed: - §5 is now RENDERED, not hand-crafted: `render-units --out-dir /etc/systemd/system` emits the 6 engine cutover drop-ins (audit-gauges is an engine consumer now, #191); `render-serving` emits the nginx `root` + FPM `open_basedir`, applied with sed-from-the-rendered-value so the values are deterministic from host.yaml. - Docroot is the #3 shared cache `/var/cache/kayak/docroot` everywhere (layout, OUTPUT_DIR, nginx root, open_basedir) — the release no longer carries a docroot. The open_basedir drops the /opt/kayak/releases entry (PHP execs the self-contained docroot, never the release). - New step-2 host.yaml install (the cutover flips the generated-cache paths off the read-only release); new /var/cache/kayak/{docroot,map-layers,gauge-metadata} dirs + the docroot recursive+default www-data ACL in §6 (else freshly built pages 403). - §5d sets the serving-path knobs the gate now requires (KAYAK_NGINX_DOCROOT_CONF / KAYAK_FPM_POOL, fail-closed); KAYAK_HOST_UNITS removed (gate sources its set from `render-units --list-units`, #196). ENGINE_BRANCH=main (#190 merged). - The "Still [4C-TODO] for the renderers" section is resolved; what remains is the increment-7 VM re-validation of the rendered flow + the live cutover, and the deferred genericization (vhost server_names / certbot-root knobs). The base install (steps 1–4, 6–8) is VM-validated; step 5's rendered flow is the spec for the increment-7 cutover rehearsal (renderers unit-tested; `nginx -t` / `systemd-analyze verify` of the output happen on the VM). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drove the full runbook on a virgin Debian 13 VM (levels-mac) end-to-end — virgin
net-install → serving paired-release host, homepage + DB-backed description.php
both 200, the activation gate passing on render-units --list-units / nginx / FPM.
Findings folded in:
- §5a: create the self-signed /etc/nginx/ssl/dummy.{crt,key} the default-server
block (deploy/nginx-default-server, 444 on bare-IP) requires — `nginx -t` fails
on the missing file otherwise. This was the one hard break; it's undocumented in
SETUP.md's legacy path too (noted).
- Header: status updated to VALIDATED end-to-end (incl. the rendered step 5), and
clarified that the live WKCC *migration* (vs. this virgin install) is increment 7.
- §2 / §5d: UNCOMMENT the deploy.env example lines (ENGINE_BRANCH, SERVING_CUTOVER,
the serving knobs) via sed instead of appending, so there's no commented+active
duplicate.
- §5c: note that `systemd-analyze verify` reporting the ExecStart "not executable"
pre-activation is expected (same as the render-units warning) — `current` doesn't
exist until step 7; the merged unit structure is valid.
- "Still open": the virgin VM validation is done; what remains is the live
migration (keep old serving up, reuse the live DB, retire scripts/deploy.sh).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… runbook Rehearsed the live cutover (running host → paired-release) end-to-end on a live-equivalent VM clone (full live DB — 4.46M obs, 77 migrations) with ZERO serving disruption, and captured it as a "Migrating a RUNNING host" section. The validated no-downtime ordering: - Phase 0: pre-stage the paired-release ALONGSIDE the running site (dirs, host.yaml, deploy.env, stage-only) — no mutation, public_html keeps serving. - Phase 1: apply the cutover config to the FILES only (render-serving → nginx root + FPM open_basedir via sed; render-units → drop-ins) and stop the timers, but DO NOT reload nginx/FPM — the running services hold the old in-memory config, so public_html keeps serving. Skip install-config.sh (pool/runtime-config already exist; a reload now would serve the empty new docroot). - Phase 2: activate — the gate passes on the FILES, `migrate` reports "No pending" against the live DB (no init-db — reuse it), the build writes the new docroot, symlink switch. Still serving public_html. - Phase 3: graceful `nginx reload` + `fpm restart` → flip to the new docroot. Confirmed: serving stayed 200 throughout (public_html → new docroot at the flip); the live DB migrated clean; and the cutover FIXED the live audit-gauges unit (the #191 promotion lands via render-units, which scripts/deploy.sh never did). "Still open" trimmed to: execute the real cutover + the deferred genericization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…no-outage) Second migration rehearsal on a fresh live-equivalent clone (verbatim from the runbook) passed end-to-end — release staged alongside the running site, config applied to files with no reload (public_html kept serving 200), activation gate passed + `migrate` "No pending" on the live DB, build → new docroot, then the flip. One real finding: `systemctl restart php8.4-fpm` in the flip drops all workers and has a brief not-ready window — I caught a transient `description.php` 404 curling right after it (the file/ACL/open_basedir were all fine; a re-curl was 200). Switch Phase 3 to `systemctl reload php8.4-fpm` + `php-fpm8.4 -t`: verified on the VM that reload is graceful (200 immediately after, no window) AND re-reads the pool config (dropping the docroot from open_basedir + reload → 404; restoring → 200). So a live cutover flips with zero PHP outage. (The first rehearsal used restart and got lucky on timing.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran a failed-first-cutover rollback rehearsal on a live-equivalent clone: forced the activation to fail at the health check (bad HEALTH_URL) AFTER the DB mutation + symlink switch. Confirmed the safety net: - the deployer restored the 625 MB DB backup (obs back to 4,465,536) + the runtime config, removed `current`, left maintenance on + consumers stopped (no-prior- release path); - the OLD public_html kept serving 200 throughout — a failed cutover does NOT take the site down, because Phase 1-2 never reload nginx/FPM; - recovery by fixing the cause + retrying (`rm -f /opt/kayak/maintenance`; re-run kayak-deploy.sh) re-activated cleanly and the Phase-3 flip served the new docroot, DB intact. Added the recovery note to the migration section (retry vs full abort), and noted that the config files point at the new docroot post-failure so a stray nginx reload would switch to it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Where the old single-tree layout maps to" table to the running-host migration section, answering "where did ~/kayak, ~/.venv, ~/kayak_data, ~/public_html, ~/DB, ~/var, ~/logs, ~/.config/kayak, ~/backups go?" Distinguishes the four dispositions, each verified against the units/config on the live host: - moved into the immutable release: the engine (~/kayak src + ~/.venv) → /opt/kayak/current/venv, the dataset (~/kayak_data) → current/dataset; - moved to the regenerable cache: ~/public_html → /var/cache/kayak/docroot (#3), plus the two in-tree caches ~/kayak/{var/osmb,Gauge-metadata-cache}; - unchanged by design: ~/DB, ~/var/status.html, ~/logs, ~/backups (mutable state + host-level units, all keyed off HostConfig fields / open_basedir); - kept-but-in-place: the ~/kayak checkout + ~/.venv stay because the cutover does NOT re-point the shell-script consumers (kayak-recap/heartbeat/ config-drift/healthcheck/cert-expiry/backup-*, which ExecStart from ~/kayak/{scripts,systemd}/ and recap.sh still calls ~/.venv/bin/python3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two additions to the running-host migration section, both grounded in the
deployer's actual behavior:
Post-cutover cleanup (one-time, after a few days' soak) — what's safe to remove
once the paired release is proven: the old ~/public_html docroot (superseded by
/var/cache/kayak/docroot) and the two in-tree caches that relocated
(~/kayak/var/osmb, ~/kayak/Gauge-metadata-cache → /var/cache/kayak/{map-layers,
gauge-metadata}). Plus an explicit DO-NOT-REMOVE list: ~/kayak + ~/.venv stay
(the un-re-pointed shell consumers — recap/heartbeat/config-drift/healthcheck/
cert-expiry/backup-* — still ExecStart from them, and recap.sh runs
~/.venv/bin/python3), and the mutable/unchanged-by-design dirs stay. Corrects
the prior line that called ~/.venv "unused".
Ongoing disk hygiene (what accumulates per release) — a table of growth +
bound: /opt/kayak/releases/ auto-prunes to KAYAK_KEEP_RELEASES (default 5,
never current/previous); /opt/kayak/.staging auto-cleans on success but is
deliberately kept (CLEAN_SCRATCH=0) on a failed/rolled-back deploy to preserve
the ~650 MB pre-activate DB backup — the one recurring manual sweep, after the
restore is verified. Net: steady state is self-bounding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…at, cleanup) Three additions validated by the 2026-06-14 live amd64 stage-only dry run: - Pre-flight smoke check: stage-only needs only ENGINE_REPO/DATASET_REPO, so passing them inline proves prod's amd64 wheel build + dataset validation + digest with zero config files and zero mutation. Documented in the running-host migration section as the first de-risking step before any Phase-0 setup. - Release-id caveat (step 3): a no-host.yaml dry run computes a DIFFERENT release id than the activation (the id folds in the host-config fingerprint + normalized runtime-config). Calls it out so the differing id isn't read as instability. - Staged-dir cleanup (step 3): stage-only RETAINS releases/<id>/ for inspection; note to rm it (only .staging auto-cleans on exit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review — PR #197 (Batch 4C pt6: the paired-release install + live-cutover runbook)Doc-only, but this is the runbook that will drive the live cutover of the WKCC host, so I reviewed it as an operational procedure — cross-checking every Verified correct against the live host
1. (Medium) Internal contradiction: "3 engine units" vs the actual 6The
That's wrong — it's 6, and the same doc says so elsewhere (the cleanup DO-NOT-REMOVE list: 2. (Medium) The Phase 1 → Phase 2 window roots nginx at an empty docroot — guard it against a stray reloadPhase 1 edits the nginx file to
3. (Medium-low) The "pre-flight already disabled the legacy auto-deploy" is referenced but never a stepThe cleanup section (line ~443) asserts "the pre-flight already disabled any cron/timer that git-pulls main or runs 4. (Low) "
|
|
Correction to finding #3 (the "legacy auto-deploy" point). I checked this against the live host and could not locate any auto-pull/auto-deploy mechanism: no So finding #3 reduces to a doc-consistency point only: the cleanup section asserts "the pre-flight already disabled any cron/timer that git-pulls main or runs Findings #1 (3-vs-6 engine units), #2 (empty-docroot reload window; — Claude Opus-4.8(1M)/xhigh |
All four cross-checked against the live host's installed config: 1. (Medium) Mapping table `~/.venv` row said the cutover re-points "3 engine units" — it's 6. Verified all of pipeline/decimate/editor-retention/status/ fetch-osmb/audit-gauges currently ExecStart=~/.venv/bin/levels (= render-units --list-units). Fixed the count + list; noted the basis. 2. (Medium) Empty-docroot reload window: Phase 1 roots nginx at the Phase-0-empty docroot and defers the reload, so any nginx reload before the Phase-2 build 404s live serving — and certbot.timer is active on this host. Added an explicit guard (stop certbot.timer in Phase 1, restart in Phase 3) + documented the pre-build-the-docroot alternative with its schema caveat. 3. (consistency) The cleanup section referenced a "pre-flight already disabled" step that didn't exist (and the reviewer verified no auto-deploy scheduler exists here). Added a real read-only Pre-flight checklist (no-auto-deploy check, df headroom, the Phase-1 sed targets, ~/kayak clean+on-main) and reworded the cleanup reference to point at it. 4. (Low) Softened the live-migration "migrate reports No pending" wording: if the DB lags the deployed engine, migrate APPLIES the pending migrations (deployer backs up first) — both outcomes are normal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — all four addressed in #1 (3-vs-6 engine units) — fixed. Confirmed all six ( #2 (empty-docroot reload window) — guarded. Added an explicit #3 (dangling "already disabled" ref) — both halves done. Your verification (no root/ #4 ( Also for the record from the live host since this review: Phase 0 is now placed and verified on |
Batch 4C — increment 6: the validated paired-release install and live-cutover runbook
Started as "rewrite the install runbook §5 to use the renderers"; it grew into the single end-to-end runbook for both a virgin install and the no-downtime migration of the running WKCC host, every section rehearsed on a VM (and now the stage-only path on the live host itself).
What changed
The rendered install (the original scope):
render-units --out-dir /etc/systemd/systememits the six engine cutover drop-ins (audit-gauges is an engine consumer now, refactor(audit): promote audit_gauges tolevels audit-gauges(4C prep) #191);render-servingemits the nginxroot+ FPMopen_basedir, applied withsed-from-the-rendered-value so the values are deterministic fromhost.yamlrather than hand-typed. The deployer's gate (step 7) re-verifies all of it./var/cache/kayak/docrootthroughout (layout table,OUTPUT_DIR, nginx root,open_basedir) — the release no longer carries a docroot, andopen_basedirdrops the/opt/kayak/releasesentry.host.yamlinstall (step 2) flips the generated-cache paths off the read-only release; new/var/cache/kayak/{docroot,map-layers,gauge-metadata}dirs + the docroot recursive+defaultwww-dataACL in §6. §5d sets the serving knobs the gate requires (KAYAK_NGINX_DOCROOT_CONF/KAYAK_FPM_POOL, fail-closed);KAYAK_HOST_UNITSremoved (the gate sources its set fromrender-units --list-units, Batch 4C pt4: gate sources engine set fromrender-units --list-units(D-CONSUMER) #196).The live-cutover material (added after rehearsing it):
reloadflip).reloadnotrestartfor FPM (restart drops workers → transient 404).public_htmlkeeps serving; recover by retry.~/kayak,~/.venv,~/kayak_data,~/public_html,~/DB,~/var,~/logs,~/.config/kayak,~/backups→ their new homes, with the four dispositions (into the immutable release / into the cache / unchanged-by-design / kept-in-place for the un-re-pointed shell consumers).KAYAK_KEEP_RELEASES;.stagingauto-cleans on success, manual sweep only after a failed deploy).ENGINE_REPO/DATASET_REPO), the release-id-differs-without-host.yamlcaveat, and the staged-dir cleanup.Validation status
audit-gaugesstale unitpublic_htmlnever dropped, retry recoversvalidate-datasetOK, digest computed; no mutationThe renderers are unit-tested;
nginx -t/systemd-analyze verifyof the rendered output happen on the VM, not in CI.CI
Doc-only. CI shellchecks
deploy/*.shbut not.md, and no test references the runbook (deploy/is outside the ruff/mypy scope) — verified referenced files exist.Remaining (not in this PR)
server_names, real Turnstile keys, prod disk math, live drain timing) — the deferred genericization knobs land there.🤖 Generated with Claude Code