一个成熟的仓库,合并到 main 的每一次提交都应该是「经过验证、经过审查、可追溯」的。分支保护(Branch protection)与新版 Rulesets 正是这道闸门:它们决定谁能推、谁必须审、哪些检查必须先绿。但这条链路里埋了大量坑——check 名字对不上、路径过滤让必需检查永远 pending、admin 绕过规则、CODEOWNERS 写错位置不生效。本文系统梳理 Rulesets 的规则类型、必需状态检查的命名机制、bypass actors 与 enforcement、CODEOWNERS 语法,以及用 gh api 与 Terraform 把治理规则代码化的完整方案。
一、从 Branch protection rules 到 Rulesets
1.1 两代机制的关系
经典的分支保护规则(Branch protection rules)与新版 Rulesets 目前并存,同一分支可以同时被两者约束,实际生效的是「并集」——任何一条规则说不能推,就推不上去。理解这一点很重要,因为它解释了「为什么我在 Rulesets 里放开了,还是被拦住」。
Branch protection rules(经典)
- 每个分支一条规则,用 pattern 匹配(如 main、release/*)
- 配置分散在 Settings → Branches
- 只有 "apply to administrators" 一个总开关决定 admin 是否豁免
- 没有 evaluate 模式,只能全量生效
- 不支持组织级统一下发
Rulesets(新版,推荐)
- 一条 ruleset 可以覆盖多个分支或标签
- 支持 enforcement 三态:active / evaluate / disabled
- 支持 bypass actors 精细授权(团队、角色、应用、Deploy key)
- 支持组织级 ruleset,向下级仓库下发
- 可导出为 JSON,天然适合 IaC 管理
1.2 迁移建议
迁移不必一步到位。稳妥路径是:先用 Rulesets 的 evaluate 模式导入与经典规则等价的配置,观察一段时间(evaluate 只上报不拦截),确认没有误伤后再切到 active,最后才删除经典规则。
第 1 步:读取现有经典规则,确认目标分支列表
第 2 步:在 Rulesets 中重建同等规则,enforcement 设为 evaluate
第 3 步:观察 1~2 周,确认 PR 没有被意外拦截
第 4 步:enforcement 切换为 active
第 5 步:删除 Settings → Branches 中的经典规则
1.3 优先级与叠加
当经典规则与 Rulesets 同时存在,或者组织级 ruleset 与仓库级 ruleset 同时存在时,规则是叠加而非覆盖。组织级 ruleset 无法被仓库级规则「放宽」,只能被收紧。这是排查「规则明明删了还生效」的第一检查点。
二、Rulesets 规则类型详解
2.1 一条完整的 Ruleset 结构
Rulesets 支持导出与导入 JSON,这是把它纳入版本管理的基础。下面是一条针对 main 的完整定义:
{
"name": "protect-main",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH", "refs/heads/release/*"],
"exclude": []
}
},
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{ "type": "required_linear_history" },
{ "type": "required_signatures" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 2,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": true,
"require_last_push_approval": true,
"required_review_thread_resolution": true
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [
{ "context": "test (3.11)" },
{ "context": "lint" }
]
}
}
]
}
2.2 规则类型速览
deletion 禁止删除匹配的分支
non_fast_forward 禁止强推(force push),即禁止非快进更新
required_linear_history 禁止 merge commit,只允许 squash 或 rebase
required_signatures 所有提交必须签名(GPG / SSH / S/MIME)
pull_request 必需 PR 审查,附带一批子参数
required_status_checks 必需状态检查,CI 的 job 名要与之对齐
creation 限制分支创建(限制谁/哪种命名能新建)
update 限制对分支的更新(一般与 bypass 配合)
required_deployments 要求指定 environment 先部署成功
workflows 要求指定工作流在特定事件下必须成功
2.3 required_linear_history 的代价
开启线性历史后,GitHub 的「Merge pull request」按钮中的 merge commit 选项会消失,只剩 squash 与 rebase。这对习惯了 merge commit 的团队是行为突变,建议在迁移前先与团队沟通,并同步更新 PR 模板里的合并指引。
2.4 required_deployments 与环境的联动
required_deployments 要求某个 environment 已经被成功部署过,才允许合并到受保护分支。它常与 GitHub Actions 的 environments 门禁配合:PR 阶段先部署到 staging 环境,人工审批通过后才解锁合并。
三、必需状态检查的坑
3.1 context 必须与 job 的 name 完全一致
这是最高频的踩坑点。required status checks 里的 context 字符串,匹配的是 job 显示在 PR 上的那个名字,而不是 workflow 文件名,也不是 job 的 id。
# .github/workflows/ci.yml
name: CI
jobs:
build-and-test: # job id
name: Build and Test # ← 这个才是 context
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
上面的 job,正确的 context 是 Build and Test,写成 build-and-test 会永远匹配不上,PR 就会一直卡在 Expected — Waiting for status to be reported。
3.2 matrix job 的名字会展开
矩阵策略会把每个组合展开成独立的 check 名。若 job 的 name 里包含 ${{ matrix.* }},那么 context 必须逐个列出展开后的字面量。
jobs:
test:
name: test (${{ matrix.python-version }})
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
产生的三个 check 分别是 test (3.10)、test (3.11)、test (3.12)。如果你只在必需列表里填了 test (3.11),那另外两个即使失败也不影响合并——这是一个隐蔽的安全漏洞,务必逐个补齐。
3.3 路径过滤导致 check 永远 pending
当 workflow 使用 paths 或 paths-ignore 过滤时,不匹配的 PR 根本不会触发该 workflow,于是那个必需检查永远不会被上报,PR 卡死。
on:
pull_request:
paths:
- "src/**"
修复方式有三种,推荐第一种:
方案 A(推荐):去掉 workflow 级 paths 过滤,改为在 job 内做条件判断
方案 B:额外配置一个同名的 "always pass" job,用 if 判断后直接成功
方案 C:把该检查从必需列表中移除(会削弱保护,不推荐)
方案 B 的写法:
jobs:
docs-check:
name: docs-check
runs-on: ubuntu-latest
steps:
- id: changed
uses: tj-actions/changed-files@v44
with:
files: docs/**
- name: Run or skip
if: steps.changed.outputs.any_changed == 'true'
run: npm run lint:docs
3.4 skipped 的 job 算不算通过
默认情况下,一个被 if 跳过(skipped)的 job,其检查状态是 neutral 而非 success。在经典分支保护里 neutral 会被视为通过,但在某些严格配置下会被判为未满足。稳妥做法是让跳过的 job 显式输出成功,或避免让可能被跳过的 job 出现在必需列表里。
3.5 strict 模式的含义
strict_required_status_checks_policy: true 表示「分支必须与 base 保持最新」。开启后,如果 main 上有新提交,你的 PR 分支需要先合并/变基到最新才能合并,这能防止「基于旧代码测试通过」的问题,但会显著增加 PR 的往返次数。团队小、合并频繁的仓库建议开启;反之可关闭。
四、Bypass actors 与 enforcement
4.1 bypass actors 的配置
bypass actors 决定谁可以无视规则。它可以是组织管理员、特定团队、特定角色、GitHub App 或 Deploy key。
{
"bypass_actors": [
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "always"
},
{
"actor_id": 123456,
"actor_type": "Team",
"bypass_mode": "pull_request"
},
{
"actor_id": null,
"actor_type": "DeployKey",
"bypass_mode": "always"
}
]
}
actor_type 常见取值:RepositoryRole(actor_id 5 对应 admin)、OrganizationAdmin(actor_id 1)、Team、Integration(GitHub App)、DeployKey。
4.2 bypass_mode 的区别
always 完全绕过,可直接 push,不受任何规则限制
pull_request 只能绕过「必需 PR 审查」,仍受状态检查等其它规则约束
把 admin 设为 always 是最常见的默认配置,但它也是「为什么管理员能强推」的直接答案。安全要求高的仓库应改为 pull_request,或干脆不配置 RepositoryRole 的 bypass。
4.3 enforcement 三态
active 规则生效,违规操作被拦截
evaluate 规则不拦截,只记录「本应被拦截」的事件,用于灰度验证
disabled 规则完全不生效,但配置保留
新规则上线强烈建议先走 evaluate。它能在不打扰开发流程的前提下,暴露「哪些 PR 会被误伤」,是灰度上线的安全垫。
4.4 组织级 ruleset 的下发
组织级 ruleset 会下发到符合条件的仓库。它的 conditions 里可以按仓库名、仓库属性(如 repository_name、repository_property)筛选目标。
{
"name": "org-baseline",
"target": "branch",
"enforcement": "active",
"conditions": {
"repository_name": {
"include": ["*"],
"exclude": ["sandbox-*", "docs-*"]
}
}
}
注意组织级规则无法被仓库级规则放宽,只能被收紧,因此 exclude 列表要谨慎设计。
五、以代码管理 Rulesets
5.1 gh api 读取与写入
# 列出仓库的全部 rulesets
gh api repos/OWNER/REPO/rulesets
# 查看某条 ruleset 的完整定义
gh api repos/OWNER/REPO/rulesets/RULESET_ID
# 创建(从 JSON 文件导入)
gh api repos/OWNER/REPO/rulesets \
--method POST \
--input ruleset.json
# 更新
gh api repos/OWNER/REPO/rulesets/RULESET_ID \
--method PUT \
--input ruleset.json
# 导出为文件做备份
gh api repos/OWNER/REPO/rulesets/RULESET_ID > backup.json
5.2 用工作流同步规则
把 ruleset JSON 放进仓库,用工作流在变更时同步,可以实现「治理即代码」。
name: Sync Rulesets
on:
push:
branches: [main]
paths:
- ".github/rulesets/**"
permissions:
contents: read
administration: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Apply rulesets
env:
GH_TOKEN: ${{ secrets.RULESET_ADMIN_TOKEN }}
run: |
for f in .github/rulesets/*.json; do
echo "Applying $f"
gh api repos/${{ github.repository }}/rulesets \
--method POST --input "$f" || \
gh api repos/${{ github.repository }}/rulesets \
--method PUT --input "$f"
done
5.3 Terraform 方案
resource "github_repository_ruleset" "main" {
name = "protect-main"
repository = github_repository.app.name
target = "branch"
enforcement = "active"
conditions {
ref_name {
include = ["~DEFAULT_BRANCH"]
exclude = []
}
}
rules {
deletion = true
non_fast_forward = true
required_linear_history = true
pull_request {
required_approving_review_count = 2
dismiss_stale_reviews_on_push = true
require_code_owner_review = true
require_last_push_approval = true
required_review_thread_resolution = true
}
required_status_checks {
strict_required_status_checks_policy = true
required_check {
context = "test (3.11)"
}
required_check {
context = "lint"
}
}
}
}
Terraform 的优势在于规则是声明式的、可 diff 的、可回滚的,且能跨仓库批量下发。缺点是 provider 版本更新滞后于 GitHub 新特性,遇到新规则类型可能需要等 provider 支持或退回 gh api。
5.4 权限与 Token 选择
管理 rulesets 需要仓库的 administration: write 权限。组织级 ruleset 需要 admin:org 权限。CI 中不要用长期 PAT,优先用 GitHub App 的 installation token,或通过 OIDC 换取短期凭证。
六、CODEOWNERS 与仓库治理
6.1 CODEOWNERS 的三个位置
.github/CODEOWNERS ← 推荐,随代码一起版本化
CODEOWNERS ← 仓库根目录
docs/CODEOWNERS ← 已废弃,兼容保留
GitHub 按上述顺序查找,命中第一个即生效,不会合并多个文件。
6.2 语法要点
# 注释行
* @org/platform-team # 兜底:所有文件
/docs/ @org/docs-team # 目录下全部文件
*.js @org/frontend # 按扩展名
/src/api/*.ts @alice @bob # 多个人
apps/web/ @org/web-team
!apps/web/generated/ # 取反需配合前面的规则
关键规则:
- 最后匹配的规则优先(顺序很重要,兜底的 * 放最前面)
- 目录写法 /docs/ 只匹配该目录,docs/ 会匹配任意层级
- 团队必须对该仓库有 write 权限,否则 CODEOWNERS 不生效
- 只有开启 require_code_owner_review,CODEOWNERS 才会成为合并门禁
6.3 与必需审查的联动
CODEOWNERS 单独存在时只是「自动请求审查」,并不阻塞合并。要让它成为硬门禁,必须在 ruleset 的 pull_request 规则里开启 require_code_owner_review。此外 require_last_push_approval 会在作者推送新提交后,让之前的批准失效,防止「批准后偷偷改代码」。
6.4 标签保护与推送规则
除了分支,标签(tag)同样需要保护,防止有人重写已发布的版本标签。
{
"name": "protect-tags",
"target": "tag",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/tags/v*"],
"exclude": []
}
},
"rules": [
{ "type": "deletion" },
{ "type": "update" },
{ "type": "creation" }
]
}
update 与 creation 规则可以配合 bypass actors,实现「只有发布机器人能创建 v* 标签」。
6.5 合并队列 merge queue
开启合并队列后,PR 不再直接合入 main,而是进入一个临时队列分支,GitHub 会把队列中的 PR 依次组合并跑一遍必需检查,全部通过才真正合入。
on:
merge_group: # 合并队列会触发这个事件
要点:
- workflow 必须监听 merge_group 事件,否则队列里的检查不会跑
- 必需检查的 context 在队列中与 PR 中保持一致
- strict 模式与合并队列二选一即可,同时开启会互相拖慢
- 合并队列能消除「两个 PR 各自通过、合并后互相破坏」的竞态
6.6 与依赖安全的协同
分支保护不是孤立的,它与 Dependabot 依赖安全协同工作时效果最好:Dependabot 提的 PR 同样要走必需检查与审查,避免「自动化 PR 绕过门禁」的盲区。
七、排错与速查
7.1 为什么 PR 卡在 Waiting for status to be reported
排查顺序:
1. context 拼写是否与 job 的 name 完全一致(含空格、大小写、括号)
2. workflow 是否被 paths / paths-ignore 过滤掉,导致未触发
3. workflow 是否只在特定分支触发,而目标分支不在其中
4. 检查是否来自第三方 App(如 Codecov),其 context 与 job 名不同
5. 是否有 job 因 if 条件被跳过,导致状态为 neutral
6. workflow 文件是否有 YAML 语法错误,导致整个 workflow 未加载
7.2 为什么管理员能强推
原因:bypass actors 中存在 RepositoryRole(admin) 且 bypass_mode 为 always
修复:
- 将 admin 的 bypass_mode 改为 pull_request
- 或直接移除 RepositoryRole 的 bypass 条目
- 注意经典规则里的 "Do not allow bypassing the above settings" 也要勾选
7.3 规则不生效的常见原因
- 组织级 ruleset 与仓库级叠加,被更严的一方拦住
- enforcement 仍是 evaluate 或 disabled
- conditions 的 ref_name 写的是 refs/heads/main 而非 ~DEFAULT_BRANCH
- 规则目标 target 写成了 branch 但想保护的是 tag
- 分支名 pattern 用了 glob 但写法不符合 GitHub 语法
7.4 速查表
概念 关键值
--------------------------------------------------------
必需检查 context 与 job 的 name 字段逐字一致
matrix check 名 name: test (${{ matrix.x }}) 会展开
强制最新 strict_required_status_checks_policy
线性历史 required_linear_history
提交签名 required_signatures
CODEOWNERS 位置 .github/CODEOWNERS 优先
CODEOWNERS 生效开关 require_code_owner_review
绕过模式 always / pull_request
生效状态 active / evaluate / disabled
管理权限 administration: write
合并队列事件 merge_group
默认分支写法 ~DEFAULT_BRANCH
总结
分支保护与 Rulesets 的核心,是把「什么样的提交可以进入主干」这件事从口头约定变成机器强制。Rulesets 相比经典规则,最大的进步是支持 evaluate 灰度、bypass actors 精细授权和组织级下发,这让治理规则的推广变得可控。实际落地时,最容易出问题的是必需状态检查:context 必须与 job 的 name 逐字一致、matrix job 会展开成多个 check、paths 过滤会让检查永远 pending。CODEOWNERS 只是「自动请求审查」,必须配合 require_code_owner_review 才能成为门禁。最后,把 ruleset 导出为 JSON 交给 gh api 或 Terraform 管理,治理规则才能像代码一样被审查、diff 与回滚——这才是仓库治理真正可持续的形态。
延伸阅读:
- GitHub Actions PR 自动化 — 标签、审查与 PR 生命周期自动化
- GitHub Actions 安全加固 — 工作流权限与供应链安全
- GitHub Actions 环境门禁 — environments 与人工审批
- GitHub Actions 依赖安全 — Dependabot 与依赖审查
- GitHub Actions OIDC 云认证 — 免密钥的短期凭证
- DevOps 专题 — 工程治理与流程规范
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。