fix: preserve Durable Object migration compatibility - #1649
fix: preserve Durable Object migration compatibility#1649simple-agent-manager[bot] wants to merge 12 commits into
Conversation
Independent deep verification of this PR (quadruple-check requested by Raphaël)A separate session re-verified this PR from scratch with local tests, wrangler-source inspection, live read-only Cloudflare API checks, and two adversarial subagent reviews (cloudflare-specialist + general logic reviewer). Verdict: the core fix is correct — no correctness-breaking path was found. Findings below are hardening/verification gaps for human judgment, not blockers to the fix's logic. Independently verified ✅
Findings (ranked)
Local gates re-run in this verification: scripts/quality vitest suite 129/129 pass, deploy-scripts tsc, |
Follow-ups from the independent PR verification (see PR comment): - Extract the migration-state probe/resolver into scripts/deploy/durable-object-migrations.ts (sync-wrangler-config.ts was at 797 lines; rule 18) with re-exports so existing imports keep working. - Retry the read-only migration-state probe a bounded number of times before failing closed. The first-deploy second pass re-lists Workers seconds after pass 1 created the API Worker; a transient listing lag previously hard-failed the final step of a fresh install. Env-configurable via DO_MIGRATION_STATE_PROBE_ATTEMPTS / DO_MIGRATION_STATE_PROBE_RETRY_DELAY_MS (defaults 3 / 2000ms), passed through both deploy-reusable.yml sync steps. - Give every fail-closed error operator recovery guidance: transient states say re-running is safe; tag-not-in-history explains restoring dropped fork-local migration entries; all destructive-recovery paths warn that deleting the Worker destroys all Durable Object data. Self-hosting docs gain a matching recovery section. - Document in rule 07 that wrangler resolves migrations via inheritable() (the 'not inherited by environments' grep-guard cannot protect this field), that the probe retries transient failures, and that the migrations array is append-only with sequential tags. - Enforce the append-only invariant with a sequential v1..vN tag test (proven discriminating: fails when two history entries are swapped). - Make the token-requirement tests env-independent: they previously made a real Cloudflare API call and failed whenever CF_API_TOKEN was exported (normal in deploy shells). Both now stub env + fetch and assert no network call. New tests cover retry-then-succeed, bounded exhaustion, and invalid env overrides. - Sync the stale CLAUDE.md line still claiming migrations are copied verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scripts/quality vitest suite — including the Durable Object migration compatibility regression tests protecting the #1614 fix — previously ran only via the manual quality:specialist-review:test script and no CI workflow. A revert of resolveDurableObjectMigrations would have kept CI green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wiring the scripts/quality suite into CI surfaced that five checker scripts run main() unconditionally at import. check-specialist-review-evidence.ts evaluated the real PR event inside the test runner (GITHUB_EVENT_NAME is set in Actions) and process.exit(1)'d the suite; the migration/observability checkers were one env var away from the same failure. All five now use the same direct-execution guard as sync-wrangler-config.ts, verified to still run via their pnpm quality:* invocations. Also rename quality:specialist-review:test to quality:scripts:test (it runs the whole scripts suite) and drop npx so the CI step executes the pinned local vitest (SonarCloud flagged npx-on-demand installs in workflows). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit's guard batch only landed for check-specialist-review-evidence.ts; the migration-safety, migration-ordering, do-migration-safety, and observability-noise checkers still ran main() on import. In CI (no CF_TOKEN) the observability checker's import-time getConfig() called process.exit(2) inside the vitest worker and failed the suite; locally it passed only because CF_TOKEN happens to exist. All tested checker modules are now import-safe, verified with a CI-faithful run (tokens unset, GITHUB_EVENT_* set): 133/133, zero unhandled errors, and every pnpm quality:* direct invocation still executes its check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Verification follow-ups implemented (medium/high findings from the review above)Four commits (
Also: the migration-state logic moved to Latent CI bug found and fixed while wiring the suite inFive quality-checker scripts ran Verification evidence
The |



