Repository navigation
fix(migrations): baseline adoption for never-migrated DBs + strict Ansible invocation (#10001, #10026) - #10039
Merged
Merged
Conversation
…est for baseline adoption (#10001) Phase A of #10001/#10026: native deploys built schemas via create_all while the Ansible alembic task silently failed (no -c flag + failed_when: false), leaving schema-without-stamp databases. 32/37 table-creating migrations use unguarded op.create_table, so a strict 'upgrade head' aborts at 001 with DuplicateTable. - test_raw_upgrade_on_unstamped_schema_fails: pins the stranding mechanism (green; permanent documentation of why baseline adoption must exist) - test_bootstrap_adopts_unstamped_schema_and_reaches_head: acceptance test for the Phase B baseline entrypoint — RED until #10001 lands - disposable-Postgres fixtures (AUTOBOT_MIGRATION_TEST_ADMIN_URL) + new migration_gate pytest marker Verified red-first: 1 failed (No module named migrations.baseline), 1 passed. Discovery confirmed while building fixtures: LLCBase.metadata.create_all fails on Postgres at head (#9980 enum NAMES vs value defaults), so stranded fleet schemas are UM-Base-shaped only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
) Phase B: 'python -m migrations.baseline' classifies the database before 'alembic upgrade head' and adopts schema-without-stamp deployments: 1. EMPTY -> no-op; upgrade runs the full chain 2. STAMPED (known revision) -> no-op 3. SCHEMA_NO_STAMP -> adoption: a. autogenerate diff vs head models empty -> stamp head b. else probe-ladder bracketing: per-revision artifacts (created tables, added columns, TIMESTAMPTZ markers) AST-extracted from the migration files themselves (self-maintaining); minimal-candidate selection so unobservable revisions re-run rather than being skipped c. ambiguous bracketing -> exit 3, REFUSE with recovery-doc pointer; never guess a stamp 4. STAMPED unknown revision -> compatibility table (empty today: #9759 changed zero IDs, #8225 kept its ID) else exit 4, refuse env.py URL resolution extracted to migrations/db_url.py (shared, unchanged behavior). Exit codes: 0 proceed, 2 error, 3 ambiguous, 4 foreign stamp. Tests (16, all green; Phase A acceptance test flipped red->green): - every state-machine branch incl. probe-ladder bracketing of a 010-era schema, dry-run, mixed-schema refusal, interleaved create_all refusal, foreign-stamp refusal - ladder self-checks: full chain coverage, literal create_table enforcement, observability allowlist (043/045/052), anchor spot-checks Bugs caught red-first by the suite: helper-function create_table calls (migration 010) invisible to upgrade()-only extraction -> bracketed one rung low; cross-metadata FK resolution broke autogenerate fast path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… only schema authority (#10001) Phase C: runtime metadata.create_all can no longer build schema on the alembic-managed database: - migrations/schema_bootstrap.py: ensure_create_all_allowed(dialect) — SQLite (app-owned local data files) stays allowed; anything else refuses with a pointer to migrations.baseline + the recovery doc unless AUTOBOT_DB_CREATE_ALL=true is set explicitly (dev/compose profiles only) - lifespan._init_skills_tables (the one remaining runtime create_all, skills SQLite engine) now calls the guard before create_all - static schema-authority sweep: AST scan of all backend runtime code fails on any metadata.create_all outside the conscious allowlist; proven by scratch-offender injection (trips on offender, green on clean tree) - guard unit tests for all three branches (sqlite / blocked / opt-in) + AST check that lifespan actually calls the guard Ground truth recorded: the historical Postgres faucet was already removed from runtime code — fleet DBs were stranded by past create_all paths; this phase prevents recurrence. Compose smoke unaffected (single_user = SQLite). Verified red-first: 4 failed before the guard existed, 5 passed after; sweep red-proven via scratch offender. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… + no swallowed failures (#10001, #10026) Phase D, safe only now that baseline adoption (#10001 Phase B) exists. Both playbooks (setup-user-backend.yml, update-all-nodes.yml): - gate the sequence on Postgres-backed AUTOBOT_USER_MODE (single_user has no Postgres schema; previously alembic ran unconditionally and failed) - pg_dumpall backup before migrating (backup-node-data.yml pattern); loud warning when Postgres is remote and no local backup is possible - 'python -m migrations.baseline' adoption step BEFORE upgrade head - alembic invoked WITH '-c migrations/alembic.ini' (the old invocation could never find its config) and .env sourced so env.py can resolve the database URL - failed_when: false REMOVED — migration failure aborts the deploy with the captured alembic output update-all-nodes.yml additionally: - migration block moved after the .env regen (fresh credentials) and before the service restart (post-#9989 ORM needs columns at startup) - Play 2 sets any_errors_fatal: true — a failed node stops the rollout instead of continuing to the next node with a diverged fleet Contract pinned by tests/migrations/test_ansible_migration_contract.py (5 tests, proven red against the old playbooks, green after). ansible-playbook --syntax-check passes for both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nbook (#10026, #10002) Phase E — permanent gates: - .github/workflows/migration-gate.yml: postgres:16 service, runs the full matrix on every migrations/models/playbook change: (a) empty -> head, (b) stamped-intermediate -> head, (c) schema-without-stamp -> baseline adoption -> head, (d) head -> downgrade -1 -> head (new test, green on first run — the current chain's downgrade is healthy; this is a regression gate) plus baseline state-machine refusals, probe-ladder self-checks, the schema-authority sweep and the Ansible invocation contract. Minimal dependency install (exactly what env.py imports) keeps the gate fast and prevents silent dependency growth in migrations. - docs/operations/migration-recovery.md: how to read your revision, how adoption decides (state table + probe ladder), the manual re-stamp procedure for the refused/ambiguous case, foreign-stamp handling. None of the five #9759 failure classes was CI-detectable before this gate. Full local run: 27 passed against disposable Postgres 16. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion (#10001, #10026) State machine diagram, probe-ladder contents, 12 fleet states tested, red-first evidence per phase, ambiguity go/no-go assessment, and the explicit ordering statement: #10026 strictness landed only after #10001 adoption logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
This was referenced Jun 12, 2026
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…dowing (#10001) Self-review findings on PR #10039: - requirements-ci had no alembic, so ci.yml's repo-wide pytest (runs on push to Dev_new_gui) would fail the probe-ladder self-checks post-merge. Added alembic==1.18.4 to requirements-ci/storage.txt AND pytest.importorskip('alembic') in the self-check module so any environment without alembic skips instead of failing. - registered ansible var 'backend_user_mode' shadowed the identically named template variable consumed by backend.env.j2 (render currently happens before the register, but the landmine is real) — renamed the register to 'backend_user_mode_probe' in both playbooks. 27/27 tests green; both playbooks pass ansible-playbook --syntax-check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jun 12, 2026
Owner
Author
|
Self-review pass (requested follow-up audit) produced one hardening commit and three filed issues: Fixed on the branch (commit
Filed as follow-ups (none blocking this PR):
27/27 tests re-verified green after the hardening commit; both playbooks pass |
Owner
Author
Review (high effort — 7 finder angles, verified)Verdict: no blocking findings — approving for merge. Zero confirmed correctness bugs; CI fully green (smoke-test, code-quality, migration-matrix all pass; hardened-smoke-test is the pre-existing #9663 chromadb flake). Findings (all non-blocking, severity-ranked)[
{"file": "autobot-slm-backend/ansible/playbooks/update-all-nodes.yml", "line": 593, "summary": "Mode gate silently skips migrations when .env lacks AUTOBOT_USER_MODE", "failure_scenario": "PLAUSIBLE edge: node misconfigured so .env says single_user while operator intended Postgres → migrations skip. Consistent-by-design: the gate reads the exact file systemd feeds the service (#2824), so a skipped migration implies the runtime is also single_user. Residual risk documented; #10045 will make the backup/migration path fail-closed."},
{"file": "autobot-slm-backend/ansible/playbooks/update-all-nodes.yml", "line": 428, "summary": "any_errors_fatal: true covers all of Play 2, not just the migration block", "failure_scenario": "A transient non-migration failure (e.g. health check) now aborts the whole rollout. Conscious trade-off per the #10026 mission ('failure on one node must not continue'); documented in the commit. Narrowing to a dedicated migrate play would conflict with the required ordering (after .env regen, before restart)."},
{"file": "autobot-backend/migrations/schema_bootstrap.py", "line": 27, "summary": "create_all_opted_in() hand-parses an env bool; autobot_shared.env_registry.env() exists", "failure_scenario": "Reuse: registering AUTOBOT_DB_CREATE_ALL in the env registry would consolidate semantics — but couples the dependency-light guard to autobot_shared. Noted on #9841 (env-helper consolidation) rather than blocking."},
{"file": "autobot-backend/tests/migrations/conftest.py", "line": 76, "summary": "run_alembic/run_baseline duplicate subprocess env setup; script_head() duplicates baseline._script_directory() config", "failure_scenario": "Test-internal duplication (~15 lines). Ride-along candidate for the #10044 PR which touches the same tooling."},
{"file": "autobot-backend/migrations/db_url.py", "line": 36, "summary": "as_async_url() has 3 string-replace siblings in autobot-slm-backend/migrations", "failure_scenario": "Cross-service divergence risk on URL driver coercion; SLM has its own migration system by design. Consolidation note only."},
{"file": "autobot-backend/migrations/baseline.py", "line": 289, "summary": "stamp_revision() writes alembic_version via raw SQL instead of alembic API", "failure_scenario": "PLAUSIBLE maintenance cost if alembic ever changes its version-table schema (stable for a decade). In-process alembic stamp would re-enter env.py's async engine path — raw SQL is the deliberate, simpler choice; covered by tests asserting stamp == alembic's own table shape."}
]Notable refutations (claims checked against code)
|
mrveiss
added a commit
that referenced
this pull request
Jun 21, 2026
…s + tz round-trip (#10026) (#10368) Closes the last untested fleet state of the repaired Alembic chain (umbrella #9920). Cases 1 (stamped-intermediate) and 3 (unstamped-populated → baseline adoption) landed via #10039; case 2 — the 018 naive→TIMESTAMPTZ conversion on a legacy DB stamped <018 — was assumed, not tested. Diagnosed the real chain shape: process_runs/agent_sessions are migration-created and naive at rev 017 (so the 017→018 path IS case 2 for them); skill_* are create_all-only (absent) → seeded naive to model a legacy fleet DB. Then: - upgrade 018 converts all 8 of 018's (table,column) pairs to timestamptz; - a tz-aware datetime round-trips on a converted column with its offset intact. 2 Postgres tests (migration-gate). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Closed
4 of 10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #10001. Closes the blocking part of #10026 (case 3 + legacy upgrade-path gates); contributes the CI gate requested in #10002.
Thinking Path
The ordering is the mission: both Ansible migration tasks invoked alembic without
-c migrations/alembic.ini(guaranteed "No config file" failure) and swallowed it withfailed_when: false— so native Postgres deployments never migrated and their schemas (built by historicalcreate_all) carry noalembic_versionstamp. 32/37 table-creating migrations use unguardedop.create_table, so making the invocation strict first would strand every such database at migration 001. Therefore: reproduce the stranding (A) → baseline adoption logic (B) → close the create_all faucet (C) → only then strict invocation (D) → permanent gates (E). One commit per phase, every phase proven red-first (details in the report).What Changed
python -m migrations.baseline(new): classifies the DB into 4 states (empty / stamped / schema-without-stamp / foreign stamp). Adoption brackets the schema with a self-maintaining probe ladder (per-revision artifacts AST-extracted from the migration files; TIMESTAMPTZ markers for type-only revisions) and stamps the bracketed revision; ambiguous schemas are refused loudly (exit 3/4, nothing written) with a pointer to the new recovery runbook. Never guesses a stamp.migrations/schema_bootstrap.pyguard — runtimemetadata.create_allallowed only for app-owned SQLite data files unlessAUTOBOT_DB_CREATE_ALL=true; static schema-authority sweep keeps the pattern from returning.setup-user-backend.yml,update-all-nodes.yml): gate on Postgres-backed mode →pg_dumpallbackup → baseline adoption →alembic -c migrations/alembic.ini upgrade headwithfailed_whenremoved;update-all-nodesPlay 2 getsany_errors_fatal: trueand migrations moved after the.envregen / before the restart.migration-gate.ymlCI workflow (postgres:16 service) running the (a) empty→head, (b) stamped-intermediate→head, (c) unstamped→baseline→head, (d) head→downgrade-1→head matrix + all static checks;docs/operations/migration-recovery.mdoperator runbook.Verification
20260315_010schema, refusal cases, downgrade round-trip).MIGRATION_BASELINE_REPORT.md(Phase A committed failing; C guard 4-red; D contract 5-red against old playbooks; sweep red-proven via scratch offender).ansible-playbook --syntax-checkpasses for both playbooks.Dev_new_gui.Report:
MIGRATION_BASELINE_REPORT.md(state machine, ladder contents, fleet states, deliberate non-goals incl. #9980 confirmation and the remote-Postgres backup gap).Model Used
Claude Fable 5 (claude-fable-5)
🤖 Generated with Claude Code