尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于GitHub Actions的自动化代码评审助手:open-code-review实战

基于GitHub Actions的自动化代码评审助手:open-code-review实战 我在团队里负责代码评审code review已经快三年了最大的感受不是“找不到问题”而是“没有时间找问题”。PR 一多review 就变成一种走流程——点开、扫几眼、点 approve真正的质量问题全靠后续 bug 反推。这也是我写 open-code-review 这个开源项目的原由它不是一个替代人类 reviewer 的机器人而是一个把人从“扫雷”中解放出来的评审助手。open-code-review 基于 GitHub Actions 运行在每次 PR 触发时自动拉取 diff跑一组可配置的规则生成分级报告并直接发布到 PR 页面。它既能在 diff 上逐行标注可疑改动也能在 Check 状态里给出 error / warning 级别的总结。这个项目特别适合独立开发者、小团队以及所有把代码托管在 GitHub 且不想折腾额外服务器的人——你把工作流文件放进去剩下的交给机器人就行。下面我把这个项目的设计思路、核心实现、接入方式和踩坑记录完整写出来希望对你做同类工具或者选型有参考。1. 项目立项现成方案一大堆为什么还要自己写1.1 我踩过的 code review 工具坑在动手之前我把市面上的方案大概捋了一遍结论是“能用的都有痛点”。首先是 Gerrit。这个工具在代码评审领域是老前辈了权限模型和审核流非常严谨很多大厂内部都在用它。但对一个 5 到 20 人的团队来说它最大的问题是重需要自建服务、配数据库、维护守护进程还要让团队成员重新学一套和 GitHub 完全不同的工作流。评审质量确实高但引入成本直接劝退。其次是 GitHub 自带的 PR Review 功能。它的体验确实好reviewer 可以逐行评论、提交修改建议、设置 merge 保护。但本质上它依然是一个纯人工流程没有任何自动化能力。一个刚改完的 PR如果 reviewer 没有及时看它就一直挂着。我理想中的工具应该能在“人看之前”先把明显的问题筛掉让人的注意力集中在设计层面。还有一个方向是第三方 AI 代码评审服务比如一部分商业产品或者用大模型 API 自建一个 review bot。这个思路很吸引人但存在两个问题一是代码要被送到外部 API对很多公司来说是合规红线二是定价通常按代码行数或 API 调用量走对一个长期活跃的仓库来说是一笔不可忽视的开销。1.2 三个核心设计目标基于上面的痛点我给自己定了三个设计目标。第一是轻量。整个项目就是一个 GitHub Action用户复制一个 workflow 文件到仓库里就完成接入。不用额外部署服务不用维护数据库也不改变团队现有的 PR 流程。这个目标直接决定了后续所有架构选择。第二是可扩展。每个人对“什么是好代码”的理解不一样我希望工具内置一组合理的默认规则但要允许用户通过 YAML 配置甚至写一段 Python 函数来自定义规则。这样团队就能把自己的工程规范沉淀到规则里而不是只靠口头约定。第三是辅助人而不是替代人。我特别反感那种“AI 已经帮你 review 完了”的说法。open-code-review 的定位是给 reviewer 提前过滤低级问题、指出可疑模式最终的判断权依然在人手里。所以它的检查结果只分 error、warning、info 三个级别error 只在确实能确定有问题时才报绝不滥用。2. 架构拆解一个 Actions 插件如何跑完整套评审流程2.1 模块划分与执行链路open-code-review 的代码结构在设计时就按“一条数据流”来切分整个执行链路非常清晰GitHub Actions 收到pull_request事件触发 workflowAction 从事件 payload 里取出仓库名和 PR 编号用 PyGithub 拉取 PR 的完整 diff用 unidiff 库解析 diff得到“文件 - 代码块 - 行号”的结构化数据规则引擎对每个文件、每个新增行执行检查把检查结果汇总为 Markdown 报告并发布到 PR 评论和 Check 状态。拆成模块之后每个部分都可以单独测试。我在开发时写了一套基于本地 fixture diff 的测试数据不依赖真实网络跑起来很快。这个习惯帮我少走了很多弯路尤其是后面改规则引擎的时候回归测试一跑就知道有没有破坏旧行为。项目的目录结构大致这样open-code-review/ ├── action.yml # GitHub Action 元数据 ├── main.py # 入口串联整个流程 ├── requirements.txt # 依赖清单 ├── rules/ │ ├── __init__.py │ ├── base.py # 规则基类与结果模型 │ ├── builtin.py # 内置规则实现 │ └── custom.py # 用户自定义规则加载 ├── templates/ │ └── report.md.j2 # 评论报告模板 └── .open-code-review.example.yml2.2 为什么选 GitHub Actions 而不是独立服务这是整个项目里最关键的取舍。我第一版其实是用 Flask 写了一个独立 webhook 服务部署在一台小服务器上GitHub 把事件推过来服务处理后再把评论写回去。能用但我很快就放弃了。原因很简单维护成本。独立服务意味着我要管服务器、管证书、管进程守护、管 webhook 的 secret 轮换。对一个本来就是为了“提高效率”的工具来说这个成本完全不可接受。GitHub Actions 则把这些全部吞掉了它是事件驱动的PR 一有变化就自动跑它按次计费个人仓库或者小团队用量下基本不花钱它跑完就销毁环境不需要你维护任何常驻进程。还有一点很重要权限模型天然安全。Action 的 token 是 GitHub 自动生成的作用域可以限制在当前仓库且每次运行都是独立环境。而独立服务如果被攻破相当于你把自己服务器的密钥暴露了。用 Actions 等于把安全边界交给 GitHub对自己的代码量和能力要求都低很多。2.3 数据权限与安全边界在 GitHub Actions 里环境变量和 token 的传递有一套规范。open-code-review 在 action.yml 里定义了一个token输入项默认值写成${{ github.token }}。这样用户不需要自己生成任何密钥只要 workflow 里带上权限声明permissions: contents: read pull-requests: write就能工作。我强烈建议读者在自定义 Action 的时候严格遵循最小权限原则只需要读代码就只给contents: read需要发评论才给pull-requests: write。之前见过不少现成 action 上来就要一堆权限其实完全没有必要权限越大风险越高。3. 核心实现diff、规则引擎和评论发布是怎么做到的3.1 diff 解析重命名、冲突这些坑怎么处理处理 diff 是整个项目最容易被低估的部分。很多人觉得 diff 不就是一段文本吗用正则劈开就行。但真实情况远没有那么简单光“定位行号”这一个问题上就有很多坑。我一开始用正则解析 unified diff结果在处理重命名文件、二进制文件、以及文件末尾无换行符\ No newline at end of file这三种情况时疯狂出错。后来换成unidiff这个 Python 库一行代码就能拿到结构化的 PatchSet每个 Hunk 里有起始行号和新增行列表再也没出过解析错误。有一个细节值得单独强调GitHub 默认生成的 diff 对“重命名”的处理是不稳定的。如果你在 PR 里把一个文件从foo.py改成bar.pyGitHub 展示的时候偶尔会显示为“删除 foo.py 新增 bar.py”偶尔会显示为“重命名”。我的做法是在 workflow 里显式加上git diff --find-renames来生成更规范的 diff再用对应的行号去匹配。绕开这个坑之后规则在重命名场景下基本不会误报。还有冲突问题也需要注意。如果 PR 的源分支落后于目标分支GitHub 生成的 diff 可能是基于一个过时的合并基这就导致行号不准确。我最终的方案是在 workflow 里先执行一次git fetch并用 merge base 生成精确的 three-dot diffHEAD...origin/main确保评审的是真正会合入的那些改动。3.2 规则引擎内置规则和自定义规则统一抽象规则引擎是整个工具的“大脑”我设计了一个非常薄的抽象层。每一条规则本质上就是一个“给定上下文返回若干问题”的函数。为了统一我定义了一个RuleResult模型dataclass class RuleResult: rule_id: str file_path: str line: int level: str # error / warning / info message: str内置规则和用户自定义规则都实现同一个接口输入是单个文件信息和 diff 解析结果输出是一个RuleResult列表。这样用户不需要理解整个引擎的内部结构只需要写好自己那一条逻辑。内置规则我做了 6 个规则 ID检查内容默认级别no-print-left检查是否留下调试输出print / console.logwarningfunction-too-long检查单个函数长度是否超过阈值warningfile-too-large检查单个文件新增行数是否过大infomissing-test检查新增功能文件是否有对应测试修改infokeyword-todo检查 TODO 是否包含负责人信息和日期warninghardcoded-secret检查疑似 token、密码等硬编码error默认阈值都写得比较宽松因为工具刚上手的时候如果误报太多团队很快就会失去耐心。宁可漏检一些也不要让机器人天天制造噪音。3.3 评论与检查报告让结果真正被人看到结果展示方式决定了工具会不会被团队接受。我采用了两种方式同时输出。第一种是逐行评论。对每条 error 级别的问题用 GitHub API 在对应代码行下面发一条 inline comment这样开发者打开 PR 就能在 context 里看到问题非常直接。第二种是汇总报告。在 PR 页面发布一条整体评论按文件和严重级别分组展示所有问题并给出统计信息。为了更醒目我还会把关于“本次 PR 是否可以直接合入”的建议写在最前面但不会强制阻塞 merge把最终决定权留给 reviewer。用到的 GitHub API 大概是这几个大家可以照着用# 逐行评论 POST /repos/{owner}/{repo}/pulls/{pull_number}/comments { body: 这里可能会泄漏敏感信息建议用环境变量管理, commit_id: ..., path: src/config.py, line: 42 } # 整体评审报告 POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews { body: ## open-code-review 报告 ..., event: COMMENT }这里有个小技巧如果要让错误级别的问题直接显示在 Merge 按钮旁边应该用 Checks API 而不是 review API。用POST /repos/{owner}/{repo}/check-runs创建一个 conclusion 为failure的 check run比请求变更request changes更优雅 —— 它不会继承“代码所有权”的限制也不会被个人 reviewer 权限影响。4. 实操接入从零给仓库配置 open-code-review4.1 最小化配置5 分钟跑通第一次自动评审我假设你已经有一个托管在 GitHub 上的仓库接下来只需要两步。第一步在仓库根目录新建.github/workflows/code-review.yml内容如下name: open-code-review on: pull_request: types: [opened, synchronize] permissions: contents: read pull-requests: write jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: Run open-code-review uses: your-name/open-code-reviewv1 with: token: ${{ github.token }} config_path: .open-code-review.yml第二步在仓库根目录放一个.open-code-review.yml配置文件如果不想自定义任何规则甚至可以空着让工具跑默认配置。配置文件长这样thresholds: max_function_lines: 50 max_file_added_lines: 200 rules: - rule: no-print-left level: warning - rule: function-too-long level: warning这两步做完下一次有人开 PR 时工具就会自动在下方贴出评审报告。整个接入过程用不了五分钟。4.2 参数详解从阈值到忽略路径很多人一开始会忽略fetch-depth: 0这个参数。它表示把仓库的完整历史拉下来而不是默认的浅克隆。没有它我在前面说的git diff --find-renames和 merge base 计算都无法工作行号会错乱。这个坑我建议你直接抄作业不要等踩了再改。再看一下几个我反复调整过的参数max_function_lines函数行数超过这个值算超长。我让默认为 50但这个值要看你团队的编码风格。Java 项目建议 80Python 项目 50 够用SQL 脚本建议直接关掉这条规则。max_file_added_lines单次改动新增行数上限。超过之后工具会提醒“这个 PR 太大了建议拆分”。ignore_paths有些目录不想让规则管。我自己的项目就配了example/和docs/。block_on_error默认是 false。你可以配置让 error 级别的检查阻塞 merge但我不建议一开始就打开等团队熟悉之后再收紧比较好。4.3 本地调试不 push 也能验证规则效果定期给 PR 发评论会污染 GitHub 记录所以我强烈建议在本地把规则跑通之后再推到仓库里触发真实验证。本地调试我用的是一个取巧的方法直接在本地生成一个 mock PR 的结构化 diff。你只需要准备一个 JSON 文件模拟 GitHub webhook payload 里的字段然后调用 main.py 的run_review(repo_name, pull_number, diff_patch)函数。为了尽量贴近真实我还在仓库里放了一组fixtures/目录里面有几个典型的 diff 片段和期望输出跑一遍测试就知道有没有回归。如果你还是想模拟整个 Actions 环境可以用act这个工具在本地容器里跑 workflow。但是有一个注意点act默认不支持pull_request事件里的所有字段需要手动传入-e事件 payload 文件。我试过几次之后觉得性价比不高最后还是回归到“本地构造 diff 单测”的方式。5. 规则编写进阶让评审机器人真正懂你的项目5.1 内置规则的适用场景内置规则是给大多数人兜底的它解决的问题都是“通用且具体”的。比如hardcoded-secret这条它检查 diff 里是否出现形如password ...、api_key ...的模式。这类问题在真实 review 里特别常见而人眼去看往往容易漏掉。no-print-left则是很多团队的刚需。我见过前端同事把console.log留在线上代码里、后端同事把print留在生产环境日志里虽然不是致命错误但它们会在排查问题时制造额外的噪音。这条规则在 diff 里看到这些关键词就报警能省下不少后端同学和前端同学互相扯皮的时间。内置规则还有一个好处它是“项目无关的”。不管你是刚起步的开源仓库还是一堆遗留代码的老项目这些规则几乎不会误伤。如果你的项目有特殊的工程规范比如数据库迁移文件、代码生成器产物那就需要自定义规则来处理了。5.2 自定义规则从正则到 Python 函数自定义规则我给了两种写法分别对应不同水平的用户。简单场景用正则就够了。比如团队约定所有新的接口注释必须包含since标记你就可以在配置里写rules: - rule: custom-regex name: require-since-tag pattern: (def |async def )[\\w\\.]\\((.*)\\): in_file_extensions: [.py] if_content_matches: then_require_match: since message: 新增接口需要补充 since 注释 level: warning复杂场景建议写 Python 函数。在.open-code-review.yml里指定一个custom_rules_module然后在这个模块里暴露rules列表。每个规则是一个实现evaluate方法的类可以访问文件的完整上下文。这个设计本质上是我 3.2 节里说的标准化接口的一个自然延伸。下面是一个真实例子我所在团队要求所有含 SQL 操作的新函数必须经过评审标记。# custom_rules.py import re from open_code_review.rules.base import Rule, RuleResult class SQLMustBeReviewed(Rule): rule_id sql-must-be-reviewed def evaluate(self, file_path, file_content, diff_lines): results [] for line_no, line in diff_lines: if cursor.execute in line or session.execute in line: results.append(RuleResult( rule_idself.rule_id, file_pathfile_path, lineline_no, levelwarning, messageSQL 操作需要额外人工确认建议贴出执行计划 )) return results自定义规则的最大价值不是“发现更多问题”而是把团队里的隐性共识给显式化了。新人加入时不用背厚厚的规范文档机器人已经替你把规范执行了一部分。5.3 误报抑制和排序策略规则再多、写得再好也一定会出现误报。处理不当的话团队里很快就会出现“机器人说的话没人看”的局面。我在 open-code-review 里做了两个机制来降低这种风险。第一个是行内忽略标记。如果开发者认为某一行是被误报的可以在代码行尾加上# noqa: open-code-reviewPython 风格或// review-ignoreJS 风格工具在报告结果前会先检查这些标记。这样就保证了“人可以推翻机器人的结论”而且是有记录地推翻而不是默默忽略。第二个是结果排序。展示报告时我会优先显示 error 级别的问题然后才是 warning 和 info。error 一定会显示在报告最顶端warning 会折叠起来info 则干脆默认不展示除非用户在配置里显式打开。这种“有损但务实”的做法是为了保证工具的输出能被人真正消费掉。6. 常见问题与排查技巧实录6.1 高频问题速查表我在开源之后收到了不少 Issue总结下来大家遇到的问题高度集中在这几类。现象可能原因解决方案Action 没运行workflow 文件路径写错或事件类型不对检查.github/workflows/下的 YAML 语法确认on.pull_request.types包含opened评论没发出来token 权限不足检查permissions块确认有pull-requests: write行号错乱没有完整 git 历史在 checkout 步骤加fetch-depth: 0规则一直不生效配置文件路径写错确保config_path指向仓库内的真实文件注意大小写误报太多阈值设置不合理先用默认配置跑几周积累数据后再调整阈值和分支保护冲突check-run 结论为 failure把block_on_error设为 false或者把检查状态设为 neutral6.2 一例典型的“规则不生效”排查过程有一个 Issue 让我印象特别深用户说no-print-left规则没有生效但他在本地测试时明明能检测到console.log。我让他打开 workflow 的日志发现实际上规则已经跑了只是没有输出。原因是我们的 diff 解析逻辑默认只处理新增行而他在测试时只是修改了某一行那一行里虽然有console.log但它不是严格意义上的“新增行”。换句话说一个本来就在代码里的字符串被改了一个字符它的 diff 类型是 modified而不是 added。这个设计初衷是避免在上下文行上重复报警但也确实会让一部分规则漏检。后来我在规则引擎里加了一个选项include_modified_lines: true让用户可以根据自己的场景决定是否把修改行也纳入检查。这个例子也提醒我写规则引擎的时候一定要把 diff 的类型语义考虑清楚否则再强的规则也会在边界情况下失灵。7. 从工具到习惯在团队里推 open-code-review 的几点体会技术方案写完最后聊点使用层面的心得。open-code-review 这个项目最初是我自己一个人在维护后来放到团队里用再到开源整个过程里我最大的体会是工具能用和团队愿意用是两回事。让团队真正接受这种自动评审机器人关键在于克制。一开始不要把所有规则都打开不要设置任何阻塞 merge 的条件让它先做“观察员”只点评不阻断。等大家看到它指出的问题确实有价值再逐步提高规则严重级别和开关范围。我这个项目里block_on_error默认是 false就是这个原因——我不想让机器人代替人做判断我只想让它把问题摆到台面上来。另外定期回看机器人的历史报告也很有价值。有一次我统计了三个月的数据发现大约 7% 的 error 级别问题是真实且严重的安全隐患比如硬编码密钥直接提交。这个数字让我决定继续维护下去因为哪怕机器人的提醒能拦下一次线上事故这些付出就都值了。如果你也想在自己仓库里试试建议先从最简单的规则跑起跑通之后再慢慢加。
返回列表