Summary
apps/api/wrangler.toml.migration_tagbefore generating environment configuration. Applied migrations remain structurally unchanged; only pendingnew_classescreates becomenew_sqlite_classes.Closes #1614
Migration reasoning and compatibility
Cloudflare migration tags are ordered and applied once, and namespace storage cannot be changed in place. A global rewrite of old
new_classesentries would therefore be unsafe for existing KV-backed deployments. The resolver preserves the complete applied prefix and converts only pending creates:new_classes, 17 SQLite class creates;v17: output is structurally identical to the checked-in history;On a first two-pass deployment, pass one creates all classes as SQLite. Pass two reads
v17; Wrangler has no pending migration to replay. Future deployments append and submit only tags after the deployed tag.Validation
pnpm lintpnpm typecheckpnpm test(6,208 API/Worker tests and 2,740 web tests)pnpm build; deployment-script TypeScript; Wrangler binding, D1/DO migration safety and ordering, AST, file-size, source-contract, manifest, and specialist-review harness gatesStaging Verification (REQUIRED for all code changes — merge-blocking)
44548e8cbapp.sammy.partysam-api-stagingatv17; both API Worker deploys succeeded; tag and namespace backends were unchangedscripts/deploy/, but the change is limited to Worker migration preflight/config generation. The existing secret-backed staging workflow does not provide a real-workspace/heartbeat scenario, and the task shell cannot read its smoke credential. This remains a human-review gate.Staging Verification Evidence
Before and after the deployment, read-only Cloudflare namespace listings contained the same 17 classes.
AdminLogs,CodexRefreshLock,GitHubUserAccessTokenLock,GitLabUserAccessTokenLock,NodeLifecycle,TaskRunner, andTrialEventBusremained KV-backed; the other ten remained SQLite-backed. The Worker migration tag remainedv17. No production resource was mutated.A live clean-account deployment could not be performed: the only accessible Cloudflare accounts are canonical staging and production, and both already contain legacy KV namespaces. Creating a throwaway Worker in either account would not exercise Cloudflare's account-level “no existing legacy namespace” restriction. Automated generated-TOML proof and Cloudflare's official contract cover that path; a genuinely clean account remains the exact external verification gap.
UI Compliance Checklist (Required for UI changes)
End-to-End Verification (Required for multi-component changes)
Data Flow Trace
main()inscripts/deploy/sync-wrangler-config.tsloads Pulumi outputs and derives the API Worker name.getDeployedWorkerMigrationTag()proves the account listing is readable, readsmigration_tagfor an existing Worker, and uses an exact settings 404 only to confirm a missing Worker.resolveDurableObjectMigrations()locates the applied tag, preserves entries through it, and converts only pending legacy creates.generateApiWorkerEnv()places the resolved list in the generated environment; TOML serialization is exercised byscripts/quality/do-migration-compatibility.test.ts..github/workflows/deploy-reusable.ymlreceive the deploy token and feed the same environment to Wrangler.Untested Gaps
scripts/deploy/.Post-Mortem (Required for bug fix PRs)
What broke
Fresh SAM installations on newer Cloudflare accounts could not deploy the API Worker because historical
new_classesmigrations attempted to create the account's first KV-backed Durable Object namespace and Cloudflare returned error 10099.Root cause
SAM used a single static migration list for two incompatible states: immutable existing deployments whose historical namespaces are KV-backed, and clean accounts where new KV namespaces are no longer supported. The generator copied that list verbatim.
Class of bug
Externally versioned infrastructure history was treated as state-independent configuration despite immutable storage backends and account-dependent bootstrap capability.
Why it wasn't caught
Tests verified binding copying but did not distinguish a clean target from an already-migrated Worker. Miniflare config bypasses Cloudflare's remote migration contract.
Process fix included in this PR
.claude/rules/07-env-and-urls.mdnow requires immutable applied history, SQLite for new namespaces, fail-closed state detection, and clean/existing tests.scripts/quality/do-migration-compatibility.test.tsprotects clean, v17, partial, duplicate, unknown, missing, unreadable, and generated-TOML behavior.Post-mortem file
tasks/archive/2026-07-21-cloudflare-do-clean-install-migrations.mdSpecialist Review Evidence (Required for agent-authored PRs)
needs-human-reviewis applied and merge is deferred to a human44548e8cb; 37 focused tests passExceptions (If any)
Agent Preflight (Required)
Classification
External References
Official documentation from Cloudflare consulted before implementation:
Codebase Impact Analysis
scripts/deploy/sync-wrangler-config.ts: state probe, resolver, generated environment wiringscripts/deploy/types.ts: complete legacy migration directive typing.github/workflows/deploy-reusable.yml: inspected; both existing generator passes already provide token/account contextapps/api/wrangler.toml: inspected complete v1-v17 history; intentionally unchangedDocumentation & Specs
.claude/rules/07-env-and-urls.mdapps/www/src/content/docs/docs/guides/self-hosting.mdxtasks/archive/2026-07-21-cloudflare-do-clean-install-migrations.mdConstitution & Risk Check
Principle XI was checked. The Cloudflare API URL is an external service contract and the page size is a bounded control-plane request, not deployment business configuration. Primary risks are incorrect deployed-state inference and migration replay; list-first readability proof, exact missing-Worker confirmation, unknown/duplicate-tag rejection, applied-prefix preservation, and staging snapshots mitigate them.