#!/usr/bin/env bash # ============================================================================= # QMDSearch 自动部署脚本(本地 CI/CD 入口) # # 设计原则(贴合 NAS 自托管 + 挂载式部署): # 1. 检测变化 —— 对比 origin/main,区分 backend / frontend / deps 范围 # 2. 校验 —— 本地跑 pytest(CI 门禁),坏代码绝不上线 # 3. 按需构建 —— 仅前端变化时本地预构建(dist 被 gitignore,NAS 端会再构建) # 4. 提交推送 —— git commit + push origin main,推送后 SSH 到 NAS 执行真正部署 # # 用法: # ./scripts/deploy.sh 正常流程:检测 → 校验 → 按需构建 → 提交推送 → SSH 到 NAS 部署 # ./scripts/deploy.sh --dry 零副作用自测:只 检测/校验/构建,不提交不推送 # ./scripts/deploy.sh --no-push 提交但不推送(准备好后手动 push 或再跑一次) # ./scripts/deploy.sh --no-ssh 推送后不自动 SSH 部署(仅同步代码到 NAS git) # ./scripts/deploy.sh --skip-tests 跳过 pytest(谨慎) # ./scripts/deploy.sh --skip-fe 跳过前端构建(即便前端变化) # ./scripts/deploy.sh --build-fe 强制重新构建前端 # ./scripts/deploy.sh --help # ============================================================================= set -euo pipefail REMOTE="origin" BRANCH="main" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" NO_PUSH=0 DRY=0 NO_SSH=0 SKIP_TESTS=0 SKIP_FE=0 FORCE_FE=0 while [[ $# -gt 0 ]]; do case "$1" in --no-push) NO_PUSH=1 ;; --dry) DRY=1 ;; --no-ssh) NO_SSH=1 ;; --skip-tests) SKIP_TESTS=1 ;; --skip-fe) SKIP_FE=1 ;; --build-fe) FORCE_FE=1 ;; --help|-h) sed -n '1,22p' "$0"; exit 0 ;; *) echo "未知参数: $1"; exit 2 ;; esac shift done log(){ printf '\033[36m[deploy]\033[0m %s\n' "$*"; } ok(){ printf '\033[32m[ ok ]\033[0m %s\n' "$*"; } err(){ printf '\033[31m[err ]\033[0m %s\n' "$*" >&2; } # ---- 1. 变化检测 -------------------------------------------------------------- log "同步远端引用..." git fetch --quiet "$REMOTE" "$BRANCH" 2>/dev/null || log "fetch 失败(离线?继续本地流程)" LOCAL_CHANGES="$(git status --porcelain)" AHEAD="$(git rev-list --count "$REMOTE/$BRANCH..HEAD" 2>/dev/null || echo 0)" # 汇总「已提交未推送」+「未提交」的变更文件,去重 CHANGED_FILES="$( { git diff --name-only "$REMOTE/$BRANCH" HEAD 2>/dev/null; echo "$LOCAL_CHANGES" | awk '{print $2}'; } \ | grep -v '^$' | sort -u )" has(){ echo "$CHANGED_FILES" | grep -qE "$1"; } BACKEND_CHANGED=0; FRONTEND_CHANGED=0; DEPS_CHANGED=0 if has '^(app/|scripts/|tests/|.*\.py$|Dockerfile|docker-compose\.yml|\.env\.example)'; then BACKEND_CHANGED=1; fi if has '^frontend/(src/|public/|index\.html|package\.json|vite\.config|package-lock\.json)'; then FRONTEND_CHANGED=1; fi if has '^(pyproject\.toml|uv\.lock|Dockerfile)'; then DEPS_CHANGED=1; fi if [[ -z "$CHANGED_FILES" && "$AHEAD" -eq 0 ]]; then ok "无代码变化,无需部署。" exit 0 fi log "变更范围 → backend=$BACKEND_CHANGED frontend=$FRONTEND_CHANGED deps=$DEPS_CHANGED (ahead=$AHEAD)" # ---- 2. 校验(本地 CI gate)--------------------------------------------------- if [[ "$SKIP_TESTS" -eq 0 && "$BACKEND_CHANGED" -eq 1 ]]; then log "运行后端测试 (uv run pytest)..." if ! uv run pytest -q --no-header 2>&1 | tail -n 30; then err "测试失败,已中止部署。请修复后再运行 deploy.sh" exit 1 fi ok "后端测试通过" # 顺带做一次非阻断的静态检查 if command -v uv >/dev/null 2>&1; then log "运行 ruff 静态检查(不阻断)..." uv run ruff check app tests 2>&1 | tail -n 15 || true fi fi # ---- 3. 按需构建前端 ---------------------------------------------------------- if [[ ("$FRONTEND_CHANGED" -eq 1 || "$FORCE_FE" -eq 1) && "$SKIP_FE" -eq 0 ]]; then log "构建前端 (npm ci && npm run build)..." if ! (cd frontend && npm ci --no-audit --no-fund && npm run build); then err "前端构建失败,已中止。" exit 1 fi ok "前端构建完成 -> frontend/dist" log "提示: frontend/dist 已被 gitignore,NAS 端 post-receive 会重新构建以生效。" fi # ---- 4. 提交 + 推送 ----------------------------------------------------------- if [[ "$DRY" -eq 1 ]]; then log "(--dry) 零副作用模式:跳过提交与推送。以上为检测结果,确认无误后可去掉 --dry 正式部署。" exit 0 fi if [[ -n "$LOCAL_CHANGES" ]]; then log "暂存并提交本地变化..." git add -A # .gitignore 已排除 .env / data / node_modules / .workbuddy / .trae 等 SCOPE="" [[ "$BACKEND_CHANGED" -eq 1 ]] && SCOPE="$SCOPE backend" [[ "$FRONTEND_CHANGED" -eq 1 ]] && SCOPE="$SCOPE frontend" [[ "$DEPS_CHANGED" -eq 1 ]] && SCOPE="$SCOPE deps" MSG="chore(deploy): 自动部署 $(date '+%Y-%m-%d %H:%M')${SCOPE:+ |$SCOPE}" git commit -q -m "$MSG" ok "已提交: $MSG" fi if [[ "$NO_PUSH" -eq 1 ]]; then log "(--no-push) 未推送。当前领先远端 $AHEAD 个提交,确认无误后去掉 --no-push 再跑。" exit 0 fi export QMDS_SKIP_PREPUSH=1 # 避免与 pre-push hook 重复跑测试 log "推送到 $REMOTE/$BRANCH(触发 NAS 自动部署)..." if ! git push "$REMOTE" "$BRANCH"; then err "推送失败:可能远端有更新,请先 git pull 解决冲突后重试。" exit 1 fi ok "已推送。代码已同步至 NAS git。" if [[ "$NO_SSH" -eq 0 ]]; then log "通过 SSH 在 NAS 上执行部署..." if bash "$ROOT/scripts/nas-deploy-ssh.sh"; then ok "NAS 部署完成。" else err "NAS 部署脚本返回非 0(详见上方 NAS 输出)。" exit 1 fi else log "(--no-ssh) 已跳过 NAS 端部署。如需部署请运行: ./scripts/nas-deploy-ssh.sh" fi