feat: auto-seed PR environments with SEED_ON_DEPLOY flag

Release command now runs migrations + seeds test users when
SEED_ON_DEPLOY=true. Tree seeding runs as a background task
on startup via HTTP API. Everything is idempotent and non-fatal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-19 07:59:59 -05:00
parent 7f520772ec
commit 30ab5a5907
4 changed files with 103 additions and 1 deletions

View File

@@ -72,6 +72,9 @@ class Settings(BaseSettings):
"""Check if Stripe is configured.""" """Check if Stripe is configured."""
return self.STRIPE_SECRET_KEY is not None and self.STRIPE_WEBHOOK_SECRET is not None 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 - 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"] CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173", "http://localhost:5174"]
FRONTEND_URL: Optional[str] = None FRONTEND_URL: Optional[str] = None

View File

@@ -1,4 +1,6 @@
import asyncio
import logging import logging
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -18,6 +20,42 @@ setup_logging()
logger = logging.getLogger(__name__) 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 @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Application lifespan handler.""" """Application lifespan handler."""
@@ -33,9 +71,17 @@ async def lifespan(app: FastAPI):
async with async_session_maker() as db: async with async_session_maker() as db:
await load_all_schedules(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 yield
# Shutdown # Shutdown
if seed_task and not seed_task.done():
seed_task.cancel()
scheduler.shutdown(wait=False) scheduler.shutdown(wait=False)
logger.info("Shutting down ResolutionFlow API server...") logger.info("Shutting down ResolutionFlow API server...")

View File

@@ -7,4 +7,4 @@ healthcheckPath = "/health"
healthcheckTimeout = 100 healthcheckTimeout = 100
restartPolicyType = "on_failure" restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3 restartPolicyMaxRetries = 3
releaseCommand = "alembic upgrade head" releaseCommand = "python -m scripts.release"

View File

@@ -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()