Defense-in-depth for the deploy pipeline. Today a backend image shipped ahead
of an un-applied migration; the Tree model selected columns the DB didn't have
yet, so every trees query 500'd with an opaque UndefinedColumnError and the UI
showed no trees. The root cause (deploys not running migrations) is fixed
separately; this makes the *symptom* impossible to miss.
- app/core/schema_version.py: compare the DB's stamped alembic head to the
head(s) baked into the image's migration scripts. A DB with no alembic_version
table (e.g. a create_all test DB) is treated as current, so this stays quiet
outside real deployments. Uses to_regclass so a missing table never poisons
the caller's transaction.
- /health/ready: returns 503 with an explicit "drift: db=… expected=…" message
when the schema is behind, instead of reporting ready and serving 500s.
- Startup lifespan: logs CRITICAL on drift (advisory — never blocks startup).
Liveness (/health) is untouched, so a drifted container isn't killed into a
crash-loop — it's loudly degraded and self-heals once migrations apply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>