Files
sanguo_moziplus_v2/docs/design/23-toolchain-pr-lifecycle.md
T
cfdaily 73454c0787
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 8s
CI / notify-on-failure (pull_request) Successful in 1s
refactor(auto-deploy): YAML config + post_deploy list + deploy failure mail
- New config/deploy-targets.yaml: centralized deploy target config
- Rewrite auto-deploy in _handle_pr_closed to use YAML config
- Add _send_deploy_failure_mail helper (reuses deploy_failure template)
- Support post_deploy command list (not just pm2 restart)
- Docs-only changes skip post_deploy
- Add pyyaml to pyproject.toml dependencies
- Update design doc §23 with new architecture
2026-06-12 13:57:55 +08:00

207 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# §23 — 工具链事件中枢补全:PR 全生命周期通知
> 状态:草案,待评审
> 作者:庞统
> 日期:2026-06-11
> 框架:基于 §20 Task Type Architecture + §13 工具链设计
## 背景
### 问题
工具链事件中枢(`toolchain_routes.py`)当前只覆盖了 PR 生命周期中约一半的交互节点。review 驳回后 PR 作者修改代码,没有机制通知 reviewer 重新 review——流程在这里断链。
### 当前覆盖
| 事件节点 | handler | 模板 | 状态 |
|---|---|---|---|
| PR 创建 → 通知 reviewer | `_handle_pull_request` (opened) | `review_request.md` | ✅ |
| Review 通过 → 通知 PR 作者 | `_handle_pull_request_review` (APPROVED) | `review_result.md` | ✅ |
| Review 驳回 → 通知 PR 作者 | `_handle_pull_request_review` (REQUEST_CHANGES) | `review_result.md` | ✅ |
| Issue 指派 → 通知被指派人 | `_handle_issues` (assigned) | `issue_assigned.md` | ✅ |
| CI 失败评论 → 通知 | `_handle_issue_comment` ([CI]) | `ci_failure.md` | ✅ |
| 部署失败 Issue → 通知 | `_handle_issues` (opened+"部署失败") | `deploy_failure.md` | ✅ |
### 缺失节点
| # | 事件节点 | Gitea 事件 | 优先级 | 理由 |
|---|---|---|---|---|
| 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 流程仍需通知。PR #43:含自动部署(git pull + pm2 restart |
| E3 | Review 评论(COMMENTED)→ 通知 PR 作者 | `pull_request_review` (COMMENTED) | 中 | reviewer 讨论提问,作者应知道 |
| E4 | PR 上普通评论 → 通知相关人 | `issue_comment` (on PR) | 低 | 非关键路径 |
## 方案
### 框架对齐
按 §20 Task Type Architecture,新增事件处理遵循:
1. `_EVENT_HANDLERS` 映射 → 路由到对应 handler 函数
2. handler 提取变量 → `render_template()` 渲染模板
3. `_TEMPLATE_MAP` 注册模板名 → `templates/toolchain/` 下新建模板文件
4. 通知目标通过 Gitea username → `to_agent_id()` 映射
### 新增 Handler 1`_handle_pull_request_synchronize`
**触发**`pull_request` 事件 + `action=synchronize`PR 分支有新 push
**通知对象**PR 的 reviewer(从 PR 的 `requested_reviewers` 或最近一次 non-COMMENTED review 的提交者)
**实现**
修改 `_handle_pull_request` 的 action 过滤,从只处理 `opened` 扩展为同时处理 `synchronize`
```python
async def _handle_pull_request(payload: Dict[str, Any]) -> None:
action = payload.get("action", "")
if action == "opened":
await _handle_pr_opened(payload)
elif action == "synchronize":
await _handle_pr_synchronize(payload)
# 其他 action 忽略
```
新增 `_handle_pr_synchronize`
1. 从 payload 取 PR 信息(number、title、author、head sha
2. 查询最近一次 reviewGitea API `GET /repos/{owner}/{repo}/pulls/{number}/reviews`)取 reviewer
3. 如果没有 review 记录(`_fetch_latest_reviewer()` 返回 None),fallback 到默认 reviewer `simayi-challenger`,而非跳过通知(PR #38 改动:确保无 review 历史时也能通知默认审查者)
4. 渲染 `review_updated.md` 模板,发送 Mail 给 reviewer
**关键设计决策**
- 不用 `requested_reviewers`(可能为空),用最近 review 的提交者
- 无 review 历史时 fallback 到默认 reviewer `simayi-challenger`PR #38:避免 opened + synchronize 间隔较短时 reviewer 未收到任何通知)
- Mail from 用 `system`
### Handler 2`_handle_pr_closed`PR 合并通知)— PR #38 恢复
**触发**`pull_request` 事件 + `action=closed` + `merged=true`
**通知对象**PR 作者
**实现**
修改 `_handle_pull_request` 的 action 分发,新增 `closed` 分支:
```python
async def _handle_pull_request(payload: Dict[str, Any]) -> None:
action = payload.get("action", "")
if action == "opened":
await _handle_pr_opened(payload)
elif action == "synchronize":
await _handle_pr_synchronize(payload)
elif action == "closed" and payload.get("pull_request", {}).get("merged"):
await _handle_pr_closed(payload)
# 其他 action 忽略
```
新增 `_handle_pr_closed`
1. 从 payload 取 PR 信息(number、title、merged_by
2. `merged_by` 优先从 `payload["pull_request"]["merged_by"]` 取,若为空则 fallback 到 `payload["sender"]`PR #38:兼容不同 Gitea 版本和 merge 方式)
3. 渲染 `review_merged.md` 模板,发送 Mail 给 PR 作者
**恢复说明**:此前因与 §22 CD 成功通知重叠而删除。但实际场景中 CD 通知发的是部署状态,PR 作者更关心的是"谁帮我 merge 了"这个信息,两者语义不同。且 CD 流程不一定每次都触发(如文档 PR),merge 通知仍需独立存在。(PR #38 恢复)
### 新增 Handler 3review COMMENTED 处理
**触发**`pull_request_review` 事件 + `state=COMMENTED`
**通知对象**PR 作者(不是 reviewer
**实现**
修改现有 `_handle_pull_request_review`,当前逻辑是"非 COMMENTED 才通知",改为 COMMENTED 也通知,但用不同模板:
```python
# 现有逻辑:非 COMMENTED 通知 PR 作者
if state in ("APPROVED", "REQUEST_CHANGES"):
template_name = "review_result"
elif state == "COMMENTED":
template_name = "review_comment"
else:
return # PENDING 等忽略
```
### 新增模板
| 模板文件 | 变量 | 说明 |
|---|---|---|
| `review_updated.md` | repo, pr_number, pr_title, pr_author, branch, new_sha, reviewer | PR 有新 commit,请重新 review |
| `review_merged.md` | repo, pr_number, pr_title, pr_author, merged_by | PR 已合并,通知作者(PR #38 恢复) |
| `review_comment.md` | repo, pr_number, pr_title, reviewer, comment_body | reviewer 提交了评论 |
### `_EVENT_HANDLERS` 无需改动
`synchronize``closed` 都是 `pull_request` 事件的 action 子类型,已映射到 `_handle_pull_request`。COMMENTED 是 `pull_request_review` 的 state 子类型,已映射到 `_handle_pull_request_review`
所以 **`_EVENT_HANDLERS` 不需要修改**,只需修改 handler 内部的 action/state 分发逻辑。
### PR 合并后自动部署(PR #43
**触发**`_handle_pr_closed` 合并事件处理完成后
**逻辑**
1. 读取 `config/deploy-targets.yaml`,查找 `repo` 对应的部署目标
2. 不在配置中 → 跳过(未来新项目加一条配置即可)
3. `git pull origin main`(开发目录)
4. `rsync` 同步到安装目录(排除项由配置指定)
5. 判断是否需要执行 post_deploy:文件路径包含 `src/``templates/``frontend/``*.py` 后缀
6.`docs/` 变更 → 只 pull + rsync,不执行 post_deploy
7. 部署失败复用 `deploy_failure.md` 模板通知 `jiangwei-infra` + `pangtong-fujunshi`
**配置文件**`config/deploy-targets.yaml`(集中管理所有仓库的部署目标)
```yaml
targets:
sanguo/sanguo_moziplus_v2:
dev_dir: ~/.openclaw/sanguo_projects/sanguo_moziplus_v2
install_dir: ~/.sanguo_projects/sanguo_moziplus_v2
pm2_name: sanguo-moziplus-v2
rsync_exclude: [.git/, node_modules/, __pycache__/, data/]
health_check: http://localhost:8083/health
post_deploy: [pm2 restart sanguo-moziplus-v2]
```
**设计决策**
- **集中式 YAML 配置**(姜维建议):部署是平台级能力,非仓库级。一个文件管所有仓库,新增项目零代码改动
- **YAML 而非 JSON**:支持注释,方便临时禁用某个仓库或排除项
- **post_deploy 列表**:支持任意 shell 命令,不只是 pm2 restart。未来可扩展(如 pip install -e .
- **health_check 字段**:预留,后续可用于部署后健康检查
- **失败通知复用**:CD 失败和 CI 失败用同一套通知机制(deploy_failure.md 模板 + _send_mail
- git pull 在开发目录(有 `.git`),rsync 到安装目录
- 全异步调用(asyncio.create_subprocess_exec
### 不做的事
| 项 | 理由 |
|---|---|
| E4 PR 上普通评论通知 | 低优,非关键路径,后续按需加 |
| Issue 关闭通知 | 低优,关怀性质 |
| reviewer 从 `requested_reviewers` 取 | 不可靠(可能为空),用最近 review 记录更稳定 |
## 改动范围
| 文件 | 改动 |
|---|---|
| `src/api/toolchain_routes.py` | 修改 `_handle_pull_request`(扩展 action 分发 + closed 分支)+ 新增 `_handle_pr_synchronize` + `_handle_pr_closed` + 修改 `_handle_pull_request_review`(支持 COMMENTED |
| `templates/toolchain/review_updated.md` | 新增 |
| `templates/toolchain/review_merged.md` | 新增(PR #38 恢复) |
| `templates/toolchain/review_comment.md` | 新增 |
| `src/daemon/toolchain_templates.py` | `_TEMPLATE_MAP` 新增 3 个映射 |
| `docs/design/23-toolchain-pr-lifecycle.md` | 本文档 |
## 验证计划
`sanguo/moziplus-v2` 测试仓库上 E2E 验证:
1. **synchronize**:创建 PR → review 驳回 → push 新 commit → 验证 reviewer 收到"请重新 review" Mail
2. **synchronize fallback**PR #38):创建 PR → push commit(无 review 历史)→ 验证默认 reviewer (`simayi-challenger`) 收到通知
3. **merge 通知**PR #38 恢复):PR merge → 验证 PR 作者收到合并通知 Mail
4. **COMMENTED**:review 提交纯评论 → 验证 PR 作者收到通知
## 风险评估
- **风险等级**:低。新增事件处理,不修改现有 handler 逻辑
- **幂等性**:复用现有 `_is_duplicate` 机制
- **性能**synchronize handler 有一次 Gitea API 调用(查 review 历史),频率低(只在 push 后触发)