1. 为什么要做一套"开放式"代码审查:先承认手动 Review 靠不住
深夜十一点,我在飞书群里被 @ 了一次。同事把一支 800 行的 PR 推到群里,留言是"求 review,明天要合主干"。我点开 Diff,前前后后翻了十分钟,勉强看完一半,最后只能回一句"改动比较大,我看完再给意见"。第二天早上,那个 PR 被另外两个人点了 Approve,合进去了。两周后线上出问题,追查变更记录,就是这个 PR 里一个边界条件没处理。
这种事情发生多了,你就必须承认:手动 Code Review 在团队协作里天然是失效的。不是谁不够负责,是人的注意力、时间、情绪状态决定了我们不可能对每一行 Diff 都保持同等浓度的专注。于是我开始认真思考一个问题:能不能把代码审查里面那些"机械的、可被规则覆盖的部分"交给自动化去做,把人解放出来,只处理那些真正需要人脑判断的东西?
这就是"open-code-review"这个项目在我日常工作中落地实践的起因。
名字叫 open-code-review,直译是"开放代码审查"或"开放式代码评审"。我自己的理解它有两层含义:第一层,是审查这件事本身要开放,规则、结论、过程数据对团队成员全部可见;第二层,是整个审查流程要像开源项目一样可以被扩展、被定制,规则不锁死在某个平台里,而是以配置文件的形式沉淀在仓库里。简单说,它并不是一个"装完就完事"的单一工具,而是一套把代码审查标准转变成自动化检查项的工作流方案,配套提交信息检查器、变更范围分析器、CI 机器人、通知回调这几块基础能力。
如果你问这套东西到底适合谁,我的回答是:任何一个还在靠"人肉盯 Diff"维持代码质量的团队,尤其是 10 人以上、PR 数量每天超过 20 个的团队,都应该往这个方向迈一步。它不适合那种几个人写脚本、互相之间非常了解的小圈子,那种场景下靠口头沟通效率反而更高,引入规则是自找麻烦。
这篇文章,我会把为什么做这套体系、规则怎么设计、代码怎么落地、怎么接入 GitHub Action/GitLab CI、以及我在这几个月踩到的几个真实坑,从头到尾讲一遍。代码量不大,思路比代码重要。
1.1 我遇见的评审失效现场
先说说我观察到的评审失效的三个典型现场,看看你所在团队是不是也有类似情况。
第一个是"橡皮图章"现场。PR 一出来,Reviewer 扫一眼标题,没明显语法错误,点 Approve。倒不是懒惰,而是大家手上都排着开发任务,心理上默认"写代码的人自己应该测过了"。第二个是"前端堆积"现场。Diff 太大、逻辑太绕,Reviewer 看了一半,丧失耐心,留下一句"这部分我看过了,其他你们再看看",实际上审查覆盖率可能不到 30%。第三个是"老好人"现场。团队同学互相之间不好意思提意见,偶尔提一两条也不痛不痒,几十条评论集中在代码风格上,真正的设计问题反而不在讨论范围内。
这三个现场的共同点是:流程在形式上存在,但在效果上缺失。Review 变成了一个"合入流程里必须走的一步审批",而不是一次真正的质量检查。open-code-review 希望做的,不是消灭人,而是把人的精力从那些一眼扫过就能发现的问题里抽出来,让人聚焦在真正需要讨论的架构、性能、安全、边界条件上面。
1.2 这套流程要解决的三个具体问题
我把需求收敛成三个用大白话能说清的问题。
第一,提交信息好不好懂。我们团队早期的 commit message 五花八门,有 "fix bug"、有 "update"、有 "wip"、还有一条只有一个句号。等到回溯问题时,git log根本没法看。这个问题靠自动化检查完全能覆盖:提交信息必须符合约定格式,比如feat(module): description、fix(api): handle timeout。
第二,变更范围和 PR 描述对不对得上。很多 Review 失效的根源是 Reviewer 不知道这个 PR 在干什么,因为描述里只有一句话或者干脆是空的。这个问题也可以规则化:PR 描述必须写清楚背景、测试方式;变更文件清单会自动生成,和描述里宣称的改动范围做一致性检查,比如"你改了数据库迁移文件,但描述里没提",这种就直接提醒。
第三,重要的质量信号是不是被忽略。比如新增了方法但没有配套测试、改动超过了限定的行数但没触发二次评审、包含 TODO/FIXME 关键字的代码被合入主干。这些问题不一定是硬错误,但它们是重要信号,值得提醒 Reviewer 重点看。
这三个问题定下来之后,整个 open-code-review 的规则体系就开始围绕它们展开。我在后面写规则一节里,会给出实际配置和代码,你可以直接拿去改。
2. 规则先行:把评审经验写成可用的配置与代码
工具是壳,规则是魂。一个 code review 自动化体系能不能在团队里活下来,关键在规则设计得是否"知情识趣"。
我在最初的版本里犯了所有新手都会犯的错误:规则定得太密、太死。每个文件必须有测试、注释率不低于 20%、圈复杂度不能超过 10……结果就是机器人满屏跑红,开发同学被提醒条目刷到麻木,最后没人看评论,和当年设计这个规则的初衷背道而驰。后来我把整个规则体系重构了一版,分成两个层次:阻塞项与提醒项。
2.1 两层规则体系:阻塞项与提醒项
阻塞项,只要触发,CI 就失败,PR 禁止合入。它只用于那些"晚发现不如早发现"的高成本问题,我总共就留了五条:
| 规则 | 说明 | 触发条件 |
|---|---|---|
| 提交信息规范 | commit message 必须匹配约定格式 | 正则不匹配 |
| WIP 禁止合入 | 防止把草稿误合入主干 | 标题或描述包含 WIP/Draft 关键字 |
| TODO/FIXME 门禁 | 主干禁止遗留未解决的标记 | 新增代码行包含 TODO/FIXME |
| 回调地址校验 | 配置中 Webhook 地址必须合法 | URL 不合法 |
| 基础变更保护 | 锁定文件(如 workflow、pipeline)必须双人评审 | 涉及锁定文件 |
提醒项,只发通知,不阻止合入。它负责给出信号,让 Reviewer 多留个心。比如:
- 新增代码行数超过 400 行,提醒"这个 PR 偏大,建议拆开评审"。
- 改了公共接口定义,提醒"可能影响调用方,请确认兼容性"。
- 新增文件没有对应测试文件,提醒"是不是遗漏了测试"。
这个设计的核心逻辑是:阻断要少而准,提醒要多而全。阻断太多,流程会被绕开;提醒太多,重要信号会被淹没。这个平衡我们后面还踩了很多次,第三节我会专门讲我怎么调优的。
2.2 提交信息与变更范围检查:核心代码片段
规则定完了,就得落代码。开源版 open-code-review 我选了两门语言分别实现:核心规则引擎用 Python 写,便于团队二次修改;与 GitHub/GitLab 的集成层用 TypeScript 写,方便复用各家平台的 Webhook 生态。这里我贴一段核心的提交信息检查器的伪代码实现,思路比语法重要:
import re COMMIT_PATTERN = re.compile( r'^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)' r'(\([a-z0-9_-]+\))?!?: .{1,72}$' ) def check_commit_messages(commits): """检查一个 PR 内所有提交信息是否符合规范。""" errors = [] for commit in commits: message = commit["message"].strip().split("\n")[0] if not COMMIT_PATTERN.match(message): errors.append({ "sha": commit["sha"][:8], "message": message, "reason": "格式不匹配,期望如 feat(module): description" }) return errors这段代码的逻辑很简单,就是拿正则去匹配提交信息首行。但要注意几个细节:第一,{1,72}限制首行不要超过 72 个字符,这是沿用了 Git 社区的习惯,超过的话在终端和网页端都会被截断;第二,允许!:这种感叹号标记,比如feat(api)!: change response format,这是 breaking change 的约定式写法;第三,正则只检查第一行,是因为 commit body 是给人工看的详细介绍,用机器去严格约束并不合适。
变更范围检查器稍微复杂一点。它会拉取目标分支和当前分支的 Diff,提取出所有变更文件的路径。然后,分三路处理:
def analyze_changed_files(diff_files): lock_file_hits = [] missing_tests = [] generated_hits = [] for path, meta in diff_files.items(): # 1. 锁定文件只要动过,就必须提醒 if path in LOCKED_FILES: lock_file_hits.append(path) # 2. 新增了代码文件,但没有同名的 test 文件 if meta["status"] == "added" and path.endswith(".py"): test_file = path.replace(".py", "_test.py") if test_file not in diff_files: missing_tests.append(path) # 3. 改动是否涉及生成目录/迁移文件 if "/generated/" in path or path.endswith(".sql"): generated_hits.append(path) return lock_file_hits, missing_tests, generated_hits这段代码里我加了/generated/和.sql的检查,这是团队自己的特殊需求。开发框架的前端代码有一部分是自动生成的,经常有人手改生成文件,改完一重新生成就覆盖了,查半天查不出问题。加上这个提醒后,这种情况明显少了。所以说,这套系统里的规则一定要贴近自己团队的真实痛点,不要照抄别人家的规则列表。开源版的配置是在rules.yaml里维护的,rules.yaml里每一项规则都可以单独开关、调整阈值,详见 2.3 节。
2.3 规则阈值怎么定:我和团队的调试过程
规则引擎写好后,最先要回答的问题就是阈值定多少。这个没有标准答案,我跟团队花了接近一周去调。具体做法是:先拉出过去三个月的 PR 数据做回放,把这些 PR 跑一遍新的规则引擎,看每个规则会触发多少次、误报率有多高。
比如"新增代码行数超过 400 行提醒",我最初定的阈值是 300 行,回放后发现团队有 35% 的 PR 都会触发这条,这就不合理了。因为很多 PR 是从后端模板自动生成的 CRT 文件,行数天然很多。把阈值上调到 500 行,加上"排除自动生成文件"和"排除 lock 文件"的后置条件,触发率降到了 8%,这 8% 的 PR 确实普遍偏大,提醒是有价值的。
回放还有一个好处:你能跟团队成员展示"这个规则不是拍脑袋定的,是拿我们自己的数据验证过的"。在推行新流程的时候,这点信任基础特别重要,不然很容易变成"工具来教育人了",那阻力会巨大。
权重上,我建议按"影响成本"排优先级。一次错误合入如果导致线上事故,修复成本可能是几千块钱的报酬和通宵加班;而一次"觉得规则太烦"的吐槽,成本只是聊聊天。所以阻塞项宁可定得少,也要定在最容易出大事的地方,比如改数据库表结构、改流水线配置、改对外接口签名,这些必须强制双人甚至三人评审。
3. 从本地到远端:把检查织进现有工作流
规则引擎本身再强大,如果每次都要手动跑命令,到最后一定没人用。好用的工具是藏在工作流里的,而不是放在工作流外的。所以 open-code-review 落地的时候,我把检查分成了三个执行阶段,分别嵌在开发循环里:
3.1 本地阶段:pre-commit 与 pre-push 钩子
本地阶段最有价值的是 pre-commit 和 pre-push 两个 Git Hooks。pre-commit 管单个文件级问题,pre-push 管提交信息规范和审查信号。
我的做法是给仓库根目录放一份.git-hooks/pre-push脚本,然后通过git config core.hooksPath .git-hooks/让团队所有成员自动启用,不需要每个人都手动复制脚本到.git/hooks目录。pre-push 脚本里做了这样几件事:
- 运行提交信息检查器,扫描即将推送到远端的所有 commit message。
- 运行变更范围分析器,输出本次推送涉及的新增/修改文件清单。
- 如果存在 TODO/FIXME 关键字,不阻断推送,但在控制台打印警告信息。
- 如果提交信息格式不对,阻断推送,提示用
commitizen或手写符合规范的 message。
这就是"本地能拦住的问题绝不放飞到 CI 上"的原则。CI 是有排队时间的,本地拦截一秒钟出结果,体验好太多。刚开始有人抱怨"怎么我推送被拦了一下",后来习惯了,反而觉得踏实,因为他不担心自己犯低级错误害大家 CI 挂掉。
不过这里也要给一个提醒:Git Hooks 不是强制的拦截手段,用户完全可以用--no-verify跳过。所以本地钩子解决的是"降低低级错误率",不是"保证质量",真正的底线还是放在 CI 阶段。
3.2 CI 阶段:GitHub Actions 集成示例
CI 阶段是 open-code-review 的核心现场。这里我以 GitHub Actions 为例,贴一段实际在用的 workflow 配置,GitLab CI 的原理大同小异,只是 YAML 关键字不同:
name: open-code-review on: pull_request: types: [opened, synchronize, reopened, ready_for_review] jobs: review: runs-on: ubuntu-latest permissions: checks: write pull-requests: write steps: - uses: actions/checkout@v4 - name: Run open-code-review uses: your-registry/open-code-review@v1 with: config: .github/open-code-review.yml github-token: ${{ secrets.GITHUB_TOKEN }} mode: review api-endpoint: ${{ secrets.REVIEW_API_ENDPOINT }} - name: Publish review report run: cat review-report.json > "$GITHUB_STEP_SUMMARY" if: always()这里面有两个细节值得说。第一,on.pull_request.types里加上了ready_for_review,这很重要。很多团队用 Draft PR 做 WIP,把 PR 从 Draft 切到 Ready 的时候,才是真正需要 Review 的时刻,这时候跑一次检查最合适。第二,workflow 里拉取review-report.json之后我把它汇总到$GITHUB_STEP_SUMMARY,这样 PR 页面直接能看到摘要,不用点进 CI 日志里翻。
mode: review是机器人模式。它不止会打印检查结果,还会直接把评论贴到 PR 的 conversation 里,按照"阻塞/提醒"两级分级展示。评论的格式是固定的,每条都带上规则 ID 和触发文件位置,方便 Reviewer 快速定位。
3.3 结果如何反馈:评论机器人的分级通知
评论机器人是最后一块拼图。规则引擎跑完只是一堆 JSON,如何把它变成团队成员高频率愿意看的信息,直接决定了这套系统有没有用。
我的做法是:PR 更新会触发检查,检查结果发布后,机器人只会做两件事。第一,在 PR 页面上更新"检查报告"置顶评论;第二,根据规则级别决定要不要在群里推送。
阻塞项一但触发,立刻推到团队群,附上 PR 链接和失败原因,文案大概是这样的:"PR #128 未通过检查:commit message 不规范(a1b2c3d4 / fix bug)。详见 check report。" 提醒项不推群,只在 PR 页面上静默展示,避免打扰。
这里我有个很深的体会:人在看到通知的时候,第一反应是判断"这事跟我有没有关系"。群里如果每天被无关痛痒的 PR 通知刷屏,到最后所有人都对群消息免疫,真正重要的信息也会被淹没。所以推送策略宁少勿多,我甚至建议把群通知的阈值调到一个"团队每周不超过 10 条"的程度,这样每一条推送都是有分量的。
还有一点是在设计评论模板的时候,我特意要求每条评论都要提供"如何修复"的链路,而不只是报错。比如"提交信息格式不匹配,请运行git commit --amend后按feat(module): description格式修改"。给出解决路径,比单纯红牌警告有效得多。
4. 上线三个月,我踩过的三个典型坑
自动化规则这条路上,规则本身不复杂,难的是规则在真实团队的土壤里长不长得活。从第一版上线到运行三个月,我们至少踩了三个大坑,每一个都让我对"工具"二字有了新的理解。我挑最有代表性的三个讲讲完整排查思路,这里面的坑很多人都会踩。
4.1 误报太多:规则怎么"学会闭嘴"
上线第一周,我在群里看到最多的消息是"怎么又报红了"。点开一看,清一色都是误报:新增代码行数超过阈值被提醒的 PR 里,有接近一半是前端自动生成的路由表文件,因为/* eslint-disable */没被正确排除;TODO/FIXME 检查把一些注释里的"见 TODO 列表"这种描述也算进去了,导致写文档的人被警告。
误报的本质是规则太"字面"了,它不理解上下文。所以我给规则引擎加了一个系统性的机制:每一条规则都必须能声明自己的排除条件。
比如 TODO/FIXME 检查,我加了"只在新增行内检测,且排除note:、q:、TODO list:等上下文",并允许用正则白名单忽略特定目录。修复后重新回放了这三个月的 PR 数据,误报率降到了 4% 以下,才算能见人。
调试这类问题有个技巧:规则引擎每跑完一次,都必须输出一份review-report.json,里面除了检查结果,还要记录每个规则"为什么没有触发"的摘要信息。很多人只看触发列表,不看未触发原因,结果误报率永远调不明白。有了这份未触发原因日志,你才能知道哪个排除条件是不是写得过宽了、把正常问题也放过去了。
4.2 新增代码量评估失真:重排代码也算改动?
第二个坑出现在"PR 偏大提醒"这条规则上。团队里有人做了一次大范围的 import 排序和工具类迁移,改动文件数量 60+,但实际逻辑变更不到 100 行。系统提醒"这个 PR 偏大,建议拆分",结果这个同学很不高兴,因为他觉得这批改动是机械操作,风险很低。
这个问题的根子在于"规模度量口径"太粗。git diff --stat只会告诉你文件数和增删行数,不会告诉你哪些行是纯移动、哪些是删了又加、哪些是格式重排。
我后来在分析器里加了两层过滤:
- 基于 AST 的变更识别:对于 Python 和 TypeScript 语法,用
ast和@babel/parser解析成语法树后再做 diff,只统计真正发生变化的节点数和行号,重排、重命名变量这类不影响语法结构的修改会被识别出来,降低加权值。 - 忽略"机械变更"目录:比如
migrations/、generated/、lock files,这些目录的改动不计入规模提醒的统计。
加了这两层之后,提醒准确率明显好转,那个抱怨的同学后来告诉我,他开始接受这条规则了,因为"它分得清什么是抄写,什么是写代码"。这也是 open-code-review 相比普通 diff 统计工具更贴近真实 Review 场景的地方。
4.3 团队抵触心理:从"被检查"到"被帮助"
工具层面的坑都好填,最大的坑在"人"这里。推行到第四周,我在周会里提了一句"规则命中率还有 15% 误报",结果一个同事当场说:"我觉得这个工具就是来卡我们进度的。"
他想表达的意思是:他花了很多时间在解释"这个 PR 为什么大""为什么要改这里",而这些解释本来在代码里和 commit body 里已经写了,可机器不认。
这事情本质是沟通成本从"代码评审过程"挪到了"和机器人解释"上。你设计了规则,规则就会要求人回答规则提出的问题,这些问题的答案有时候人不想回答。
我的解决思路很朴素:给规则引擎加了一个"申诉"通道。任何规则触发后,作者可以补充一段说明,类似"本 PR 涉及前端路由拆分,文件多但逻辑不变,建议按 AST 结果重新评估"。系统收到说明后会让这对规则对这一个 PR 降级为提醒项,并在评论里标识"作者已解释,降至提醒"。这个机制运行后,抵触的声音小了很多。
为什么?因为人们在意的往往不是规则本身,而是自己有没有解释的机会。你给他一个出口,他反而自愿会用符合规范的方式来提交代码,因为解释的成本比直接改掉提交信息高得多。
4.4 配置同步问题:规则更新了,大家还在用旧配置
再补充一个我们遇到过的挺有意思的问题——配置同步。规则文件在仓库里维护,但本地 pre-push 钩子用的配置是缓存的,经常出现"CI 用的是新规则,本地跑的是旧规则"的分叉。这个问题不起眼,但一旦出现,就会让开发者对工具产生不信任感:"为什么本地说可以通过,推送上去却被拦截了?"
解决思路也不复杂:pre-push 钩子每次运行前,先拉取远端规则文件,对比 commit hash,不一致就提示更新。或者干脆本地只做"最低限度检查",比如 commit message 格式,其余全部交给 CI 统一执行。我在团队内部最终采用的是第二种思路:本地只拦截提交信息格式和明显的 WIP,其他规则全部以 CI 结果为准。这样虽然牺牲了一点点反馈速度,但保持了唯一事实源,避免了双轨不统一带来的混乱。
5. 几个真实的使用体会:这套东西怎么用才不翻车
运行到第五个月的时候,队里的人基本已经感受不到它的存在了。这反而是我觉得最成功的信号:好的代码质量工具应该隐身在流程里,而不是天天跳出来刷存在感。
在这几个月的使用里,我自己形成了一条明确的判断逻辑:遇到一个规则反复误报,先别急着删规则,要去看这个规则想解决的场景是不是已经不存在了。比如旧版规则"禁止提交调试日志",后来框架升级引进了统一的 trace 日志,导致这条规则彻底失效,正确做法是调整规则匹配模式,而不是硬留着让它在 90% 的场景里误报。
另外,我个人对想在自己的团队里复制这套方案的同行有几个建议。
一,先从两三条规则开始。不要一上来就铺开 20 条规则,团队消化不了。挑最痛的一个点,比如提交信息规范,先跑两周,让大家感受到"好像确实好找历史记录了",再逐步加新规则。规则的增量引入,就是在给团队做一次渐进式习惯培养,节奏错了,工具再强也会被抵制。
二,配置一样要放在版本管理里。规则文件和工作流模板一起入仓库,任何改动都走 PR 流程,有记录、可回滚,这和管代码是一样重要的。我见过不少团队把规则配置放在某个无名无姓的服务器目录里,后来维护的人一走,整个系统就变成一个谁也不敢碰的黑箱。
三,定期做一次"规则审计"。每季度回放一次最近三个月的 PR 数据,看看哪些规则一次都没触发过,那就说明它对当前团队没有价值,删掉它。哪些规则命中率高但是误报也多,那就要调阈值调排除条件。审计之后把结果公示给团队,让大家知道规则不是一潭死水,它是跟着团队节奏生长的。
最后再说一个小技巧。如果你已经把提交信息规范跑起来了,后续可以考虑把规格再往前推一步,在 CI 里接入自动生成 CHANGELOG 的流程。这样一来,规范的收益就直接体现在产品维度上了——每次发版本,CHANGELOG 是按照上一批合入主干的 feat/fix 自动汇聚生成的,不再需要对着 git log 手动整理。这一步做出来,团队里最不关心流程的人也会意识到:之前那些"反人类的 commit 规范",其实最后是省了自己的事。
代码审查这条路,我自己的感触是:它不需要什么天才设计,也不需要多么复杂的算法,它需要的是你愿意观察团队的真实协作状态,并且把观察到的痛点转化成一个又一个细小的规则。而 open-code-review 这种"把规则写在配置里、把配置放在代码里、把结果反馈到流程里"的做法,恰好给这件事提供了一个非常低的起点。你可以从一条 commit message 规范开始,慢慢让它长成一套属于你自己团队的审查文化。