Skip to content

docs(migration): production upgrade runbook + search-indexing policy (already deployed) - #845

Merged
audreyt merged 95 commits into
mainfrom
feat/prod-upgrade-runbook
Aug 10, 2026
Merged

audreyt merged 95 commits into
mainfrom
feat/prod-upgrade-runbook

Conversation

@audreyt

@audreyt audreyt commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Status: already on production

This branch is already deployed to production ethercalc.net.

Live Worker version 0e024a32-5ce8-4315-a4eb-a4c5c2a8f780 @100%
Ramped 2026-08-10 12:24:35Z @10% → 12:25:56Z @50% → 12:27:58Z @100%
Rollback floor CURRENT_PROD_VERSION_ID = bd76bda5-3161-4576-b159-dbdb97d774c2

Rollback (do not run unless aborting):

vp exec wrangler versions deploy bd76bda5-3161-4576-b159-dbdb97d774c2@100% \
  --config=packages/worker/wrangler.toml --env="" --yes
# then Cloudflare Dashboard → Caching → Purge Everything
# prove with unique-room X-Robots-Tag ABSENT (not /robots.txt alone — sticky edge cache)

This PR exists so the source behind that Worker version is recoverable and reviewable. Do not squash or rewrite history in a way that drops production code (see c789249 note below).

Why the plan changed (baseline correction)

Live probes on 2026-08-10 showed production was not tag 0.20260717.0 / commit 149ebcf. Passkeys, AuthDO, and private rooms were already live (GET /_auth/whoami → enabled:true; root serves ./static/passkey/ui.js). The original three-phase plan’s soak of ETHERCALC_AUTH="0" would have made getSessionPrincipal null and 403-locked every private room for its owner — a self-inflicted outage. Corrected procedure: single gradual ramp with auth left at "1" throughout (docs/migration/PROD_UPGRADE_PLAN.md §4; superseded three-phase material only in Appendix A).

Candidate window remains [d2afa90, b7d8840) under the working premise of a source-wrangler.toml upload (exact git SHA still open — no version annotation; §4.0 generated-config redirect can mask compat/asset markers).

What this ships (user-visible)

  • Session cookie rename ec_sess → __Host-ec_sess with no dual-read: every signed-in passkey user appears signed out until they passkey-login again. Credentials remain in AuthDO; only the browser cookie is orphaned.
  • Search-indexing policy: real GET /robots.txt (text/plain, User-agent: *, Allow: /, no Disallow); non-landing paths get X-Robots-Tag: noindex, nofollow, noarchive; only / stays indexable. (Accidental dual-purpose revert restored via c789249.)
  • Security-audit delta already on main path: externalized root page scripts (static/index-bootstrap.js et al.), CSP/__Host hardening, WS/frame caps, command-rejection 413 propagation, etc.

Verification

Gate Result
Worker test:node 53 files / 1526 tests PASS (re-run at pre-ramp HEAD)
Worker test:workers 13 files / 197 tests PASS
Worker coverage 100% (2706 / 1936 / 298 / 2416)
Mutation ratchet exit 0 at tip 327fa3d (docs-only since; packages/+scripts/ empty vs that tip)
Staging rehearsal (§3) PASS — upload → PRE@100%+SHIP@0% → override smoke → 10/50/100 → rollback control-plane + X-Robots edge proof
Production override smoke @0% PASS — robots html→plain; unique-room X-Robots; whoami enabled:true; bootstrap 404→200
Production ramp PASS — mix samples at 10% (1/20) and 50% (8/13 plain robots); never enabled:false

Load-bearing history note

Commit c789249 restores production search-indexing / robots behavior after an accidental dual-purpose-commit revert. Its subject may look docs-oriented, but it carries runtime code. Any squash, subject-based triage, or interactive rebase must not drop it or production robots/X-Robots behavior will regress on the next deploy.

Outstanding (not blocking this PR open)

  • Edge purge pending — owner Cloudflare Dashboard: Caching → Configuration → Purge Everything for ethercalc.net (and www if needed). Uncached probes already prove ship is live; bare /robots.txt and bare /static/index-bootstrap.js can still show sticky pre-ship cache hits until purge.
  • Optional D1 SQL export; ETHERCALC_MIGRATE_TOKEN only if PITR/migrate routes are in-window (production has run without it).

Runbook entry point

docs/migration/PROD_UPGRADE_PLAN.md (+ companions in docs/migration/).

Do not merge until review. Production is already at 100% ship; merging is documentation/source recovery, not a deploy trigger unless someone runs deploy-production separately.

