fix: 支持 test→prd 两级流转的自动合并与 synchronize 事件

1. 自动合并门槛过严
   上一版要求 PR 目标必须等于 managed_branch 才自动合并,
   这会让两级流转(feature -> test -> prd)的第一级永远合不进去。
   改为:目标命中任一检查分支即可自动合并。
   managed_branch 退化为「push 无 PR 时的对比基准」与默认值。

2. Gitea 的 PR 更新事件名是 synchronized
   Gitea webhook 的 payload.action 用 synchronized,
   只有它的 Actions runner 才会改写成 GitHub 风格的 synchronize。
   原先只认 synchronize,导致「PR 有新提交」永远不触发。
   现在两种拼写都接受。

验证(真实 webhook,非手工触发):
- feature -> test 开 PR:pull_request.opened 自动入队并审查
- 再推一个 commit:pull_request.synchronized 自动入队
- 无阻断问题 -> commit status success -> 自动 squash 合并,PR #4 已 merged
- 向未受保护分支推送会被正确过滤
This commit is contained in:
2026-09-20 14:54:33 +08:00
parent 34f2897888
commit ab509eee0d
3 changed files with 27 additions and 8 deletions
+15 -2
View File
@@ -162,7 +162,7 @@ curl -fsS http://localhost:8090/api/health
| 字段 | 说明 |
| --- | --- |
| `repo_url` | 仓库 Git 地址,`owner` / `name` 由此自动解析 |
| `managed_branch` | 唯一的管理分支,所有审查都相对它做对比,也是唯一允许自动合并的目标 |
| `managed_branch` | 主管理分支。push 事件没有对应 PR 时用它做对比基准;自动合并不限于它,任一检查分支都可作为合并目标 |
| `check_branches` | 受保护的目标分支列表,逗号分隔。任何**合并进**这些分支的 PR 都会审查(功能分支不必列出),向这些分支推送也会审查 |
| `review_scope` | `both` / `pr` / `push` |
| `block_severity` | 阻断级别阈值,如 `critical,high`;留空则不看级别 |
@@ -217,7 +217,17 @@ curl -X POST http://localhost:8090/api/review \
## 行为说明
**分支与合并**:`check_branches` 是**被保护的目标分支**。任何合并进这些分支的 PR 都会被审查,所以功能分支不必列进去;向这些分支直接推送也会审查。审查基准是该改动实际要合入的分支(PR 用 PR 自己的目标分支,push 用管理分支),也就是「这次改动合进去会怎样」。自动合并只作用于目标为 `managed_branch` 的 PR。
**分支与合并**:`check_branches` 是**被保护的目标分支**。任何合并进这些分支的 PR 都会被审查,所以功能分支不必列进去;向这些分支直接推送也会审查。审查基准是该改动实际要合入的分支(PR 用 PR 自己的目标分支,push 用管理分支),也就是「这次改动合进去会怎样」。
自动合并作用于目标为**任一检查分支**的 PR,因此两级流转可以直接用:
```text
feature/xxx ──PR──> test ──PR──> prd
↑ 第一级:审查通过即自动合并
↑ 第二级:同样审查通过才合并
```
`managed_branch` 只决定「push 事件没有对应 PR 时,拿哪个分支做对比基准」,以及新仓库的默认值。
**审查范围**:PR 事件优先用 PR 的目标分支作为基准;push 事件优先用 webhook 里的 `before`,当 `before` 是新建分支的全零值或缺失时,退回到与管理分支的 merge-base。
@@ -246,6 +256,9 @@ docker compose build # 构建镜像
**PR 提交了却没有触发审查**
先看仓库列表的 Webhook 列:显示「未配置」说明 Gitea 不会发事件,点「修复 Webhook」即可。其次确认 `check_branches` 是否包含该 PR 的**目标分支**(不是功能分支)。
**审查通过了却没有自动合并**
看任务日志里的 `auto-merge skipped` 原因。常见几类:`auto_merge` 开关没打开;PR 目标分支不在 `check_branches` 里;存在阻断问题;审查覆盖不完整(OCR 报了 partial/警告/token 预算截断);提交状态不是 success;PR head 已变化或不可合并。
**Webhook 投递失败,提示 `webhook can only call allowed HTTP servers`**
Gitea 拦截了内网地址。按上文给 `[webhook] ALLOWED_HOST_LIST` 加上本服务地址,重启 Gitea。
+5 -5
View File
@@ -654,11 +654,11 @@ export class ReviewEngine {
appendLog("auto-merge skipped: no pull request for this branch");
return false;
}
// Only merges into the managed branch are performed; a PR targeting any
// other branch is reviewed but never auto-merged.
const managed = repo.managed_branch || repo.base_branch;
if (job.base_ref && managed && job.base_ref !== managed) {
appendLog(`auto-merge skipped: PR targets ${job.base_ref}, managed branch is ${managed}`);
// A two-stage flow (feature -> test -> prd) legitimately merges into a
// checked branch that is not the managed branch, so the gate is "is the
// target one of the branches we protect", not "is it the managed branch".
if (job.base_ref && !branchMatches(job.base_ref, repo.check_branches)) {
appendLog(`auto-merge skipped: PR targets ${job.base_ref}, which is not a checked branch`);
return false;
}
if (reviewIncomplete) {
+7 -1
View File
@@ -216,9 +216,15 @@ async function handlePush(payload, cfg) {
return { queued: 1, jobId };
}
// Gitea's webhook payload uses "synchronized"; only its Actions runner rewrites
// that to the GitHub-style "synchronize", so accept every spelling.
const PR_ACTIONS = new Set([
"opened", "reopened", "synchronize", "synchronized", "ready_for_review",
]);
async function handlePullRequest(payload, cfg) {
const action = payload.action;
if (!["opened", "synchronize", "reopened", "ready_for_review"].includes(action)) {
if (!PR_ACTIONS.has(action)) {
return { queued: 0, reason: `action ${action} ignored` };
}
const owner = payload.repository?.owner?.username || payload.repository?.owner?.login;