Files
classroomio/docker/smoke-test.sh
T

101 lines
3.8 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
#
# Pre-publish smoke test: boot a freshly built image and confirm it actually starts
# before we push it to Docker Hub. Run from CI against ephemeral postgres + redis.
#
# bash docker/smoke-test.sh <service> <image-ref>
# service = api | dashboard | jobs
# image-ref = local image tag built with --load (e.g. cio-smoke-api:test)
#
# Exits non-zero (failing the publish) if the container crashes or never becomes ready.
set -euo pipefail
SERVICE="${1:?usage: smoke-test.sh <api|dashboard|jobs> <image-ref>}"
IMAGE="${2:?missing image ref}"
DB_URL="postgresql://postgres:postgres@localhost:5432/classroomio"
REDIS_URL="redis://localhost:6379"
NAME="cio-smoke-${SERVICE}"
cleanup() { docker rm -f "${NAME}" >/dev/null 2>&1 || true; }
trap cleanup EXIT
container_alive() { [ -n "$(docker ps -q -f name="^${NAME}$")" ]; }
dump_and_fail() {
echo "SMOKE FAIL (${SERVICE}): $1"
echo "----- container logs (tail) -----"
docker logs "${NAME}" 2>&1 | tail -60 || true
exit 1
}
case "${SERVICE}" in
api)
docker run -d --name "${NAME}" --network host \
-e NODE_ENV=production -e PORT=3081 \
-e DATABASE_URL="${DB_URL}" -e PRIVATE_DATABASE_URL="${DB_URL}" -e REDIS_URL="${REDIS_URL}" \
-e BETTER_AUTH_SECRET=smoke-test-secret-not-for-production \
-e PRIVATE_SERVER_KEY=smoke \
"${IMAGE}"
# entrypoint runs db:setup against the ephemeral DB (also exercises migrations), then serves.
for _ in $(seq 1 40); do
container_alive || dump_and_fail "container exited during startup"
if curl -fsS --max-time 5 http://localhost:3081/ >/dev/null 2>&1; then
echo "SMOKE OK (api): healthcheck passed"; exit 0
fi
sleep 3
done
dump_and_fail "api did not become healthy on :3081 within timeout"
;;
jobs)
# Workers query Postgres on boot (analytics rollups, media jobs, etc.).
# Apply schema first — this job has its own empty postgres, unlike api which
# runs db:setup in its entrypoint.
docker run --rm --network host \
-e DATABASE_URL="${DB_URL}" -e PRIVATE_DATABASE_URL="${DB_URL}" \
--entrypoint pnpm \
"${IMAGE}" \
--filter @cio/db db:migrate
docker run -d --name "${NAME}" --network host \
-e NODE_ENV=production -e DATABASE_URL="${DB_URL}" -e REDIS_URL="${REDIS_URL}" \
"${IMAGE}"
for _ in $(seq 1 30); do
container_alive || dump_and_fail "worker container exited during startup"
if docker logs "${NAME}" 2>&1 | grep -q "all-workers-running"; then
echo "SMOKE OK (jobs): workers booted"; exit 0
fi
sleep 3
done
dump_and_fail "jobs did not report 'all-workers-running' within timeout"
;;
dashboard)
docker run -d --name "${NAME}" --network host \
-e NODE_ENV=production -e PORT=3082 -e ORIGIN=http://localhost:3082 \
-e PUBLIC_IS_SELFHOSTED=true \
-e PRIVATE_SERVER_URL=http://localhost:3081 -e PRIVATE_SERVER_KEY=smoke \
"${IMAGE}"
# Boot check: the SvelteKit server should listen even if the API is absent.
for _ in $(seq 1 30); do
container_alive || dump_and_fail "dashboard container exited during startup"
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 http://localhost:3082/ 2>/dev/null || true)"
# Accept any listening response (2xx/3xx, or the expected 404 when the API is absent),
# but FAIL on 5xx — a server-error on every route means a broken build, not a healthy boot.
if [ -n "${code}" ] && [ "${code}" != "000" ] && [[ "${code}" != 5* ]]; then
echo "SMOKE OK (dashboard): server listening (HTTP ${code})"; exit 0
fi
if [[ "${code}" == 5* ]]; then
dump_and_fail "dashboard returned server error HTTP ${code}"
fi
sleep 3
done
dump_and_fail "dashboard did not listen on :3082 within timeout"
;;
*)
echo "unknown service: ${SERVICE}" >&2; exit 1
;;
esac