#!/bin/sh set -eu previous_image="" app_container_id="" wait_for_health() { attempt=1 while [ "$attempt" -le 40 ]; do status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$app_container_id" 2>/dev/null || echo unknown)" [ "$status" = healthy ] && return 0 [ "$status" = unhealthy ] && return 1 sleep 3 attempt=$((attempt + 1)) done return 1 } verify_routes() { docker exec -i "$app_container_id" node --input-type=module - <<'NODE' const request = (path, init) => fetch(`http://127.0.0.1:3000${path}`, { signal: AbortSignal.timeout(20000), ...init }); for (const [path, text] of [["/", "Bereit für Ihre neue Website"], ["/kalender/", "Kalender"], ["/robots.txt", "Sitemap:"]]) { const response = await request(path); if (!response.ok || !(await response.text()).includes(text)) throw new Error(`${path} failed`); } const events = await request("/api/events"); const etag = events.headers.get("etag"); if (!events.ok || !etag || !Array.isArray(await events.json())) throw new Error("events failed"); if ((await request("/api/events", { headers: { "If-None-Match": etag } })).status !== 304) throw new Error("ETag failed"); for (const path of ["/health/live", "/health/ready"]) if (!(await request(path)).ok) throw new Error(`${path} failed`); NODE } current_id="$(docker compose ps -q app 2>/dev/null || true)" if [ -n "$current_id" ]; then previous_image="$(docker inspect --format '{{.Config.Image}}' "$current_id" 2>/dev/null || true)"; fi if ! docker compose up -d --no-build --remove-orphans app; then exit 1; fi app_container_id="$(docker compose ps -q app)" if ! wait_for_health || ! verify_routes; then docker compose ps || true docker compose logs app || true if [ -n "$previous_image" ]; then APP_IMAGE_NAME="$previous_image" docker compose up -d --no-build --remove-orphans app; fi exit 1 fi