Publish Docker Images / Smoke test (api) (push) Canceled after 0s
Publish Docker Images / Smoke test (dashboard) (push) Canceled after 0s
Publish Docker Images / Smoke test (jobs) (push) Canceled after 0s
Publish Docker Images / build-and-push (./docker/Dockerfile.api, api, api) (push) Canceled after 0s
Publish Docker Images / build-and-push (./docker/Dockerfile.dashboard, dashboard, dashboard) (push) Canceled after 0s
Publish Docker Images / build-and-push (./docker/Dockerfile.jobs, jobs, jobs) (push) Canceled after 0s
Jobs workers query analytics and media tables on boot. The smoke job uses an empty database, so the worker never logs all-workers-running and Publish Docker Images never pushes dashboard/api/jobs. Apply drizzle migrations first, matching what the API entrypoint already does.
101 lines
3.8 KiB
Bash
101 lines
3.8 KiB
Bash
#!/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
|