feat(deploy): Phase4 代码晋升脚本 promote.sh + runbook

Mac→NAS(rsync代码镜像)+Mac→VPS(scp生产部署)单向晋升管线。
- 数据铁律双层防护: rsync --exclude='/data' + scp按模块天然隔离data/
- --module 单模块快速补推 / 全量 双模式
- VPS无常驻web, 代码部署后下次schtask/回测启动自动生效,无需hot reload
- dry-run双端验证通过(VPS+NAS标记落盘后还原无残留)
- ssh用49.232.102.198(严禁ssh vps,fake-ip劫持198.18.1.254)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-29 21:47:11 +08:00
parent bdad396243
commit 851d7d49b7
2 changed files with 280 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
#!/bin/bash
# Phase4: Mac→NAS(代码镜像) + Mac→VPS(生产部署) 单向代码晋升
#
# 用法:
# 全量晋升(所有模块+根文件): bash scripts/nas_sync/promote.sh
# 单模块晋升(快速/验证): bash scripts/nas_sync/promote.sh --module sanguo_portfolio
#
# 设计:
# - NAS 走 rsync(增量快),VPS 无 rsync 走 scp -r(按模块逐个推).
# - --module X: NAS+VPS 都只推该模块,不推根文件(用于快速补推/小范围验证).
# - 数据铁律: 严禁推 data/ (rsync --exclude '/data'; scp 按模块天然隔离).
# - reload: VPS 无常驻 web 服务,代码部署后下次 schtask/回测启动自动生效,
# 无需 hot reload. 如需立即生效见 runbook.
# - ssh 用 `49.232.102.198`(~/.ssh/config 已配 key),严禁 `ssh vps`(fake-ip 劫持).
set -eu
VPS=49.232.102.198
NAS=sanguo-nas
NAS_CODE=/volume1/stock/sanguo_vnpy_v2
VPS_CODE='C:/sanguo_vnpy_v2' # scp 目标用正斜杠
ALL_MODS=(sanguo_api sanguo_backtest sanguo_common sanguo_data sanguo_factor
sanguo_live sanguo_orchestrator sanguo_portfolio sanguo_qmt_bridge
sanguo_research sanguo_trader sanguo_web scripts config tests)
ROOT_FILES=(pyproject.toml pytest.ini requirements-lock.txt run_web.py)
# 解析 --module 参数
PUSH_MOD=""
if [[ "${1:-}" == "--module" ]]; then
if [ -z "${2:-}" ]; then
echo "ERROR: --module 需要模块名参数" >&2
echo "可用模块: ${ALL_MODS[*]}" >&2
exit 1
fi
PUSH_MOD="$2"
if [ ! -d "$PUSH_MOD" ]; then
echo "ERROR: 本地目录不存在: $PUSH_MOD" >&2
exit 1
fi
fi
# rsync exclude: 前导 / = 仅根级匹配,不影响模块内同名子目录
RSYNC_EXCLUDE=(--exclude='/data' --exclude='/data_backup' --exclude='/logs'
--exclude='__pycache__' --exclude='.git' --exclude='.pytest_cache'
--exclude='.mypy_cache' --exclude='.ruff_cache'
--exclude='/vnpy_v4.4.0' --exclude='/vnpy_qmt_v0.3.3'
--exclude='/htmlcov' --exclude='/docker' --exclude='/docs'
--exclude='/examples' --exclude='/venv310' --exclude='/venv'
--exclude='/requirements' --exclude='*.log'
--exclude='build*.log' --exclude='rebuild*.log'
--exclude='test.db' --exclude='*.pyc' --exclude='*.pyo'
--exclude='.coverage' --exclude='*.egg-info')
echo "=============================================="
echo " Phase4 代码晋升 $(date '+%F %T')"
if [ -n "$PUSH_MOD" ]; then
echo " 模式: 单模块 [$PUSH_MOD] (NAS+VPS 只推该模块,不推根文件)"
else
echo " 模式: 全量 (所有模块+根文件)"
fi
echo "=============================================="
# ---- Step 1/3: Mac → NAS (rsync 代码镜像) ----
echo ""
echo "[1/3] rsync Mac → NAS ($NAS:$NAS_CODE) ..."
if [ -n "$PUSH_MOD" ]; then
rsync -az "${RSYNC_EXCLUDE[@]}" "$PUSH_MOD/" "$NAS:$NAS_CODE/$PUSH_MOD/"
echo " ✅ NAS 单模块镜像完成: $PUSH_MOD"
else
# 不用 --delete! NAS 有 data_backup 等独有目录,删了就丢数据
rsync -avz "${RSYNC_EXCLUDE[@]}" ./ "$NAS:$NAS_CODE/"
echo " ✅ NAS 全量镜像完成"
fi
# ---- Step 2/3: Mac → VPS (scp 按模块/根文件推) ----
echo ""
echo "[2/3] scp Mac → VPS ($VPS:$VPS_CODE) ..."
if [ -n "$PUSH_MOD" ]; then
MODS=("$PUSH_MOD")
ROOT_PUSH=() # 单模块不推根文件
else
MODS=("${ALL_MODS[@]}")
ROOT_PUSH=("${ROOT_FILES[@]}")
fi
PUSHED=0
FAILED=0
for m in "${MODS[@]}"; do
if [ ! -d "$m" ]; then
echo " skip(本地缺失): $m"
continue
fi
if scp -r -C -o ConnectTimeout=20 "$m" "$VPS:$VPS_CODE/" 2>/dev/null; then
echo " pushed $m"
PUSHED=$((PUSHED+1))
else
echo " FAIL $m (scp 退出非零)"
FAILED=$((FAILED+1))
fi
done
# ROOT_PUSH 在 --module 模式下为空数组; macOS bash 3.2 + set -u 下 "${arr[@]}" 会报错,
# 用 ${#arr[@]} 计数守卫绕过(空数组长度=0, 跳过循环)
if [ "${#ROOT_PUSH[@]}" -gt 0 ]; then
for f in "${ROOT_PUSH[@]}"; do
if [ ! -f "$f" ]; then
echo " skip(本地缺失): $f"
continue
fi
if scp -C -o ConnectTimeout=20 "$f" "$VPS:$VPS_CODE/" 2>/dev/null; then
echo " pushed $f"
PUSHED=$((PUSHED+1))
else
echo " FAIL $f"
FAILED=$((FAILED+1))
fi
done
fi
if [ "$FAILED" -gt 0 ]; then
echo ""
echo "$FAILED 个文件推送失败,请检查上方日志。已成功 $PUSHED 个。"
exit 1
fi
echo " 共推送 $PUSHED 个模块/文件"
# ---- Step 3/3: VPS 落盘验证 ----
echo ""
echo "[3/3] VPS 落盘验证 ..."
# 验证目标: 取第一个推送的模块,列其 .py 文件的 mtime
VERIFY_MOD="${MODS[0]}"
VERIFY_CMD="powershell -NoProfile -Command \"Get-ChildItem -Path 'C:\\sanguo_vnpy_v2\\${VERIFY_MOD}' -Filter '*.py' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 3 Name,Length,LastWriteTime | Format-Table -Auto | Out-String\""
if ssh -o ConnectTimeout=20 "$VPS" "$VERIFY_CMD" 2>/dev/null | grep -q '\.py'; then
echo " ✅ VPS 落盘确认: $VERIFY_MOD/*.py 已就位"
else
echo " ⚠️ VPS 验证未取到 $VERIFY_MOD 的 .py 文件(可能目录为空或 ssh 超时,请手动核查)"
fi
echo ""
echo "=============================================="
echo " ✅ 晋升完成 $(date '+%F %T')"
echo " - NAS 代码镜像: $NAS:$NAS_CODE"
echo " - VPS 生产代码: $VPS:$VPS_CODE"
echo " - reload: VPS 无常驻 web,下次 schtask/回测启动自动生效"
echo "=============================================="