`A && B || C` in bash fires C when A is false (service intentionally skipped). Replaced with if/fi blocks so || FAILED=1 only triggers when the health check itself fails. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
5.3 KiB
Bash
Executable file
125 lines
5.3 KiB
Bash
Executable file
#!/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 <sha>"
|