audreyt added 30 commits August 10, 2026 14:57
Previously, POST /_/:room dispatched commands to the room Durable Object and checked authorization status via authVerdict(writeRes). However, if the DO rejected a command for non-auth reasons (such as a 413 error when a command exceeds sheet limits), the worker ignored writeRes.ok and unconditionally returned HTTP 202 Accepted. This created a silent write-loss path where callers were informed that commands were accepted when they were actually rejected by the DO.

Propagate non-2xx status and response text from the Durable Object fetch response back to the caller in both JSON and plain-text dispatch handlers. Also include unit tests for DO error propagation, legacy room accessibility, oversize sheet handling, and ETHERCALC_AUTH kill-switch behavior.
…verify

The read_score() function in scripts/ratchet-verify.sh previously attempted to read a mutationScore property from Stryker's JSON report schema using jq and node. However, Stryker JSON reports do not contain a top-level mutationScore field; mutant results are recorded under .files[].mutants[] with status strings. As a result, read_score() returned empty, causing ratchet-verify.sh to fail with exit code 2 ('parse error').

Update read_score() in both jq and node paths to calculate the score from individual mutant statuses as (Killed + Timeout) / (Killed + Timeout + Survived + NoCoverage) * 100.
Add production upgrade runbook and supporting audit documentation in docs/migration/ to guide the EtherCalc modern stack deployment. Includes the production upgrade plan, delta audit between legacy and modern worker implementations, WebSocket skew and reconnect handling strategy, preflight validation results, and complete component inventory. Also update AGENTS.md key docs index.
Document the 413 rejection response and WebSocket 1008 close behavior in API.md under POST /_/:room. Add Unreleased changelog entries in Changes.txt for the POST /_/:room error propagation fix and the ratchet-verify Stryker JSON parser fix.
Module scripts require an exact JS Content-Type, and Socket.IO 0.x
clients parse handshake fields positionally — pin both contracts that
real clients depend on.
Hosted Cloudflare Assets pass text/javascript through; standalone
workerd rewrites octet-stream via mimeForPath. Document the split in
the upgrade runbook and refresh the now-pinned socket.io notes.
Standalone config.capnp has no DB/cron/email bindings. Spell out empty
room-index behavior, missing D1 audit/chat recovery, dead settimetrigger
persistence, external /_timetrigger setup, PITR vs filesystem backup, and
the upgrade-critical DO uniqueKey storage anchor.
Map every RoomDO cross-room hop (DO→DO vs Worker→DO), diff the /_do/*
protocol from 149ebcf to HEAD, and record the Phase 2 gradual-ramp
verdict: public-room traffic is DEGRADED (sheet-limit asymmetry) not
BROKEN, while /_do/init-private is BROKEN against Phase 1 DOs and must
stay unreachable until the ramp is at 100% (harden §9 item 9).
/_do/access 501 is not an authz hole: HTML/edit only skip UX redirect
hints, DELETE is fail-closed, RoomDO remains the sole boundary, and a
Phase 1 DO cannot be private. Keep init-private 501 as the real
correctness reason Phase 2 must hit 100% before AUTH=1, stacked on the
original rollback justification. Clarify workbook-kind unknown impact.
Record D1 database_size headroom as a go/no-go baseline, reframe mass
PITR as bounded-window only, page private-room discovery under the 30s
query ceiling, and budget multi-GB d1 export wall-clock.
Add inspect→branch→act cards at Phase 1 unknown outcome, Phase 2 ramp
abort, and Phase 3 private-data rollback, plus a cutover-log artifact
table and a soak pointer to the D1 capacity ceiling.
Move Card 1 immediately after the Phase 1 deploy fence and link §0.2
instead of restating CLI; collapse Card 3 to ordered commands; make the
Path B WAF default deny-all on both production hosts (optional operator
IP only) and require /_/, /_ws/, /{room}, /{room}.xlsx block checks
before Phase 1 traffic.
Re-running time-travel info after cutover yields a current bookmark, not
the pre-cutover one. Recover only from saved output or a recorded UTC
timestamp via §6.4; otherwise mark indeterminate.
Mark each companion as supporting evidence with runbook authority.
Correct D1 scope (no sheet snapshots; private index exclusion vs
unfiltered audit/chat), front-door POST /_/:room 413 propagation
(superseding always-202), player.js MIME split, ~1.8M/10GB scale,
and self-host absence of D1 — without erasing historical audit notes.
…notes

Replace DELTA exec-summary "backup DO storage" with WAF/lockout/bounded
PITR guidance and never-strip-meta:access. Mark PREFLIGHT gate 3/4 counts
as matching current runbook §1 (1520/196), retiring draft 1514/195
discrepancy claims.
Feed the genuine oracle MINIMAL_SCSAVE (recorded pre-3.1.0) through
createSocialCalcFactory()/createSpreadsheet and assert cell values plus
semantic round-trip. Reclassify DELTA_AUDIT item j FORWARD-COMPATIBLE;
document that first re-save is not byte-identical and that oracle
"10/13" is an intentional static-asset subset, not three failures.
The suite loads all recorded fixtures and only hard-asserts nine named
HTTP scenarios plus a semantic socialcalc.js check; it does not filter
or fail an exact 13-scenario set.
audreyt added 29 commits August 10, 2026 19:22
Re-ran scripts/ratchet-verify.sh end-to-end (828s, exit 0). All six
packages remain above break floors; worker is still narrowest at
90.03% (+0.03 vs 90%). robots.ts measured 100% (9 killed).
Every audited package sits within 1pp of its Stryker break floor;
@ethercalc/worker is +0.03pp. Pre-cutover PRs can trip mutation-gate
even when functional tests pass — run ratchet-verify locally first;
equivalent mutants get written // Stryker disable justifications.
HEAD had moved past the inspected tree, leaving the comparison target
ambiguous. Mirror DELTA_AUDIT: pin d2afa90 → 76ce06a, record the
packages/ spot-check (c789249 robots restore only; no skew impact), and
note later commits are documentation-only.
Room URLs are unlisted by design; stamp noindex on every non-landing
response, serve a real robots.txt without Disallow so crawlers can still
observe noindex headers, and keep only / indexable.
Record the operator runbook deliverable, the overturned 149ebcf premise,
AUTH=1 single-ramp correction, cookie rename regression, robots restore
caution, tip measurements, and remaining OPERATOR-VERIFY items.
Latest becomes the operator runbook session (false 149ebcf premise,
AUTH=1 single ramp, cookie rename, robots restore); prior security
audit compresses into Prior.
Rotation requires the former Latest paragraph under Prior unchanged
aside from the label; reinstate the full security-audit wording.
Point operators at PROD_UPGRADE_PLAN.md and its critical-path quick reference; list the four companion evidence files with the runbook's own one-line read-when framing and the [d2afa90, b7d8840) baseline caveat.
Record the 570 Survived mutants from the Gate 7 Stryker JSON at tip
327fa3d (score 90.034%, floor 90, +0.03pp): 552 REAL GAP, 18 EQUIVALENT,
0 UNCERTAIN. Analysis only — no source/test/threshold changes. Point §8
mutation-fragility at MUTATION_SURVIVORS.md.
Pin room.ts/command-limits REAL GAP counts and expand ellipsized
Original/Mutated/Reason cells; headline metrics unchanged.
Record CURRENT_PROD_VERSION_ID bd76bda5…, D1 size/Time Travel, and
live probes in the §4.3 cutover log. Rewrite §0.1 so absent
ETHERCALC_KEY is status-quo posture, not a hard provision demand.
Keep the candidate-range and still-stands tables inside one continuous
blockquote after the baseline-pin wording landed.
Live compat/asset markers are consistent with pre-b7d8840 source
uploads but not exclusive if dist/ethercalc/wrangler.json redirected.
Replace bare blank lines inside the STOP banner with quoted blanks so
the whole banner renders as one blockquote.
Staging rehearsal proved Version-Overrides only select versions already
in the active deployment. Document verified CLI forms, robots/X-Robots
discriminators, sticky-cache rollback caveat, and 2026-08-10 results.
Canonical post-rollback edge proof is unique-room X-Robots-Tag; robots
alone is forward-only after ship pollutes cache. Mark worker preflight
and staging rehearsal checklist items satisfied at HEAD.
SHIP 0e024a32… ramped 0→10→50→100; CURRENT bd76bda5 remains rollback
floor. Uncached probes green; edge purge still needed for sticky cache.
…compat

CI clean installs only lockfile packages; probing .bun/socialcalc@3.0.8
orphans never works there. Add socialcalc-308=npm:socialcalc@3.0.8
devDependency and resolve the reverse-compat UMD through that alias
without disturbing primary socialcalc@3.1.0.
Job cancelled at 20m mid-Stryker on worker-only src diff (run
31388973185) with zero score output. Local full ratchet ~828s;
45m ≈ 2.5× that baseline so CI can finish and report a real score.
Same stryker.conf and mutate scope; gate uses total mutationScore
(not covered). Local 90.03 (+0.03) vs CI 90.61 (+0.61) is Timeout/
Killed/Survived flapping, not a second harness. Provenance on run
31390939451 / head 09673a4.
Correct socialcalc-308 scope (worker loads headless; alias only in
headless reverse-compat test). Drop unproven Timeout-flapping /
mutant-generation language; keep aggregate deltas only.
@audreyt
audreyt merged commit 12f9266 into main Aug 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant