基于 OpenCodeReview 的 webhook 服务:监听 Gitea 的 push 与 Pull Request 事件, 调用 OCR 审查 diff,把结果发布回 Gitea,并按阻断阈值决定是否自动合并。 主要能力: - Push / PR 事件触发,支持分支 glob 过滤与 PR-only / push-only 范围 - PR 内联评论(按 diff 行号定位)、汇总评论、Issue 生命周期、提交状态 - 可配置阻断阈值(严重级别 / 类别 / 任意意见) - 无阻断问题时自动合并,审查覆盖不完整时拒绝合并 - 内置 Web 后台:仓库配置、任务日志、失败重跑、连通性自检 - SQLite 持久化,worker 重启回收卡死任务,失败自动重试 实现为独立服务而非 Gitea Action:本机 act_runner 指向的实例不可达, 且后台配置与任务历史需要独立进程承载。
219 lines
10 KiB
Markdown
219 lines
10 KiB
Markdown
# 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 + 提交状态
|
||
↓ 无阻断问题且开启自动合并
|
||
自动合并 / 等待检查通过后合并
|
||
```
|
||
|
||
## 功能
|
||
|
||
**审查触发**
|
||
- 监听分支推送,自动对比上次提交或与目标分支的 merge-base
|
||
- 监听 Pull Request 的 opened / synchronize / reopened / ready_for_review
|
||
- 按分支 glob 过滤(`*`、`release/*`、`main`),可选只审 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 不可合并时自动放弃
|
||
|
||
**运维**
|
||
- 内置 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_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` | 队列轮询间隔与失败重试次数 |
|
||
|
||
### 按仓库(后台「仓库」)
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `branch_patterns` | 监听的分支 glob,逗号分隔;`*` 为全部 |
|
||
| `review_scope` | `both` / `pr` / `push` |
|
||
| `base_branch` | 对比基准分支,也是无 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/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>"}'
|
||
```
|
||
|
||
## 行为说明
|
||
|
||
**审查范围**:PR 事件用 `base.sha..head.sha`;push 事件用 webhook 里的 `before..after`,如果 `before` 是新建分支的全零值,则退回到与 `base_branch` 的 merge-base。
|
||
|
||
**阻断判定**:一条意见命中任一条件即为阻断 —— 严重级别 ≥ `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`。
|
||
|
||
## 故障排查
|
||
|
||
**Webhook 投递失败,提示 `webhook can only call allowed HTTP servers`**
|
||
Gitea 拦截了内网地址。按上文给 `[webhook] ALLOWED_HOST_LIST` 加上本服务地址,重启 Gitea。
|
||
|
||
**后台打开后要求输入访问 Token**
|
||
这是 `CR_ADMIN_TOKEN` 在生效。输入 `.env` 里的值即可,Token 只存在浏览器本地。想免登录就把它留空并重启容器(仅限不对外暴露的网络)。
|
||
|
||
**任务一直停在 `reviewing`**
|
||
LLM 慢或不可达。在后台「设置」点「测试 LLM」,或在「任务」页查看日志;调大 `CR_REVIEW_TIMEOUT_MS`。
|
||
|
||
**内联评论变成了汇总里的条目**
|
||
OCR 给的行号不在本次 diff 内(常见于问题定位到未改动的上下文行)。服务会保留意见并汇总展示,不丢结果。
|
||
|
||
**自动合并没有发生**
|
||
看任务日志的 `auto-merge skipped` 原因:存在阻断问题、审查覆盖不完整、提交状态非 success、PR head 已变化或 PR 不可合并。Gitea 侧还需该 Token 有合并权限。 |