1. 先说清楚:open-code-review 到底解决什么问题
先说个现象。我见过太多团队的 Code Review 流于形式了。PR 挂着三五个小时没人管,偶尔出现一个 reviewer,回一句 LGTM 就算完事。等到合并之后发现 bug,大家才想起来“当时没人细看”。我参与维护的几个开源项目里,这类场景更常见,因为贡献者互相不认识,评审质量完全取决于运气。
所以我花了一段时间,把整条代码评审链路重新捋了一遍,搭了一套可以自托管、完全基于开源工具、从提交那一刻就开始运转的方案。这套方案我叫 open-code-review,核心思路是八个字:机器先跑,人做判断。它不是一个单体软件,而是一套流水线设计,由 GitHub Actions、Reviewdog、SonarQube、ESLint、commitlint 等组件组合而成,仓库里的配置即代码,任何人都能通过改配置来调整评审规则。
它解决的核心问题有三个:评审没有责任人、检查标准不统一、人工被重复劳动拖垮。适合三类人参考——开源项目维护者、中小规模团队的技术负责人,以及想给团队补上自动化质量门禁的开发者。下面我把整套方案从设计到落地一点点拆开讲,包括我踩过的坑和调参经验。
1.1 代码评审为什么会“形同虚设”
评审走形式,问题多半不是“人不愿意看”,而是“不知道该看什么”。人面对一个改动 500 行的 PR,如果没有任何前置过滤,reviewer 要同时做格式检查、命名判断、逻辑推演、安全边界确认,信息量太大,最后大概率只会点开两个文件随便扫一眼。而当评审标准没有被文档化、没有被机器强制执行时,每个人标准都还不一样:有人揪着代码风格不放,有人只看逻辑不看边界,有人连看都不看就批准。
我在一个五人后端小组里做过统计,三个月时间里合入了超过两百个 PR,平均每个 PR 的讨论评论不到两条,一半的 PR 在创建后两小时内就被合并。问题不是大家态度差,而是没有一套机制帮 reviewer 区分“需要人判断的问题”和“机器可以判断的问题”。open-code-review 这个流程想做的就是这件事:把能自动核验的部分全部往前移,让人工评审聚焦在唯一不可替代的部分——架构合理性、业务语义、边界条件、以及长期维护成本。
1.2 这套体系适合谁,解决什么问题
先说适合谁。
- 开源项目维护者:贡献者水平参差,Commit 格式、Lint 规范、测试门禁如果不自动化,维护者每天都在处理完全可以交给机器的琐碎问题。
- 中小团队技术负责人:人少,事务杂,能拿出来专门做评审的时间非常有限,更需要一套自动拦截系统帮团队守住底线。
- 想引入代码质量平台但不想把代码上传到第三方商业服务的团队:这套链路所有组件都能自托管,代码留在自己可控的服务器上。
不适合的场景也很明显:单人开发或者纯实验性项目,硬套整套流程会带来额外负担。对这种项目,只需保留最基本的一条:PR 必须清楚描述改了什么、为什么改,以及测试是怎么跑的。
这套方案真正解决的是三件事:一是责任明确,每个目录、每个关键模块里都有明确的人来兜底;二是标准统一,风格、安全、覆盖率这些争议点全部交给工具,用规则说话而不是靠吵架;三是反馈加速,机器检查结果、扫描报告、质量门禁全部自动出现在 PR 里,reviewer 打开一个页面就能看到全貌。
2. 整体设计思路:把“人机协作”变成一条流水线
2.1 三层检查模型:机器先跑,人再做判断
我设计 open-code-review 的时候,核心是把代码评审拆成三个层级。
第一层是格式与规范层。常见实现是 ESLint、Prettier、Stylelint、Hadolint、commitlint。这一层最机械,成本最低,收益却很直观。你只要跑一次格式检查,就能在 PR 页面直接看到行号级提醒,而不用 reviewer 在评论里为了一个缩进跟作者争论半小时。
第二层是静态缺陷与质量趋势层。我用 SonarQube 承担。它不只检查单个文件有没有明显 bug,还会做重复代码检测、复杂度统计、覆盖率追踪、已知安全漏洞扫描。质量门禁可以直接拦截,相当于在合并按钮前多了一道闸门。
第三层才是人类评审层。经过前两层之后,reviewer 需要关心的问题被大大收敛:这段逻辑有没有可能空指针?这个接口设计是否考虑到后续扩展?这个改动的性能影响范围在哪里?这些顶层问题才是人的价值所在。
我第一次给团队讲这个模型时,用了过马路的类比:机器是红绿灯和斑马线,帮你把最容易出错的地方强制管住;人是过马路时左右看的那个角色,需要根据即时路况做判断。红绿灯永远不会替你判断司机是否闯红灯,但它能大幅降低你过马路的整体风险。代码评审也同理。
2.2 工具选型:为什么是这几种组合
坦白讲,可用的工具非常多,但每一项我都试过之后才选现在这组。这里提一个非常重要的选型原则:优先选“配置即代码”的开源工具,并且能力要单一清晰,方便插拔。
GitHub Actions 作为编排层,优点是和 GitHub 仓库天然集成,几乎不需要额外写胶水逻辑。如果你用的是 GitLab,对应层是 GitLab CI,思路完全通用。Reviewdog 本身不是检查器,它像一个“中间人”,接收任意 linter 的输出,把结果转换成 GitHub PR 评论或检查项。这么设计的价值在于,你的工具链随时可以换——今天用 ESLint,明天想换成 Biome,只要改一行命令,不需要动整个流程。
SonarQube 的选择则需要多说一句。很多人觉得它重,因为要自建服务和数据库。但它在团队内部环境下的优势非常明显:支持多语言、覆盖面广、质量门禁成熟,数据完全由团队自己控制。对比一些云端代码扫描服务,SonarQube 不需要把代码送去外部平台解析,对数据留存位置有要求的团队更放心。代价是运维量增加,如果团队完全没有服务器资源,也可以先用 GitHub 自带的 CodeQL 和 Dependabot 做替代,等仓库规模大了再评估要不要换 SonarQube。
2.3 关键设计:规则与配置本身也要被评审
open-code-review 和很多“配置完就没人管”的工具链最大的区别,在于我把所有评审规则和配置文件都放进代码仓库,而不是散落在某一个管理后台。仓库里会有一个 .github 目录、一个 sonar-project.properties、一个 commitlint.config.js,所有规则变更都必须走和业务代码一样的 PR 流程。
这个设计有两点好处。第一,可追踪:谁在什么时候改了什么规则,为什么改,都写在 PR 描述里。第二,可讨论:每次规则变更都会触发团队讨论,而不是某个人偷偷在管理后台把某个检查关了。我见过不少团队,一开始严格做了质量门禁,后来某次上线太赶,负责人在后台直接把门禁关了,之后再也没有人开过。把规则放进仓库,至少能保证关闭规则的动机和过程是透明的。
这里有个小细节值得分享:规则文件的目录命名我特意保持标准,比如 .github/CODEOWNERS、.github/workflows,因为平台只认这些固定路径,放错地方不会报错,只是静默失效,这是最常见的坑之一,后面会在排查部分单独展开。
3. 从零搭建 open-code-review 的关键步骤
3.1 第一步:固化主干分支保护规则
先在最外层建立防线:对 main(或 master)分支启用分支保护。位置在 GitHub 仓库的 Settings -> Branches -> Add rule,把 main 填入 Branch name pattern。然后勾选以下三个项目:
- Require a pull request before merging:禁止直接往主干推送,必须通过 PR。
- Require status checks to pass before merging:CI 检查没有通过时不能合并。
- Require review from Code Owners:关键目录必须由对应负责人批准。
我一般还会勾上“Do not allow bypassing the above settings”(不允许管理员绕过)。这一步容易被忽略,但不勾的话,遇到上线紧急,管理员就会直接绕过规则,整条流水线瞬间失去意义。既然是团队定的规则,就一视同仁。顺便提一句,这条规则只对新建规则以后的分支保护生效,老仓库里有大量历史分支的话,不必急着全部清理,先在主干上启用,后续慢慢收敛。
3.2 第二步:用 CODEOWNERS 锁定关键目录
CODEOWNERS 是代码托管平台的代码责任人声明文件。它在 .github/CODEOWNERS 路径下,内容写的是“路径匹配规则 + 责任人用户名或团队名”。
我用一个例子来演示,假设仓库结构是:
. ├── src/api ├── src/core ├── docs └── scripts那么一个典型的 CODEOWNERS 文件长这样:
# 默认兜底责任人,所有未被下面规则覆盖的文件都由这个团队负责 * @your-org/platform-core # 关键 API 目录,只有后端 reviewer 团队可以批准 /src/api/ @your-org/backend-reviewers # 文档目录,由文档维护者负责 /docs/ @your-org/docs-maintainers # 核心算法模块,必须由指定负责人批准,不能由其他模块的人代审 /src/core/ @user-alice @user-bob这里有几个官方文档不会特意强调的细节。第一,路径必须写绝对路径,以 / 开头才表示从仓库根目录开始匹配;如果不写斜杠,会匹配任意层级下的同名目录,容易造成“你以为管住了 src/api,结果 target/src/api 也被管住了”的误伤。第二,规则是按顺序匹配的,最后一个匹配的规则生效,如果你希望核心目录优先匹配,就把通用规则写在文件最上面。第三,CODEOWNERS 不会阻止非 owner 提交 PR,它只强制“必须有人批准”,相当于把责任明确给具体的人。
3.3 第三步:接入 SonarQube 质量门禁
SonarQube 的部署我建议直接用 Docker Compose 方式起步,至少需要两个容器:一个 sonarqube 服务,一个 PostgreSQL 数据库。生产环境不建议用内置 H2 数据库,数据丢了会非常痛。我这里给出一个最小化的 Compose 片段,数据库账号密码请自己替换:
version: "3" services: sonarqube: image: sonarqube:community ports: - "9000:9000" environment: - SONAR_JDBC_URL=jdbc:postgresql://db:5432/sonar - SONAR_JDBC_USERNAME=sonar - SONAR_JDBC_PASSWORD=change_me depends_on: - db db: image: postgres:13 environment: - POSTGRES_USER=sonar - POSTGRES_PASSWORD=change_me - POSTGRES_DB=sonar volumes: - sonar_db:/var/lib/postgresql/data volumes: sonar_db:服务起来后,在 Web 界面创建项目,拿到令牌,然后把项目和扫描配置写进仓库。sonar-project.properties 的示例:
sonar.projectKey=my-project sonar.projectName=My Project sonar.sources=src sonar.sourceEncoding=UTF-8 sonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,**/*.min.js sonar.javascript.lcov.reportPaths=coverage/lcov.info注意开头的 sonar.projectKey 一定和 Web 后台创建的项目保持一致,否则扫描报告会不知道往哪里传。接下来在 GitHub Actions 中,只需要两步:执行扫描、获取质量门禁结果。扫描和门禁可以放在同一个 workflow 里,先跑 scan,再跑 quality-gate。
3.4 第四步:用 Reviewdog 把静态检查结果钉到评论里
Reviewdog 是整个流程里体验提升最明显的一个组件。它接收 linter 的解析结果,把它变成 PR 里的行内评论。这样大家不用去 CI 日志里翻错误,打开 PR 的 Files changed 页签就能看到问题所在。
以 ESLint 为例,一个最小配置是这样的:
name: open-code-review on: pull_request: types: [opened, synchronize, reopened] permissions: contents: read pull-requests: write jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: npx eslint . --format checkstyle --output-file eslint-report.xml - uses: reviewdog/action-eslint@v1 with: reporter: github-pr-review level: warning fail_on_error: false这里有两个容易踩坑的点。第一是 permissions 配置,工作流默认的 GITHUB_TOKEN 没有写 PR 评论的权限,必须在 workflow 层级显式打开,不然 action 跑完了一切正常,但评论就是发不出去。第二是 level 参数,我强烈建议新团队从 warning 而不是 error 起步。你设成 error 并且 fail_on_error 为 true 的时候,任何一个小 lint 错误都会让 CI 变红,PR 合不进去。表面上看起来门禁很严格,实际会逼着大家把规则整体放宽,最后形同虚设。先让评论出现,让团队习惯在评论里解决问题,稳定之后再慢慢升严格度,我觉得安全得多。
3.5 第五步:约定提交信息规范,并且强制校验
提交信息看起来和代码评审没关系,但它是历史的一部分。Review 一个 PR 时,我经常先看提交历史,如果是一堆 wip、fix、update,说明开发者自己都没想清楚改动是怎么演进的,这时就要提醒重写提交信息。为了让这个提醒不依赖人肉自觉,我引入了 commitlint 配合 @commitlint/config-conventional 规则。
在 package.json 中安装之后,配置一个 commitlint.config.js:
module.exports = { extends: ["@commitlint/config-conventional"], rules: { "type-enum": [ 2, "always", ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"] ], "header-max-length": [2, "always", 100] } };常见的 commit type 含义做一个快速表格方便大家抄作业:
| type | 场景 | 示例 |
|---|---|---|
| feat | 新功能 | feat: add login page |
| fix | 修 bug | fix: correct timeout unit |
| refactor | 重构,不改外部行为 | refactor: extract parser |
| perf | 性能优化 | perf: lazy load routes |
| test | 补测试 | test: add e2e for checkout |
| ci | 改 CI 流程本身 | ci: pin action versions |
校验可以挂在 CI 里:在 Actions workflow 中加入一个 job 检查 PR 的全部 commit message。我建议同时校验 PR 标题,因为很多团队合并方式用的是 Squash merge,最终历史里只有 PR 标题,标题不规范等于提交信息白校验了。做一个简单 action 检查 PR title 的开头类型即可,规则和 commitlint 保持同一套。
4. 真正让评审流程“转起来”的协作细节
4.1 评论分级:Blocking、Nit、Question 怎么用
工具链把机械工作接管之后,人类的评论质量就成为评审质量的关键。我观察到一个常见问题:很多 reviewer 把所有意见都写得像命令,语气一样重。为了减少无效争论,我在团队约定里推行一套评论前缀分级法。这不是新概念,很多知名开源项目都在用类似规则,但真正把规则写进 CONTRIBUTING 文档、并且每天执行的团队不多。
三个前缀足够用:
- Blocking:表示这是必须修改的问题,不改不能合入,比如正确性错误、安全问题、数据丢失风险。
- Nit:表示非阻塞的小建议,比如命名微调、注释措辞、格式偏好。作者可以选择忽略。
- Question:表示 reviewer 没看明白,需要作者解释,不一定是代码有问题,只是需要补充沟通。
用一个表格对比:
| 前缀 | 是否阻塞合并 | 典型场景 | 作者该怎么回应 |
|---|---|---|---|
| Blocking | 是 | 空指针风险、逻辑错误、隐私泄露 | 修改代码并重新 push |
| Nit | 否 | 变量名、注释表达、小重构建议 | 采纳则改,不采纳说明理由 |
| Question | 否(除非太严重) | 业务语义不清、上下文缺失 | 在评论中解释,必要时补注释 |
这套分级极大减少了作者和 reviewer 之间“揪着无关痛痒问题互怼”的情况。因为 Nit 天然是低优先级的,大家不用为了每个建议都据理力争。而一旦出现 Blocking,所有人也会更认真对待,不会把它淹没在一堆格式建议中。
4.2 响应时效与 Review 轮次控制
评审流程跑得顺畅,必须管理两个时间指标:首次响应时间和最终合入时间。
首次响应时间指的是 PR 创建到第一条 review 意见出现的时间。即便意见只是“我还在看,预计两小时内给完整反馈”,都比让作者干等要强。我见过最大的流程阻力就是“PR 交了三四天没人看一眼”,这种折磨足以让人不再愿意发起 PR。小团队可以约定 6 小时内必须回复,开源项目可以更宽松些,但至少要有明确的响应预期。
另一个指标是 Review 轮次。一个 PR 反复来回收 5 轮以上,效率极低。我在团队里定了一条红线:超过 3 轮 Review 之后,如果还在围绕同一块代码打转,建议直接视频会议或者面对面看代码。很多时候文字交流效率太低,两个人各说各话,代码一打开说话就懂了。
还可以配一个简单的机器人提醒。用 Actions 写一个 cron 任务,扫描超时未合并而且没有评论的 PR,在群里发提醒。关键是把“催”这件事机器化,不要每次都由作者私下找人,太消耗个人关系。
4.3 两种极端情况怎么处理
极端情况我整理了两类,几乎每个团队都会遇到。
第一类是“拖单”。PR 创建后无人理会,或者 reviewer 被点名后一周没动静。处理思路是让机制来提醒,而不是靠人催。除了上面说的 cron 提醒,GitHub 也支持在 PR 超过一定时限后自动重新分配 reviewer,配置在仓库的 settings 里可以启用。如果拖单是因为这个模块只有一个人懂,那问题的根源不是流程,而是知识过于集中,需要在平时做结对或者写文档来稀释。
第二类是“刷屏式评论”。有的 reviewer 事无巨细,把 lint 本来就该检查的格式问题逐条写评论,一篇文章能提 40 个问题。你说他不对吧,他确实认真了;但这种方式说白了就是把机器该干的活拿回来干,还顺带污染了评论区间。解决办法:把所有风格类规则写进 lint 配置和文档,把“这就是规定”从评论中拿掉。对于风格争议,给作者留一条“如果不同意规则,请单独提 PR 改 lint 配置”的出路,在评论区不争论。
5. 常见问题与排查经验速查
5.1 自动评论为什么没触发
跑这套流程的头两个月,我反复遇到的第一个问题就是:PR 提了,CI 也绿了,但 PR 页面就是看不到 reviewdog 的评论。
这里从现象反推排查方向,我按出现频率排了序:
- 第一步:打开 Actions 页面看具体 job 日志,这永远是第一步,不要凭感觉猜。
- 第二步:确认 workflow 的触发条件是 pull_request,并且没有在 paths 过滤里把代码路径排除掉。
- 第三步:确认 permissions 里给了 pull-requests: write,旧版本的 workflow 模板里经常没有这一项。
- 第四步:确认 reporter 参数是 github-pr-review 而不是 github-check。GitHub Check 只在 Conversation 页面有一条记录,不会出现在 Files changed 页面,很多人以为工具没跑其实只是没看到位置。
Code Owners 不生效是另一类“没生效”。最常见原因是 .github/CODEOWNERS 文件名大小写不对,注意是全大写;或者文件放到了仓库根目录而不是 .github 目录下。另一个原因是我前面提过的路径匹配错误,不止一个同事把目录写成了 src/api(没带前导斜杠),结果匹配的是所有层级下的 api 目录。
5.2 扫描范围失控与占用时间过长
SonarQube 接入后,团队第二次爆发的问题是扫描时间越来越长,从最初的两分钟涨到十几分钟。查下去,发现扫描器把 node_modules 或者构建产物都扫进去了,配置了 sonar.exclusions 之后才恢复正常。所以首次配置时,务必第一时间确认排除目录,并且跟团队成员说清楚:排除掉的目录不会再被扫描,如果哪天发现某条规则在产出物里不再触发,先检查是不是被排除规则吞了。
还有一个常见问题:仓库历史非常长,首次全量扫描很慢。这种情况我建议先在主干上手动跑一次全量分析,把基线建立起来,之后 CI 里只分析新代码。不要每次 PR 都从头分析一遍整个历史,也没必要。SonarQube 的增量分析能力默认就基于 commit 差异工作,但你需要保证每次 CI 使用的 sonar.projectKey 是同一个,否则无法关联历史版本,又变回全量扫描。
5.3 工具误报太多怎么办
静态分析和 lint 工具都会有误报,完全不误报的工具基本不存在。关键是怎么防止误报让团队失去信心。
我的经验是设置一个“报告周期”。每个季度挪出半天时间,专门过一遍过去一个季度里被标记为误报的扫描报告。如果某一个规则上线后误报率超过一半,就说明这个规则在本仓库里不适合,要么调整参数,要么先禁用。如果只是偶发误报,就在 SonarQube 里标记为“不会修复”,同时在代码里写一行注释说明原因,未来任何人看到都能快速理解,而不是被同一个坑反复绊倒。
Reviewdog 的评论有一个特别好的地方:每条 comment 都可以回复。我们约定,如果作者认定 Lint 结果是误报,可以直接在评论下回复理由,再结合 fail_on_error 设置为 false 的配置,作者就不至于被错误阻塞。等到误报同类问题累积到一定量,再由负责规则的同学统一调整。
5.4 老仓库历史规则变更后的回扫问题
流程跑起来以后,规则一定会持续调整,比如新加了一个安全规则,或者把某个 lint 规则从 warning 升到 error。这时团队一般会提出一个疑问:已经存在的 PR 是不是会被门禁卡住?会不会突然冒出一堆历史问题?
我的处理方式:规则变更和业务代码变更一样走 PR,在 PR 描述里写清楚变更动机,并指定负责人。合并规则变更后,在 SonarQube 里对目标分支触发一次重新分析,把质量门禁结果刷新。至于历史存量 PR,尽量不要批量“翻旧账”,否则那些已经开了很多轮、即将合入的 PR 会被突然打回,非常伤团队士气。可以把存量问题单独建一个技术债任务,定期清理,而不是卡在业务 PR 上。
5.5 一点朴实的工程心得
整套 open-code-review 的方案从设计到今天,我最大的体会是:工具解决的是“一致性”问题,解决不了“责任心”问题。一个团队如果连 PR 描述都不愿意写,你贴再多自动检查也只是让流程看起来热闹。
所以最后分享一个特别具体的小习惯:每条 PR 的模板里,我都放了三行必填问题——改动背景是什么、设计思路是什么、测试怎么覆盖。刚开始大家嫌麻烦,但我坚持了一个月后,效果非常明显。因为回答这三个问题的过程,本身就是一次“自我评审”,很多人写着写着就会发现自己的方案有问题。接下来再交给自动化工具和 reviewer 时,讨论的起点已经高出很多了。
如果你也准备在团队推进类似流程,我建议不要一次性把质量门禁拉到最严。先把 CODEOWNERS 建起来,把 ESLint 或者同类工具接入 reviewdog,让自动评论出现在 PR 里,这一件事已经能赢过大多数团队。后续再慢慢加 SonarQube、加 commitlint,每加一层都要观察团队的反应再做调整,别让流程变成了新的负担。