124 lines
3.5 KiB
Bash
Executable File
124 lines
3.5 KiB
Bash
Executable File
#!/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
|