1. 项目来源与初始思路
1.1 我为什么还要再做一款代码审查工具
先说清楚一个背景:我所在团队大概二十来人,维护着四五个微服务仓库,日常开发流程是标准的 GitLab MR + 人工审查。听起来没什么问题,但真正跑起来之后,痛点一个接一个冒出来。
最让我受不了的是两件事。第一,MR 一多,reviewer 根本看不过来,很多 MR 从提起到合并要卡两三天,而真正等代码审核的时间可能只有十几分钟,其余时间全在等人。第二,代码风格、基础规范、明显的低级错误,比如把密码硬编码进配置文件、日志里打印了敏感字段、空指针风险、数据库查询忘加 limit,这些问题其实完全可以靠工具自动拦下来,却非要消耗人肉去 review。我统计过一个月的数据,团队大概有 37% 的 review 评论是在重复说同一类低级问题。
当时我也试过市面上的方案。商业 SaaS 产品成本高、数据要出网,我们这边合规上过不去;开源的通用 lint 工具,比如 ESLint、Checkstyle,确实能抓一部分问题,但它们是“规则引擎”,不是“审查流程”,只能告诉你代码哪里不符合规范,却无法回答“这个 MR 改了哪些高风险文件”“谁应该被拉进来审查”“这次改动跟最近的故障有没有关联”。
所以我想做一件事:搭一个自托管的、能和现有 Git 平台深度集成的代码审查辅助层,把机械性的检查交给机器,把真正需要人判断的问题留给人类。这个项目就是open-code-review,一个开源的、面向团队协作的代码审查辅助平台。
1.2 它到底解决什么问题
open-code-review 不是什么颠覆性的东西,它做的事情说白了就是三件:
- 在 MR/PR 创建时自动拉取 diff,跑一轮静态分析和自定义规则,把低风险但高频的问题直接标出来,reviewer 打开页面就能先看到机器的结论。
- 根据改动文件的范围和内容,自动推荐合适的 reviewer,避免“每个 MR 都艾特所有人”这种低效模式。
- 提供审查数据看板,让技术负责人能看清每个仓库、每个人的 review 负载和响应时间,及时发现流程里的瓶颈。
从定位上说,它更像一个“审查流程的辅助驾驶”,而不是“自动驾驶”。它不替代人工审查,也不强制任何规范,而是把机械劳动接走,让人的注意力集中在架构设计、业务逻辑、潜在风险这些真正需要判断力的地方。
适用对象我总结下来有三类:一是受困于 review 阻塞的开发团队;二是同时维护多个仓库、需要统一审查口径的平台组;三是想给开源项目加上自动化审查能力的维护者。如果你只是一个人写代码,这个工具帮助有限,它天生是为协作场景设计的。
2. 核心功能拆解与技术选型
2.1 三大核心能力的实现逻辑
先讲功能,再讲技术,这样好理解我为什么做这些选型。
diff 分析与规则引擎
这是整个项目的基石。每次 MR 触发后,服务会向 Git 平台 API 拉取该 MR 的 diff 数据,解析成标准化的文件修改列表。然后规则引擎逐文件、逐 diff 块执行检查。规则分几层:
- 基础层:文件路径匹配,比如禁止
.env文件提交、禁止在src/main/resources下出现config类敏感文件。 - 代码层:针对不同语言做 token 级检查,比如 Python 的
assert不能用于生产校验、Java 里避免printStackTrace、JS 里禁止eval。 - 语义层:需要结合上下文判断,比如新增了数据库查询但没有加 limit、API 接口新增字段但没有写文档注释。
规则引擎我采用了“管道 + 插件”的架构。管道负责串联不同检查阶段,插件提供具体能力。每种语言一个插件,插件内部可以调用对应语言的 linter 暴露的结果,也可以自己写精确匹配。这样新增语言支持不需要改核心代码,只要写一个插件。
reviewer 自动推荐
这部分当时考虑了很久。最朴素的方案是看历史代码归属,用 git blame 统计数据,谁最近改动过这个文件,谁就是潜在 reviewer。但这样做有个问题:新人刚来的头几周没有任何 blame 记录,他们改的代码会被系统判为“无人认领”。
所以我用了一个混合策略。先设一个基准分:文件历史修改者权重最高,目录 owner 文件次之,最近合并过相邻模块的同事再降一档,最后用随机加权补足新人。实际效果还算满意,推荐的命中率在七成左右,剩下三成由人工调整。
审查数据看板
看板不是核心功能,但它是推动工具落地的关键。人都是看数据说话的,如果工具上线后拿不出一组“节省了多少时间、拦截了多少问题”的数字,很难说服团队持续使用。我就用定时任务,每天把 MR 状态、review 耗时、评论数、工具拦截数汇总到一张表里,再用一个轻量图表页展示。
2.2 技术栈选择的几个关键考量
技术选型方面,我遵循一个原则:尽量选团队已经熟悉、生态成熟、部署轻量的东西,不追求新潮。
- 后端:Go。主要原因是想做成单二进制部署,方便在各种 CI 环境里跑。而且 Go 写并发任务很顺手,diff 分析师有时要同时处理几十个仓库的扫描请求,goroutine 天然合适。
- 数据库:PostgreSQL。数据量不大,但关系复杂,MR、文件、规则命中、reviewer 推荐记录之间有很多关联查询,PG 的 JSONB 类型也让规则配置变灵活。
- 任务队列:Redis + 简单的轮询。没有引入重量级消息队列,是因为单机部署场景下 Redis 足够,而且部署时少一个依赖就少一个出问题的环节。
- 前端:Vue 3 + Element Plus。管理后台性质的应用,不需要太重的框架,Vue 的上手成本对后端开发者友好。
- 反向代理:Caddy。自动 HTTPS,配置简洁,比 Nginx 省心很多。
这里有个容易被忽视的点:和 Git 平台的集成方式决定了整个系统的复杂度。open-code-review 没有选择为每一种平台写适配器,而是定义了一套统一抽象,GitLab、GitHub、Gitea 各自实现对应的客户端。第一次接入用 GitLab,因为团队内部最熟,后续再补其他平台只需要按接口规范实现。
注意:如果是企业内部使用,GitLab 的版本差异会带来一些兼容问题。比如老版本 MR webhook 的事件格式和新版本不完全一样,解析时一定要做字段存在性判断,不能假设所有版本行为一致。
2.3 数据模型设计里的几个取舍
MR 审查系统的实体不算复杂,但关系要理清楚。核心表我设计了六张:
repositories:仓库元信息,包括平台类型、项目 ID、Webhook Secret。merge_requests:MR 主表,存储标题、源分支、目标分支、状态、发起人。review_comments:工具生成的审查评论,关联 MR 和具体文件位置。rule_hits:规则命中记录,比评论更细粒度,一条 MR 可能命中多条规则,但可以汇总成一条评论。reviewer_suggestions:推荐 review 记录,包含推荐理由和用户是否采纳。scan_tasks:扫描任务表,记录每次 diff 分析的状态,失败时保存错误信息,方便排查。
这里我踩过一个坑:一开始把rule_hits和review_comments合并成一张表,结果发现规则命中和最终评论往往不是一一对应,比如 5 条同类型命中可能聚合为 1 条评论。拆开后数据更清晰,后续做统计和运维也方便。
关于性能,我的经验是不要提前优化。刚开始只部署一个实例、一个 PostgreSQL,撑住一个几十人团队的扫描量完全没问题。单个仓库的 diff 扫描时间控制在 3 秒以内,几千行的大 MR 也就十几秒,这个量级根本不需要上分布式。
3. 部署配置与接入实操
3.1 用 Docker Compose 快速拉起整个服务
我尽量让部署过程保持“一条命令跑起来”。项目根目录提供了docker-compose.yml,包含三个服务:postgres、redis、ocr-server。克隆代码后,修改.env文件里的数据库密码、Git 平台地址和回调密钥,执行docker compose up -d就能启动。
version: "3.8" services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: ocr POSTGRES_PASSWORD: change-me POSTGRES_DB: open_code_review volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redisdata:/data restart: unless-stopped ocr-server: image: open-code-review/server:latest ports: - "8080:8080" environment: DB_DSN: host=postgres user=ocr password=change-me dbname=open_code_review sslmode=disable REDIS_ADDR: redis:6379 GIT_PLATFORM: gitlab GIT_BASE_URL: https://gitlab.example.com GIT_ADMIN_TOKEN: <token> WEBHOOK_BASE_URL: https://review.example.com depends_on: - postgres - redis restart: unless-stopped volumes: pgdata: redisdata:实际部署时有几个细节可以注意一下:
GIT_ADMIN_TOKEN建议使用只读权限的 token,open-code-review 只需要读取仓库信息和 MR 数据,不需要写仓库的权限。用最小权限 token,即使泄露影响也可控。WEBHOOK_BASE_URL是回调地址,需要能从 GitLab 服务器访问到。如果你把 open-code-review 部署在内网,要确保 GitLab 所在网络能通到这个地址。- 首次启动后,访问
http://server-ip:8080会进入初始化页面,要求配置管理员账号。这里的密码策略我建议至少 12 位,因为部署在公网的实例很容易被扫描器盯上。
3.2 配置文件逐项说明
除了环境变量,系统还支持一个 YAML 配置文件,用来定义审查规则和策略。默认配置在config/default.yml,核心内容如下:
server: port: 8080 database: dsn: "host=postgres user=ocr password=change-me dbname=open_code_review sslmode=disable" redis: addr: "redis:6379" git_platform: type: gitlab base_url: "https://gitlab.example.com" admin_token: "xxxx" # Webhook 回调相对路径,会拼上 WEBHOOK_BASE_URL webhook_route: "/webhook/gitlab" review_rules: # 扫描哪些类型的文件 include_paths: - "**/*.go" - "**/*.java" - "**/*.py" - "**/*.js" - "**/*.ts" - "**/*.vue" exclude_paths: - "**/vendor/**" - "**/node_modules/**" - "**/dist/**" - "**/target/**" # 规则列表 rules: # 硬编码密钥检测 - id: hardcoded_secret name: "检测硬编码的敏感信息" description: "识别代码中的 AK/SK、密码等敏感信息的硬编码" level: error # error / warning / info engine: regex pattern: "(?i)(password|passwd|secret|token)\\s*[=:]\\s*['\"][^'\"]{6,}['\"]" # 禁止提交的敏感文件 - id: sensitive_file name: "敏感文件禁止入库" description: ".env、.pem、.key 等文件不允许提交到仓库" level: error engine: path pattern: "(\\.env$)|(\\.pem$)|(\\.key$)|(id_rsa)" # 空指针风险提示 - id: null_dereference name: "空指针风险提示" description: "检测未判空就调用的高风险写法(目前只做基础启发式规则)" level: warning engine: regex pattern: "(\\w+)\\.(get|find|query|fetch)\\(\\)\\.(get|value|execute)\\(" # 调试代码残留 - id: debug_printf name: "调试输出残留" description: "检测常见的调试打印语句" level: warning engine: regex pattern: "(console\\.log|System\\.out\\.print|print\\()" # TODO 统计(只标记 INFO) - id: todo_left name: "遗留 TODO" description: "提示代码中仍有 TODO 未清理" level: info engine: regex pattern: "TODO|FIXME|HACK"规则引擎设计的几个层次,我在开发过程中反复调过优先级。error级别的命中会直接阻止 MR 合并(如果开启了门禁),必须人工确认;warning只做标记,不阻断流程;info纯粹是信息提示,让开发者心里有数。这里容易犯的错是一开始把太多规则设成error,结果团队怨声载道,后来我调整了策略:第一版只把“硬编码密钥”和“敏感文件入库”设为 error,其余全部默认 warning,等人接受这个工具了再逐步收紧。
这个策略救了我一命。工具上线第一周,如果每天弹几十条阻断性错误,开发同学直接就把 webhook 关了。
3.3 接入 GitLab 并下发第一条审查任务
配置好服务之后,接入仓库大概是四步。
第一步,在 GitLab 上创建一个项目访问令牌,权限只需要read_api和read_repository。因为 open-code-review 需要拉取 MR diff,需要这两个权限才能正常工作。
第二步,在 open-code-review 后台添加仓库。填写仓库地址和项目 ID,系统会自动从 GitLab 读取仓库信息并注册 webhook。
第三步,去 GitLab 对应项目里确认 webhook 已经创建成功。GitLab 会在 webhook 列表里显示一条指向 open-code-review 服务的记录。可以点测试按钮,发送一个测试事件,观察 open-code-review 日志里是否收到请求。
第四步,创建一条测试 MR。修改一个文件、加一个明显的调试输出,然后提交 MR。正常情况下,系统会在十几秒内自动在 MR 下追加一条评论,告诉你有几条 error、几条 warning,以及具体在哪一行。
这是第一次跑通时最有成就感的瞬间。整个流程从环境部署到看见第一条自动评论,熟练的话大概半小时内能完成。
# 创建一条测试 MR(在测试仓库里操作) git checkout -b test-review echo "console.log('test debug');" >> test.js git add test.js git commit -m "test: add debug output for review" git push origin test-review # 在 GitLab 网页上创建 MR,等十几秒,刷新页面MR 下方会出现类似这样的自动评论:
open-code-review 扫描完成:发现 2 个 warning、0 个 error。
test.js:1debug_printf:调试输出残留 (console.log)test.js:1todo_left:遗留 TODO
看到这条评论出来,整个链路就通了。
4. 常见问题与排查技巧实录
4.1 权限类问题:webhook 收到事件但扫描无结果
上线前测试时我遇到过最诡异的情况:GitLab 后台的 webhook 测试是绿的,发送事件也显示成功,但 open-code-review 这边一条日志都没有。
排查思路我最终确定是按这个顺序走的。先看服务日志里有没有收到 webhook 请求,如果没收到就说明是网络层问题,检查反向代理配置和防火墙规则。如果收到了,那大概率出在消息解析上——GitLab 不同的 webhook 事件(Push Hook、Merge Request Hook、Note Hook)数据格式差异很大,解析时如果按一个固定的 JSON 结构去取字段,很容易取到空值。
我当时的问题是 GitLab 配置的 Secret Token 和 open-code-review 环境变量里的不一致,导致 webhook 请求被默认拒绝。这个错误级别只打了一行 debug 日志,不仔细看根本发现不了。建议部署初期把日志级别调成 debug,跑通主流程后再改回 info。
4.2 大仓库扫描超时:diff 分析性能优化
另一个高频问题是:仓库太大,几百个文件的 MR 提交过来,扫描任务经常超时。
我的解决方案分了三步走。第一步,给扫描任务加了一个 Google 风格的并发控制,每个仓库同时只跑一个任务,其余排队,避免多个大 MR 同时进来把 CPU 打满。第二步,将超大 diff 按文件类型做裁剪,只扫描配置里include_paths命中的文件,vendor、node_modules、target这类目录直接跳过。第三步,设置单次扫描的 diff 行数上限,超过一万行的 MR 自动降级为“抽样扫描”,只检查高风险路径上的文件。
这三步做完,线上最慢的扫描从 60 秒降到了 8 秒左右,而单次扫描的命中率并没有明显下降。顺便说一句,exclude_paths里的目录一定要在项目配置初期就维护好,否则后期加规则时会把一堆第三方代码误报成问题。
4.3 规则误报太多:如何调出“准而不吵”的规则
规则引擎最头疼的问题就是误报。我试过把硬编码密钥的正则写宽,结果大量password字段的关键字被误伤;也试过把某些语言的内置 API 当成业务代码扫,产生了一堆没价值的噪音。
后来我总结出几条调规则的实战经验:
- 宁可漏报,不可误报。误报率一高,开发者就会对工具的评论免疫,最后变成“点掉不看”。想找回信任非常难。
- 每一条规则都要有“真实案例支撑”。我是这么操作的:看到一条真实事故或一次线上故障,才把对应的检测规则模型抽象出来,而不是凭想象写规则。
- 规则分阶段上线。先作为
info观察一周,统计触发频率和人工确认率,如果确认率高再升到warning,确认率低于三成的规则直接回炉重练。 - 用“白名单”对冲不确定规则。比如敏感文件检测,允许仓库根目录放一个
.ocr-whitelist文件,把确实需要提交的特殊文件列进去。
4.4 团队落地的三个关键动作
技术问题解决之后,真正的难题是让人愿意用。我见过太多内部工具死在“没人看、没人反馈、最后悄悄下线”这个循环里。我的经验是做好三件事。
第一件事是上线第一天就拉出“第一批战果”。我们上线第一周,工具拦截了两个生产事故级别的风险——一个是有同事把数据库连接串明文提交到了公开仓库,另一个是某次批量删除操作忘加WHERE条件。我把这两个案例截图发到团队群里,效果比任何动员都有说服力。
第二件事是建立一个“规则反馈群”。任何人觉得规则误报、漏报或者需要新增规则,都可以在群里提。我每周五汇总一次,能改的当场改,不能改的说明原因。这个习惯保持了三个月,规则库从最初的 18 条增长到 46 条,误报率从 12% 降到 3%。
第三件事是别把工具做成“卡点”。很多人会用它来做强制门禁,比如 MR 必须 0 error 才能合并。但我的建议是:第一版千万别这么干。先让工具以“辅助”的角色存在,等人习惯了再看要不要开强制。一旦强制,你会瞬间站到所有人的对立面。我们是在上线两个月后、团队反馈普遍正向的前提下,才正式启用了 error 级阻断门禁。
5. 项目后续可以怎么扩展
5.1 接入 CI 管道和更多平台
目前 open-code-review 的核心流程是“webhook 触发 → 扫描 → 评论”,这决定了它只能处理“已经创建出来的 MR”。我下一步计划是让它在 CI 管道里也能跑,比如在 GitLab CI 里提供一个ocr-check命令,由各项目在.gitlab-ci.yml中显式调用,这样扫描结果就能直接反映在 pipeline 状态上,而不是只出现在评论里。
# 示例:在 GitLab CI 中集成 open-code-review review: stage: test image: open-code-review/cli:latest variables: OCR_SERVER_URL: "https://review.example.com" OCR_REPO_ID: "123" script: - ocr-cli scan --mr $CI_MERGE_REQUEST_IID --output json only: - merge_requests平台侧,GitHub 的 App 版本也在规划中。GitHub 的审查生态和 GitLab 差别很大,API 调用方式、权限模型、webhook 签名方式都不一样,需要重新写一层适配,但核心的 diff 分析引擎和规则引擎可以直接复用。
5.2 把规则引擎做成可共享的生态
代码审查规则这种东西,通用场景其实很多,但每个团队又有自己的个性化需求。我想做的是一个类似“规则市场”的目录,让不同团队可以把自己积累的规则包导出、分享、导入。规则格式目前是 YAML,内部做了一版简单的 schema 校验,后续再加一层版本管理,规则包就能像 npm 包一样被安装和升级了。
这个功能的技术难度不高,难的是规则本身的质量。我计划后续把规则仓库独立出来,维护一套贡献指南和审核流程,至少保证共享出去的规则是经过真实项目验证过的,而不是拍脑袋写的正则。
6. 最后分享几点开发中的体会
这个项目从第一行代码到现在,前后用了大约三个月的业余时间。技术上的收获固然有,比如 diff 分析的处理、并发任务编排、与 Git 平台 API 对接时那些文档里没写清楚的细节,但我更想说的是另外几件事。
一个是,做内部工具最忌讳“把自己当成平台的替代品”。open-code-review 从头到尾都没有打算替代 GitLab、GitHub 自带的 code review 功能,它做的是“辅助”。这个定位想清楚之后,很多设计上的问题都迎刃而解了——我不用去重写评论 UI,不用做 MR 列表页,只需要把数据送给现有的交互界面。
另一个是,规则类工具的生命力在于规则本身,而不在于框架。框架再漂亮,没有高质量的规则堆上去,还是空壳子。所以哪怕功能再忙,我都坚持每周花几小时看看真实项目里新出现的 bug 和事故,把它们沉淀成新规则。这个过程有点像“把团队的教训代码化”。
最后一点,如果你是个人开发者想做个类似的工具,我建议一定要找到一个真实的使用场景和一两个愿意陪你把第一版跑通的人。没有真实场景驱动,很容易做出一个技术完美但没人用的东西;而有人愿意用,哪怕第一天只有两三个人,整个开发的动力和方向感都会完全不一样。
这个项目目前还在持续演进中,欢迎对代码审查、团队工具链感兴趣的朋友一起交流。说不定你实际用起来之后,踩到的第一个坑就是我还没遇到过的那种。