90 lines
4.6 KiB
Bash
Executable File
90 lines
4.6 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
|
cd "$repo"
|
|
project="rf4-bootstrap-$$"
|
|
export COMPOSE_PROJECT_NAME="$project"
|
|
export BOOTSTRAP_API_PORT=${BOOTSTRAP_API_PORT:-18000}
|
|
export BOOTSTRAP_WEB_PORT=${BOOTSTRAP_WEB_PORT:-14321}
|
|
export POSTGRES_PASSWORD=bootstrap-postgres-password
|
|
export DATABASE_URL=postgresql+psycopg://rf4:bootstrap-postgres-password@db:5432/rf4_spotter
|
|
compose="docker compose --env-file .env.production.example -f compose.production.yaml -f deploy/compose.bootstrap.yaml"
|
|
|
|
cleanup() {
|
|
status=$?
|
|
if [ "$status" -ne 0 ]; then
|
|
$compose ps >&2 || true
|
|
$compose logs --no-color api web db minio >&2 || true
|
|
fi
|
|
$compose down --volumes --remove-orphans >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
$compose up --build -d --wait db minio api web
|
|
curl -fsS "http://127.0.0.1:$BOOTSTRAP_API_PORT/ready" >/dev/null
|
|
curl -fsS "http://127.0.0.1:$BOOTSTRAP_WEB_PORT/" >/dev/null
|
|
curl -fsS -D - -o /dev/null "http://127.0.0.1:$BOOTSTRAP_API_PORT/health" | grep -qi '^x-frame-options: DENY'
|
|
curl -fsS -D - -o /dev/null "http://127.0.0.1:$BOOTSTRAP_API_PORT/health" | grep -qi '^cross-origin-opener-policy: same-origin'
|
|
|
|
# A10: Validate Caddy configuration without running the proxy
|
|
echo "Validating Caddy configuration..."
|
|
docker compose --env-file .env.production.example -f compose.production.yaml -f deploy/compose.bootstrap.yaml run --rm --no-deps --entrypoint "caddy adapt --config /etc/caddy/Caddyfile --pretty" proxy >/dev/null 2>&1 || {
|
|
echo "ERROR: Caddy configuration is invalid" >&2
|
|
exit 1
|
|
}
|
|
echo "Caddy configuration valid ✓"
|
|
|
|
# A10: Check scheduler can start without external network
|
|
echo "Validating community scheduler..."
|
|
$compose up --build -d --wait community-scheduler
|
|
# Scheduler runs in a loop, check it's healthy by verifying the process is running
|
|
$compose exec -T community-scheduler python -c "from app.community_scheduler import schedule_interval; print('scheduler module loads OK')" >/dev/null 2>&1 || {
|
|
echo "ERROR: Community scheduler failed to start" >&2
|
|
$compose logs --no-color community-scheduler >&2
|
|
exit 1
|
|
}
|
|
echo "Community scheduler valid ✓"
|
|
# Stop scheduler to free resources
|
|
$compose stop community-scheduler
|
|
|
|
# A10: Extract Alembic revision ID programmatically, handle multiple heads
|
|
ALEMBIC_HEADS_OUTPUT=$($compose exec -T api alembic heads 2>/dev/null || true)
|
|
# Extract revision IDs (first field before space or '(head)'), handle multiple heads
|
|
ALEMBIC_HEAD=$(echo "$ALEMBIC_HEADS_OUTPUT" | grep -oE '^[a-f0-9]+' | head -1)
|
|
DB_VERSION=$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select version_num from alembic_version')
|
|
|
|
if [ -z "$ALEMBIC_HEAD" ]; then
|
|
echo "ERROR: alembic heads returned empty or invalid output: $ALEMBIC_HEADS_OUTPUT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$DB_VERSION" ]; then
|
|
echo "ERROR: alembic_version table is empty" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Handle multiple heads: check if DB version matches any head
|
|
HEAD_COUNT=$(echo "$ALEMBIC_HEADS_OUTPUT" | grep -cE '^[a-f0-9]+' || true)
|
|
if [ "$HEAD_COUNT" -gt 1 ]; then
|
|
echo "WARNING: Multiple Alembic heads detected ($HEAD_COUNT), checking if DB version matches any..."
|
|
if ! echo "$ALEMBIC_HEADS_OUTPUT" | grep -q "^$DB_VERSION"; then
|
|
echo "ERROR: DB version $DB_VERSION does not match any head. Heads: $ALEMBIC_HEADS_OUTPUT" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
# Single head: exact match required
|
|
test "$DB_VERSION" = "$ALEMBIC_HEAD"
|
|
fi
|
|
|
|
echo "Alembic head: $ALEMBIC_HEAD, DB version: $DB_VERSION ✓"
|
|
|
|
index_count=$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c "select count(*) from pg_indexes where schemaname = 'public' and indexname in ('ix_catch_report_activity_lookup','ix_catch_report_spot_feed','ix_catch_report_moderation_queue','ix_catch_report_official_records','ix_official_import_source_status_started','ix_external_observation_review_queue','ix_submission_attempt_client_created','ix_moderation_event_created_at')")
|
|
test "$index_count" = "8"
|
|
test "$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select count(*) from fish')" = "2"
|
|
test "$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select count(*) from waterbody')" = "2"
|
|
test "$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select count(*) from catch_report')" = "0"
|
|
WEB_URL="http://127.0.0.1:$BOOTSTRAP_WEB_PORT" BOOTSTRAP_API_URL="http://127.0.0.1:$BOOTSTRAP_API_PORT" BOOTSTRAP_ADMIN_TOKEN=replace-with-at-least-32-random-characters npm --prefix apps/web run test:bootstrap
|
|
curl -fsS -D - -o /dev/null -H 'Authorization: Bearer replace-with-at-least-32-random-characters' "http://127.0.0.1:$BOOTSTRAP_API_PORT/api/v1/admin/catch-reports" | grep -qi '^cache-control: no-store'
|
|
echo "Production bootstrap passed from empty volumes"
|