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

Gitea 自动代码审查

基于 OpenCodeReview(OCR)的 Gitea 自动代码审查服务。 监听仓库推送和 Pull Request,调用 OCR 分析 diff,把结果发布回 Gitea,并按规则决定是否自动合并。

它做什么

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、排除路径、并发数

目录

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 或容器环境变量中加:

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. 配置并启动

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:

  1. 如果设置了 CR_ADMIN_TOKEN,页面会先要求输入该 Token(保存在浏览器本地,可随时「清除访问 Token」)
  2. 在「设置」里确认 Gitea 地址、LLM 端点,点「测试 Gitea」和「测试 LLM」验证连通
  3. 在「仓库」里新增要审查的仓库,配置分支过滤、阻断阈值、是否自动合并
  4. 在 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 连通性

手动触发一次审查:

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,因此两级流转可以直接用:

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 会被忽略。

开发

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 有合并权限。

S
Description
git-codereview, 配置llm定时检查pr和pull, 提issue需求等等。
Readme
340 KiB
Languages
JavaScript 89.8%
HTML 6.2%
CSS 3.6%
Dockerfile 0.4%