#!/usr/bin/env bash # scripts/deploy.sh — Rolling redeploy for sandbox-notebookllamalm-nextjs # # Usage: # bash scripts/deploy.sh # full build + deploy # bash scripts/deploy.sh --no-build # restart containers only (env-change redeploy) # bash scripts/deploy.sh --backend-only # build + restart backend only # bash scripts/deploy.sh --frontend-only # build + restart frontend only # bash scripts/deploy.sh --branch feat/x # deploy a specific git branch set -euo pipefail REPO_DIR="${REPO_DIR:-/opt/sandbox-notebookllamalm-nextjs}" BRANCH="${DEPLOY_BRANCH:-main}" BACKEND_PORT=9000 FRONTEND_PORT=4000 HEALTH_RETRIES=12 HEALTH_INTERVAL=5 RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' ok() { echo -e "${GREEN}✓${NC} $*"; } fail() { echo -e "${RED}✗${NC} $*" >&2; } warn() { echo -e "${YELLOW}!${NC} $*"; } NO_BUILD=false SERVICES="" for arg in "$@"; do case "$arg" in --no-build) NO_BUILD=true ;; --backend-only) SERVICES="backend" ;; --frontend-only) SERVICES="frontend" ;; --branch=*) BRANCH="${arg#--branch=}" ;; esac done on_error() { local code=$? fail "Deploy failed (exit $code at line ${BASH_LINENO[0]})" docker compose -f "${REPO_DIR}/docker-compose.yml" logs --tail=60 2>/dev/null || true exit $code } trap on_error ERR # ── Preflight ──────────────────────────────────────────────────────────────── [[ -d "$REPO_DIR" ]] || { fail "Repo not found at ${REPO_DIR}"; exit 1; } cd "$REPO_DIR" docker info &>/dev/null || { fail "Docker is not running"; exit 1; } docker compose version &>/dev/null || { fail "docker compose v2 not found"; exit 1; } [[ -f "backend/.env" ]] || { fail "backend/.env not found"; exit 1; } for key in OPENAI_API_KEY ANTHROPIC_API_KEY GOOGLE_API_KEY ELEVENLABS_API_KEY pgql_user pgql_psw pgql_db; do grep -qE "^${key}=.+" backend/.env 2>/dev/null || warn "backend/.env: ${key} missing" done ok "Preflight" # ── Git pull ───────────────────────────────────────────────────────────────── GIT_USER="${SUDO_USER:-}" _git() { [[ -n "$GIT_USER" ]] && sudo -u "$GIT_USER" git "$@" || git "$@"; } _git diff --quiet 2>/dev/null || { warn "Unstaged changes — stashing"; _git stash -u; } _git fetch --prune origin &>/dev/null _git checkout "$BRANCH" &>/dev/null _git pull --ff-only origin "$BRANCH" &>/dev/null DEPLOYED_SHA=$(_git rev-parse --short HEAD) ok "Git ${BRANCH} @ ${DEPLOYED_SHA}" # ── DB migration ────────────────────────────────────────────────────────────── docker compose up -d postgres &>/dev/null for i in $(seq 1 20); do docker compose exec -T postgres pg_isready -U postgres &>/dev/null 2>&1 && break [[ $i -eq 20 ]] && { fail "Postgres did not become ready"; exit 1; } sleep 3 done docker compose run --rm --no-deps backend \ /app/.venv/bin/python -c " import sys; sys.path.insert(0, '/app/src/notebookllama') from database import run_studio_migration run_studio_migration() " &>/dev/null && ok "DB migration" || warn "DB migration skipped (schema may be up to date)" # ── Build ───────────────────────────────────────────────────────────────────── if [[ "$NO_BUILD" == "false" ]]; then docker compose build --pull ${SERVICES} ok "Build ${SERVICES:-all}" else ok "Build skipped (--no-build)" fi # ── Rolling restart ─────────────────────────────────────────────────────────── docker compose up -d --remove-orphans ${SERVICES} &>/dev/null ok "Containers up" # ── Health checks ───────────────────────────────────────────────────────────── check_health() { local name="$1" url="$2" attempt=0 while (( attempt < HEALTH_RETRIES )); do attempt=$(( attempt + 1 )) local code code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 "$url" 2>/dev/null || echo "000") [[ "$code" == "200" ]] && { ok "Health ${name}"; return 0; } sleep "$HEALTH_INTERVAL" done fail "Health ${name} FAILED (HTTP ${code})" return 1 } FAILED=0 if [[ -z "$SERVICES" || "$SERVICES" == "backend" ]]; then check_health "backend" "http://localhost:${BACKEND_PORT}/api/health" || FAILED=1 fi if [[ -z "$SERVICES" || "$SERVICES" == "frontend" ]]; then check_health "frontend" "http://localhost:${FRONTEND_PORT}/notebookllama" || FAILED=1 fi if [[ "$FAILED" -eq 1 ]]; then fail "Health check failed — last 50 log lines:" docker compose logs --tail=50 exit 1 fi echo "" ok "Deploy complete sha=${DEPLOYED_SHA} branch=${BRANCH}" echo " Frontend : http://localhost:${FRONTEND_PORT}/notebookllama" echo " Backend : http://localhost:${BACKEND_PORT}/api/health" echo " Rollback : bash scripts/rollback.sh "