From 5d54e22f72d3dd60407042d03f7f248c98fafd78 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 31 Jul 2026 15:14:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(ci):=20CI/CD=E5=88=86=E5=B1=82(NAS?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=AA=8C=E8=AF=81+VPS=20dispatch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit promote.sh 加 --target nas|vps|all(拆 NAS/VPS,默认 all 兼容)。ci-cd.yml 重构: push→test→nas-deploy(推NAS后端+前端dist+restart容器)→nas-verify(login+strategy/list+dbbardata 秒级 gate),不再自动推 VPS。vps-deploy.yml 新建: workflow_dispatch+sha input,checkout 指定 commit→promote --target vps→verify,严格版本一致(VPS=NAS验证过的 commit)。两种 approve: 人工 Gitea UI Run workflow / agent MCP dispatch_workflow。原因: Gitea 不支持 environment approval gate,此前 promote.sh 同时推 NAS+VPS 无隔离,push 即上 prod。 --- .gitea/workflows/ci-cd.yml | 123 ++++++++++++++++---------------- .gitea/workflows/vps-deploy.yml | 42 +++++++++++ scripts/nas_sync/promote.sh | 58 ++++++++------- 3 files changed, 136 insertions(+), 87 deletions(-) create mode 100644 .gitea/workflows/vps-deploy.yml diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index b0ef386..626a6ef 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -1,16 +1,15 @@ name: CI/CD -# 三机流水线:push master → Mac runner 单测 → NAS 冒烟(ssh) → [prod gate] → VPS 部署+冒烟 -# Runner: 复用 online 的 mac-mini-arm64 (label: macos-arm64),host executor 直接跑 Mac shell。 -# 关键:①不用 actions/checkout(避免从 github 拉 action 国内 EOF),改 git fetch Gitea 本仓库 -# ②env HOME=/Users/chufeng 恢复 Mac 全环境(ssh config/key + venv310),act_runner 默认 HOME 是临时目录 -# ③git config --local http.proxy "" 禁代理(LAN Gitea 直连,不动全局 .gitconfig) +# 三机流水线(NAS 分层验证 + VPS 单独 dispatch): +# push master → test → nas-deploy(推 NAS 后端+前端+restart) → nas-verify(API健康+数据可读) +# VPS 部署走单独 vps-deploy.yml(workflow_dispatch 人工 UI 点 / agent API dispatch),不再这里自动推。 +# Runner: 复用 online mac-mini-arm64 (label macos-arm64), host executor 跑 Mac shell。 +# 关键: ①不用 actions/checkout(国内 EOF),改 git fetch Gitea ②HOME=/Users/chufeng ③git config --local http.proxy "" on: push: branches: [master] workflow_dispatch: {} jobs: - # ---------- P2: 验证 ---------- test: runs-on: macos-arm64 env: @@ -26,74 +25,76 @@ jobs: git remote add origin "http://oauth2:${TOKEN}@192.168.2.154:3000/${GITHUB_REPOSITORY}.git" git fetch --depth 1 origin "${GITHUB_SHA}" git checkout FETCH_HEAD - - name: pytest 冒烟 (venv310 python 绝对路径, 数据层核心) - # CI 冒烟=数据层核心(memory验证112 pass); source activate非交互不生效故用venv绝对python; - # api/live 需 fastapi(venv310未装)留各session自测; PYTHONPATH=workspace 跑 checkout 版本 + - name: pytest 冒烟 (venv310 数据层核心) run: | PY=/Users/chufeng/.openclaw/sanguo_projects/sanguo_vnpy_v2/venv310/bin/python PYTHONPATH="$GITHUB_WORKSPACE" $PY -m pytest tests/data_platform -q - nas-smoke: + # ---------- NAS 部署(后端代码 + 前端 dist + restart 容器) ---------- + nas-deploy: needs: test runs-on: macos-arm64 env: HOME: /Users/chufeng steps: - - name: NAS 冒烟 (replica dbbardata 可读) - # smoke_e2e.py 的 docker -v $(pwd):/app 覆盖镜像 /app 致 entrypoint/vnpy 丢失, 改直接查 NAS 副本冒烟 + - name: checkout + env: + TOKEN: ${{ github.token }} + run: | + git init . + git config --local http.proxy "" + git config --local https.proxy "" + git remote add origin "http://oauth2:${TOKEN}@192.168.2.154:3000/${GITHUB_REPOSITORY}.git" + git fetch --depth 1 origin "${GITHUB_SHA}" + git checkout FETCH_HEAD + - name: 推后端代码到 NAS (promote --target nas, 不推 VPS) + run: bash scripts/nas_sync/promote.sh --target nas + - name: build 前端 + 部署 dist 到 NAS + run: | + export PATH="/opt/homebrew/bin:$PATH" + cd frontend + npm ci + npm run build + rsync -az --delete frontend/dist/ sanguo-nas:/volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2/frontend/dist/ + - name: restart NAS 容器(加载新后端代码) + 等 web 就绪 + run: | + ssh sanguo-nas '/var/packages/Docker/target/usr/bin/docker restart sanguo_vnpy_v2' + for i in $(seq 1 30); do + curl -sf http://192.168.2.154:8000/ -o /dev/null && { echo "web ready"; exit 0; } + sleep 2 + done + echo "web 未就绪超时"; exit 1 + + # ---------- NAS 验证(API 健康 + 数据可读,秒级 gate) ---------- + nas-verify: + needs: nas-deploy + runs-on: macos-arm64 + env: + HOME: /Users/chufeng + steps: + - name: NAS web API 健康(login + strategy/list 200 非空) + run: | + BASE=http://192.168.2.154:8000 + token=$(curl -s -X POST $BASE/api/v1/auth/login -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"Ccf7561523*"}' \ + | python3 -c "import json,sys;print(json.load(sys.stdin).get('access_token',''))") + [ -n "$token" ] || { echo "❌ login 失败"; exit 1; } + echo "✅ login OK" + code=$(curl -s -o /tmp/sl.json -w "%{http_code}" $BASE/api/v1/strategy/list -H "Authorization: Bearer $token") + [ "$code" = "200" ] || { echo "❌ strategy/list HTTP $code"; exit 1; } + [ -s /tmp/sl.json ] || { echo "❌ strategy/list 空响应"; exit 1; } + echo "✅ strategy/list 200 非空, 后端路由通" + - name: NAS 副本数据可读(dbbardata) run: | ssh sanguo-nas "python3 -c 'import sqlite3 c=sqlite3.connect(\"/volume1/stock/sanguo_vnpy_v2/data_backup/quant_trading.db\") n=c.execute(\"SELECT COUNT(*) FROM dbbardata WHERE symbol=?\",(\"600519\",)).fetchone()[0] print(\"dbbardata 600519 rows:\", n) exit(0 if n>0 else 1)'" + - name: 提示 VPS 部署方式(验证通过后才推 prod) + run: | + echo "::notice::NAS 验证通过 ✅ 。部署 VPS 触发 vps-deploy.yml, 传 sha=${GITHUB_SHA::7}" + echo " 人工: Gitea → Actions → vps-deploy.yml → Run workflow, sha=${GITHUB_SHA}" + echo " agent: dispatch_workflow workflow_id=vps-deploy.yml inputs={sha:${GITHUB_SHA}}" - # ---------- 前端 build + 部署 NAS(与 nas-smoke 并行; VPS 无 web 不部署前端) ---------- - nas-frontend: - needs: test - runs-on: macos-arm64 - env: - HOME: /Users/chufeng - steps: - - name: checkout - env: - TOKEN: ${{ github.token }} - run: | - git init . - git config --local http.proxy "" - git config --local https.proxy "" - git remote add origin "http://oauth2:${TOKEN}@192.168.2.154:3000/${GITHUB_REPOSITORY}.git" - git fetch --depth 1 origin "${GITHUB_SHA}" - git checkout FETCH_HEAD - - name: build 前端 (node 在 /opt/homebrew/bin; npm ci 复用 HOME 的 ~/.npm cache) - run: | - export PATH="/opt/homebrew/bin:$PATH" - cd frontend - npm ci - npm run build - - name: 部署 dist 到 NAS (容器 bind mount homes/admin->/app; StaticFiles 实时读文件无需 restart) - run: | - rsync -az --delete frontend/dist/ sanguo-nas:/volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2/frontend/dist/ - - # ---------- P3: VPS CD (prod gate) ---------- - vps: - needs: [nas-smoke, nas-frontend] - runs-on: macos-arm64 - env: - HOME: /Users/chufeng - environment: prod # 人工审批 gate;删此行 = 验证通过全自动推 VPS - steps: - - name: checkout - env: - TOKEN: ${{ github.token }} - run: | - git init . - git config --local http.proxy "" - git config --local https.proxy "" - git remote add origin "http://oauth2:${TOKEN}@192.168.2.154:3000/${GITHUB_REPOSITORY}.git" - git fetch --depth 1 origin "${GITHUB_SHA}" - git checkout FETCH_HEAD - - name: promote 代码到 NAS+VPS - run: bash scripts/nas_sync/promote.sh - - name: VPS 冒烟 (LocalUnifiedProvider 真数据) - run: ssh 49.232.102.198 'cd C:\sanguo_vnpy_v2 && C:\Python310\python.exe -X utf8 scripts/data_platform/verify_unified_e2e.py' + # 注: VPS 部署已拆到 .gitea/workflows/vps-deploy.yml (workflow_dispatch 人工/agent 触发) diff --git a/.gitea/workflows/vps-deploy.yml b/.gitea/workflows/vps-deploy.yml new file mode 100644 index 0000000..b02e542 --- /dev/null +++ b/.gitea/workflows/vps-deploy.yml @@ -0,0 +1,42 @@ +name: VPS Deploy +# VPS 生产部署(人工 UI 点 Run workflow 或 agent 通过 API dispatch 触发)。 +# 与 ci-cd.yml 分离: ci-cd(push) 只到 NAS 验证; VPS 必须 nas-verify 绿后由此 workflow 主动触发。 +# +# 严格版本一致: 接收 sha input, checkout 该 commit(= NAS 验证通过的那个), 部署到 VPS。 +# 不会部署 nas-verify 未验证过的代码。 +# +# 触发方式: +# 人工: Gitea → Actions → vps-deploy.yml → Run workflow → 填 sha(NAS 验证通过的 commit) +# agent: MCP dispatch_workflow workflow_id=vps-deploy.yml ref=master inputs={sha:} +# 或 curl POST /api/v1/repos/sanguo/sanguo_vnpy_v2/actions/workflows/vps-deploy.yml/dispatches +on: + workflow_dispatch: + inputs: + sha: + description: "NAS 验证通过的 commit SHA(完整 40 位, ci-cd 的 nas-verify 输出)" + required: true + type: string + +jobs: + vps: + runs-on: macos-arm64 + env: + HOME: /Users/chufeng + steps: + - name: checkout 指定 commit(= NAS 验证通过的版本) + env: + TOKEN: ${{ github.token }} + run: | + set -e + SHA="${{ inputs.sha }}" + git init . + git config --local http.proxy "" + git config --local https.proxy "" + git remote add origin "http://oauth2:${TOKEN}@192.168.2.154:3000/${GITHUB_REPOSITORY}.git" + git fetch --depth 1 origin "$SHA" + git checkout FETCH_HEAD + echo "部署 commit: $(git rev-parse HEAD)" + - name: promote 代码到 VPS (只推 VPS, 不动 NAS) + run: bash scripts/nas_sync/promote.sh --target vps + - name: VPS 冒烟 (LocalUnifiedProvider 真数据) + run: ssh 49.232.102.198 'cd C:\sanguo_vnpy_v2 && C:\Python310\python.exe -X utf8 scripts/data_platform/verify_unified_e2e.py' diff --git a/scripts/nas_sync/promote.sh b/scripts/nas_sync/promote.sh index ac4b11e..9fa1244 100755 --- a/scripts/nas_sync/promote.sh +++ b/scripts/nas_sync/promote.sh @@ -24,20 +24,22 @@ ALL_MODS=(sanguo_api sanguo_backtest sanguo_common sanguo_data sanguo_factor sanguo_research sanguo_trader sanguo_web scripts config tests) ROOT_FILES=(pyproject.toml pytest.ini requirements-lock.txt run_web.py) -# 解析 --module 参数 +# 解析参数: --module <名>(单模块) / --target (目标机器,默认 all) 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 +TARGET="all" +while [[ $# -gt 0 ]]; do + case "$1" in + --module) + [ -z "${2:-}" ] && { echo "ERROR: --module 需要模块名. 可用: ${ALL_MODS[*]}" >&2; exit 1; } + PUSH_MOD="$2"; shift 2 + [ ! -d "$PUSH_MOD" ] && { echo "ERROR: 本地目录不存在: $PUSH_MOD" >&2; exit 1; } + ;; + --target) + case "${2:-}" in nas|vps|all) TARGET="$2"; shift 2;; *) echo "ERROR: --target 需 nas|vps|all" >&2; exit 1;; esac + ;; + *) echo "ERROR: 未知参数 $1 (支持 --module <名> / --target )" >&2; exit 1;; + esac +done # rsync exclude: 前导 / = 仅根级匹配,不影响模块内同名子目录 RSYNC_EXCLUDE=(--exclude='/data' --exclude='/data_backup' --exclude='/logs' @@ -60,21 +62,24 @@ else 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 全量镜像完成" +if [[ "$TARGET" == "nas" || "$TARGET" == "all" ]]; then + # ---- Step: Mac → NAS (rsync 代码镜像) ---- + echo "" + echo "[*] 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 fi -# ---- Step 2/3: Mac → VPS (scp 按模块/根文件推) ---- +if [[ "$TARGET" == "vps" || "$TARGET" == "all" ]]; then +# ---- Step: Mac → VPS (scp 按模块/根文件推) ---- echo "" -echo "[2/3] scp Mac → VPS ($VPS:$VPS_CODE) ..." +echo "[*] scp Mac → VPS ($VPS:$VPS_CODE) ..." if [ -n "$PUSH_MOD" ]; then MODS=("$PUSH_MOD") ROOT_PUSH=() # 单模块不推根文件 @@ -134,10 +139,11 @@ if ssh -o ConnectTimeout=20 "$VPS" "$VERIFY_CMD" 2>/dev/null | grep -q '\.py'; t else echo " ⚠️ VPS 验证未取到 $VERIFY_MOD 的 .py 文件(可能目录为空或 ssh 超时,请手动核查)" fi +fi # end target=vps|all echo "" echo "==============================================" -echo " ✅ 晋升完成 $(date '+%F %T')" +echo " ✅ 晋升完成 $(date '+%F %T') [target=$TARGET]" echo " - NAS 代码镜像: $NAS:$NAS_CODE" echo " - VPS 生产代码: $VPS:$VPS_CODE" echo " - reload: VPS 无常驻 web,下次 schtask/回测启动自动生效"