sandbox-notebookllamalm-nextjs/scripts/deploy.sh
Vadym Samoilenko 8c5e01f660 Fix deploy.sh: health check false-failure on --backend-only / --frontend-only
`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>
2026-04-24 15:18:10 +01:00

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>"