#!/usr/bin/env bash # ============================================================================= # QMDSearch NAS 端部署(通过 SSH 直连执行,密钥免密) # # 环境约束(已实测): # - NAS 上有 docker compose v2,但【没有 node/npm】→ 前端必须在本地构建后 rsync 过去 # - compose 的 app 服务只有 image: 无 build: → 依赖变化用 `docker build -t qmdsearch-app:latest .` # - kp 已在 docker 组,可免 sudo 跑 docker # - 部署目录 /vol1/qmdsearch 是 git 工作树,以 origin/main 为唯一真相源 # # 流程: # 1. SSH 到 NAS:git fetch + git reset --hard origin/main(同步受管代码),并算出变更范围 # 2. 本地:若前端变化 → 本地 npm 构建 → rsync frontend/dist 到 NAS(NAS 无 node) # 3. SSH 到 NAS:依赖变化则 docker build + force-recreate;否则 restart app # 4. 健康检查 /health;失败回滚到上一稳定提交 # # 用法: # ./scripts/nas-deploy-ssh.sh 正式部署 # ./scripts/nas-deploy-ssh.sh --dry-run 只预览将要部署的变更,不重启 # 环境变量:NAS_HOST(默认 nas) DEPLOY_DIR(默认 /vol1/qmdsearch) APP_PORT(默认 8000) # ============================================================================= set -uo pipefail DEPLOY_HOST="${NAS_HOST:-nas}" DEPLOY_DIR="${DEPLOY_DIR:-/vol1/qmdsearch}" APP_PORT="${APP_PORT:-8000}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DRY_RUN=0 [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1 # 前置检查 if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$DEPLOY_HOST" true 2>/dev/null; then echo "[local] 无法免密 SSH 到 $DEPLOY_HOST,请先配置 SSH 密钥登录。" >&2 exit 1 fi command -v rsync >/dev/null 2>&1 || { echo "[local] 本机缺少 rsync,请先安装。" >&2; exit 1; } # ---- Phase 1: NAS 同步受管代码 + 计算变更范围 ---- SCOPE=$(ssh "$DEPLOY_HOST" "bash -s -- '${DEPLOY_DIR}'" <<'REMOTE' DEPLOY_DIR="$1" set -uo pipefail cd "$DEPLOY_DIR" || { echo "NOCD"; exit 1; } git fetch origin main 2>&1 | tail -n 2 NEW=$(git rev-parse origin/main) PREV=$(cat .last_deployed_commit 2>/dev/null || echo "NONE") if [[ "$PREV" == "$NEW" ]]; then echo "UP_TO_DATE"; exit 0; fi OLD_HEAD=$(git rev-parse HEAD) git reset --hard origin/main 2>&1 | tail -n 2 BASE="$OLD_HEAD" [[ "$PREV" != "NONE" ]] && BASE="$PREV" CHANGED=$(git diff --name-only "$BASE" "$NEW") FE=0; DEPS=0 echo "$CHANGED" | grep -qE '^frontend/(src/|public/|index\.html|package\.json|vite\.config|package-lock\.json)' && FE=1 echo "$CHANGED" | grep -qE '^(pyproject\.toml|uv\.lock|Dockerfile)' && DEPS=1 echo "SCOPE FE=$FE DEPS=$DEPS PREV=$PREV NEW=$NEW" REMOTE ) if echo "$SCOPE" | grep -q "NOCD"; then echo "[nas] 无法进入 $DEPLOY_DIR"; exit 1; fi if echo "$SCOPE" | grep -q "UP_TO_DATE"; then echo "[nas] 已是最新,无需更新。"; exit 0; fi FE=$(echo "$SCOPE" | grep -oE 'FE=[01]' | cut -d= -f2) DEPS=$(echo "$SCOPE" | grep -oE 'DEPS=[01]' | cut -d= -f2) PREV=$(echo "$SCOPE" | grep -oE 'PREV=[0-9a-f]+' | cut -d= -f2) NEW=$(echo "$SCOPE" | grep -oE 'NEW=[0-9a-f]+' | cut -d= -f2) echo "[deploy] 将部署 $PREV -> $NEW (FE=$FE DEPS=$DEPS)" if [[ "$DRY_RUN" == "1" ]]; then echo "[deploy][dry-run] 变更文件:" if [[ "$PREV" != "NONE" ]]; then ssh "$DEPLOY_HOST" "cd $DEPLOY_DIR && git diff --name-only $PREV $NEW" 2>/dev/null | sed 's/^/ /' else echo " (首次部署,将全量同步受管代码)" fi exit 0 fi # ---- Phase 2: 前端本地构建 + rsync 到 NAS(NAS 无 node)---- if [[ "$FE" == "1" ]]; then echo "[local] 本地构建前端..." if ! (cd "$ROOT/frontend" && npm ci --no-audit --no-fund && npm run build); then echo "[local][WARN] 前端构建失败(当前环境可能受限或缺少 node 依赖),跳过前端同步;后端仍会部署,但 frontend/dist 可能为旧版本。待在可正常构建的环境中重跑即补齐。" else echo "[local] rsync frontend/dist -> $DEPLOY_HOST:$DEPLOY_DIR/frontend/dist" if ! rsync -az --delete "$ROOT/frontend/dist/" "$DEPLOY_HOST:$DEPLOY_DIR/frontend/dist/"; then echo "[local][WARN] rsync 失败,跳过前端同步。" fi fi fi # ---- Phase 3: NAS 容器部署 + 健康检查 ---- ssh "$DEPLOY_HOST" "bash -s -- '$DEPLOY_DIR' '$APP_PORT' '$DEPS' '$NEW' '$PREV'" <<'REMOTE' DEPLOY_DIR="$1"; APP_PORT="$2"; DEPS="$3"; NEW="$4"; PREV="$5" set -uo pipefail cd "$DEPLOY_DIR" if [[ "$DEPS" == "1" ]]; then echo "[nas] 依赖变化,重建镜像: docker build -t qmdsearch-app:latest ." if ! docker build -t qmdsearch-app:latest . 2>&1 | tail -n 20; then echo "[nas] 镜像构建失败,回滚到 $PREV"; git checkout -f "$PREV"; exit 1 fi echo "[nas] 用新镜像重建容器: docker compose up -d --force-recreate app" docker compose up -d --force-recreate app 2>&1 | tail -n 10 else echo "[nas] 仅代码/前端变化,重启 app: docker compose restart app" docker compose restart app 2>&1 | tail -n 5 fi echo "[nas] 健康检查 http://localhost:$APP_PORT/api/v1/health ..." for i in $(seq 1 20); do if curl -fsS "http://localhost:$APP_PORT/api/v1/health" >/dev/null 2>&1; then echo "$NEW" > .last_deployed_commit echo "[nas] 健康检查通过 ✓ 部署完成" exit 0 fi sleep 3 done echo "[nas] 健康检查失败,回滚到 $PREV" git checkout -f "$PREV" docker compose restart app exit 1 REMOTE