- Multi-stage Dockerfile (builder+runner, <500MB target) - docker-compose.yml: app(x4) + postgres + redis + prometheus + grafana + nginx - .env.example with all required secrets (never hardcoded) - requirements.txt with all dependencies including prometheus-client, alembic - GitHub Actions CI: lint (flake8+bandit+safety) + tests + Docker build/push - GitHub Actions CD: staging deploy -> smoke tests -> production deploy + rollback - Alembic migration setup + initial PostgreSQL schema (001_initial_schema) - SQLite→PostgreSQL data migration script - Prometheus metrics module (HTTP, ML, DB, business metrics) - Prometheus alert rules (5xx >1%, latency >2s, disk >80%, ML accuracy) - Grafana dashboard (overview: req/s, p95, ML accuracy, error rate) - Nginx reverse proxy config (HTTPS/TLS, rate limiting, security headers) - Structured JSON logging module - Automated daily DB backup script (pg_dump + 30-day retention) Branch: feature/devops-cicd Co-Authored-By: Paperclip <noreply@paperclip.ing>
69 lines
1.9 KiB
Python
69 lines
1.9 KiB
Python
"""Alembic env.py — Turf SaaS database migrations."""
|
|
|
|
import os
|
|
from logging.config import fileConfig
|
|
|
|
from sqlalchemy import engine_from_config, pool
|
|
from alembic import context
|
|
|
|
# Alembic Config object — gives access to .ini values
|
|
config = context.config
|
|
|
|
# Set logging from config
|
|
if config.config_file_name is not None:
|
|
fileConfig(config.config_file_name)
|
|
|
|
|
|
# Override sqlalchemy.url from environment variables
|
|
def get_db_url():
|
|
user = os.environ.get("POSTGRES_USER", "turf")
|
|
password = os.environ.get("POSTGRES_PASSWORD", "")
|
|
host = os.environ.get("POSTGRES_HOST", "localhost")
|
|
port = os.environ.get("POSTGRES_PORT", "5432")
|
|
db = os.environ.get("POSTGRES_DB", "turf_saas")
|
|
url = os.environ.get(
|
|
"DATABASE_URL", f"postgresql://{user}:{password}@{host}:{port}/{db}"
|
|
)
|
|
return url
|
|
|
|
|
|
config.set_main_option("sqlalchemy.url", get_db_url())
|
|
|
|
# No declarative model — we use raw DDL migrations
|
|
target_metadata = None
|
|
|
|
|
|
def run_migrations_offline() -> None:
|
|
"""Run migrations in 'offline' mode (no live DB connection needed)."""
|
|
url = config.get_main_option("sqlalchemy.url")
|
|
context.configure(
|
|
url=url,
|
|
target_metadata=target_metadata,
|
|
literal_binds=True,
|
|
dialect_opts={"paramstyle": "named"},
|
|
)
|
|
with context.begin_transaction():
|
|
context.run_migrations()
|
|
|
|
|
|
def run_migrations_online() -> None:
|
|
"""Run migrations in 'online' mode (uses live DB connection)."""
|
|
connectable = engine_from_config(
|
|
config.get_section(config.config_ini_section, {}),
|
|
prefix="sqlalchemy.",
|
|
poolclass=pool.NullPool,
|
|
)
|
|
with connectable.connect() as connection:
|
|
context.configure(
|
|
connection=connection,
|
|
target_metadata=target_metadata,
|
|
)
|
|
with context.begin_transaction():
|
|
context.run_migrations()
|
|
|
|
|
|
if context.is_offline_mode():
|
|
run_migrations_offline()
|
|
else:
|
|
run_migrations_online()
|