feat(toolchain): add PR synchronize and review comment notifications
CI / lint (pull_request) Successful in 6s
CI / test (pull_request) Successful in 8s
CI / notify-on-failure (pull_request) Successful in 0s

- pull_request.synchronize: notify reviewer to re-review after push
- pull_request_review COMMENTED: notify PR author of review comments
- New templates: review_updated.md, review_comment.md
- Idempotency: add review ID to content dedup key
- Design doc: docs/design/23-toolchain-pr-lifecycle.md
This commit is contained in:
cfdaily
2026-06-11 14:00:44 +08:00
parent d6612de6de
commit a4bb752d71
6 changed files with 346 additions and 8 deletions
+81
View File
@@ -0,0 +1,81 @@
# 22 — CD 生产环境落地方案
> 状态:草案,待评审
> 作者:庞统
> 日期:2026-06-11
## 背景
CD 管道已在测试仓库 `sanguo/mojiplus-v2` 验证通过(run#282 全链路 success)。
现需将 CD 落地到生产仓库 `sanguo/sanguo_moziplus_v2`
### 当前状态
| 组件 | 状态 |
|---|---|
| `deploy.sh` | ✅ 完整(rsync + build + pm2 restart + health check + deploy history |
| `deploy.yml` | ⚠️ deploy job 已调用 deploy.sh,但缺少成功通知 |
| CI 失败通知 | ✅ ci.yml → PR comment → webhook → Mail |
| Deploy 失败通知 | ✅ deploy.yml → Issue → webhook → Mail |
| Deploy 成功通知 | ❌ 缺失 |
## 改动方案
### 改动 1deploy.yml 增加 deploy 成功通知
在 deploy job 最后增加一个 stepdeploy 成功后:
1. 从 Gitea API 查询触发 commit 关联的 merged PR
2. 获取 PR 作者
3. 通过 Mail API 发送成功通知给 PR 作者 + pangtong-fujunshi
4. 如果是 direct push(非 PR merge),只通知 jiangwei-infra + pangtong-fujunshi
**文件**`.gitea/workflows/deploy.yml`
**改动范围**deploy job 内新增 1 个 step(约 30 行 shell
**关键逻辑**
```bash
# 查询关联 PR
PR_INFO=$(curl -sf \
-H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$REPO/pulls?state=closed&limit=5" | \
python3 -c "
import json,sys
for pr in json.load(sys.stdin):
if pr.get('merge_commit_sha','') == '$COMMIT_SHA':
print(pr['user']['login'])
break
" 2>/dev/null || echo "")
# 发 Mail
curl -s -X POST http://localhost:8083/api/mail \
-H "Content-Type: application/json" \
-d "{\"from\":\"daemon\",\"to\":\"$PR_AUTHOR\",\"title\":\"...\",\"text\":\"...\",\"type\":\"inform\"}"
```
**约束**
- 使用 `if: always()` + shell 判断 `needs.deploy.result == "success"`,确保只在成功时执行
- GITEA_TOKEN 通过 secrets 注入
- Mail API 调用超时 5 秒,失败不影响部署结果
### 不改的文件
| 文件 | 原因 |
|---|---|
| `src/api/toolchain_routes.py` | 不新增 webhook 事件,deploy 成功通知在 yml 内闭环 |
| `src/daemon/toolchain_handler.py` | 不涉及 |
| `templates/toolchain/*.md` | 不新增模板,通知内容直接在 shell 中构建 |
| `scripts/deploy.sh` | 已完整,不需改动 |
## 影响范围
- **风险**:低。只在 deploy job 末尾追加通知 step,不修改已有的 deploy/notify 逻辑
- **回退**:删除新增 step 即可
- **测试**push main 后观察 deploy workflow 执行结果
## E2E 验证计划
1. 在生产仓库创建测试分支,push → PR → merge → 触发 deploy
2. 验证 deploy 成功后 Mail 通知到达 PR 作者
3. 验证部署文件同步到 `~/.sanguo_projects/sanguo_moziplus_v2/`
4. 验证 health check 通过
+144
View File
@@ -0,0 +1,144 @@
# §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 成功通知重叠,已删~~ |
| 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 记录(首次 push 后 reviewer 还没 review),跳过(opened 事件已经通知过了)
4. 渲染 `review_updated.md` 模板,发送 Mail 给 reviewer
**关键设计决策**
- 不用 `requested_reviewers`(可能为空),用最近 review 的提交者
- 只在有 review 历史时才通知(避免 opened + synchronize 重复通知)
- Mail from 用 `system`
### ~~Handler 2PR 合并通知~~ — 已删除
> 司马懿 review 指出与 §22 CD 成功通知重叠。CD 成功通知已隐含合并信息,无需单独发 merged 通知。
### 新增 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 |
| ~~`pr_merged.md`~~ | ~~已删除~~ | ~~—~~ |
| `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 分发逻辑。
### 不做的事
| 项 | 理由 |
|---|---|
| E4 PR 上普通评论通知 | 低优,非关键路径,后续按需加 |
| Issue 关闭通知 | 低优,关怀性质 |
| reviewer 从 `requested_reviewers` 取 | 不可靠(可能为空),用最近 review 记录更稳定 |
## 改动范围
| 文件 | 改动 |
|---|---|
| `src/api/toolchain_routes.py` | 修改 `_handle_pull_request`(扩展 action 分发)+ 新增 `_handle_pr_synchronize` + 修改 `_handle_pull_request_review`(支持 COMMENTED |
| `templates/toolchain/review_updated.md` | 新增 |
| ~~`templates/toolchain/pr_merged.md`~~ | ~~已删除~~ |
| `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. merged~~:已删除
3. **COMMENTED**:review 提交纯评论 → 验证 PR 作者收到通知
## 风险评估
- **风险等级**:低。新增事件处理,不修改现有 handler 逻辑
- **幂等性**:复用现有 `_is_duplicate` 机制
- **性能**synchronize handler 有一次 Gitea API 调用(查 review 历史),频率低(只在 push 后触发)