#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" APP_PORT="${APP_PORT:-8000}" BASE_URL="${BASE_URL:-http://127.0.0.1:${APP_PORT}}" USE_SPA_UI="${USE_SPA_UI:-false}" ALLOW_SPA_CUTOVER="${ALLOW_SPA_CUTOVER:-0}" KEEP_STACK_RUNNING="${KEEP_STACK_RUNNING:-1}" BOOTSTRAP_MVP_ARGS="${BOOTSTRAP_MVP_ARGS:-}" ARTIFACT_DIR="${ARTIFACT_DIR:-${ROOT_DIR}/artifacts/local}" ARTIFACT_FILE="${ARTIFACT_FILE:-${ARTIFACT_DIR}/smoke-$(date -u +%Y%m%dT%H%M%SZ).json}" HEALTH_RETRIES="${HEALTH_RETRIES:-60}" HEALTH_SLEEP_SECONDS="${HEALTH_SLEEP_SECONDS:-2}" require_command() { local command_name="$1" local hint="$2" if ! command -v "${command_name}" >/dev/null 2>&1; then echo "[local-smoke] missing command: ${command_name}" >&2 echo "[local-smoke] ${hint}" >&2 exit 1 fi } if [[ "${USE_SPA_UI}" == "true" ]] && [[ "${ALLOW_SPA_CUTOVER}" != "1" ]]; then echo "[local-smoke] USE_SPA_UI=true is outside the canonical MVP smoke path" >&2 echo "[local-smoke] set ALLOW_SPA_CUTOVER=1 only for separate SPA cutover verification" >&2 exit 1 fi case "${ARTIFACT_FILE}" in "${ROOT_DIR}"/*) ;; *) echo "[local-smoke] ARTIFACT_FILE must live inside ${ROOT_DIR}" >&2 exit 1 ;; esac require_command "docker" "install Docker Desktop or Docker Engine before running the local smoke flow" require_command "curl" "install curl so the script can poll the local /healthz endpoint" mkdir -p "$(dirname "${ARTIFACT_FILE}")" COMPOSE_CMD=(docker compose) if [[ "${USE_SPA_UI}" == "true" ]]; then COMPOSE_CMD+=(--profile spa) fi print_failure_context() { echo "[local-smoke] docker compose state after failure" >&2 ( cd "${ROOT_DIR}" "${COMPOSE_CMD[@]}" ps >&2 || true "${COMPOSE_CMD[@]}" logs --tail=80 app db redis >&2 || true ) } cleanup() { local exit_code="$1" if [[ "${exit_code}" -ne 0 ]]; then print_failure_context fi if [[ "${KEEP_STACK_RUNNING}" != "1" ]]; then echo "[local-smoke] shutting down docker compose stack" ( cd "${ROOT_DIR}" "${COMPOSE_CMD[@]}" down --remove-orphans ) fi } trap 'cleanup "$?"' EXIT wait_for_healthz() { local attempt for ((attempt = 1; attempt <= HEALTH_RETRIES; attempt += 1)); do if curl -fsS "${BASE_URL}/healthz" >/dev/null; then echo "[local-smoke] healthz OK" return 0 fi echo "[local-smoke] waiting for healthz (${attempt}/${HEALTH_RETRIES})" sleep "${HEALTH_SLEEP_SECONDS}" done echo "[local-smoke] healthz did not become ready: ${BASE_URL}/healthz" >&2 return 1 } run_compose_exec() { ( cd "${ROOT_DIR}" "${COMPOSE_CMD[@]}" exec -T app "$@" ) } echo "[local-smoke] starting docker compose stack" ( cd "${ROOT_DIR}" export USE_SPA_UI APP_PORT "${COMPOSE_CMD[@]}" up -d --build ) wait_for_healthz echo "[local-smoke] bootstrap MVP host + demo questions" if [[ -n "${BOOTSTRAP_MVP_ARGS}" ]]; then # shellcheck disable=SC2206 bootstrap_args=(${BOOTSTRAP_MVP_ARGS}) run_compose_exec python manage.py bootstrap_mvp "${bootstrap_args[@]}" else run_compose_exec python manage.py bootstrap_mvp fi container_artifact="/app/${ARTIFACT_FILE#${ROOT_DIR}/}" echo "[local-smoke] gameplay smoke -> ${ARTIFACT_FILE}" run_compose_exec python manage.py smoke_staging --artifact "${container_artifact}" echo "[local-smoke] artifact: ${ARTIFACT_FILE}" if [[ "${KEEP_STACK_RUNNING}" == "1" ]]; then echo "[local-smoke] stack left running for manual UI follow-up at ${BASE_URL}" else echo "[local-smoke] stack will be stopped on exit" fi