2020 年我接手一个十人左右的开源团队时最头疼的还不是写代码而是每次 merge request 的评审都像开盲盒有人只回一句“LGTM”有人花半小时争论缩进还有人把仓库里所有 lint 报错全部贴回评论区。后来我干脆花了一个假期把散落在脚本、CI 和脑内的审查规则整理成一个独立项目起名 open-code-review。它不是什么颠覆性创新就是一套开源、可自托管的代码审查辅助系统核心逻辑一句话让机器先把机械检查跑完把人肉评审留给真正需要判断的地方。如果你正在维护一个中大型仓库或者团队规模过了十个人还在靠“随缘 review”这篇文章值得看完。我会把这个项目从定位、架构、部署到上线后踩过的坑整体复盘一遍所有配置和命令都是实际跑过的可以直接参考。1. 我决定做这个项目时瞄准的三个具体问题1.1 评审意见大量停留在“行内琐事”层面我统计过自己团队的 200 个 merge request评论里约有 60% 属于“这里少个空格”“变量名建议改成 xxx”“这个工具函数是不是可以抽一下”这类问题。不是说这些问题不该提而是它们完全可以让静态检查工具在提交代码时就拦住根本不该占用评审人的注意力和耐心。更麻烦的是人工提这些琐事会拉高评审成本。reviewer 需要看完整个 diff找到小问题再组织语言写评论一套流程下来一个 200 行的 MR 可能要耗掉 40 分钟。等到真正的设计问题出现时大家已经累了反而没人认真审。1.2 团队里的检查工具太散没有统一入口很多团队并不缺工具ESLint 管 JavaScriptgofmt 管 Gomarkdownlint 管文档commitlint 管提交信息再加上覆盖率统计、重复代码检查……但问题是这些工具是零散的CI 里跑归跑结果没有汇总到同一个地方更不会直接落到 MR 评论区。有好几次我看到 CI 里明明有一堆 lint 报错但 MR 还是被合并了因为压根没人打开 CI 日志去看。代码评审本身已经不便宜还要让人在不同系统之间来回跳工具的边际价值就接近零了。1.3 新手评审不知道该看什么团队扩张之后最明显的变化是新人不知道 review 的“标准”到底在哪里。老手说“这个实现不够优雅”新人一脸懵因为没人告诉他什么算优雅哪些点必须改哪些点只是建议。这就是一个隐性知识沉淀问题。我想要的不是写一份没人看的《评审规范》文档而是把规则通过检查器落地让新手在提交 MR 时直接被机器指出“这里有问题”在真实场景里学会判断。由这三个问题出发open-code-review 的产品边界就清晰了它不是一个 linter而是一个连接已有 linter、测试门禁、仓库托管平台和评审流程的“审查调度中枢”。2. 架构取舍为什么选“开源 插件化 门禁”三条主线2.1 核心工作流拆解open-code-review 的运行时流程大概是这样的Git 托管平台触发 webhook 事件push、merge request open/update。事件适配器把平台请求解析成统一的“变更事件”结构。收集器获取本次变更的文件列表、diff 内容和提交信息。执行器并行运行配置好的各类检查器命令或脚本。汇总器把检查结果按文件、行号归类去掉重复项。报告器把结果写成行内评论并向平台上报检查状态pending / success / failed。门禁引擎根据严重级别决定是否允许合并。理论上你完全可以用一堆脚本 CI 拼出这套流程但 open-code-review 的价值在于统一了数据结构和交互界面让检查结果按同一套体系呈现而不是三十个工具各说各话。如果你只是给个人项目用这个架构稍微有点重但如果面对团队仓库统一报告这层价值非常关键。人脑处理信息的上限很低同一时间只能接收一种格式、一套术语。2.2 方案对比和定位边界市面上不是没有现成方案Gerrit、Reviewable、GitHub CodeQL 都做过类似的事。但我在选型时大致列了一个对比维度方案部署方式检查器扩展行内评论合并门禁适合场景Gerrit自托管依赖 Jenkins 等外部 CI支持强大型 monorepo 的严格审核流程Reviewable云服务有限支持弱小团队、低运维成本GitHub CodeQL云/企业版固定模式支持中安全漏洞扫描专项open-code-review自托管任意命令/脚本支持可配置需要自定义审查规则的中小型团队open-code-review 的定位就是“中间层”它不重新发明检查器不试图覆盖安全分析等专业领域而是把已有工具高效地组织起来按团队自己的规则输出结果。Gerrit 的问题在于重Reviewable 的问题在于黑盒很多团队真正需要的是一个可以随便改规则的开放式系统。2.3 插件化检查器的设计逻辑我始终坚持让检查器以外部命令的形式存在而不是在项目里内置一堆检查逻辑。原因有两个。第一检查器更新很快。ESLint 的规则库每月都在变如果内置就得跟着发版插件化之后团队自己升级依赖即可open-code-review 只判断命令退出码和输出格式。第二团队的语言栈会变。今天用 JavaScript明年可能引入 Go 或 Rust插件化意味着新语言接入只需要写一个适配脚本不需要改主程序。这里我定义了一个非常简单的协议检查器执行完毕后向 stdout 输出 JSON 数组每条包含文件路径、行号、严重级别、规则编号和描述。没有复杂的 SDK任何语言都能实现。[ { file: src/index.js, line: 42, level: warning, rule: no-console, message: Unexpected console statement. } ]这个设计让接入成本降到最低。我试过让实习生写一个自定义检查器35 行 Python 就搞定了。3. 部署 open-code-review 的一次完整实操记录3.1 环境准备与最小化启动我的建议是在内网一台 2 核 4G 的 Linux 机器上部署Docker 是唯一硬依赖。open-code-review 服务端本身只做一个 HTTP 服务资源占用很小跑在 Git 托管平台旁边网络延迟最低。先写好一个最小化的 docker-compose 文件services: ocr-server: image: open-code-review:1.4.2 ports: - 8080:8080 environment: OCR_CONFIG: /data/config.yaml OCR_TOKEN: ${OCR_TOKEN} volumes: - ./config:/data - /var/run/docker.sock:/var/run/docker.sock挂载 Docker socket 是为了让每个检查器在独立容器里运行避免检查器之间依赖冲突。这也是我最开始踩坑的地方——如果你不隔离两个检查器同时依赖不同版本的 Node系统很快就会乱套。接着在 config 目录创建主配置我贴一个接近生产环境的示例version: 1.0 project: demo-service adapter: type: gitea base_url: http://git.internal:3000 token_env: OCR_TOKEN collector: diff_filter: exclude: - package-lock.json - go.sum - docs/** max_diff_lines: 8000 executors: - name: eslint image: node:18-alpine command: [sh, -c, npx eslint --format json .] workdir: /repo level_map: error: error warning: warning - name: gofmt image: golang:1.21 command: [sh, -c, gofmt -l . | awk {print \{\\\file\\\:\\\\$1\\\\,\\\line\\\:1,\\\level\\\:\\\warning\\\,\\\rule\\\:\\\gofmt\\\,\\\message\\\:\\\file not formatted\\\}\}] workdir: /repo - name: commitlint image: node:18-alpine command: [sh, -c, commitlint --from HEAD~1 --to HEAD --json] level: error reporter: inline: true summary_in_comment: true max_comments_per_file: 20 policy: block_levels: - error require_reviewer_count: 1 auto_approve_if_passed: false这个配置在团队里跑了三个月基本稳定。注意diff_filter.exclude里我特意排除了锁文件和文档目录原因是误报大头都来自这些区域。后面详聊。3.2 配置适配器与 Token 权限配置里的adapter.type: gitea对应的是自己托管的 Gitea 实例。选择 Gitea 不是因为别的平台不行而是它 API 对 webhook 和评论接口支持得干净还可以完全控制适合放在内网。Token 通过环境变量传入不写进配置文件。建议创建一个单独的机器人账号只授予写评论和读取仓库内容的权限别用管理员账号。我最开始图省事用了管理员 Token后来审计的时候才发现它理论上可以修改仓库配置这是很大的安全隐患。3.3 注册 Webhook 并完成首个验证服务起来之后在 Gitea 仓库的 Settings → Webhooks 里添加一个 webhook目标 URLhttp://ocr-server:8080/webhooks/gitea内容类型application/json触发事件勾选 Push 和 Pull Request这里一个容易踩的坑是网络通路。如果 Gitea 在内网、open-code-review 也在内网问题不大但如果你在本地调试Gitea 服务在 Docker 网格里就得注意不能用localhost要用容器名或宿主机 IP。注册完成后随便开一个测试 MR给其中一行代码故意留下 lint 错误。如果配置正确open-code-review 会在十几秒内把评论打到 MR 对应行并在右上角显示检查状态。我第一次跑通这个流程时感觉整个 review 流程一下子“有了骨架”。3.4 与现有 CI 流水线的协同我见过一种误解open-code-review 把所有检查做完后就不需要 CI 了。不是这样。CI 里还要做构建、测试、镜像推送这些更重的任务open-code-review 只是在“评审信息”层面做聚合。为了让两者不打架我在 CI 里把 open-code-review 的检查器统一肉前到独立阶段只输出报告不做阻断真正的硬门禁构建失败、单元测试失败依旧由 CI 负责。换句话说机器检查一个管风格一个管正确性分工明确。4. 降低噪音是上线前最重要的工作4.1 原因是一个全量跑规则的机器人会把团队逼疯第一次上线 open-code-review 时我把所有能开的规则都开了结果就是每个 MR 评论数量轻松破百开发者打开 MR 就像进了菜市场。有个同事在群里发了一张截图评论区清一色“line 5: expected indentation of 4 spaces but found 2”连新写的代码都被历史问题淹没大家逐渐形成“机器人说的都是废话”的反射。这就是典型的噪音覆盖信号。为了让规则真正被重视必须分清楚哪些信息需要阻断合并哪些信息只需要看一眼。4.2 严重级别分级把规则分成四档我在配置里演进出来的做法是把规则严重级别分四档级别含义处理策略error明显的 bug 或安全风险必须修改阻断合并无法绕过warning代码风格、潜在问题建议修改不阻断但在 MR 顶部汇总提醒info约定、偏好不强制只出现在详情页不在评论区逐行刷style格式化相关只提示一次不再逐条评论规则映射到级别不是死板的。比如no-console在业务代码里可能是 error在 CLI 工具里就是 info。所以 open-code-review 的配置文件允许按目录覆盖级别这个能力后来证明非常有用——一个团队的“规范”本来就不是全仓库统一的。4.3 增量检查的落地经验另一个有效手段是让检查器只在 diff 涉及的变更文件上运行而不是对整个仓库全量扫描。原因很直接老代码的历史债务不该在这一次 MR 里全部显现全量扫只会让新改动埋在一堆警告里。我的配置里设置了max_diff_lines: 8000超过这个行数的 MR 不执行行内评论只在 MR 顶部输出一个“变更集过大建议拆分为多个 MR”的提示。这既是对检查器性能的保护也是在倒逼团队控制单次变更的粒度。4.4 从最终被采纳的规则清单里抽取几个例子上线三个月后真正在团队里长期有效的规则其实不多我挑几个有代表性的禁止在业务代码里使用 console.logerror除非项目本身就是 CLI 工具否则一律走日志组件。TODO 必须有责任人和日期warning没有归属的 TODO 等于永久债务。新增依赖必须附带理由warning通过检查 package.json 的 diff结合 commit message 判断。超过 500 行的文件不建议继续追加info只提示不阻断。这些规则的共同点是每一条都有明确的维护意图不是为了“听起来专业”。规则数量控制在 30 条以内超过这个数机器人又变成第二个噪音源。5. 第一周就被反馈最多的运行问题与修复复盘5.1 偶发重复评论幂等性设计没有到位上线第三天有同事私聊我“为什么改了同一行代码机器人评论了两遍”我去查日志发现是 Gitea webhook 的超时重发机制导致的。平台在请求超时后会重发同一个事件而 open-code-review 当时没有“事件指纹”去重结果就重复插入了评论。修复方式不复杂对事件类型、仓库名、MR 序号、最新 commit SHA 做一次 MD5 作为指纹存到数据库里处理前查重。但因为这个问题出现在上线早期团队对工具信任度被拉低不少。这个教训是凡是入口是 webhook 的服务第一件事就是要设计好幂等而不是先把功能跑通。5.2 检查器跑在容器里导致的仓库权限问题Git 是本地环境的另一件烦心事。检查器容器里需要读取仓库内容、安装依赖但 clone 需要凭证。最开始我图方便把主服务的 Token 直接挂进容器结果发现容器里执行的git clone能拿到技术账号仓库里的 .git 目录也是可读的这在安全上非常糟糕。后来改成在检查容器启动前先用只读方式把仓库文件复制一份到临时目录再把临时目录只读挂载进检查容器。复查器层面只拿代码快照不拿 Git 历史权限范围降到最小。这个问题在云上 CI 里不太明显但自托管时非常值得警惕。5.3 合并门禁太死差点让项目不可交付最开始我想的是“所有 error 级别都必须解决才能合并”结果上线的第二周就遇到一次线上热修一个紧急 bug 修复 MR 里带了两个 lint error但都是无关旧代码格式问题如果等修复线上故障要多持续 40 分钟。最后紧急把 block 级别从“一律阻断”改成“阻断新增的 error”也就是只拦截本次 MR 新引入的 error 和原有 error 中被改动行新增的内容。这个功能在配置里对应block_on: new_issues_only: true我现在强烈建议任何团队都先按这个策略上线。原因也很简单存量债务和新增债务要分开管理。新代码严格把关旧代码慢慢还。如果你想一步到位团队阻力会大到让制度形同虚设。5.4 团队落地时的三个小技巧最后分享几个运行经验之外的小技巧对项目真正在团队里用起来帮助很大每周浏览一次机器人评论汇总发现重复或过于苛刻的规则就调低级别。规则是给团队服务的不是给团队添堵的。在仓库根目录放一个REVIEW_CHECKLIST.md把机器人覆盖不到的检查项写进去比如“数据库迁移是否需要回滚方案”“这个改动是否影响灰度开关”。open-code-review 可以把该文件加载进 MR 摘要区作为人工评审的提醒。不要追求 100% 覆盖。有些规则跑全量很浪费时间比如依赖漏洞扫描完全可以交给专业平台去处理open-code-review 做好聚合和呈现就够了。工具的意义从来不是取代人而是把人的精力留在最值钱的地方。open-code-review 在我这里做到的事情很朴素机械检查全交给机器人工评审集中看设计、逻辑和可维护性。坚持用了一年后团队 MR 的平均合并时间反而缩短了一截因为大部分琐碎争议在提交阶段就被机器提前按规则拦下真正到 reviewer 面前时评论区讨论的基本都是值得讨论的事情。