#!/usr/bin/env bash set -Eeuo pipefail DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" BACKEND_ROOT="$DEPLOY_ROOT/backend" FRONTEND_ROOT="$DEPLOY_ROOT/frontend" COMPOSE=(docker compose --project-directory "$DEPLOY_ROOT" -f "$DEPLOY_ROOT/compose.yaml") fail() { printf '[deploy] ERROR: %s\n' "$*" >&2 exit 1 } log() { printf '[deploy] %s\n' "$*" } check_repo() { local label="$1" local path="$2" [[ -d "$path/.git" ]] || fail "$label repository is missing at $path" [[ "$(git -C "$path" branch --show-current)" == "main" ]] || fail "$label must be on main" [[ -z "$(git -C "$path" status --porcelain)" ]] || fail "$label working tree is not clean" git -C "$path" rev-parse --verify origin/main >/dev/null 2>&1 || fail "$label has no origin/main; pull it manually first" [[ "$(git -C "$path" rev-parse HEAD)" == "$(git -C "$path" rev-parse origin/main)" ]] || fail "$label does not match origin/main; pull with --ff-only first" } wait_healthy() { local service="$1" local deadline=$((SECONDS + 180)) local container_id status while (( SECONDS < deadline )); do container_id="$("${COMPOSE[@]}" ps -q "$service")" if [[ -n "$container_id" ]]; then status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")" [[ "$status" == "healthy" || "$status" == "running" ]] && return 0 fi sleep 3 done "${COMPOSE[@]}" ps fail "$service did not become healthy" } cd "$DEPLOY_ROOT" [[ -f .env && -f backend/.env && -f frontend/.env ]] || fail "create .env, backend/.env, and frontend/.env first" check_repo deployment "$DEPLOY_ROOT" check_repo backend "$BACKEND_ROOT" check_repo frontend "$FRONTEND_ROOT" docker network inspect "${CADDY_NETWORK:-caddy_proxy}" >/dev/null 2>&1 || fail "the external Caddy network does not exist" IMAGE_TAG="$(git -C "$BACKEND_ROOT" rev-parse --short=12 HEAD)-$(git -C "$FRONTEND_ROOT" rev-parse --short=12 HEAD)" export IMAGE_TAG log "validating production configuration" "${COMPOSE[@]}" config --quiet log "building application images" "${COMPOSE[@]}" build backend frontend log "starting data services" "${COMPOSE[@]}" up -d postgres redis wait_healthy postgres wait_healthy redis log "applying database migrations" "${COMPOSE[@]}" --profile tools run --rm migrate log "starting API, worker, and frontend" "${COMPOSE[@]}" up -d backend worker frontend wait_healthy backend wait_healthy worker wait_healthy frontend log "deployment is healthy" printf '[deploy] deployment=%s backend=%s frontend=%s image_tag=%s\n' \ "$(git rev-parse HEAD)" \ "$(git -C "$BACKEND_ROOT" rev-parse HEAD)" \ "$(git -C "$FRONTEND_ROOT" rev-parse HEAD)" \ "$IMAGE_TAG" "${COMPOSE[@]}" ps