#!/usr/bin/env bash set -euo pipefail IMAGE="${1:-toolctrl-worker:smoke}" CONTAINER_NAME="${DOCKER_WORKER_SMOKE_CONTAINER:-toolctrl-worker-runtime-smoke-$$}" DB_CONTAINER_NAME="${DOCKER_WORKER_SMOKE_DB_CONTAINER:-toolctrl-worker-runtime-smoke-db-$$}" REDIS_CONTAINER_NAME="${DOCKER_WORKER_SMOKE_REDIS_CONTAINER:-toolctrl-worker-runtime-smoke-redis-$$}" NETWORK_NAME="${DOCKER_WORKER_SMOKE_NETWORK:-toolctrl-worker-runtime-smoke-$$}" SMOKE_REVISION="${DOCKER_WORKER_SMOKE_REVISION:-}" DB_PASSWORD="toolctrl" DB_NAME="toolctrl" DB_USER="toolctrl" cleanup() { docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true docker rm -f "$DB_CONTAINER_NAME" >/dev/null 2>&1 || true docker rm -f "$REDIS_CONTAINER_NAME" >/dev/null 2>&1 || true docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true } resolve_smoke_revision() { if [[ -n "$SMOKE_REVISION" ]]; then printf "%s" "$SMOKE_REVISION" return fi docker image inspect -f '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$IMAGE" } trap cleanup EXIT cleanup SMOKE_REVISION="$(resolve_smoke_revision)" if [[ -z "$SMOKE_REVISION" || "$SMOKE_REVISION" == "" || "$SMOKE_REVISION" == "unknown" ]]; then echo "Image ${IMAGE} does not expose a concrete org.opencontainers.image.revision; build with --build-arg APP_REVISION or set DOCKER_WORKER_SMOKE_REVISION explicitly" >&2 exit 1 fi worker_env=( -e NODE_ENV=production -e APP_NAME=ToolCTRL -e APP_BASE_DOMAIN=localhost ) expect_worker_health() { local output="$1" local expected_status="$2" local expected_enabled="$3" local expected_group="${4:-}" local expected_queue="${5:-}" node -e ' const [raw, expectedStatus, expectedEnabled, expectedRevision, expectedGroup, expectedQueue] = process.argv.slice(1); const report = JSON.parse(raw); if (report.status !== expectedStatus) { console.error(`Expected worker status ${expectedStatus}, got ${report.status}`); process.exit(1); } if (String(report.workerEnabled) !== expectedEnabled) { console.error(`Expected workerEnabled ${expectedEnabled}, got ${report.workerEnabled}`); process.exit(1); } if (report.revision !== expectedRevision) { console.error(`Expected revision ${expectedRevision}, got ${report.revision || ""}`); process.exit(1); } if (expectedGroup && report.workerGroup !== expectedGroup) { console.error(`Expected workerGroup ${expectedGroup}, got ${report.workerGroup || ""}`); process.exit(1); } if (expectedQueue && !Array.isArray(report.queues)) { console.error("Expected worker health queues to be an array"); process.exit(1); } if (expectedQueue && !report.queues.includes(expectedQueue)) { console.error(`Expected worker queues to include ${expectedQueue}, got ${report.queues?.join(",") || ""}`); process.exit(1); } if (expectedStatus === "ready" && (report.databaseStatus !== "ready" || report.redisStatus !== "ready")) { console.error(`Expected ready DB/Redis, got database=${report.databaseStatus} redis=${report.redisStatus}`); process.exit(1); } ' "$output" "$expected_status" "$expected_enabled" "$SMOKE_REVISION" "$expected_group" "$expected_queue" } docker network create "$NETWORK_NAME" >/dev/null health_output="$( docker run --rm \ "${worker_env[@]}" \ -e WORKER_ENABLED=false \ "$IMAGE" node apps/worker/dist/health.js )" expect_worker_health "$health_output" disabled false docker run -d --name "$DB_CONTAINER_NAME" --network "$NETWORK_NAME" --network-alias db \ -e POSTGRES_USER="$DB_USER" \ -e POSTGRES_PASSWORD="$DB_PASSWORD" \ -e POSTGRES_DB="$DB_NAME" \ postgres:16-alpine >/dev/null docker run -d --name "$REDIS_CONTAINER_NAME" --network "$NETWORK_NAME" --network-alias redis redis:7-alpine >/dev/null CONTAINER_DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}?schema=public" CONTAINER_REDIS_URL="redis://redis:6379/0" for attempt in $(seq 1 30); do if docker exec "$DB_CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; then break fi if [[ "$attempt" == "30" ]]; then echo "Postgres smoke database did not become ready" >&2 docker logs "$DB_CONTAINER_NAME" >&2 || true exit 1 fi sleep 1 done for attempt in $(seq 1 30); do if docker exec "$REDIS_CONTAINER_NAME" redis-cli ping >/dev/null 2>&1; then break fi if [[ "$attempt" == "30" ]]; then echo "Redis smoke broker did not become ready" >&2 docker logs "$REDIS_CONTAINER_NAME" >&2 || true exit 1 fi sleep 1 done docker run --rm --network "$NETWORK_NAME" \ -e NODE_ENV=production \ -e DATABASE_URL="$CONTAINER_DATABASE_URL" \ --entrypoint /bin/sh \ "$IMAGE" -lc 'cd /myapp && pnpm exec prisma migrate deploy --config prisma.config.ts' >/dev/null enabled_health_output="$( docker run --rm --network "$NETWORK_NAME" \ "${worker_env[@]}" \ -e WORKER_ENABLED=true \ -e WORKER_QUEUE_GROUP=general \ -e DATABASE_URL="$CONTAINER_DATABASE_URL" \ -e BULLMQ_REDIS_URL="$CONTAINER_REDIS_URL" \ "$IMAGE" node apps/worker/dist/health.js )" expect_worker_health "$enabled_health_output" ready true general toolctrl.notifications mail_health_output="$( docker run --rm --network "$NETWORK_NAME" \ "${worker_env[@]}" \ -e WORKER_ENABLED=true \ -e WORKER_QUEUE_GROUP=mail \ -e DATABASE_URL="$CONTAINER_DATABASE_URL" \ -e BULLMQ_REDIS_URL="$CONTAINER_REDIS_URL" \ "$IMAGE" node apps/worker/dist/health.js )" expect_worker_health "$mail_health_output" ready true mail toolctrl.email docker run -d --name "$CONTAINER_NAME" --network "$NETWORK_NAME" \ "${worker_env[@]}" \ -e WORKER_ENABLED=true \ -e WORKER_QUEUE_GROUP=general \ -e DATABASE_URL="$CONTAINER_DATABASE_URL" \ -e BULLMQ_REDIS_URL="$CONTAINER_REDIS_URL" \ "$IMAGE" >/tmp/toolctrl-worker-runtime-smoke.log for attempt in $(seq 1 10); do if [[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || true)" == "true" ]]; then break fi if [[ "$attempt" == "10" ]]; then echo "Worker container did not stay running" >&2 docker logs "$CONTAINER_NAME" >&2 || true exit 1 fi sleep 1 done running_health_output="$( docker exec "$CONTAINER_NAME" node apps/worker/dist/health.js )" expect_worker_health "$running_health_output" ready true general toolctrl.notifications echo "Docker worker runtime smoke passed for ${IMAGE}"