#!/usr/bin/env bash # Preflight checks before sofiia-console deploy. # # Exit codes: # 0 = OK # 1 = FAIL (or WARN in STRICT=1) # 2 = WARN-only (STRICT=0) set -euo pipefail SOFIIA_URL="${SOFIIA_URL:-http://127.0.0.1:8002}" STRICT="${STRICT:-0}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' PASS=0 WARN=0 FAIL=0 _pass() { echo -e "${GREEN}PASS${NC}: $1"; PASS=$((PASS+1)); } _warn() { echo -e "${YELLOW}WARN${NC}: $1"; WARN=$((WARN+1)); } _fail() { echo -e "${RED}FAIL${NC}: $1"; FAIL=$((FAIL+1)); } _section() { echo -e "\n-- $1 --"; } for cmd in bash curl rg python3; do command -v "$cmd" >/dev/null 2>&1 || { echo "Missing prerequisite: $cmd"; exit 1; } done echo "Sofiia Console Preflight" echo " SOFIIA_URL = ${SOFIIA_URL}" echo " STRICT = ${STRICT}" echo " $(date -u '+%Y-%m-%dT%H:%M:%SZ')" _section "1) Secrets exposure scan (tracked files, best-effort)" if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then mapfile -t scan_files < <(git ls-files | rg '(^|/)(docker-compose.*\.ya?ml|\.env(\..*)?$|config/.*\.ya?ml)$' || true) if [ "${#scan_files[@]}" -eq 0 ]; then _warn "No tracked compose/env/config files found for secrets scan" else # Patterns focus on obvious accidental leaks in committed files. secret_pattern='(OPENAI_API_KEY\s*=\s*[^\s$]|DEEPSEEK_API_KEY\s*=\s*[^\s$]|ANTHROPIC_API_KEY\s*=\s*[^\s$]|TELEGRAM_BOT_TOKEN\s*=\s*[^\s$]|SUPERVISOR_API_KEY\s*=\s*[^\s$]|ROUTER_API_KEY\s*=\s*[^\s$]|Authorization:\s*Bearer\s+[A-Za-z0-9._-]+|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9_-]{20,})' if mapfile -t exposed < <(rg --files-with-matches -e "$secret_pattern" -- "${scan_files[@]}" 2>/dev/null); then if [ "${#exposed[@]}" -gt 0 ]; then _fail "Potential secrets detected in tracked files:" for f in "${exposed[@]}"; do echo " - $f" done else _pass "No obvious plaintext secrets found in tracked compose/env/config files" fi else _warn "Secrets scan command returned non-zero (review manually)" fi fi else _warn "Not inside git repo; skipped tracked-file secrets scan" fi _section "2) Required environment variables (presence only)" required_env=("ROUTER_URL" "SOFIIA_DATA_DIR") if [ "${SOFIIA_IDEMPOTENCY_BACKEND:-inmemory}" = "redis" ] || [ "${SOFIIA_RATE_LIMIT_BACKEND:-inmemory}" = "redis" ]; then required_env+=("SOFIIA_REDIS_URL") fi for var_name in "${required_env[@]}"; do if [ -n "${!var_name:-}" ]; then _pass "env ${var_name} is set" else _warn "env ${var_name} is not set" fi done _section "3) Reachability (health + metrics)" if curl -fsS --max-time 5 "${SOFIIA_URL}/api/health" >/dev/null 2>&1; then _pass "GET ${SOFIIA_URL}/api/health reachable" else _warn "GET ${SOFIIA_URL}/api/health unreachable (service may be down pre-deploy)" fi if curl -fsS --max-time 5 "${SOFIIA_URL}/metrics" >/dev/null 2>&1; then _pass "GET ${SOFIIA_URL}/metrics reachable" else _warn "GET ${SOFIIA_URL}/metrics unreachable (service may be down pre-deploy)" fi _section "4) Filesystem / data dir writable" DATA_DIR="${SOFIIA_DATA_DIR:-/app/data}" if mkdir -p "${DATA_DIR}" 2>/dev/null; then probe="${DATA_DIR}/.preflight_write_probe_$$" if ( : > "${probe}" ) 2>/dev/null; then rm -f "${probe}" || true _pass "SOFIIA_DATA_DIR is writable (${DATA_DIR})" else _fail "SOFIIA_DATA_DIR is not writable (${DATA_DIR})" fi else _fail "Cannot create SOFIIA_DATA_DIR (${DATA_DIR})" fi _section "5) Redis connectivity (if redis backend enabled)" if [ "${SOFIIA_IDEMPOTENCY_BACKEND:-inmemory}" = "redis" ] || [ "${SOFIIA_RATE_LIMIT_BACKEND:-inmemory}" = "redis" ]; then REDIS_URL="${SOFIIA_REDIS_URL:-}" if [ -z "${REDIS_URL}" ]; then _fail "Redis backend enabled but SOFIIA_REDIS_URL is empty" else if python3 - <<'PYEOF' >/dev/null 2>&1 import os url = os.getenv("SOFIIA_REDIS_URL", "") if not url: raise SystemExit(2) try: import redis c = redis.Redis.from_url(url, socket_connect_timeout=2, socket_timeout=2, decode_responses=True) if c.ping(): raise SystemExit(0) raise SystemExit(1) except Exception: raise SystemExit(1) PYEOF then _pass "Redis ping OK (redis-python)" elif command -v redis-cli >/dev/null 2>&1 && redis-cli -u "${REDIS_URL}" --no-auth-warning ping >/dev/null 2>&1; then _pass "Redis ping OK (redis-cli)" else _fail "Redis ping failed (backend=redis)" fi fi else _pass "Redis connectivity check skipped (backend != redis)" fi echo "" echo "========================================" echo "Preflight summary: PASS=${PASS} WARN=${WARN} FAIL=${FAIL}" echo "========================================" if [ "${FAIL}" -gt 0 ]; then exit 1 fi if [ "${WARN}" -gt 0 ]; then if [ "${STRICT}" = "1" ]; then exit 1 fi exit 2 fi exit 0