From 7e32bbc43000052144ffca799d7374830a16b4d7 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Wed, 15 Apr 2026 15:08:29 +0100 Subject: [PATCH] Add idempotent deploy script Handles initial deploy and updates: git pull via SSH, docker compose rebuild, health check with timeout, pre-flight .env validation. Co-Authored-By: Claude Sonnet 4.6 --- deploy.sh | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 deploy.sh diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..965fa45 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# deploy.sh — idempotent deploy script for hp-prod-tracker +# Usage: bash deploy.sh [--skip-pull] +# +# Works for both initial deployment and updates. +# Run from /opt/hp-prod-tracker as any user with docker + git access. + +set -euo pipefail + +# ─── Config ────────────────────────────────────────────────────────────────── +APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="$APP_DIR/docker-compose.yml" +HEALTH_URL="http://localhost:3001/api/health" +HEALTH_RETRIES=30 +HEALTH_INTERVAL=3 # seconds between retries + +# ─── Colours ───────────────────────────────────────────────────────────────── +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +log() { echo -e "${GREEN}[deploy]${NC} $*"; } +warn() { echo -e "${YELLOW}[deploy]${NC} $*"; } +fail() { echo -e "${RED}[deploy]${NC} $*" >&2; exit 1; } + +# ─── Args ───────────────────────────────────────────────────────────────────── +SKIP_PULL=false +for arg in "$@"; do + [[ "$arg" == "--skip-pull" ]] && SKIP_PULL=true +done + +cd "$APP_DIR" + +# ─── Pre-flight checks ─────────────────────────────────────────────────────── +log "Running pre-flight checks..." + +command -v docker >/dev/null 2>&1 || fail "docker is not installed" +docker compose version >/dev/null 2>&1 || fail "docker compose plugin is not installed" +command -v git >/dev/null 2>&1 || fail "git is not installed" + +[[ -f "$APP_DIR/.env" ]] || fail ".env file not found at $APP_DIR/.env — copy .env.example and fill in the values" + +# Warn if critical vars are empty +for var in AUTH_SECRET AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET AUTH_MICROSOFT_ENTRA_ID_TENANT_ID AUTH_URL; do + val=$(grep -E "^${var}=" "$APP_DIR/.env" | cut -d= -f2- | tr -d '"' || true) + [[ -z "$val" ]] && warn "WARNING: $var is not set in .env" +done + +# ─── Pull latest code ──────────────────────────────────────────────────────── +if [[ "$SKIP_PULL" == false ]]; then + log "Pulling latest code..." + # Ensure remote uses SSH (avoids HTTPS credential prompts) + current_remote=$(git remote get-url origin 2>/dev/null || true) + if [[ "$current_remote" == https://* ]]; then + ssh_remote=$(echo "$current_remote" \ + | sed 's|https://bitbucket.org/|git@bitbucket.org:|' \ + | sed 's|\.git$||') + git remote set-url origin "${ssh_remote}.git" + log "Switched remote to SSH: $(git remote get-url origin)" + fi + + git fetch origin + LOCAL=$(git rev-parse HEAD) + REMOTE=$(git rev-parse @{u} 2>/dev/null || echo "") + if [[ "$LOCAL" == "$REMOTE" ]]; then + log "Already up to date ($(git rev-parse --short HEAD))" + else + git pull --ff-only || fail "git pull failed — resolve conflicts manually then re-run" + log "Updated to $(git rev-parse --short HEAD)" + fi +else + warn "Skipping git pull (--skip-pull)" +fi + +# ─── Deploy ────────────────────────────────────────────────────────────────── +log "Stopping existing containers..." +docker compose -f "$COMPOSE_FILE" down --remove-orphans + +log "Building and starting containers (this may take a few minutes)..." +docker compose -f "$COMPOSE_FILE" up -d --build + +# ─── Health check ──────────────────────────────────────────────────────────── +log "Waiting for app to be healthy..." +attempt=0 +until curl -sf "$HEALTH_URL" >/dev/null 2>&1; do + attempt=$((attempt + 1)) + if [[ $attempt -ge $HEALTH_RETRIES ]]; then + fail "App did not become healthy after $((HEALTH_RETRIES * HEALTH_INTERVAL))s — check logs:\n docker compose logs app --tail 50" + fi + echo -n "." + sleep $HEALTH_INTERVAL +done +echo "" + +# ─── Done ──────────────────────────────────────────────────────────────────── +log "Deploy complete!" +log " Commit : $(git rev-parse --short HEAD) — $(git log -1 --pretty=%s)" +log " App : https://optical-dev.oliver.solutions/hp-prod-tracker" +log " Logs : docker compose logs -f app"