diff --git a/docs/design/23-toolchain-pr-lifecycle.md b/docs/design/23-toolchain-pr-lifecycle.md index 06081c3..336f3b2 100644 --- a/docs/design/23-toolchain-pr-lifecycle.md +++ b/docs/design/23-toolchain-pr-lifecycle.md @@ -28,7 +28,7 @@ |---|---|---|---|---| | E1 | PR 更新(push 新 commit)→ 通知 reviewer | `pull_request.synchronize` | **高** | review 驳回→修改→重 review 的关键闭环 | | ~~E2~~ | ~~PR 合并通知~~ | ~~已删除~~ | ~~—~~ | ~~和 §22 CD 成功通知重叠,已删~~ | -| E2 | PR 合并 → 通知 PR 作者 | `pull_request` (closed+merged) | **高** | PR #38 恢复:CD 通知语义不同(部署状态 vs 合并信息),文档 PR 无 CD 流程仍需通知 | +| E2 | PR 合并 → 通知 PR 作者 + 自动部署 | `pull_request` (closed+merged) | **高** | PR #38 恢复:CD 通知语义不同(部署状态 vs 合并信息),文档 PR 无 CD 流程仍需通知。PR #43:含自动部署(git pull + pm2 restart) | | E3 | Review 评论(COMMENTED)→ 通知 PR 作者 | `pull_request_review` (COMMENTED) | 中 | reviewer 讨论提问,作者应知道 | | E4 | PR 上普通评论 → 通知相关人 | `issue_comment` (on PR) | 低 | 非关键路径 | @@ -136,6 +136,21 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None: 所以 **`_EVENT_HANDLERS` 不需要修改**,只需修改 handler 内部的 action/state 分发逻辑。 +### PR 合并后自动部署(PR #43) + +**触发**:`_handle_pr_closed` 合并事件处理完成后 + +**逻辑**: +1. `git pull origin main`(安装目录) +2. 获取 PR 变更文件列表(复用 `_fetch_pr_files`) +3. 判断是否需要重启:文件路径包含 `src/`、`templates/`、`frontend/` 或 `*.py` 后缀 → 重启 +4. 纯 `docs/` 变更 → 只 pull 不重启 +5. 部署失败仅 log,不影响 Mail 通知 + +**设计决策**: +- 不做优雅等待(sentinel file 方案):daemon 正在执行任务时重启,已 spawn 的子进程独立运行不受影响,最坏情况是当前 tick 中断、下一轮 PM2 拉起后继续 +- 部署失败不阻塞通知:Mail 通知和部署解耦,保证信息触达 + ### 不做的事 | 项 | 理由 | diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 12e4314..27bcb1d 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -481,6 +481,43 @@ async def _handle_pr_closed(payload: Dict[str, Any]) -> None: title = f"PR 已合并: {pr_title} ({repo}#{pr_number})" _send_mail(pr_author, title, text) + # 自动部署:git pull + 按需 pm2 restart + try: + import subprocess + import os + + install_dir = os.environ.get("SANGUO_PROJECTS_DIR", os.path.expanduser("~/.sanguo_projects")) + repo_dir = os.path.join(install_dir, "sanguo_moziplus_v2") + + # git pull + pull_result = subprocess.run( + ["git", "pull", "origin", "main"], + cwd=repo_dir, capture_output=True, text=True, timeout=30 + ) + if pull_result.returncode == 0: + logger.info("Auto-deploy: git pull success for %s", repo) + + # 判断是否需要重启:获取 PR 变更文件列表 + files = await _fetch_pr_files(repo, pr_number) + needs_restart = any( + f.startswith("src/") or f.startswith("templates/") or f.startswith("frontend/") or f.endswith(".py") + for f in files[0] + ) + + if needs_restart: + restart_result = subprocess.run( + ["pm2", "restart", "sanguo-moziplus-v2"], + capture_output=True, text=True, timeout=15 + ) + if restart_result.returncode == 0: + logger.info("Auto-deploy: pm2 restart triggered (files: %s)", ", ".join(files[0][:5])) + else: + logger.error("Auto-deploy: pm2 restart failed: %s", restart_result.stderr) + else: + logger.warning("Auto-deploy: git pull failed: %s", pull_result.stderr) + except Exception as e: + logger.error("Auto-deploy: unexpected error: %s", e) + async def _handle_issues(payload: Dict[str, Any]) -> None: """处理 issues 事件:assigned → 通知被指派人;opened+部署失败 → 通知运维。"""