|
20 | 20 | from ..run.sinks import log_registered_sinks |
21 | 21 | from ..run.worker import AsyncRunWorker |
22 | 22 | from ..storage import StorageSettings, build_repos |
23 | | -from ..storage.postgres.migrator import Migrator |
| 23 | +from ..storage.postgres.migrator import Migrator, discover_migrations |
24 | 24 | from ..utils.log_buffer import log_buffer |
25 | 25 | from .debug_routes import debug_router |
26 | 26 | from .routes import router |
|
31 | 31 |
|
32 | 32 | logger = logging.getLogger(__name__) |
33 | 33 |
|
| 34 | +_TRUE_VALUES = {"true", "1", "yes", "on"} |
| 35 | +_FALSE_VALUES = {"false", "0", "no", "off"} |
| 36 | + |
| 37 | + |
| 38 | +def _env_bool(name: str, *, default: bool) -> bool: |
| 39 | + raw = os.getenv(name) |
| 40 | + if raw is None or raw == "": |
| 41 | + return default |
| 42 | + val = raw.strip().lower() |
| 43 | + if val in _TRUE_VALUES: |
| 44 | + return True |
| 45 | + if val in _FALSE_VALUES: |
| 46 | + return False |
| 47 | + raise ValueError(f"{name} must be one of true/false/1/0/yes/no/on/off (got: {raw!r})") |
| 48 | + |
| 49 | + |
34 | 50 | try: |
35 | 51 | from dotenv import load_dotenv |
36 | 52 |
|
@@ -68,13 +84,34 @@ async def lifespan(app: FastAPI): |
68 | 84 | logger.error("Storage configuration invalid; /api/runs will not be available: %s", exc) |
69 | 85 |
|
70 | 86 | if storage_settings is not None and storage_settings.backend == "postgres": |
71 | | - logger.info("Applying any pending migrations to schema '%s'", storage_settings.schema_name) |
72 | 87 | migrator = Migrator( |
73 | 88 | dsn=storage_settings.database_url or "", |
74 | 89 | schema=storage_settings.schema_name, |
75 | 90 | lock_timeout_s=storage_settings.migrate_lock_timeout_s, |
76 | 91 | ) |
77 | | - await migrator.up() |
| 92 | + if _env_bool("AGENTEVALS_AUTO_MIGRATE", default=True): |
| 93 | + logger.info("Applying any pending migrations to schema '%s'", storage_settings.schema_name) |
| 94 | + await migrator.up() |
| 95 | + else: |
| 96 | + logger.info( |
| 97 | + "AGENTEVALS_AUTO_MIGRATE is disabled; verifying schema '%s' is up to date", |
| 98 | + storage_settings.schema_name, |
| 99 | + ) |
| 100 | + status = await migrator.status() |
| 101 | + if status.dirty: |
| 102 | + raise RuntimeError( |
| 103 | + f"schema_migrations is dirty at version {status.version}. " |
| 104 | + "Resolve manually and run 'agentevals migrate force <version>', " |
| 105 | + "or set AGENTEVALS_AUTO_MIGRATE=true to retry on startup." |
| 106 | + ) |
| 107 | + current = status.version |
| 108 | + pending = [m.version for m in discover_migrations() if current is None or m.version > current] |
| 109 | + if pending: |
| 110 | + raise RuntimeError( |
| 111 | + f"Database schema is behind: pending migrations {pending}. " |
| 112 | + "Run 'agentevals migrate up' to apply them, " |
| 113 | + "or set AGENTEVALS_AUTO_MIGRATE=true to apply on startup." |
| 114 | + ) |
78 | 115 |
|
79 | 116 | repos = await build_repos(storage_settings) |
80 | 117 | app.state.storage_settings = storage_settings |
|
0 commit comments