|
1 | 1 | #!/bin/bash |
2 | | -# Docker entrypoint: run DB migrations, then start the app. |
3 | | -# Use bash (not sh/dash) to avoid CRLF/set-option compatibility issues. |
| 2 | +# Docker entrypoint: initialize DB tables, then start the app. |
| 3 | +# Order matters: |
| 4 | +# 1. create_all - creates all tables using SQLAlchemy models (idempotent) |
| 5 | +# 2. alembic stamp head - tells alembic we are at the latest revision (skips migrations) |
| 6 | +# For existing installs that may have missing columns, safe ALTER TABLE patches run first. |
| 7 | +# 3. uvicorn - starts the FastAPI app |
| 8 | + |
4 | 9 | set -e |
5 | 10 |
|
6 | | -echo "[entrypoint] Running database migrations..." |
7 | | -alembic upgrade head |
8 | | -echo "[entrypoint] Migrations done - starting uvicorn..." |
| 11 | +echo "[entrypoint] Step 1: Creating/verifying database tables..." |
| 12 | + |
| 13 | +python << 'PYEOF' |
| 14 | +import asyncio, sys |
| 15 | +
|
| 16 | +async def main(): |
| 17 | + # Import all models to populate Base.metadata before create_all |
| 18 | + from app.database import Base, engine |
| 19 | + import app.models.user # noqa |
| 20 | + import app.models.agent # noqa |
| 21 | + import app.models.task # noqa |
| 22 | + import app.models.llm # noqa |
| 23 | + import app.models.tool # noqa |
| 24 | + import app.models.audit # noqa |
| 25 | + import app.models.skill # noqa |
| 26 | + import app.models.channel_config # noqa |
| 27 | + import app.models.schedule # noqa |
| 28 | + import app.models.plaza # noqa |
| 29 | + import app.models.activity_log # noqa |
| 30 | + import app.models.org # noqa |
| 31 | + import app.models.system_settings # noqa |
| 32 | + import app.models.invitation_code # noqa |
| 33 | + import app.models.tenant # noqa |
| 34 | + import app.models.message # noqa |
| 35 | + import app.models.chat_session # noqa |
| 36 | +
|
| 37 | + # Create all tables that don't exist yet (safe to run on every startup) |
| 38 | + async with engine.begin() as conn: |
| 39 | + await conn.run_sync(Base.metadata.create_all) |
| 40 | + print("[entrypoint] Tables created/verified") |
| 41 | +
|
| 42 | + # Apply safe column patches for existing installs that may be missing columns. |
| 43 | + # All statements use IF NOT EXISTS so they are fully idempotent. |
| 44 | + patches = [ |
| 45 | + # Quota fields added in v0.2 |
| 46 | + "ALTER TABLE users ADD COLUMN IF NOT EXISTS quota_message_limit INTEGER DEFAULT 50", |
| 47 | + "ALTER TABLE users ADD COLUMN IF NOT EXISTS quota_message_period VARCHAR(20) DEFAULT 'permanent'", |
| 48 | + "ALTER TABLE users ADD COLUMN IF NOT EXISTS quota_messages_used INTEGER DEFAULT 0", |
| 49 | + "ALTER TABLE users ADD COLUMN IF NOT EXISTS quota_period_start TIMESTAMPTZ", |
| 50 | + "ALTER TABLE users ADD COLUMN IF NOT EXISTS quota_max_agents INTEGER DEFAULT 2", |
| 51 | + "ALTER TABLE users ADD COLUMN IF NOT EXISTS quota_agent_ttl_hours INTEGER DEFAULT 48", |
| 52 | + "ALTER TABLE agents ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ", |
| 53 | + "ALTER TABLE agents ADD COLUMN IF NOT EXISTS is_expired BOOLEAN DEFAULT FALSE", |
| 54 | + "ALTER TABLE agents ADD COLUMN IF NOT EXISTS llm_calls_today INTEGER DEFAULT 0", |
| 55 | + "ALTER TABLE agents ADD COLUMN IF NOT EXISTS max_llm_calls_per_day INTEGER DEFAULT 100", |
| 56 | + "ALTER TABLE agents ADD COLUMN IF NOT EXISTS llm_calls_reset_at TIMESTAMPTZ", |
| 57 | + # agent_tools source tracking added later |
| 58 | + "ALTER TABLE agent_tools ADD COLUMN IF NOT EXISTS source VARCHAR(20) NOT NULL DEFAULT 'system'", |
| 59 | + "ALTER TABLE agent_tools ADD COLUMN IF NOT EXISTS installed_by_agent_id UUID", |
| 60 | + # chat_sessions channel tracking |
| 61 | + "ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS source_channel VARCHAR(20) NOT NULL DEFAULT 'web'", |
| 62 | + ] |
| 63 | +
|
| 64 | + from sqlalchemy import text |
| 65 | + async with engine.begin() as conn: |
| 66 | + for sql in patches: |
| 67 | + try: |
| 68 | + await conn.execute(text(sql)) |
| 69 | + except Exception as e: |
| 70 | + print(f"[entrypoint] Patch skipped ({e})") |
| 71 | +
|
| 72 | + await engine.dispose() |
| 73 | + print("[entrypoint] Column patches applied") |
| 74 | +
|
| 75 | +asyncio.run(main()) |
| 76 | +PYEOF |
| 77 | + |
| 78 | +echo "[entrypoint] Step 2: Stamping alembic to latest revision..." |
| 79 | +# Mark all migrations as applied so alembic does not re-run them on top of create_all. |
| 80 | +# This is safe: create_all already created tables with all current columns. |
| 81 | +alembic stamp head |
| 82 | + |
| 83 | +echo "[entrypoint] Step 3: Starting uvicorn..." |
9 | 84 | exec uvicorn app.main:app --host 0.0.0.0 --port 8000 |
0 commit comments