Files
git-codereview/README.md
T
kgod a0b15fb2d9 feat: 打通 推送→审查→晋级→关闭 Issue 的完整闭环
直接推送也能晋级
  往受保护分支直接推代码(没有 PR)时,审查通过后自动开一个
  <分支> → <管理分支> 的 PR 并合并它,而不是放弃自动合并。
  已存在同类 open PR 时复用它,不重复创建。

晋级 PR 不再被重复审查
  自动创建的 PR 带 PROMOTION_MARKER,其 pull_request 事件直接跳过。
  同一批提交在 push 时已经审过,重复审会在合并完成后凭空造出 Issue
  (此前确实产生了这样一个幽灵 Issue)。

合并即关闭该仓库全部审查 Issue
  两条触发路径:
  - 服务自己完成合并后立即关闭
  - 人在 Gitea 手动合并时,pull_request closed + merged=true 事件触发关闭
  手动合并是「代码已落地」最可靠的信号,不再依赖 push 侧的 merge 提交探测。

晋级使用 merge 而非 squash
  squash 会把晋级提交重写成全新提交,两个长期分支每晋级一次就多分叉
  一点,最终必然冲突(已在 offerpai_h5 上复现 add/add 冲突)。
  merge 保留共同祖先,下次晋级只携带新提交。

合并可等待性
  Gitea 异步计算 mergeable,原先只轮询 5 秒就放弃。改为指数退避约 30 秒,
  并区分「尚未算出」(继续等)与「确实冲突」(放弃)。失败时往 PR 留评论
  说明原因,不再只写服务日志。

验证(真实 webhook):
  推送 test → 审查通过 → 自动开 PR #9 (test→prd) → merge 合并
  → pull_request merged 事件 → 关闭 0 个待处理 Issue
  prd 顶端前进为合并提交,test/prd 保持一致,无幽灵 Issue 产生
2026-09-20 16:23:45 +08:00

