diff --git a/deploy.sh b/deploy.sh index 305375e..3d82955 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,125 +1,228 @@ #!/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. - +# Idempotent — safe to run multiple times (initial deploy or update) +# Run as normal user; uses sudo internally for apt/apache/ufw 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 ────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; NC='\033[0m' +info() { echo -e "${GREEN}[deploy]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +error() { echo -e "${RED}[error]${NC} $*" >&2; } -# ─── 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 ───────────────────────────────────────────────────────────────────── +# ── Args ───────────────────────────────────────────────────────────────────── SKIP_PULL=false -for arg in "$@"; do - [[ "$arg" == "--skip-pull" ]] && SKIP_PULL=true -done +for arg in "$@"; do [[ "$arg" == "--skip-pull" ]] && SKIP_PULL=true; done -cd "$APP_DIR" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" -# ─── Pre-flight checks ─────────────────────────────────────────────────────── -log "Running pre-flight checks..." +APACHE_CONF="/etc/apache2/sites-available/optical-dev.oliver.solutions.conf" +INCLUDE_LINE=" Include $SCRIPT_DIR/deploy/apache.conf" +APP_PORT=3001 -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" +# ── Sudo check ─────────────────────────────────────────────────────────────── +if ! sudo -n true 2>/dev/null; then + warn "This script needs sudo for apt/apache/ufw — you may be prompted for your password." +fi -[[ -f "$APP_DIR/.env" ]] || fail ".env file not found at $APP_DIR/.env — copy .env.example and fill in the values" +# ───────────────────────────────────────────────────────────────────────────── +# STEP 1: Prerequisites +# ───────────────────────────────────────────────────────────────────────────── +info "Step 1: Checking prerequisites..." -# 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 +install_if_missing() { + local cmd=$1 pkg=${2:-$1} + if ! command -v "$cmd" &>/dev/null; then + info " Installing $pkg..." + sudo apt-get install -y "$pkg" -qq + else + info " $cmd: OK" + fi +} -# ─── Pull latest code ──────────────────────────────────────────────────────── -if [[ "$SKIP_PULL" == false ]]; then - log "Pulling latest code..." - # Ensure remote uses SSH (avoids HTTPS credential prompts) +sudo apt-get update -qq + +install_if_missing docker docker.io +install_if_missing git git +install_if_missing curl curl +install_if_missing ufw ufw + +if ! docker compose version &>/dev/null 2>&1; then + info " Installing docker-compose-plugin..." + sudo apt-get install -y docker-compose-plugin -qq +fi + +if ! command -v apache2 &>/dev/null; then + info " Installing apache2..." + sudo apt-get install -y apache2 -qq +fi + +info " Enabling Apache modules..." +sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite -q + +# Ensure current user can run docker without sudo +if ! groups | grep -q docker; then + sudo usermod -aG docker "$USER" + warn "Added $USER to docker group — you may need to re-login for this to take effect" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# STEP 2: Pull latest code +# ───────────────────────────────────────────────────────────────────────────── +info "Step 2: Pulling latest code..." + +if [[ "$SKIP_PULL" == true ]]; then + warn " Skipping git pull (--skip-pull)" +else + # Switch to SSH remote if still on HTTPS (avoids 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)" + info " 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))" + info " 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)" + git pull --ff-only || { error "git pull failed — resolve conflicts manually then re-run"; exit 1; } + info " Updated to $(git rev-parse --short HEAD) — $(git log -1 --pretty=%s)" fi -else - warn "Skipping git pull (--skip-pull)" fi -# ─── Apache config ─────────────────────────────────────────────────────────── -APACHE_CONF="/etc/apache2/sites-available/optical-dev.oliver.solutions.conf" -INCLUDE_LINE="Include $APP_DIR/deploy/apache.conf" -INCLUDE_MARKER="hp-prod-tracker" +# ───────────────────────────────────────────────────────────────────────────── +# STEP 3: Environment file +# ───────────────────────────────────────────────────────────────────────────── +info "Step 3: Checking .env..." -if [[ -f "$APACHE_CONF" ]]; then - if grep -q "$INCLUDE_MARKER" "$APACHE_CONF"; then - log "Apache: existing hp-prod-tracker config found — replacing with canonical snippet..." - # Remove old inline block (lines between the marker comment and next comment block) - sudo sed -i '/# .*HP-PROD-TRACKER\|hp-prod-tracker.*3001/,/^[[:space:]]*$/{/ProxyPass.*hp-prod-tracker\|ProxyPassReverse.*hp-prod-tracker/d}' "$APACHE_CONF" 2>/dev/null || true - # Add include if not already present - if ! grep -qF "$INCLUDE_LINE" "$APACHE_CONF"; then - sudo sed -i "s|| $INCLUDE_LINE\n|" "$APACHE_CONF" - log "Apache: added Include directive" - else - log "Apache: Include already present — skipping" - fi +if [[ ! -f .env ]]; then + if [[ -f .env.example ]]; then + cp .env.example .env + warn " .env was missing — copied from .env.example" + warn " Edit .env with real secrets before continuing (Ctrl-C to abort)" + read -rp " Press Enter to continue or Ctrl-C to abort..." else - # Fresh server — just add the include before - sudo sed -i "s|| $INCLUDE_LINE\n|" "$APACHE_CONF" - log "Apache: added hp-prod-tracker config include" + error " .env and .env.example both missing — cannot continue" + exit 1 fi - sudo apache2ctl configtest 2>&1 | grep -v "^$" || fail "Apache config test failed — check $APACHE_CONF" - sudo systemctl reload apache2 - log "Apache: reloaded" -else - warn "Apache config not found at $APACHE_CONF — skipping (add manually if needed)" fi -# ─── Deploy ────────────────────────────────────────────────────────────────── -log "Stopping existing containers..." -docker compose -f "$COMPOSE_FILE" down --remove-orphans +# Warn on unset critical vars +for var in AUTH_SECRET AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET \ + AUTH_MICROSOFT_ENTRA_ID_TENANT_ID AUTH_URL DB_PASSWORD; do + val=$(grep -E "^${var}=" .env | cut -d= -f2- | tr -d '"' || true) + [[ -z "$val" ]] && warn " WARNING: $var is not set in .env" +done -log "Building and starting containers (this may take a few minutes)..." -docker compose -f "$COMPOSE_FILE" up -d --build +info " .env OK" -# ─── 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" +# ───────────────────────────────────────────────────────────────────────────── +# STEP 4: Build and start Docker containers +# ───────────────────────────────────────────────────────────────────────────── +info "Step 4: Building and starting containers..." + +docker compose down --remove-orphans +docker compose up -d --build + +# ───────────────────────────────────────────────────────────────────────────── +# STEP 5: Wait for Postgres +# ───────────────────────────────────────────────────────────────────────────── +info "Step 5: Waiting for Postgres..." + +MAX_WAIT=30 +for i in $(seq 1 $MAX_WAIT); do + if docker compose exec -T db pg_isready -U postgres &>/dev/null; then + info " Postgres ready (${i}s)" + break + fi + if [[ $i -eq $MAX_WAIT ]]; then + error " Postgres did not become ready within ${MAX_WAIT}s" + docker compose logs db --tail 20 + exit 1 + fi + sleep 1 +done + +# ───────────────────────────────────────────────────────────────────────────── +# STEP 6: Wait for app health check +# ───────────────────────────────────────────────────────────────────────────── +info "Step 6: Waiting for app to be healthy (Prisma migrations run on startup)..." + +HEALTH_URL="http://localhost:${APP_PORT}/api/health" +for i in $(seq 1 40); do + if curl -sf "$HEALTH_URL" &>/dev/null; then + info " App healthy (${i}s)" + break + fi + if [[ $i -eq 40 ]]; then + error " App did not become healthy within 120s" + error " Check logs: docker compose logs app --tail 50" + docker compose logs app --tail 30 + exit 1 fi echo -n "." - sleep $HEALTH_INTERVAL + sleep 3 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" +# ───────────────────────────────────────────────────────────────────────────── +# STEP 7: Apache configuration +# ───────────────────────────────────────────────────────────────────────────── +info "Step 7: Configuring Apache..." + +if [[ ! -f "$APACHE_CONF" ]]; then + warn " Apache config not found at $APACHE_CONF — skipping" + warn " Add the following line inside manually:" + warn " $INCLUDE_LINE" +else + if grep -qF "Include $SCRIPT_DIR/deploy/apache.conf" "$APACHE_CONF"; then + info " Include already present — skipping" + else + # Remove old inline block (3 lines added manually before this script existed) + sudo sed -i '/# .*HP-PROD-TRACKER\|HP-PROD-TRACKER.*3001/d' "$APACHE_CONF" + sudo sed -i '/ProxyPass.*hp-prod-tracker.*3001/d' "$APACHE_CONF" + sudo sed -i '/ProxyPassReverse.*hp-prod-tracker/d' "$APACHE_CONF" + + # Add Include before closing + sudo sed -i "s||$INCLUDE_LINE\n|" "$APACHE_CONF" + info " Added Include directive for deploy/apache.conf" + fi + + if sudo apache2ctl configtest 2>&1 | grep -q "Syntax OK"; then + sudo systemctl reload apache2 + info " Apache reloaded OK" + else + error " Apache config test failed:" + sudo apache2ctl configtest + exit 1 + fi +fi + +# ───────────────────────────────────────────────────────────────────────────── +# STEP 8: UFW Firewall +# ───────────────────────────────────────────────────────────────────────────── +info "Step 8: Configuring UFW firewall..." + +sudo ufw default deny incoming 2>/dev/null || true +sudo ufw default allow outgoing 2>/dev/null || true +sudo ufw allow 22/tcp +sudo ufw allow 80/tcp +sudo ufw --force enable +info " UFW enabled (22/tcp, 80/tcp allowed)" + +# ───────────────────────────────────────────────────────────────────────────── +# Done +# ───────────────────────────────────────────────────────────────────────────── +echo "" +docker compose ps +echo "" +info "Deploy complete!" +info " Commit : $(git rev-parse --short HEAD) — $(git log -1 --pretty=%s)" +info " App : https://optical-dev.oliver.solutions/hp-prod-tracker" +info " Logs : docker compose logs -f app"