bdd30f0a88
- deps.py: _create_redis_client 增加同步 ping 校验,Redis 不可达时正确降级为内存模式 - main.py: lifespan 复用 deps 的 UserStore 单例,避免 admin 与 API 请求实例不一致 - 前端: 强制改密弹窗(must_change_password 用户)、兼容新旧登录返回格式、markPasswordChanged - 新增 NAS SSH 部署脚本与批处理测试;gitignore 前端构建产物
121 lines
5.3 KiB
Bash
Executable File
121 lines
5.3 KiB
Bash
Executable File
#!/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
|