GitHub Actions 分支保护与 Rulesets:必需检查、CODEOWNERS 与仓库治理

GitHub Actions 分支保护与 Rulesets 实战:经典 Branch protection rules 与新版 Rulesets 的差异与迁移、必需状态检查的命名与路径过滤陷阱、必需审查与 CODEOWNERS 语法、bypass actors 与 enforcement 状态、合并队列与标签环境保护规则,以及用 gh api 与 Terraform 以代码管理 rulesets

一个成熟的仓库,合并到 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」更多文章

  1. GitHub Actions 本地调试与排错:act、workflow_dispatch、日志与重跑
  2. GitHub Actions 工作流性能与并发控制:concurrency、超时与分钟数优化
  3. GitHub Actions 容器作业与 Service 容器:job container、健康检查与集成测试