Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/ci/docker-smoke-suite.sh
T

267 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=scripts/lib/docker-runtime-smoke.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" && pwd)/docker-runtime-smoke.sh"
TARGET="${1:-all}"
DOCKER_SMOKE_RUN_ID="${DOCKER_SMOKE_RUN_ID:-local-$$}"
DOCKER_SMOKE_REVISION="${DOCKER_SMOKE_REVISION:-}"
DOCKER_SMOKE_BUILD_TIMEOUT_SECONDS="${DOCKER_SMOKE_BUILD_TIMEOUT_SECONDS:-1200}"
DOCKER_SMOKE_WEB_RUNTIME_TIMEOUT_SECONDS="${DOCKER_SMOKE_WEB_RUNTIME_TIMEOUT_SECONDS:-720}"
DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS="${DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS:-420}"
DOCKER_SMOKE_ARTIFACT_TIMEOUT_SECONDS="${DOCKER_SMOKE_ARTIFACT_TIMEOUT_SECONDS:-60}"
DOCKER_SMOKE_COMPOSE_TIMEOUT_SECONDS="${DOCKER_SMOKE_COMPOSE_TIMEOUT_SECONDS:-60}"
DOCKER_SMOKE_RUNTIME_GRACE_SECONDS="${DOCKER_SMOKE_RUNTIME_GRACE_SECONDS:-30}"
if [[ -z "$DOCKER_SMOKE_RUN_ID" || ${#DOCKER_SMOKE_RUN_ID} -gt 80 || ! "$DOCKER_SMOKE_RUN_ID" =~ ^[A-Za-z0-9_.-]+$ ]]; then
echo "DOCKER_SMOKE_RUN_ID must contain 1-80 Docker-safe characters" >&2
exit 2
fi
if [[ ! "$DOCKER_SMOKE_REVISION" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "DOCKER_SMOKE_REVISION must be a concrete 40-character Git SHA" >&2
exit 2
fi
case "$TARGET" in
all | web | worker | api | mcp | hocuspocus | portal-domain-controller) ;;
*)
echo "Docker smoke target must be one of: all, web, worker, api, mcp, hocuspocus, portal-domain-controller" >&2
exit 2
;;
esac
WEB_IMAGE="hivegrid-web:smoke-${DOCKER_SMOKE_RUN_ID}"
WORKER_IMAGE="hivegrid-worker:smoke-${DOCKER_SMOKE_RUN_ID}"
API_IMAGE="hivegrid-api:smoke-${DOCKER_SMOKE_RUN_ID}"
MCP_IMAGE="hivegrid-mcp:smoke-${DOCKER_SMOKE_RUN_ID}"
HOCUSPOCUS_IMAGE="hivegrid-hocuspocus:smoke-${DOCKER_SMOKE_RUN_ID}"
PORTAL_CONTROLLER_IMAGE="hivegrid-portal-domain-controller:smoke-${DOCKER_SMOKE_RUN_ID}"
WEB_ARTIFACT_CONTAINER_NAME="hivegrid-web-artifact-smoke-${DOCKER_SMOKE_RUN_ID}"
export DOCKER_SMOKE_RUN_ID
export DOCKER_SMOKE_REVISION
export DOCKER_SMOKE_MIGRATOR_IMAGE="$WEB_IMAGE"
export DOCKER_WEB_SMOKE_TRANSPORT="${DOCKER_WEB_SMOKE_TRANSPORT:-exec}"
export DOCKER_API_SMOKE_TRANSPORT="${DOCKER_API_SMOKE_TRANSPORT:-exec}"
export DOCKER_MCP_SMOKE_TRANSPORT="${DOCKER_MCP_SMOKE_TRANSPORT:-exec}"
export DOCKER_PORTAL_DOMAIN_CONTROLLER_SMOKE_REVISION="$DOCKER_SMOKE_REVISION"
cleanup() {
docker_smoke_cleanup_container "suite:cleanup:web-artifact" "$WEB_ARTIFACT_CONTAINER_NAME"
}
docker_smoke_install_cleanup_traps cleanup
cleanup
run_parallel_wave() {
local wave="$1"
local task
local index
local child_status
local status=0
local -a tasks=()
local -a pids=()
shift
if [[ "$#" -eq 0 ]]; then
echo "[docker-smoke] FAIL stage=wave:${wave} reason=no_tasks exit_code=2" >&2
return 2
fi
echo "[docker-smoke] WAVE_START wave=${wave} tasks=$#" >&2
for task in "$@"; do
tasks+=("$task")
"$task" &
pids+=("$!")
done
for index in "${!pids[@]}"; do
if wait "${pids[$index]}"; then
echo "[docker-smoke] WAVE_PASS wave=${wave} task=${tasks[$index]}" >&2
continue
else
child_status=$?
fi
echo "[docker-smoke] WAVE_FAIL wave=${wave} task=${tasks[$index]} exit_code=${child_status}" >&2
if [[ "$status" -eq 0 ]]; then
status="$child_status"
fi
done
if [[ "$status" -ne 0 ]]; then
return "$status"
fi
echo "[docker-smoke] WAVE_PASS wave=${wave} tasks=${#tasks[@]}" >&2
}
build_root_target() {
local target="$1"
local image="$2"
local build_args
build_args=(
docker build
--target "$target"
--build-arg "APP_REVISION=${DOCKER_SMOKE_REVISION}"
)
if [[ "$target" == "web" ]]; then
build_args+=(--build-arg VITE_HOCUSPOCUS_URL=wss://collab.invalid)
fi
build_args+=(--tag "$image" .)
docker_smoke_run "build:${target}" "$DOCKER_SMOKE_BUILD_TIMEOUT_SECONDS" "${build_args[@]}"
}
build_hocuspocus() {
docker_smoke_run "build:hocuspocus" "$DOCKER_SMOKE_BUILD_TIMEOUT_SECONDS" \
docker build \
--file apps/hocuspocus/Dockerfile \
--target hocuspocus \
--build-arg "APP_REVISION=${DOCKER_SMOKE_REVISION}" \
--tag "$HOCUSPOCUS_IMAGE" \
.
}
image_matches_revision() {
local image="$1"
local actual_revision=""
if ! docker_smoke_capture actual_revision "image:inspect" "$DOCKER_SMOKE_PROBE_TIMEOUT_SECONDS" \
docker image inspect -f '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image"; then
return 1
fi
[[ "$actual_revision" == "$DOCKER_SMOKE_REVISION" ]]
}
ensure_web_migrator_image() {
if image_matches_revision "$WEB_IMAGE"; then
echo "[docker-smoke] REUSE stage=image:web revision=${DOCKER_SMOKE_REVISION}" >&2
return
fi
build_root_target web "$WEB_IMAGE"
}
validate_web() {
docker_smoke_run "artifacts:web" "$DOCKER_SMOKE_ARTIFACT_TIMEOUT_SECONDS" \
docker run --rm --name "$WEB_ARTIFACT_CONTAINER_NAME" --entrypoint /bin/sh "$WEB_IMAGE" -lc \
'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/apps/web/build-portal/server/index.js && test -d /myapp/apps/web/build-portal/client && test -f /myapp/build/server-runtime/server.js'
docker_smoke_run_with_grace "runtime:web" "$DOCKER_SMOKE_WEB_RUNTIME_TIMEOUT_SECONDS" "$DOCKER_SMOKE_RUNTIME_GRACE_SECONDS" \
bash scripts/docker-web-runtime-smoke.sh "$WEB_IMAGE"
}
build_web() {
build_root_target web "$WEB_IMAGE"
}
build_worker() {
build_root_target worker "$WORKER_IMAGE"
}
validate_worker() {
docker_smoke_run_with_grace "runtime:worker" "$DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS" "$DOCKER_SMOKE_RUNTIME_GRACE_SECONDS" \
bash scripts/docker-worker-runtime-smoke.sh "$WORKER_IMAGE"
}
build_and_validate_worker() {
build_worker
validate_worker
}
build_api() {
build_root_target api "$API_IMAGE"
}
validate_api() {
docker_smoke_run_with_grace "runtime:api" "$DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS" "$DOCKER_SMOKE_RUNTIME_GRACE_SECONDS" \
bash scripts/docker-api-runtime-smoke.sh "$API_IMAGE"
}
build_and_validate_api() {
build_api
validate_api
}
build_mcp() {
build_root_target mcp "$MCP_IMAGE"
}
validate_mcp() {
docker_smoke_run_with_grace "runtime:mcp" "$DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS" "$DOCKER_SMOKE_RUNTIME_GRACE_SECONDS" \
bash scripts/docker-mcp-runtime-smoke.sh "$MCP_IMAGE"
}
build_and_validate_mcp() {
build_mcp
validate_mcp
}
validate_hocuspocus() {
docker_smoke_run_with_grace "runtime:hocuspocus" "$DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS" "$DOCKER_SMOKE_RUNTIME_GRACE_SECONDS" \
bash scripts/docker-hocuspocus-runtime-smoke.sh "$HOCUSPOCUS_IMAGE"
}
build_and_validate_hocuspocus() {
build_hocuspocus
validate_hocuspocus
}
build_portal_controller() {
build_root_target portal-domain-controller "$PORTAL_CONTROLLER_IMAGE"
}
validate_portal_controller() {
docker_smoke_run_with_grace "runtime:portal-domain-controller" "$DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS" "$DOCKER_SMOKE_RUNTIME_GRACE_SECONDS" \
bash scripts/docker-portal-domain-controller-runtime-smoke.sh "$PORTAL_CONTROLLER_IMAGE"
}
build_and_validate_portal_controller() {
build_portal_controller
validate_portal_controller
}
# Validate interpolation against a committed non-secret fixture. `--quiet`
# prevents Compose from echoing resolved developer or CI credentials.
docker_smoke_run "compose:config" "$DOCKER_SMOKE_COMPOSE_TIMEOUT_SECONDS" \
docker compose --env-file .env.test config --quiet
case "$TARGET" in
all)
# All images are complete before any service consumes the web migrator image.
run_parallel_wave builds \
build_web build_worker build_api build_mcp build_hocuspocus build_portal_controller
# Runtime resources are service- and run-scoped; their databases and Redis instances
# intentionally remain isolated while the six unchanged validations run concurrently.
run_parallel_wave runtimes \
validate_web validate_worker validate_api validate_mcp validate_hocuspocus validate_portal_controller
;;
web)
build_web
validate_web
;;
worker)
ensure_web_migrator_image
build_and_validate_worker
;;
api)
ensure_web_migrator_image
build_and_validate_api
;;
mcp)
ensure_web_migrator_image
build_and_validate_mcp
;;
hocuspocus)
ensure_web_migrator_image
build_and_validate_hocuspocus
;;
portal-domain-controller)
build_and_validate_portal_controller
;;
esac
echo "Docker smoke suite passed for target=${TARGET} revision=${DOCKER_SMOKE_REVISION} run=${DOCKER_SMOKE_RUN_ID}"