#!/usr/bin/env bash # Confidence Engine — Production Deployment Script # # Executes on CT 112 at /opt/confidence-engine. # Input: $1 = exact commit SHA (validated) # # Environment: # DEPLOY_DIR - target repository path (defaults /opt/confidence-engine) # HEALTH_URL - health check endpoint (defaults http://127.0.0.1:3000/api/health) # # Reads deploy.env from $DEPLOY_DIR/deploy.env for runtime values. set -euo pipefail # ── Parameters ────────────────────────────────────────────────────── DEPLOY_SHA="${1:?Error: missing commit SHA argument}" DEPLOY_DIR="${DEPLOY_DIR:-/opt/confidence-engine}" HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:3000/api/health}" # ── Validation ────────────────────────────────────────────────────── if [[ ${#DEPLOY_SHA} -lt 7 ]]; then echo "ERROR: SHA must be at least 7 hex characters." exit 1 fi if ! echo "$DEPLOY_SHA" | grep -qE '^[0-9a-fA-F]+$'; then echo "ERROR: SHA contains non-hex characters." exit 1 fi echo "Deploying SHA: $DEPLOY_SHA" cd "$DEPLOY_DIR" || { echo "ERROR: Cannot cd to $DEPLOY_DIR"; exit 1; } # ── Safety checks ─────────────────────────────────────────────────── # deploy.env must exist on the host (not tracked by Git) if [[ ! -f deploy.env ]]; then echo "DEPLOYMENT BLOCKED - deploy.env not found at deploy.env" exit 1 fi # Check for uncommitted changes that would block safe checkout if ! git diff-index --quiet HEAD -- 2>/dev/null; then echo "DEPLOYMENT BLOCKED - target working tree has uncommitted changes" exit 1 fi if [[ -n "$(git ls-files --others --exclude-standard)" ]]; then echo "DEPLOYMENT BLOCKED - target working tree has untracked files" exit 1 fi # ── Fetch and verify SHA ──────────────────────────────────────────── echo "Fetching latest refs from origin..." git fetch origin >/dev/null 2>&1 || { echo "ERROR: git fetch origin failed." exit 1 } if ! git rev-parse --verify "$DEPLOY_SHA" >/dev/null 2>&1; then echo "ERROR: SHA $DEPLOY_SHA not found in repository after fetch." exit 1 fi # ── Detached checkout (never modifies working tree or remote) ─────── echo "Checking out exactly $DEPLOY_SHA..." git checkout --detach "$DEPLOY_SHA" >/dev/null 2>&1 || { echo "ERROR: Could not checkout SHA $DEPLOY_SHA." exit 1 } # Verify the detached HEAD matches our deploy SHA CURRENT_SHA="$(git rev-parse HEAD)" if [[ "${CURRENT_SHA%"${CURRENT_SHA#?}"}" != "${DEPLOY_SHA%"${DEPLOY_SHA#?}"}" ]] || \ [[ "$CURRENT_SHA" != "$DEPLOY_SHA" && "${CURRENT_SHA:0:7}" != "${DEPLOY_SHA:0:7}" ]]; then echo "ERROR: Checked out SHA $CURRENT_SHA does not match requested $DEPLOY_SHA." exit 1 fi # ── Load runtime environment ──────────────────────────────────────── if [[ -f deploy.env ]]; then set -a # shellcheck disable=SC1091 source deploy.env set +a else echo "ERROR: deploy.env not found at $DEPLOY_DIR/deploy.env" exit 1 fi # ── Build Docker image ────────────────────────────────────────────── IMAGE_TAG="confidence-engine:${DEPLOY_SHA}" echo "Building Docker image ${IMAGE_TAG}..." docker build \ --build-arg NEXT_PUBLIC_SUPABASE_URL="${NEXT_PUBLIC_SUPABASE_URL}" \ --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY}" \ -t "${IMAGE_TAG}" \ . echo "Image ${IMAGE_TAG} built successfully." # ── Container replacement ─────────────────────────────────────────── CONTAINER_NAME="confidence-engine" # Record previous image before replacing container PREV_IMAGE="" if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then PREV_IMAGE="$(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo "")" fi echo "Stopping existing container (if running)..." docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true echo "Starting new container from ${IMAGE_TAG}..." docker run -d \ --name "$CONTAINER_NAME" \ --restart unless-stopped \ -p 3000:3000 \ -e OLLAMA_BASE_URL="${OLLAMA_BASE_URL}" \ -e OLLAMA_MODEL="${OLLAMA_MODEL}" \ "${IMAGE_TAG}" echo "Container $CONTAINER_NAME started." # ── Health check polling ──────────────────────────────────────────── HEALTH_CHECK_TIMEOUT=60 HEALTH_CHECK_INTERVAL=3 ELAPSED=0 echo "Waiting for health check at ${HEALTH_URL}..." while [[ $ELAPSED -lt $HEALTH_CHECK_TIMEOUT ]]; do if curl -sf "$HEALTH_URL" | grep -q '"healthy"[[:space:]]*:[[:space:]]*true'; then echo "" echo "=============================================" echo "DEPLOYMENT SUCCEEDED" echo "Deployed SHA: $DEPLOY_SHA" echo "Current image: $(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo 'unknown')" echo "=============================================" exit 0 fi sleep "$HEALTH_CHECK_INTERVAL" ELAPSED=$((ELAPSED + HEALTH_CHECK_INTERVAL)) done echo "" echo "=============================================" echo "DEPLOYMENT FAILED - Health check timed out after ${HEALTH_CHECK_TIMEOUT}s" echo "=============================================" # ── Rollback (one attempt) ────────────────────────────────────────── if [[ -n "$PREV_IMAGE" ]]; then echo "" echo "Attempting rollback to previous image: $PREV_IMAGE" docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true docker run -d \ --name "$CONTAINER_NAME" \ --restart unless-stopped \ -p 3000:3000 \ -e OLLAMA_BASE_URL="${OLLAMA_BASE_URL}" \ -e OLLAMA_MODEL="${OLLAMA_MODEL}" \ "$PREV_IMAGE" if [[ $? -eq 0 ]]; then echo "DEPLOYMENT FAILED - ROLLED BACK to $PREV_IMAGE" else echo "DEPLOYMENT FAILED - ROLLBACK ALSO FAILED" fi else echo "No previous image available for rollback." fi echo "DEPLOYMENT FAILED - MANUAL RECOVERY REQUIRED" exit 1