From 67d1c1ef2f7ccfae7d19b1e97ffd94d04b6a701b Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sun, 2 Aug 2026 23:25:40 +0800 Subject: [PATCH] =?UTF-8?q?ci(vps-gate):=20commit=E5=BF=85=E6=89=93[vps]/[?= =?UTF-8?q?nas]=E6=A0=87=E7=AD=BE+vps-deployed=20tag+=E5=BE=85=E6=8E=A8iss?= =?UTF-8?q?ue=20[nas]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci-cd: enforce-label(本次push无标签→fail,merge豁免) + check-vps-needed(nas-verify绿后对比vps-deployed tag,有[vps]未部署→开issue) - vps-deploy: 成功打vps-deployed tag + 关issue(gap清空) - scripts/ci/vps_pending_check.py: issue开/关/更新(CI复用) - docs: vps-impact-map触及面表 + vps-deploy-pending判定记录 - CLAUDE.md: 部署流程段补标签约定 --- .claude/CLAUDE.md | 5 ++ .gitea/workflows/ci-cd.yml | 44 +++++++++++++++-- .gitea/workflows/vps-deploy.yml | 15 ++++++ docs/vps-deploy-pending.md | 76 +++++++++++++++++++++++++++++ docs/vps-impact-map.md | 84 +++++++++++++++++++++++++++++++++ scripts/ci/vps_pending_check.py | 72 ++++++++++++++++++++++++++++ 6 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 docs/vps-deploy-pending.md create mode 100644 docs/vps-impact-map.md create mode 100644 scripts/ci/vps_pending_check.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index f42765b..bad7f2e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -140,6 +140,11 @@ NAS 上用于本项目部署的目录有两个(容器挂载源): > **职责边界(所有 session 必须遵守)**:开发/数据/策略 session 改完代码 `git push` 即完成(NAS 自动验证,到此为止);**VPS prod 部署绝不自动**,必须 nas-verify 绿后由人确认触发 `vps-deploy.yml`。**业务 session 不要自己推 VPS**(push 只到 NAS);推 VPS 是运维 agent + 人协作的决策。 +> **commit 必须打环境标签(CI 强制,所有 session 必读)**:每条 commit message 末尾带 `[vps]`(VPS 也中招,含通用代码,待推 prod)或 `[nas]`(NAS/docker 专属,VPS 不中招)。`ci-cd.yml` 的 `enforce-label` 步骤:本次 push 的 commit 无标签 → **CI 直接 fail**(merge commit 豁免)。 +> - **判定**:维度是「VPS 运行时是否触发」,不是「文件改了没」。触及面清单见 `docs/vps-impact-map.md`(VPS 触及=data/backtest/portfolio.runner_backtest/provider;不触及=orchestrator/api.web/web前端/docker/.gitea)。 +> - **`[vps]` 触发链**(Gitea 无 approval gate,用 issue+tag 替代 GitHub 的 action_required 黄色):nas-verify 绿后 CI 自动开/更新 `[待推VPS]` issue(**open=黄/待推**,持久可见);人工/agent 推 `vps-deploy.yml` 成功 → 打 `vps-deployed` tag + 自动关 issue(**close=绿/已推**)。CI run 保持绿(NAS 部署成功不污染)。 +> - **查 VPS 还差哪些**:`git log vps-deployed..HEAD --grep='\[vps\]'`(VPS 版本真相 = 移动 tag `vps-deployed`,指向最后部署 VPS 的 sha)。 + **NAS test 自动部署+验证**:改完代码 `git push origin master` → Gitea Actions 自动跑 ci-cd.yml: ``` test(pytest 数据层) → nas-deploy(推 NAS 后端+前端 dist+restart 容器) → nas-verify(login+strategy/list+dbbardata 秒级 gate) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 3571b3e..03b3afe 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -25,6 +25,31 @@ 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: enforce commit label ([vps]/[nas] 必打, merge 豁免) + if: github.event_name == 'push' + run: | + set -e + BEFORE=${{ github.event.before }} + AFTER=${{ github.event.after }} + if [[ "$BEFORE" =~ ^0+$ ]]; then + git fetch origin "$AFTER" --depth 20 + RANGE="HEAD~20..HEAD" + else + git fetch origin "$BEFORE" "$AFTER" --depth 50 + RANGE="${BEFORE}..${AFTER}" + fi + # 缺标签 = 既无 [vps]/[nas] 且非 Merge 开头 + MISSING=$(git log --format=%s "$RANGE" | grep -vE '\[(vps|nas)\]|^Merge' || true) + if [ -n "$MISSING" ]; then + echo "❌ 以下 commit 缺 [vps]/[nas] 环境标签:" + echo "$MISSING" + echo "" + echo "规范: (): [vps] # VPS 也中招, 要推 prod" + echo " (): [nas] # NAS/docker 专属, VPS 不中招" + echo "详见 docs/vps-impact-map.md" + exit 1 + fi + echo "✅ 本次 push commit 标签齐全" - name: pytest 冒烟 (venv310 数据层核心) run: | PY=/Users/chufeng/.openclaw/sanguo_projects/sanguo_vnpy_v2/venv310/bin/python @@ -96,10 +121,21 @@ jobs: 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) + - name: check-vps-needed (vps-deployed tag 对比 + 待推 issue 信号) + env: + GTOKEN: ${{ github.token }} 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}}" + set -e + git fetch origin "$GITHUB_SHA" --depth 100 2>/dev/null || true + git fetch origin refs/tags/vps-deployed:refs/tags/vps-deployed 2>/dev/null || true + if git rev-parse --verify vps-deployed >/dev/null 2>&1; then + RANGE="$(git rev-parse vps-deployed)..HEAD" + else + RANGE="HEAD~50..HEAD" + echo "⚠️ vps-deployed tag 不存在(首次/未推过 VPS), 查最近 50 commit" + fi + export VPS_LOG="$(git log --format='%h %s' "$RANGE" --grep='\[vps\]' || true)" + echo "::notice::NAS 验证通过 ✅ 。含 [vps] 则开 [待推VPS] issue(人工/agent 推 vps-deploy.yml)" + python3 scripts/ci/vps_pending_check.py # 注: VPS 部署已拆到 .gitea/workflows/vps-deploy.yml (workflow_dispatch 人工/agent 触发) diff --git a/.gitea/workflows/vps-deploy.yml b/.gitea/workflows/vps-deploy.yml index b02e542..0fdd5a0 100644 --- a/.gitea/workflows/vps-deploy.yml +++ b/.gitea/workflows/vps-deploy.yml @@ -40,3 +40,18 @@ jobs: 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' + - name: mark vps-deployed tag (VPS 版本真相, session 用 git log vps-deployed..HEAD 查未部署) + env: + TOKEN: ${{ github.token }} + run: | + set -e + SHA="${{ inputs.sha }}" + git tag -f vps-deployed "$SHA" + git push -f "http://oauth2:${TOKEN}@192.168.2.154:3000/${GITHUB_REPOSITORY}.git" refs/tags/vps-deployed + echo "✅ vps-deployed → ${SHA::7}" + - name: 关闭 [待推VPS] issue (tag=HEAD → gap 清空 = 绿) + env: + GTOKEN: ${{ github.token }} + run: | + export VPS_LOG="" + python3 scripts/ci/vps_pending_check.py diff --git a/docs/vps-deploy-pending.md b/docs/vps-deploy-pending.md new file mode 100644 index 0000000..cc955bc --- /dev/null +++ b/docs/vps-deploy-pending.md @@ -0,0 +1,76 @@ +# VPS 推送判定待办(分布式判定 + 中心汇总) + +> **机制**:各业务 session 认领自己模块的 commit,判定 `[vps]`/`[nas]`/`存疑` + 理由,回传给基建。基建汇总后**统一推一次 VPS**(清技术债 + 建立 `vps-deployed` tag 基线);判定与基建初判不一致的 case by case 讨论。 +> +> **判定标准**(详见 `docs/vps-impact-map.md`):维度是「VPS 运行时是否触发这个 bug」,**不是**「文件改了没」。 +> - VPS 触及(→`[vps]`):`sanguo_data` / `sanguo_backtest` / `sanguo_portfolio.runner_backtest` / provider / `sanguo_common` / `scripts/data_platform` +> - VPS 不触及(→`[nas]`):`sanguo_orchestrator`(仅web启) / `sanguo_api`(uvicorn) / `sanguo_web`(前端) / docker / .gitea +> +> **关键事实(已核查)**:VPS 回测入口 `sanguo_portfolio/runner_backtest.py` 直接调 `run_backtest()`,**不走 ProcessPool/orchestrator**;VPS 无常驻 web。所以 orchestrator/pool/api.uvicorn/前端 = VPS 不触及。 + +--- + +## 策略 session 认领(sanguo_backtest + sanguo_portfolio) + +| commit | message | 基建初判 | session判定 | 理由 | +|--------|---------|---------|------------|------| +| b66a6c5 | load_data 直查 dbbardata 绕 peewee(治CTA死算) | [vps] | | | +| 2ff8eca | rolling alpha/beta 向量化(治分钟级卡死) | [vps] | | | +| b2c41c7 | CTA metrics benchmark 缺失降级 | [vps] | | | +| 9588a77 | 回测可观测性 L1+L2+L3 | [vps] | | | +| 7a14d7d | strategy 字段全链透传(→runner_backtest) | [vps] | | | +| c263aa1 | cta_engine load_data logging | [nas](纯log) | | | +| 3d934f5 | cta_engine 阶段 logging | [nas](纯log) | | | +| 6d23f7b | worker _TIMEOUT 3600→10800 | [nas](orchestrator worker) | | | +| e22c8ee | max_pool 透传(model+handler+前端) | [nas](pool调度) | | | + +> ⚠️ 重点确认:`6d23f7b`/`e22c8ee` 涉及 orchestrator/pool 调度,VPS 回测若不走 ProcessPool 则 `[nas]`;若 VPS 也复用 worker/pool 逻辑则 `[vps]`。你最清楚 runner_backtest 是否触及。 + +## 前端 session 认领(sanguo_web) + +| commit | message | 基建初判 | session判定 | 理由 | +|--------|---------|---------|------------|------| +| 4c5270c | EquityCurve 去 v-if + watch flush:post | [nas] | | | +| c049e0e | MetricCards fmt 兜底字符串值 | [nas] | | | +| 857d43c | CTA结果页补净值曲线 | [nas] | | | + +> 前端 VPS 不 serve(无常驻web),应全 `[nas]`,确认即可。 + +## 后端 session 认领(sanguo_api) + +| commit | message | 基建初判 | session判定 | 理由 | +|--------|---------|---------|------------|------| +| a17f291 | /health 存活探针端点 | [nas](VPS不跑web) | | | + +> 确认 /health 改动是否只触及 uvicorn web 部分,还是有非 web 代码复用。 + +## 数据 session 认领(sanguo_data / provider) + +近期无 sanguo_data 业务 commit,本轮跳过。 + +--- + +## 已判(基建自判,业务无需复核) + +| commit | 判定 | 说明 | +|--------|------|------| +| 2cb2ab0 | [nas] | config vnpy_db,VPS 用 SANGUO_DB_PATH env 覆盖 | +| 1989a54 | [vps] | requirements-lock,VPS pip 装包参考(弱) | +| 6d786fb | [nas] | spawn PYTHONPATH+entrypoint,VPS 不跑 ProcessPool | +| c210197/0f6cdbe/5c5bd9d | [nas] | entrypoint/Dockerfile,NAS 专属 | +| ca0ec01 | [nas] | nas-verify /health gate,.gitea 不推 VPS | +| f9b7c10/7bed73a/e080885/0332aef/53daf9b | [nas] | docker/docs 系列 | + +--- + +## 最终汇总(2026-08-02,三 session 判定 + 已推 VPS) + +**[vps] 已推 VPS**(promote --target vps 推 b66a6c5,verify E2E DONE,`vps-deployed` tag→b66a6c5): +- `b66a6c5`/`2ff8eca`/`b2c41c7`(sanguo_backtest,三 session 全确认) +- `6b3d869`/`3b3e429`(数据 session 自行 scp,已到位) + +**[nas] 不推**(全员确认):`9588a77`/`7a14d7d`(**基建误判纠正**:`runner.py`≠`runner_backtest.py`,只改 web/orch/前端,VPS runner_backtest 未触及)/ `6d23f7b`/`e22c8ee`(VPS 不走 ProcessPool,max_pool 是选股池)/ `c263aa1`/`3d934f5`(纯 log)/ 前端 3 条 / `a17f291` / `2cb2ab0` + +**DB 备注**:`SANGUO_DB_PATH` env 空,`vt_setting.json` 兜底指对 VPS 库 → 不阻塞。 + +**机制交接**:后续 commit 走 `[vps]`/`[nas]` 标签,CI enforce-label 强制 + check-vps-needed 对比 `vps-deployed` tag。详见 issue #6。 diff --git a/docs/vps-impact-map.md b/docs/vps-impact-map.md new file mode 100644 index 0000000..9b7738a --- /dev/null +++ b/docs/vps-impact-map.md @@ -0,0 +1,84 @@ +# VPS 触及面清单 + Commit 环境标签约定 + +> 解决痛点:NAS 上修了一堆 bug,事后回溯"哪些 VPS 也中招、要不要推 VPS"认知负担大、易错。 +> 本文档把"事后回溯每个 commit"换成"事前对照一张稳定清单"。 +> +> **判定原则(钥匙)**:维度是「VPS 运行时是否触发这个 bug」,**不是**「文件改了没」。 +> 改了文件 ≠ VPS 中招(可能只改注释 / VPS 不跑这段);没改文件 ≠ VPS 不中招。 + +## 一、核心事实:物理层已天然隔离 + +`promote.sh` 是**白名单推送**(`ALL_MODS` + `ROOT_FILES`,见 `scripts/nas_sync/promote.sh:22-25`): +`docker/` 被 exclude(line 49),`Dockerfile` / `entrypoint.sh` / `.gitea/` 不在白名单 → **物理上进不了 VPS**。 + +所以"NAS 专属文件会不会污染 VPS"无风险。真问题是「VPS 运行时是否也中招」——用下表判。 + +## 二、VPS 触及面表(基于代码核查 2026-08-02) + +| 归属 | 模块 / 路径 | VPS 运行时场景 | +|------|-------------|----------------| +| 🔴 **VPS 触及**(改动落这 = 要考虑推 VPS) | `sanguo_data/` | schtask 每日采集(bs_eod/xt_eod/akshare) | +| 🔴 | `sanguo_backtest/` | CTA 回测入口(load_data / metrics / 引擎) | +| 🔴 | `sanguo_portfolio/runner_backtest.py` | 组合回测入口(VPS **直接调 run_backtest,不走 pool**) | +| 🔴 | `sanguo_portfolio/runner_live.py` `sanguo_live/` `sanguo_qmt_bridge/` | 实盘 / miniQMT 桥 | +| 🔴 | `sanguo_common/` | 被上述引用 | +| 🔴 | provider 取数(`sanguo_data/` 内) | verify_unified_e2e.py 验证 | +| 🔴 | `scripts/data_platform/*.py` | VPS schtask 跑的采集/merge/verify 脚本 | +| 🟡 **弱触及** | `requirements-lock.txt` | VPS 手动 pip 装,lock 仅作参考(不自动装) | +| 🟢 **NAS 专属**(不用推 VPS) | `sanguo_orchestrator/` | **仅 web(uvicorn) 启动**;VPS 不跑 web→不跑 ProcessPool→不触发 spawn | +| 🟢 | `sanguo_api/`(routes/app/uvicorn 部分) | VPS 无常驻 web | +| 🟢 | `sanguo_web/`(前端) | VPS 不 serve 前端 | +| 🟢 | `docker/` `Dockerfile` `entrypoint.sh` | VPS 无 docker | +| 🟢 | `.gitea/workflows/` | NAS/Mac runner 跑 | +| 🟢 | `config/`(vnpy_db 等 NAS 容器 config) | VPS 用 `SANGUO_DB_PATH` env 覆盖,不读此 config | + +### ⚠️ 混合模块(需看具体文件,不能整模块一刀切) + +| 模块 | VPS 触及部分 | NAS 专属部分 | +|------|--------------|--------------| +| `sanguo_portfolio/` | `runner_backtest.py` `runner_live.py`(回测/实盘主体) | 若改动仅触及 orchestrator 调度(pool/worker/timeout)→ NAS 专属 | +| `sanguo_api/` | 若 model/schema 被 runner 复用(待核对) | routes/app(web) | + +> 判定混合模块:看 diff 的具体文件路径,对照上表"VPS 触及部分"。 + +## 三、Commit 环境标签约定(CI 强制,以后 commit 必打) + +CI 的 `enforce-label` 步骤强制:本次 push 的 commit **必须**带 `[vps]` 或 `[nas]`,无标签 → **CI 直接 fail**(merge commit 豁免)。 + +``` +(): [nas] # NAS/docker 专属(VPS 不中招) +(): [vps] # VPS 也中招(含通用 shared 代码), 待推 VPS +``` + +- 一条 commit 混合多环境 → 打主导面 + 正文注明(如 `[nas] 注:runner_backtest.py 一行属 vps`)。 +- `[vps]` 触发链:nas-verify 绿后 CI 自动开/更新 `[待推VPS]` issue(**open=黄/待推**);人工/agent 推 vps-deploy.yml → 成功打 `vps-deployed` tag → issue 自动关(**close=绿/已推**)。 + +## 四、判定流程(3 步,30 秒) + +1. **看 scope/标签**:commit message 有 `[nas]/[vps]/[vps]` → 直接得结论。 +2. **无标签则对照触及面表**:diff 的文件路径落在 🔴 区 = 要推 VPS;落在 🟢 区 = 不用推;🟡/混合 = 复核。 +3. **复核运行时语义**(混合/存疑时):问"VPS 跑这段代码吗?"——对照上表 VPS 运行时场景列。 + +## 五、近期 commit 判定(2026-08-02 回溯,含技术债标注) + +| commit | scope | 标签 | VPS 适用 | 说明 | +|--------|-------|------|----------|------| +| `b66a6c5` fix(ashare_engine) load_data 绕 peewee | sanguo_backtest | **[vps]** | 🔴 **要推** | VPS CTA 回测走 load_data,库大也死锁 | +| `2ff8eca` perf(metrics) rolling 向量化 | sanguo_backtest | **[vps]** | 🔴 **要推** | 分钟级卡死,VPS 算 metrics 也卡 | +| `b2c41c7` fix(backtest) benchmark 缺失降级 | sanguo_backtest | **[vps]** | 🔴 **要推** | VPS 回测也走 metrics | +| `9588a77` feat(portfolio) 回测可观测性 | runner_backtest | **[vps]** | 🔴 **要推** | VPS 回测入口 | +| `7a14d7d` feat(portfolio) strategy 全链透传 | runner_backtest | **[vps]** | 🔴 **要推** | 改到 runner_backtest(API→orchestrator 部分是 NAS) | +| `c263aa1` `3d934f5` chore(cta_engine) logging | sanguo_backtest | [vps] | 🟡 可推 | 无害 logging | +| `6d23f7b` fix(portfolio) worker _TIMEOUT | orchestrator worker | [nas] | 🟢 不用推 | pool 超时,VPS 不走 pool | +| `e22c8ee` feat(portfolio) max_pool 透传 | model+handler+前端 | [nas] | 🟢 不用推 | pool 调度参数,VPS 不走 pool | +| `2cb2ab0` fix(config) vnpy_db NAS dbbardata | config | [nas] | 🟢 不用推 | VPS 用 env 覆盖不读 config | +| `4c5270c` `c049e0e` `857d43c` fix(frontend) | sanguo_web | [nas] | 🟢 不用推 | 前端,VPS 无 web | +| `a17f291` fix(api) /health 端点 | sanguo_api | [nas] | 🟢 不用推 | VPS 不跑 web | +| `6d786fb` fix(orchestrator) spawn PYTHONPATH | orchestrator+entrypoint | [nas] | 🟢 不用推 | VPS 不跑 ProcessPool | +| `c210197` `0f6cdbe` `5c5bd9d` entrypoint/Dockerfile | docker | [nas] | 🟢 不用推 | NAS 专属 | +| `ca0ec01` ci(nas-verify) /health gate | .gitea | [nas] | 🟢 不用推 | 不推 VPS | +| `1989a54` fix(deps) requirements-lock | deps | [vps] | 🟡 弱 | VPS pip 参考 | + +**⚠️ 技术债(🔴 标注的 5 条,VPS 回测逻辑修复)**:`b66a6c5` `2ff8eca` `b2c41c7` `9588a77` `7a14d7d`。 +若 VPS 当前版本落后于这些 commit,VPS 回测仍带病(CTA 死锁/metrics 卡死/可观测性缺失/strategy 字段)。 +→ **建议**:确认 VPS 当前 sha,若落后则触发一次 `vps-deploy.yml` 补推(nas-verify 已绿)。 diff --git a/scripts/ci/vps_pending_check.py b/scripts/ci/vps_pending_check.py new file mode 100644 index 0000000..99fa186 --- /dev/null +++ b/scripts/ci/vps_pending_check.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""CI 用: 维护 [待推VPS] issue 信号(Gitea 无 approval gate, 用 issue open/close 当黄/绿)。 + +被 ci-cd.yml 的 nas-verify 与 vps-deploy.yml 复用: + - nas-verify 后: VPS_LOG=vps-deployed..HEAD 的 [vps] commit → 有则开/更新待推 issue + - vps-deploy 成功后(已打 vps-deployed=HEAD): VPS_LOG="" → 关闭所有待推 issue + +env: + GTOKEN Gitea token(操作 issue) + GITHUB_REPOSITORY sanguo/sanguo_vnpy_v2 + GITHUB_SHA 本次 commit + VPS_LOG 待推 commit 的 "git log --format='%h %s'" 输出(空=无待推) +""" +import os +import json +import urllib.request + +API_BASE = "http://192.168.2.154:3000/api/v1/repos/" + + +def main(): + token = os.environ["GTOKEN"] + repo = os.environ["GITHUB_REPOSITORY"] + sha = os.environ["GITHUB_SHA"] + vps_log = os.environ.get("VPS_LOG", "").strip() + api = API_BASE + repo + + def req(method, path, data=None): + r = urllib.request.Request(api + path, method=method) + r.add_header("Authorization", "token " + token) + r.add_header("Content-Type", "application/json") + if data is not None: + r.data = json.dumps(data).encode() + try: + with urllib.request.urlopen(r, timeout=20) as resp: + body = resp.read() + return json.loads(body) if body else {} + except Exception as e: + print(f" API {method} {path} 失败: {e}") + return {} + + issues = req("GET", "/issues?state=open&type=issues") or [] + pending = [i for i in issues if str(i.get("title", "")).startswith("[待推VPS]")] + + if not vps_log: + # 无待推 → 关闭所有待推 issue(gap 清空 = 绿) + print("✅ 无待推 VPS commit (vps-deployed..HEAD 无 [vps])") + for i in pending: + req("PATCH", f"/issues/{i['number']}", {"state": "closed"}) + print(f" 已关闭待推 issue #{i['number']} (gap 清空)") + return + + n = len([l for l in vps_log.splitlines() if l.strip()]) + print(f"⚠️ 待推 VPS: {n} 个 [vps] commit") + print(vps_log) + body = ( + f"nas-verify 绿 @{sha[:7]}, 以下 [vps] commit 待推 VPS:\n" + f"```\n{vps_log}\n```\n\n" + f"推法: Gitea → Actions → vps-deploy.yml → Run workflow, sha={sha}\n" + f"agent: dispatch vps-deploy.yml inputs={{sha:{sha}}}" + ) + if pending: + for i in pending: + req("POST", f"/issues/{i['number']}/comments", {"body": body}) + print(f" 已更新待推 issue #{i['number']}") + else: + res = req("POST", "/issues", {"title": f"[待推VPS] {n} 个 commit 待推生产", "body": body}) + print(f" 已开待推 issue #{res.get('number', '?')} (open=黄/待推)") + + +if __name__ == "__main__": + main()