#!/usr/bin/env bash # ============================================================================= # QMDSearch NAS 端自动部署钩子 (post-receive) # # 这是真正的「持续部署(CD)」:当本地 deploy.sh 把代码 push 到 NAS 后, # 此钩子自动完成 checkout → 按需构建 → 重启/重建容器 → 健康检查 → 失败回滚。 # # 安装方式(二选一): # A. 作为 bare 仓库的 hook(推荐,全自动): # # 假设 NAS 上 bare 仓库在 /volume1/git/QMDSearch.git,工作树在 /volume1/docker/QMDSearch # cp scripts/nas-post-receive /volume1/git/QMDSearch.git/hooks/post-receive # chmod +x /volume1/git/QMDSearch.git/hooks/post-receive # # 在该 bare 仓库 config 中设置: # # [core] bare = true # # [hooks] 并确保工作树目录已 clone 过一次 # # 首次需手动:git --work-tree=/volume1/docker/QMDSearch --git-dir=/volume1/git/QMDSearch.git checkout -f # # B. 作为普通脚本手动触发(无 bare 仓库时): # cd /volume1/docker/QMDSearch && git pull && sudo bash scripts/nas-post-receive # # 前置依赖(NAS 上需具备):git、docker、docker compose、node/npm # 可调环境变量:DEPLOY_DIR(工作树)、APP_PORT、LOG_FILE # ============================================================================= set -uo pipefail DEPLOY_DIR="${DEPLOY_DIR:-/volume1/docker/QMDSearch}" # ← 改成你的 NAS 部署目录 GIT_DIR="$DEPLOY_DIR/.git" LOG_FILE="${LOG_FILE:-$DEPLOY_DIR/deploy.log}" APP_PORT="${APP_PORT:-8000}" PREV_COMMIT_FILE="$DEPLOY_DIR/.last_deployed_commit" log(){ printf '%s [nas-deploy] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"; } rollback(){ log "回滚到上一稳定提交: ${PREV:-<无>}" if [[ -n "${PREV:-}" ]]; then git --work-tree="$DEPLOY_DIR" --git-dir="$GIT_DIR" checkout -f "$PREV" (cd "$DEPLOY_DIR" && docker compose restart app) | tee -a "$LOG_FILE" echo "$PREV" > "$PREV_COMMIT_FILE" fi } # 解析 post-receive 通过 stdin 传入的 newrev=""; refname="" while read -r oldrev newrev refname; do :; done # 仅处理 main 分支的推送;手动运行时 newrev 为空,回退到 HEAD if [[ -z "$newrev" ]]; then newrev="$(git --git-dir="$GIT_DIR" rev-parse HEAD 2>/dev/null || echo "")" fi if [[ -n "$refname" && "$refname" != "refs/heads/main" ]]; then log "忽略非 main 分支推送 ($refname)" exit 0 fi log "=== 开始部署 (newrev=${newrev:0:8}) ===" # 1) 检出到工作树 git --work-tree="$DEPLOY_DIR" --git-dir="$GIT_DIR" checkout -f cd "$DEPLOY_DIR" || { log "无法进入 $DEPLOY_DIR"; exit 1; } # 2) 计算变化范围 PREV="$(cat "$PREV_COMMIT_FILE" 2>/dev/null || echo "")" if [[ -n "$PREV" && -n "$newrev" ]]; then CHANGED="$(git --git-dir="$GIT_DIR" diff --name-only "$PREV" "$newrev")" else CHANGED="$(git --git-dir="$GIT_DIR" show --name-only --format= "$newrev")" fi has(){ echo "$CHANGED" | grep -qE "$1"; } FE_CHANGED=0; DEPS_CHANGED=0 has '^frontend/(src/|public/|index\.html|package\.json|vite\.config|package-lock\.json)' && FE_CHANGED=1 has '^(pyproject\.toml|uv\.lock|Dockerfile)' && DEPS_CHANGED=1 log "变更范围 → frontend=$FE_CHANGED deps=$DEPS_CHANGED" # 3) 按需构建前端(dist 不入库,必须在此构建) if [[ "$FE_CHANGED" -eq 1 ]]; then log "检测前端变化,重新构建..." if ! (cd frontend && npm ci --no-audit --no-fund && npm run build) 2>&1 | tee -a "$LOG_FILE"; then log "前端构建失败,中止部署(保留上一版本)"; exit 1 fi ok(){ :; }; log "前端构建完成" fi # 4) 部署容器 if [[ "$DEPS_CHANGED" -eq 1 ]]; then log "依赖变化,重建镜像并重启: docker compose up -d --build app" docker compose up -d --build app 2>&1 | tee -a "$LOG_FILE" else log "仅代码变化,重启 app 服务: docker compose restart app" docker compose restart app 2>&1 | tee -a "$LOG_FILE" fi # 5) 健康检查(失败自动回滚) log "健康检查 http://localhost:$APP_PORT/health ..." for i in $(seq 1 15); do if curl -fsS "http://localhost:$APP_PORT/health" >/dev/null 2>&1; then log "健康检查通过 ✓" [[ -n "$newrev" ]] && echo "$newrev" > "$PREV_COMMIT_FILE" log "=== 部署完成 ===" exit 0 fi sleep 2 done log "健康检查失败,触发回滚" rollback exit 1