v0.34.0: containerize for per-PR preview environments (flotilla SPEC §15)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preview container entrypoint (flotilla SPEC §15).
|
||||
#
|
||||
# 1. Render nginx against Cloud Run's $PORT.
|
||||
# 2. Start the single-process uvicorn (which runs DB migrations on startup via
|
||||
# the app lifespan — §4.2: one process, one SQLite file, never scale workers).
|
||||
# 3. On a brand-new DB, apply the SYNTHETIC seed fixture so the preview shows
|
||||
# realistic content with no real user PII (§15).
|
||||
# 4. Hand the foreground to nginx.
|
||||
#
|
||||
# This is preview-only plumbing — production runs uvicorn under systemd and nginx
|
||||
# under the OS, never this script.
|
||||
set -euo pipefail
|
||||
|
||||
: "${PORT:=8080}"
|
||||
: "${DATABASE_PATH:=/opt/rfc-app/backend/data/rfc-app.db}"
|
||||
export PORT DATABASE_PATH
|
||||
|
||||
mkdir -p "$(dirname "$DATABASE_PATH")"
|
||||
|
||||
# Render the nginx config with the injected port.
|
||||
envsubst '${PORT}' \
|
||||
< /opt/rfc-app/deploy/preview/nginx.conf.template \
|
||||
> /etc/nginx/nginx.conf
|
||||
|
||||
fresh=0
|
||||
[ -f "$DATABASE_PATH" ] || fresh=1
|
||||
|
||||
# Start uvicorn (backgrounded); the app's lifespan runs migrations on boot.
|
||||
cd /opt/rfc-app/backend
|
||||
uvicorn app.main:app --host 127.0.0.1 --port 8000 &
|
||||
UVICORN_PID=$!
|
||||
|
||||
# Seed synthetic data once the schema exists (only on a fresh DB).
|
||||
if [ "$fresh" = "1" ]; then
|
||||
for _ in $(seq 1 30); do
|
||||
if [ -f "$DATABASE_PATH" ] \
|
||||
&& sqlite3 "$DATABASE_PATH" "SELECT 1 FROM schema_migrations LIMIT 1;" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if sqlite3 "$DATABASE_PATH" < /opt/rfc-app/deploy/preview/seed.sql; then
|
||||
echo "[preview-entrypoint] applied synthetic seed" >&2
|
||||
else
|
||||
# Best-effort: a seed that drifts from the schema must not block the
|
||||
# preview from booting (it still serves /api/health + an empty app).
|
||||
echo "[preview-entrypoint] WARNING: synthetic seed failed (schema drift?) — continuing" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# nginx in the foreground becomes the container's main process. (uvicorn is a
|
||||
# reparented child; for an ephemeral preview the hard stop on instance teardown
|
||||
# is fine — a process supervisor is a follow-up if graceful drain matters.)
|
||||
exec nginx -g 'daemon off;'
|
||||
@@ -0,0 +1,64 @@
|
||||
# nginx config TEMPLATE for the preview container (flotilla SPEC §15).
|
||||
# `${PORT}` is substituted by deploy/preview/entrypoint.sh (envsubst) with the
|
||||
# port Cloud Run injects. Mirrors the prod vhost
|
||||
# (deploy/nginx/ohm.wiggleverse.org.conf) minus TLS (Cloud Run terminates TLS)
|
||||
# and minus the prod security headers tuned for the public host.
|
||||
|
||||
worker_processes 1;
|
||||
pid /run/nginx.pid;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /dev/stdout;
|
||||
sendfile on;
|
||||
server_tokens off;
|
||||
|
||||
server {
|
||||
listen ${PORT};
|
||||
listen [::]:${PORT};
|
||||
server_name _;
|
||||
|
||||
root /opt/rfc-app/frontend/dist;
|
||||
index index.html;
|
||||
|
||||
# API + auth proxy to the single-process uvicorn. SSE chat streams need
|
||||
# buffering off so chunks reach the browser immediately.
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 1h;
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# SPA fallback so React Router can take over.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|woff2?|ttf|otf|eot|png|jpg|jpeg|gif|svg|ico)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
client_max_body_size 4M;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Preview environment shape (flotilla SPEC §15) — TEST values only.
|
||||
#
|
||||
# These are the env vars a per-PR preview boots with. The operator loads them
|
||||
# into flotilla's PREVIEW overlay layer (NOT the base overlay, NOT secrets):
|
||||
#
|
||||
# while IFS='=' read -r k v; do
|
||||
# [ -n "$k" ] && case "$k" in \#*) ;; *) \
|
||||
# ohm-rfc-app-flotilla overlay set ohm-rfc-app "$k=$v" --preview ;; esac
|
||||
# done < deploy/preview/preview.env.example
|
||||
#
|
||||
# CRITICAL (§15 / §3 invariant 1): a preview resolves ZERO real secret bytes.
|
||||
# Every value here is synthetic or a documented public test value. None is a
|
||||
# real credential. Do NOT bind real `secret_refs` for previews.
|
||||
|
||||
# --- Turnstile: Cloudflare's documented ALWAYS-PASS test keys (public) -------
|
||||
# Site key is also baked into the image at build time (Dockerfile ARG); the
|
||||
# secret key here makes server-side verification always succeed.
|
||||
CLOUDFLARE_TURNSTILE_SECRET=1x0000000000000000000000000000000AA
|
||||
VITE_TURNSTILE_SITE_KEY=1x00000000000000000000AA
|
||||
|
||||
# --- Email: a catch-all SMTP sink (run Mailpit/Inbucket as a sidecar/service)-
|
||||
# No mail leaves the preview; everything lands in the sink's web UI.
|
||||
SMTP_HOST=mailpit
|
||||
SMTP_PORT=1025
|
||||
SMTP_STARTTLS=0
|
||||
SMTP_PASSWORD=preview-sink-no-auth
|
||||
EMAIL_FROM=preview@preview.invalid
|
||||
EMAIL_FROM_NAME=RFC App (preview)
|
||||
|
||||
# --- Analytics: no-op'd -------------------------------------------------------
|
||||
VITE_AMPLITUDE_API_KEY=
|
||||
|
||||
# --- App identity / required env (synthetic) ---------------------------------
|
||||
# These satisfy backend/app/config.py's required vars with non-secret test
|
||||
# values. SECRET_KEY is a throwaway — sessions in an ephemeral preview need a
|
||||
# key, not a SECRET one. GITEA_* point at whatever read surface the preview
|
||||
# uses; for a content-light framework-PR preview the seeded synthetic DB
|
||||
# carries the visible state and Gitea need not be reachable.
|
||||
SECRET_KEY=preview-throwaway-not-a-secret-0000000000
|
||||
GITEA_URL=https://git.wiggleverse.org
|
||||
GITEA_BOT_USER=preview-bot
|
||||
GITEA_BOT_TOKEN=preview-not-a-real-token
|
||||
GITEA_ORG=preview
|
||||
GITEA_WEBHOOK_SECRET=preview-webhook-not-a-secret
|
||||
OAUTH_CLIENT_ID=preview-oauth-client
|
||||
OAUTH_CLIENT_SECRET=preview-oauth-not-a-secret
|
||||
# APP_URL: set to the Cloud Run service URL once `preview up` reports it, if the
|
||||
# app's OAuth redirect / absolute-URL building needs it for your review flow.
|
||||
APP_URL=http://localhost:8080
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Synthetic seed for per-PR preview environments (flotilla SPEC §15).
|
||||
--
|
||||
-- SYNTHETIC DATA ONLY. This file is version-controlled and reviewable, carries
|
||||
-- NO real user PII, and is applied by deploy/preview/entrypoint.sh onto a
|
||||
-- brand-new preview DB AFTER the app's own migrations have created the schema.
|
||||
-- It must never be applied to a production database.
|
||||
--
|
||||
-- Applied best-effort: if a future migration changes a table shape this seed
|
||||
-- references, the entrypoint logs a warning and the preview still boots (it
|
||||
-- just shows less content). Keep the inserts conservative and schema-stable;
|
||||
-- grow richer fixtures (RFCs, branches, threads, PRs) here as the preview's
|
||||
-- review value warrants — they are reproducible because they live in git.
|
||||
|
||||
-- A small synthetic user set covering each role (§6.1 owner/admin/contributor).
|
||||
-- Negative gitea_ids keep these clear of any real Gitea account id space.
|
||||
INSERT OR IGNORE INTO users (gitea_id, gitea_login, email, display_name, role) VALUES
|
||||
(-1, 'preview-owner', 'owner@preview.invalid', 'Preview Owner', 'owner'),
|
||||
(-2, 'preview-admin', 'admin@preview.invalid', 'Preview Admin', 'admin'),
|
||||
(-3, 'preview-alice', 'alice@preview.invalid', 'Alice Preview', 'contributor'),
|
||||
(-4, 'preview-bob', 'bob@preview.invalid', 'Bob Preview', 'contributor');
|
||||
Reference in New Issue
Block a user