Add transactional file storage workflows, typed API contracts, recursive category handling, frontend component separation, and Files API coverage.
154 lines
6.5 KiB
Bash
Executable File
154 lines
6.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Deploy AgentEvalTool to the t480 development server.
|
|
#
|
|
# Single entry point that guarantees the right order:
|
|
# 1. sync version (pyproject.toml → package.json)
|
|
# 2. build frontend
|
|
# 3. rsync code + dist to t480
|
|
# 4. rebuild image with version metadata (git SHA + build time)
|
|
# 5. restart containers (runs alembic upgrade head automatically)
|
|
# 6. verify /api/health reports the expected version
|
|
#
|
|
# Usage:
|
|
# scripts/deploy-t480.sh # full deploy
|
|
# scripts/deploy-t480.sh --skip-build # skip image rebuild (code-only hot reload)
|
|
# scripts/deploy-t480.sh --dry-run # print commands, don't run them
|
|
#
|
|
# The t480 host is configured in ~/.ssh/config as "sola-t480".
|
|
# Remote path: /opt/agenteval/
|
|
|
|
set -euo pipefail
|
|
|
|
HOST="${AGENTEVAL_DEPLOY_HOST:-sola-t480}"
|
|
REMOTE_DIR="${AGENTEVAL_DEPLOY_DIR:-/opt/agenteval}"
|
|
IMAGE_NAME="t480-agenteval"
|
|
COMPOSE_FILE="deploy/t480/docker-compose.yml"
|
|
|
|
SKIP_BUILD=0
|
|
DRY_RUN=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--skip-build) SKIP_BUILD=1 ;;
|
|
--dry-run) DRY_RUN=1 ;;
|
|
-h|--help) sed -n '2,18p' "$0"; exit 0 ;;
|
|
*) echo "unknown arg: $arg" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
log() { printf '\033[1;36m>> %s\033[0m\n' "$*"; }
|
|
warn() { printf '\033[1;33m!! %s\033[0m\n' "$*" >&2; }
|
|
die() { printf '\033[1;31m!! %s\033[0m\n' "$*" >&2; exit 1; }
|
|
run() {
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
printf '\033[2m[dry-run] %s\033[0m\n' "$*"
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
# ── pre-flight ─────────────────────────────────────────────────────────
|
|
log "pre-flight: ssh connectivity"
|
|
run ssh -o ConnectTimeout=5 "$HOST" "echo ok >/dev/null" \
|
|
|| die "cannot reach $HOST via ssh"
|
|
|
|
log "pre-flight: sync version (pyproject.toml → package.json)"
|
|
run python3 scripts/sync_version.py
|
|
VERSION=$(python3 -c "import re; print(re.search(r'^version\s*=\s*\"([^\"]+)\"', open('pyproject.toml').read(), re.M).group(1))")
|
|
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "no-git")
|
|
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
log "version=$VERSION commit=$COMMIT built_at=$BUILD_TIME"
|
|
|
|
# ── frontend build ─────────────────────────────────────────────────────
|
|
if [[ ! -d frontend/web/node_modules ]]; then
|
|
log "frontend: npm install"
|
|
run bash -c "cd frontend/web && npm install --silent"
|
|
fi
|
|
log "frontend: npm run build"
|
|
run bash -c "cd frontend/web && npm run build"
|
|
|
|
# Sanity check: dist must contain the chunk referenced by index.html.
|
|
CHUNK=$(grep -oE '/assets/[^"]+\.js' frontend/web/dist/index.html | head -1 | sed 's|^/assets/||')
|
|
[[ -n "$CHUNK" && -f "frontend/web/dist/assets/$CHUNK" ]] \
|
|
|| die "frontend build produced no chunk (dist stale?)"
|
|
log "frontend: OK (chunk=$CHUNK)"
|
|
|
|
# ── rsync ──────────────────────────────────────────────────────────────
|
|
log "rsync → $HOST:$REMOTE_DIR"
|
|
run rsync -az --delete \
|
|
--exclude='.git' \
|
|
--exclude='node_modules' \
|
|
--exclude='.venv' \
|
|
--exclude='__pycache__' \
|
|
--exclude='.pytest_cache' \
|
|
--exclude='.ruff_cache' \
|
|
--exclude='AGENTS.md' \
|
|
--exclude='data' \
|
|
--exclude='.env' \
|
|
--exclude='config/config.json' \
|
|
--exclude='*.db*' \
|
|
./ "$HOST:$REMOTE_DIR/"
|
|
|
|
# ── image rebuild ──────────────────────────────────────────────────────
|
|
if [[ "$SKIP_BUILD" == "1" ]]; then
|
|
warn "skipping image rebuild (--skip-build)"
|
|
else
|
|
log "docker build: $IMAGE_NAME:$VERSION"
|
|
run ssh "$HOST" \
|
|
"cd $REMOTE_DIR && docker compose -f $COMPOSE_FILE build \
|
|
--build-arg BUILD_COMMIT=$COMMIT \
|
|
--build-arg BUILD_TIME=$BUILD_TIME \
|
|
agenteval"
|
|
log "docker tag: $IMAGE_NAME:$VERSION + $IMAGE_NAME:latest"
|
|
run ssh "$HOST" "\
|
|
docker tag $IMAGE_NAME:latest $IMAGE_NAME:$VERSION 2>/dev/null || true && \
|
|
docker tag $IMAGE_NAME:latest $IMAGE_NAME:latest"
|
|
fi
|
|
|
|
# ── restart ────────────────────────────────────────────────────────────
|
|
log "docker compose up -d"
|
|
run ssh "$HOST" "cd $REMOTE_DIR && docker compose -f $COMPOSE_FILE up -d"
|
|
|
|
# In --skip-build mode the image and compose config are unchanged, so
|
|
# `compose up -d` is a no-op and the container keeps running the OLD uvicorn
|
|
# process. The backend is volume-mounted, so its code on disk is already the
|
|
# new version — but Python won't reload it without a restart. Force one.
|
|
if [[ "$SKIP_BUILD" == "1" ]]; then
|
|
log "docker compose restart agenteval (reload volume-mounted backend code)"
|
|
run ssh "$HOST" "cd $REMOTE_DIR && docker compose -f $COMPOSE_FILE restart agenteval"
|
|
fi
|
|
|
|
# ── verify ─────────────────────────────────────────────────────────────
|
|
log "waiting for /api/health"
|
|
for i in $(seq 1 30); do
|
|
BODY=$(ssh "$HOST" "curl -sf http://localhost:8001/api/health 2>/dev/null" || true)
|
|
if [[ -n "$BODY" ]]; then break; fi
|
|
sleep 1
|
|
done
|
|
|
|
[[ -n "$BODY" ]] || die "/api/health did not respond after 30s"
|
|
log "/api/health: $BODY"
|
|
|
|
REMOTE_VERSION=$(echo "$BODY" | python3 -c "import sys, json; print(json.load(sys.stdin).get('version', ''))")
|
|
REMOTE_COMMIT=$(echo "$BODY" | python3 -c "import sys, json; print(json.load(sys.stdin).get('commit', ''))")
|
|
|
|
if [[ "$REMOTE_VERSION" != "$VERSION" ]]; then
|
|
die "version mismatch! local=$VERSION remote=$REMOTE_VERSION (stale image?)"
|
|
fi
|
|
if [[ "$REMOTE_COMMIT" != "$COMMIT" && "$COMMIT" != "no-git" ]]; then
|
|
if [[ "$SKIP_BUILD" == "1" ]]; then
|
|
# /api/health's commit comes from the image build-arg, which --skip-build
|
|
# doesn't update. The volume-mounted code was rsync'd + the container was
|
|
# restarted, so the code IS current — only the metadata label lags.
|
|
warn "commit label still $REMOTE_COMMIT (--skip-build: image not rebuilt; code rsync'd + restarted)"
|
|
else
|
|
die "commit mismatch! local=$COMMIT remote=$REMOTE_COMMIT (image not rebuilt?)"
|
|
fi
|
|
fi
|
|
|
|
log "deploy OK: version=$VERSION commit=$COMMIT"
|
|
log "open http://192.168.8.145:8001 to verify"
|