PR 检查与分支保护
这一课你将能做到
- 配置
on.pull_request(含 paths 过滤),让改动 backend/** / platform/** 时跑到对应检查
- 在仓库 Branch protection / Rulesets 里勾选 required status checks,使红测无法合并进
main
- 说明 fork PR 默认拿不到 write secrets 的原因,以及对
pull_request_target 的谨慎态度
为什么要学
CI 写得再漂亮,若大家还能「红着合并」,门禁等于海报。GitHub 把 workflow 的 job 结果变成 PR 上的 status check;再配合分支保护,main 才成为可信基线。
monorepo 还要省分钟数:只改文档时狂跑 Maven/npm 是浪费,paths 过滤是常用刀。但 paths 也会带来新坑——required check 没跑出来,PR 永远不能合。
本课把「何时跑」与「跑红能不能合」钉在一起,为综合练习收尾。
先建立图像
开发者开 PR → main
│
▼
pull_request 事件触发 ci.yml
│
├─ job backend → 成功/失败
└─ job platform → 成功/失败
│
▼
PR 页显示 checks
│
Branch protection: require backend + platform
│
全绿才可 merge(可再加 review 规则)
人话:workflow 是考卷;分支保护是「不及格不准毕业」的校规。没有校规,考卷只是建议。
| 机制 | 作用 |
|---|
on: pull_request | 何时跑检查 |
| job 结论 | 产生 check 名称 |
| Branch protection | 哪些 check 必须绿才能合 |
paths | 哪些文件变化才跑 |
跟着做
步骤 1:确认 PR 触发与 paths
name: CI
on:
pull_request:
branches: [main]
paths:
- 'backend/**'
- 'platform/**'
- '.github/workflows/**'
push:
branches: [main]
paths:
- 'backend/**'
- 'platform/**'
- '.github/workflows/**'
permissions:
contents: read
jobs:
backend:
runs-on: ubuntu-latest
# … 第 7 课的 setup-java + verify …
platform:
runs-on: ubuntu-latest
# … 第 8 课的 npm ci / lint / test / build …
注意:仅改 README.md 时,带 paths 的 workflow 整份不跑——这是预期。若希望文档 PR 也有一条可勾的检查,需另设计小 job(进阶,见卡点急救)。
步骤 2:开一条真实 PR 看 checks
从 feature 分支改一行 backend 注释 → 开 PR。PR 页 Checks 应出现 job;点进日志确认触发事件是 pull_request。
步骤 3:配置分支保护
仓库 Settings → Branches(新版界面在 Rules → Rulesets):
- 目标分支:
main
- 勾选 Require a pull request before merging(建议)
- 勾选 Require status checks to pass:选 backend、platform(名称须与 job 名一致)
再用一个故意失败的 PR 验证:Merge 按钮应被挡住。这一步才是「门禁有牙齿」的证明。
步骤 4:核对 check 名称
backend:
name: Backend Verify
则保护规则里出现的是 Backend Verify 而不是 backend。实操顺序:先让 workflow 在 main 上跑成功一次,再在保护规则的下拉列表里选真实出现的名字。凭记忆手敲名称常出 Expected 悬挂。
步骤 5:理解 fork 与 secrets
来自 fork 的 PR:workflow 以只读 token 运行,拿不到 repository secrets(防恶意 workflow 偷钥)。因此「PR 里直接部署生产」在开源协作中通常不可行——这是安全特性不是缺陷。看到 fork PR 里部署步骤跳过/失败时,先对照该安全模型,而不是急着换事件类型。
步骤 6:记录 pull_request_target 警告
pull_request_target 在基础仓库上下文运行,能读 secrets,常用于「给贡献者自动评论」这类场景。但它与 checkout PR 分支代码组合执行时,曾多次造成真实安全事件。本课规则:默认不用;确需使用,先读完官方安全指南,严禁 checkout 并执行 PR 分支里的不可信脚本,权限收到最小。
讲清楚要点
- 因为合并后修复成本更高,所以检查应挂在 pull_request 上——把失败留在分支里。
- 因为没有保护规则时红测也能合,所以必须配 Branch protection / Rulesets——门禁要有牙齿。
- 因为 check 名来自 job,所以 YAML 里的 job
name 与保护规则勾选项必须对得上——改名后要同步更新规则。
- 因为 monorepo 改动面不同,所以用
paths 节省分钟数——但要接受「只改文档可能无 CI」的后果或另补小 job。
- 因为 fork PR 不可信,所以默认不给 secrets——这是开源安全模型的一部分。
- 因为 workflow 文件本身也是被改对象,所以把
.github/workflows/** 纳进 paths——改 CI 的 PR 仍会被验证。
你可能会懵的地方
-
「为什么我的 PR 没有任何检查?」
查四点:workflow 是否已存在于默认分支;paths 是否把本次改动全过滤掉了;PR 目标分支是否匹配 branches: [main];仓库 Actions 是否被禁用。
-
「push 到 feature 分支与 PR 检查跑两次?」
开着 PR 时主要是 pull_request 在跑;若 workflow 还监听了 push 且不限分支,两个事件都会触发。用 branches 过滤 push 到 main、靠 pull_request 管分支检查,可去重。
-
「required check 一直显示 Expected?」
保护规则引用了一个从未跑出的名字(手敲错、或改名后旧名残留)。让 workflow 真跑一次,从下拉列表重选。
-
「管理员为什么还能合并红 PR?」
保护规则常含「允许管理员绕过」选项,或管理员点了 admin merge。要硬门禁就关掉绕过;团队约定上,红测合并必须留痕说明。
-
「paths 该怎么配才不会卡住 required checks?」
原则:required check 引用的 job 必须在任何目标分支的 PR 上都有机会跑。要么 paths 放宽到覆盖所有可能改动,要么对文档类 PR 跑一个轻量占位 job 保证 check 出现并变绿。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 有 CI 无分支保护 | 红也能合,门禁形同虚设 | require status checks |
| check 名与 job 名不一致却硬勾 | 一直 Expected 卡合并 | 跑通后从列表选真实名 |
| paths 太窄(漏了 workflows/**) | 改 CI 无验证;或 required check 不跑 | 把 .github/workflows/** 纳入 paths |
为省事对 fork 开 pull_request_target 跑不可信脚本 | 密钥与供应链风险 | 默认 pull_request;例外先读安全文档 |
卡点急救
- required check 一直等不出现(PR 卡死不能合):本次 PR 的改动没命中 paths,workflow 没跑、check 不存在。处理:① 给 workflow 补上命中的路径;② 或临时用 admin 权限合入并修正 paths;③ 长期方案是给文档类改动配置一个轻量
docs 检查 job。
- PR 红了但仍能点合并:分支保护没生效——确认规则真的勾了 required status checks、目标分支写的是
main、且当前账号不是绕过角色;规则保存后开一条新 PR 验证。
- fork PR 的 job 失败提示缺 secret:这是预期安全模型。本仓库协作者可由维护者把分支拉进本仓库重跑(有 secrets);不可信内容绝不能用
pull_request_target 绕过。
面试常问
-
如何为一个 monorepo 设计 PR 门禁?
分四层答:① 触发层——pull_request 指向 main,paths 按模块过滤,workflow 自身目录纳入 paths;② 检查层——各模块独立 job(backend verify、platform 四件套),便宜的检查靠前;③ 强制层——分支保护勾 required checks,禁止绕过,配合至少一人 review;④ 成本层——concurrency 取消过时 run,PR 内只跑快速检查、全量留给 main。核心权衡是「严格性 vs 分钟数」,并把策略写进仓库文档。
-
pull_request 和 pull_request_target 有什么区别?为什么后者危险?
pull_request 在 PR 合并后的代码上下文运行,fork PR 只有只读 token、无 secrets,安全;pull_request_target 在基础仓库上下文运行,带 secrets 与写 token,本意是服务贡献者(自动打标签/评论)。危险在于:如果 checkout 了 PR 分支的代码并执行其中脚本,等于让外部代码拿着仓库密钥运行——历史上有多次真实供应链事故。原则:不可信代码与高权限上下文必须隔离。
-
分支保护里 required checks 显示 Expected 卡住合并,怎么排查?
三步:① 对比规则里的 check 名与实际 run 里的 check 名(job 改名后旧名残留最常见);② 确认该 workflow 在本次 PR 上真的运行(paths、分支过滤、workflow 是否在默认分支);③ 若该检查由条件跳过(if: 为 false 时显示 skipped),required check 会等待——用 if: always() 或调整条件让 job 明确成功/失败而不是 skip。
自测三问
- 分支保护里勾选的 status check 名称通常来自哪里?改名后要注意什么?
- 为什么 fork PR 读不到部署密钥反而是件好事?
- 给 monorepo 的
paths 列出三个应包含的模式,并解释为什么包含 .github/workflows/**。
小结一句话
PR 触发检查,分支保护强制全绿;paths 控成本,fork 默认无密钥是安全特性。
延伸