diff --git a/backend/app/core/config.py b/backend/app/core/config.py index adc81277..eb073d08 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -72,6 +72,9 @@ class Settings(BaseSettings): """Check if Stripe is configured.""" return self.STRIPE_SECRET_KEY is not None and self.STRIPE_WEBHOOK_SECRET is not None + # Deployment – auto-seed test data on PR environments + SEED_ON_DEPLOY: bool = False + # CORS - set FRONTEND_URL in production (e.g., https://patherly.up.railway.app) CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173", "http://localhost:5174"] FRONTEND_URL: Optional[str] = None diff --git a/backend/app/main.py b/backend/app/main.py index 5536e832..0c5da246 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,6 @@ +import asyncio import logging +import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -18,6 +20,42 @@ setup_logging() logger = logging.getLogger(__name__) +async def _seed_trees_background() -> None: + """Background task: seed trees via HTTP API after server is ready.""" + await asyncio.sleep(5) # Wait for server to be fully ready + port = os.environ.get("PORT", "8000") + api_url = f"http://127.0.0.1:{port}/api/v1" + email = "admin@resolutionflow.example.com" + password = "TestPass123!" + + try: + import httpx + # Login to get token + async with httpx.AsyncClient(base_url=api_url, timeout=30) as client: + login_resp = await client.post("/auth/login/json", json={"email": email, "password": password}) + if login_resp.status_code != 200: + logger.warning("[seed] Could not login as admin — skipping tree seeding") + return + + token = login_resp.json()["access_token"] + # Check if trees already exist + trees_resp = await client.get("/trees", headers={"Authorization": f"Bearer {token}"}) + if trees_resp.status_code == 200 and len(trees_resp.json()) > 0: + logger.info(f"[seed] {len(trees_resp.json())} trees already exist — skipping tree seeding") + return + + # Trees don't exist yet — run the full seed script + logger.info("[seed] No trees found — running seed_trees...") + import scripts.seed_trees as seed_trees_mod + seed_trees_mod.API_BASE_URL = api_url + seed_trees_mod.ADMIN_EMAIL = email + seed_trees_mod.ADMIN_PASSWORD = password + await seed_trees_mod.seed_database() + logger.info("[seed] Tree seeding complete!") + except Exception as e: + logger.warning(f"[seed] Tree seeding failed (non-fatal): {e}") + + @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan handler.""" @@ -33,9 +71,17 @@ async def lifespan(app: FastAPI): async with async_session_maker() as db: await load_all_schedules(db) + # Auto-seed trees in background on PR environments + seed_task = None + if settings.SEED_ON_DEPLOY: + logger.info("[seed] SEED_ON_DEPLOY=true — scheduling background tree seeding") + seed_task = asyncio.create_task(_seed_trees_background()) + yield # Shutdown + if seed_task and not seed_task.done(): + seed_task.cancel() scheduler.shutdown(wait=False) logger.info("Shutting down ResolutionFlow API server...") diff --git a/backend/railway.toml b/backend/railway.toml index 2c439905..a2b6bd6f 100644 --- a/backend/railway.toml +++ b/backend/railway.toml @@ -7,4 +7,4 @@ healthcheckPath = "/health" healthcheckTimeout = 100 restartPolicyType = "on_failure" restartPolicyMaxRetries = 3 -releaseCommand = "alembic upgrade head" +releaseCommand = "python -m scripts.release" diff --git a/backend/scripts/release.py b/backend/scripts/release.py new file mode 100644 index 00000000..6118da0b --- /dev/null +++ b/backend/scripts/release.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Railway release command — runs migrations + optional seed data. + +Set SEED_ON_DEPLOY=true in Railway env vars to auto-seed test users +on PR environments. Seeding is idempotent (skips existing records). + +Usage (called by railway.toml releaseCommand): + python -m scripts.release +""" + +import asyncio +import subprocess +import sys + +from app.core.config import settings + + +def run_migrations() -> None: + """Run alembic upgrade head.""" + print("\n[release] Running database migrations...") + result = subprocess.run( + ["alembic", "upgrade", "head"], + capture_output=False, + ) + if result.returncode != 0: + print("[release] ERROR: Migrations failed!") + sys.exit(1) + print("[release] Migrations complete.") + + +async def seed_test_data() -> None: + """Seed test users (direct DB, no HTTP needed).""" + print("\n[release] Seeding test users...") + from scripts.seed_test_users import main as seed_users + await seed_users() + print("[release] Test users seeded.") + + +def main() -> None: + run_migrations() + + if settings.SEED_ON_DEPLOY: + print("[release] SEED_ON_DEPLOY=true — seeding test data...") + asyncio.run(seed_test_data()) + else: + print("[release] SEED_ON_DEPLOY not set — skipping seed data.") + + print("\n[release] Release complete!") + + +if __name__ == "__main__": + main()