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:
@@ -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
|
||||||
|
|||||||
@@ -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...")
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
53
backend/scripts/release.py
Normal file
53
backend/scripts/release.py
Normal 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()
|
||||||
Reference in New Issue
Block a user