292 lines
16 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.
# Gitea 自动代码审查
基于 [OpenCodeReview](https://github.com/alibaba/open-code-review)(OCR)的 Gitea 自动代码审查服务。
监听仓库推送和 Pull Request,调用 OCR 分析 diff,把结果发布回 Gitea,并按规则决定是否自动合并。
## 它做什么
```text
Push / Pull Request 事件
↓ Gitea Webhook(HMAC 签名)
gitea-codereview
↓ git fetch + OCR 审查
OpenCodeReview CLI → LLM
↓ 结构化 JSON(文件 / 行号 / 类别 / 严重级别)
Gitea:内联评论 + 汇总评论 + Issue + 提交状态
↓ 无阻断问题且开启自动合并
自动合并 / 等待检查通过后合并
```
## 功能
**仓库接入**
- 「从 Gitea 导入」一键列出全局 Token 可见的仓库,点「添加」即建好监控配置
- 手动新增时只填 Git 地址,自动识别 `所有者/仓库名`,旁边可「测试连接」验证
- 支持 https / ssh / scp 三种地址写法
- 保存仓库时**自动在 Gitea 里创建 Webhook**,无需手工配置;仓库列表会显示
Webhook 状态,未配置可一键「修复 Webhook」
**分支模型**
- 一个「管理分支」作为唯一合并目标
- 多个「检查分支」全部纳入监控,点选即可增减
- 管理分支不存在时可「克隆创建」,从任意已有分支复制
- 审查始终以管理分支为对比基准,即「这些改动合入管理分支会怎样」
**审查触发**
- 监听检查分支的推送,对比管理分支的 merge-base
- 监听 Pull Request 的 opened / synchronize / reopened / ready_for_review
- 可选只审 PR 或只审 push
- 同一 commit 在队列中只入队一次;webhook 按 delivery id 去重
**发布结果**
- PR 内联评论,带 `[category · severity]` 标记和 `suggestion` 代码块
- 每条内联评论都校验是否落在本次 diff 的行上,落不到的汇总进审查总结
- 提交状态 `code-review/ocr`(success / failure / error),可配成必需检查
- 同一仓库同一分支只维护一个 Issue:有问题时创建或更新,修好后自动关闭
- 可配置阻断阈值:按严重级别、按类别,或任何意见都算
**自动合并**
- 仅在无阻断问题、审查覆盖完整、提交状态为 success 时才触发
- 两种模式:`when_checks_succeed`(等分支保护检查通过)和 `immediate`
- 合并方式、是否删除分支可配
- PR head 已变化或 PR 不可合并时自动放弃
**审查历史**
- 「审查历史」页按仓库汇总每次审查的摘要,可一键跳转 PR
- 每条记录保留完整上下文:触发来源、PR 链接、审查范围、阻断阈值、发布方式、
自动合并设置、排除路径、LLM 模型、token 消耗、耗时、本次填写的需求背景
- 详情弹窗列出本次全部审查意见,标注严重级别与是否阻断
**AI 摘要**
- 独立的摘要模块,与代码审查的提示词完全分开,但共用同一份全局 LLM 设置
- 专用提示词把一次审查压缩成固定四节、500 字内的记录:
`结论 / 范围 / 问题 / 要点`
- 摘要失败不影响审查结果,可单条重新生成
**运维**
- 内置 Web 后台:仓库配置、审查历史、任务列表、实时日志、重跑、连通性自检
- 每次审查的完整日志和结果 JSON 落库
- worker 重启后自动回收卡住的任务,失败任务自动重试一次
- 全局或按仓库覆盖 Gitea token、LLM 端点 / 模型 / Key、排除路径、并发数
## 目录
```text
gitea-codereview/
├── app/
│ ├── server.js HTTP 服务:webhook、REST API、静态后台
│ ├── lib/
│ │ ├── db.js SQLite 结构与查询
│ │ ├── gitea.js Gitea REST 客户端
│ │ ├── ocr.js 调用 OCR CLI,解析 JSON
│ │ ├── diff.js 解析 unified diff,定位内联评论锚点
│ │ ├── review.js 审查流水线:发布、Issue、提交状态、自动合并
│ │ └── queue.js 串行任务队列
│ └── static/ 后台前端
├── tests/core.test.js
├── Dockerfile
└── docker-compose.yml
```
## 部署
### 1. 准备 Gitea
**创建访问 Token**:Gitea → 用户设置 → 应用 → 生成令牌,勾选 `repo` 与 `issue` 权限。
**允许 Gitea 向内网投递 Webhook**(Gitea 默认拦截内网地址)。在 Gitea 的 `app.ini` 或容器环境变量中加:
```yaml
GITEA__webhook__ALLOWED_HOST_LIST: "192.168.31.51"
```
多个地址用逗号分隔,也可写 CIDR(`192.168.31.0/24`)或内置名(`private`、`loopback`)。
### 2. 准备 LLM 端点
OCR 需要一个 OpenAI 兼容或 Anthropic 兼容的接口,填进 `.env` 的 `CR_LLM_URL` / `CR_LLM_TOKEN` / `CR_LLM_MODEL`。
用 `CR_LLM_PROTOCOL=openai` 或 `anthropic` 指定协议。
### 3. 配置并启动
```bash
cd gitea-codereview
cp .env.example .env
# 填入 CR_GITEA_TOKEN、CR_WEBHOOK_SECRET、CR_ADMIN_TOKEN、CR_LLM_*
docker compose build
docker compose up -d
docker compose ps
curl -fsS http://localhost:8090/api/health
```
打开后台 `http://<服务器>:8090`:
0. 如果设置了 `CR_ADMIN_TOKEN`,页面会先要求输入该 Token(保存在浏览器本地,可随时「清除访问 Token」)
1. 在「设置」里确认 Gitea 地址、LLM 端点,点「测试 Gitea」和「测试 LLM」验证连通
2. 在「仓库」里新增要审查的仓库,配置分支过滤、阻断阈值、是否自动合并
3. 在 Gitea 仓库 Settings → Webhooks 添加 Webhook:
- 目标 URL:`http://<服务器>:8090/webhook/gitea`
- 内容类型:`application/json`
- 密钥:填 `.env` 里的 `CR_WEBHOOK_SECRET`
- 事件:Push 与 Pull Request
也可以只配 `.env` 不打开后台;后台的修改会持久化到 SQLite 卷。
## 配置项
### 全局(`.env` 或后台「设置」)
| 变量 | 说明 |
| --- | --- |
| `CR_GITEA_URL` | Gitea 根地址,如 `http://192.168.31.51` |
| `CR_GITEA_TOKEN` | 发布评论、Issue、提交状态用的 Token |
| `CR_WEBHOOK_SECRET` | Webhook HMAC 密钥;设置后拒绝签名不符的请求 |
| `CR_ADMIN_TOKEN` | 保护后台和 REST API 的 Bearer Token;设置后打开后台需先输入;留空则不校验 |
| `CR_WEBHOOK_URL` | Gitea 回调本服务的地址;留空则自动推导为「Gitea 主机名 + 本服务端口」 |
| `CR_LLM_URL` / `CR_LLM_TOKEN` / `CR_LLM_MODEL` | LLM 端点与凭据 |
| `CR_LLM_PROTOCOL` | `openai` 或 `anthropic` |
| `CR_LLM_AUTH_HEADER` | 自定义认证头名,默认由协议决定(如 `x-api-key`) |
| `CR_LLM_EXTRA_HEADERS` | 附加请求头,`K=V,K=V` |
| `CR_LLM_TIMEOUT` | 单次 LLM 请求超时秒数,默认 `180` |
| `CR_RULE_PATH` | 自定义 OCR 规则 JSON 的容器内路径 |
| `CR_HOST_PORT` | 后台映射到宿主机的端口,默认 `8090` |
| `CR_CONCURRENCY` | 单个审查任务的并发文件数 |
| `CR_REVIEW_TIMEOUT_MS` | 单次审查超时,默认 45 分钟 |
| `CR_MAX_TOKENS_BUDGET` | 单次审查 token 上限,`0` 为不限 |
| `CR_OCR_COMMAND` | OCR 可执行文件名,默认 `ocr` |
| `CR_DATA_DIR` / `CR_DB_PATH` | 数据目录与 SQLite 路径,默认容器内 `/data` |
| `CR_POLL_MS` / `CR_MAX_ATTEMPTS` | 队列轮询间隔与失败重试次数 |
### 按仓库(后台「仓库」)
| 字段 | 说明 |
| --- | --- |
| `repo_url` | 仓库 Git 地址,`owner` / `name` 由此自动解析 |
| `managed_branch` | 主管理分支。push 事件没有对应 PR 时用它做对比基准;自动合并不限于它,任一检查分支都可作为合并目标 |
| `check_branches` | 受保护的目标分支列表,逗号分隔。任何**合并进**这些分支的 PR 都会审查(功能分支不必列出),向这些分支推送也会审查 |
| `review_scope` | `both` / `pr` / `push` |
| `block_severity` | 阻断级别阈值,如 `critical,high`;留空则不看级别 |
| `block_categories` | 额外按类别阻断,如 `security` |
| `fail_on_findings` | 打开后任何意见都视为阻断 |
| `auto_merge` | 无阻断问题时自动合并 |
| `auto_merge_mode` | `when_checks_succeed` / `immediate` |
| `merge_method` | `squash` / `merge` / `rebase` / `rebase-merge` / `fast-forward-only` |
| `delete_branch` | 合并后删除源分支 |
| `publish_mode` | `inline`(PR 内联评论)/ `issue-only` |
| `create_issue` | 是否创建 / 更新 Issue |
| `issue_labels` | Issue 标签,不存在时自动创建 |
| `excludes` | OCR 排除路径,gitignore 风格,逗号分隔 |
| `max_comments` | 单次最多发布多少条意见 |
| `gitea_token` / `llm_token` / `llm_model` | 覆盖全局配置 |
## REST API
所有 `/api/*` 接口在设置 `CR_ADMIN_TOKEN` 后需要 `Authorization: Bearer <token>`。
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/api/health` | 健康检查与队列统计(免鉴权) |
| `GET` `PUT` | `/api/settings` | 读写全局设置 |
| `GET` `POST` | `/api/repos` | 列出 / 新增仓库配置 |
| `GET` `PATCH` `DELETE` | `/api/repos/:id` | 读取 / 修改 / 删除 |
| `POST` | `/api/repos/:id/discover` | 校验连接,返回默认分支与分支列表 |
| `GET` | `/api/gitea/repos` | 列出全局 Token 可见的仓库,供一键导入 |
| `POST` | `/api/repos/parse` | 解析 Git 地址,返回 `owner` / `name` |
| `POST` | `/api/repos/branches` | 用 Git 地址查询分支(仓库尚未保存时使用) |
| `POST` | `/api/repos/:id/branches` | 创建分支,可从指定分支克隆 |
| `GET` | `/api/repos/:id/webhook` | 查询该仓库的 Webhook 是否已安装 |
| `POST` | `/api/repos/:id/webhook` | 安装或修复该仓库的 Webhook |
| `GET` | `/api/records` | 审查历史(`?repo_id=`、`?limit=`) |
| `GET` | `/api/records/:id` | 单条记录,含参数、意见与摘要 |
| `POST` | `/api/records/:id/summarise` | 重新排队生成摘要 |
| `GET` | `/api/jobs` | 任务列表(`?repo_id=`、`?limit=`) |
| `GET` | `/api/jobs/:id` | 任务详情,含日志 |
| `POST` | `/api/jobs/:id/retry` | 重跑任务 |
| `POST` | `/api/review` | 手动排队一次审查 |
| `POST` | `/api/gitea/test` | 测试 Gitea 连接 |
| `POST` | `/api/selftest` | 测试 OCR 与 LLM 连通性 |
手动触发一次审查:
```bash
curl -X POST http://localhost:8090/api/review \
-H "Authorization: Bearer $CR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"owner":"kgod","name":"myrepo","ref":"main","sha":"<commit-sha>"}'
```
## 行为说明
**分支与合并**:`check_branches` 是**被保护的目标分支**。任何合并进这些分支的 PR 都会被审查,所以功能分支不必列进去;向这些分支直接推送也会审查。审查基准是该改动实际要合入的分支(PR 用 PR 自己的目标分支,push 用管理分支),也就是「这次改动合进去会怎样」。
自动合并作用于目标为**任一检查分支**的 PR,因此两级流转可以直接用:
```text
feature/xxx ──PR──> test ──PR──> prd
↑ 第一级:审查通过即自动合并
↑ 第二级:同样审查通过才合并
```
**直接推送**也走同一条链路:往 `test` 推代码(没有 PR)时,审查通过后服务会
自动开一个 `test → prd` 的 PR 并合并它,然后关闭该仓库所有审查 Issue。
自动开的晋级 PR 会带一个标记,它自身的 webhook 事件会被忽略——同一批提交在
push 时已经审过,重复审会在代码合并后凭空造出 Issue。
晋级 PR 始终使用 `merge` 而非 `squash`:squash 会把提交重写成全新提交,
两个长期共存的分支每晋级一次就多分叉一点,最终必然冲突;真正的 merge
保留共同祖先,下次晋级只携带新提交。
`managed_branch` 只决定「push 事件没有对应 PR 时,拿哪个分支做对比基准」,以及新仓库的默认值。
**Issue 生命周期**:只要发生合并(服务自动合并、或人在 Gitea 里手动合并),
该仓库所有由本服务创建的 open Issue 都会被评论并关闭——代码已经落地,
上一轮的问题描述的是不再存在的状态。下一个问题会在下一次审查时重新开 Issue。
**审查范围**:PR 事件优先用 PR 的目标分支作为基准;push 事件优先用 webhook 里的 `before`,当 `before` 是新建分支的全零值或缺失时,退回到与管理分支的 merge-base。
**审查背景**:仓库配置里的「审查背景 / 需求」是多行文本,既作为 OCR 的 `--background` 传给模型,也会完整记录到审查历史,方便回溯「当时是按什么需求审的」。
**阻断判定**:一条意见命中任一条件即为阻断 —— 严重级别 ≥ `block_severity` 中任一项,类别在 `block_categories` 中,或打开了 `fail_on_findings`。阻断会创建 Issue、把提交状态置为 `failure`,并阻止自动合并。
**审查不完整时**:OCR 报告 `partial` / `failed`,或存在文件级警告,或 token 预算被截断时,仍会发布已有意见,但不会自动合并,提交状态也不会是 success。宁可漏合,不可错合。
**Issue 生命周期**:标题格式 `[OCR] <owner>/<repo> · <branch> 存在阻断级代码问题`。同一分支再次出现阻断问题时更新该 Issue 并追加一条说明;该分支复查通过后自动评论并关闭。`create_issue` 关闭时不新建 Issue,但仍会关闭此前开过的。
**重复保护**:同一 commit 已在队列中或正在审查时不重复入队;Gitea 重投的同一 delivery id 会被忽略。
## 开发
```bash
node --test tests/core.test.js # 单元测试
node app/server.js # 本地直接运行(需先装 ocr CLI)
docker compose build # 构建镜像
```
本地运行需要 Node ≥ 22.5(用到内置 `node:sqlite`)、`git` ≥ 2.41,以及 `npm i -g @alibaba-group/open-code-review`。
## 故障排查
**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。
**后台打开后要求输入访问 Token**
这是 `CR_ADMIN_TOKEN` 在生效。输入 `.env` 里的值即可,Token 只存在浏览器本地。想免登录就把它留空并重启容器(仅限不对外暴露的网络)。
**历史记录一直显示「摘要生成中…」**
摘要与审查串行执行,一次只跑一个;如果 LLM 不可达会在记录里留下失败原因,可在详情里点「重新生成摘要」。摘要失败不会影响审查结论与自动合并。
**任务一直停在 `reviewing`**
LLM 慢或不可达。在后台「设置」点「测试 LLM」,或在「任务」页查看日志;调大 `CR_REVIEW_TIMEOUT_MS`。
**内联评论变成了汇总里的条目**
OCR 给的行号不在本次 diff 内(常见于问题定位到未改动的上下文行)。服务会保留意见并汇总展示,不丢结果。
**自动合并没有发生**
看任务日志的 `auto-merge skipped` 原因:存在阻断问题、审查覆盖不完整、提交状态非 success、PR head 已变化或 PR 不可合并。Gitea 侧还需该 Token 有合并权限。