跳过正文
  1. 博客文章/

Gerrit Code Owners 实战:用 OWNERS 做目录级审批门禁

·1385 字·7 分钟·
DevOps CI/CD Gerrit Code Owners OWNERS Code Review 权限管理
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录
上一篇 Gerrit 实验Code-Review +2、Jenkins Verified +1 和 Submit 串了起来。接下来遇到的问题更像单仓库团队的日常:frontend/ 应该由前端负责人批准,backend/ 应该由后端负责人批准,一个 Change 同时改两边时,缺任何一方都不能合并。

我原先以为 Gerrit 能像 SVN 一样直接做目录 ACL。实际不是。Gerrit 核心权限落在 Project 和 Ref 上,用户只要能读取项目,就能 fetch 完整 Git 对象。Code Owners 解决的是“谁必须批准这些文件”,不是“谁能看见这些文件”。需要目录保密时,仍然要拆仓库。

文中的域名、邮箱、账号和 Group UUID 都是示例值。截图已经脱敏。随文附件不包含密码、HTTP Token、SSH 私钥或真实内部地址。

实验结果
#

实验环境沿用上一篇文章中的 Gerrit 和 Jenkins:

项目实测结果
Gerrit3.14.2,容器健康
Code Ownersstable-3.14 build 6,版本 b419796519
插件制品SHA-256 校验通过,启动日志无加载错误
全局策略默认 disabled=true,新项目不会自动套用门禁
实验项目gerrit-demo 单独启用 Code Owners
frontend ownerfrontend-owner@example.internal
backend ownerbackend-owner@example.internal
CIJenkins 对实验 Change 回写 Verified +1
阻塞结果backend 未批准时,Code-OwnersUNSATISFIED
审批结果backend owner 投 Code-Review +1 后,门禁变为 SATISFIED
最终状态所有要求满足后由管理员 Submit,Change 合并
flowchart LR
    A["全局保持禁用"] --> B["安装并校验插件"]
    B --> C["配置恢复通道"]
    C --> D["提交 OWNERS 基线"]
    D --> E["启用单个项目"]
    E --> F["验证跨目录审批"]

OWNERS 和 Override 还没准备好就启用插件,代码 Change 会被无负责人路径挡住,配置错误也难以修复。

0. 范围、版本与附件
#

0.1 实验边界
#

本次只验证目录级审批、已有项目的分阶段启用,以及 Code Owners 与 Jenkins Verified 的并行门禁。文件读取隔离、多站点、自定义 backend、自动 Submit 和大规模 OWNERS 治理不在实验范围内。

0.2 版本契约
#

组件版本说明
Gerrit3.14.2官方 Docker 镜像
Code Ownersb419796519stable-3.14 build 6
插件 API3.14.2-SNAPSHOT以插件清单为准
backendfind-owners从仓库中的 OWNERS 文件解析负责人

Code Owners 不是 Gerrit 核心模块。升级 Gerrit 时,不能默认旧 JAR 仍然兼容。先在测试环境加载对应 stable 分支的制品,检查 API 版本、启动日志和真实 Change,再碰生产实例。

0.3 随文配置包
#

Page Bundle 附带本次实验的脱敏配置:

下载后先检查文件:

set -euo pipefail
KIT_BASE_URL="https://blog.treesir.pub/posts/gerrit-code-owners-directory-gate/files"
mkdir -p gerrit-code-owners-lab
cd gerrit-code-owners-lab

curl -fsSLO "$KIT_BASE_URL/SHA256SUMS"
while IFS= read -r manifest_line; do
  if [[ ! $manifest_line =~ ^[0-9a-f]{64}[[:space:]]{2}([A-Za-z0-9._/-]+)$ ]]; then
    echo "Invalid manifest entry: $manifest_line" >&2
    exit 1
  fi

  relative_path=${BASH_REMATCH[1]}
  case "$relative_path" in
    /*|.|..|./*|../*|*/.|*/..|*/./*|*/../*|*//*)
      echo "Unsafe manifest path: $relative_path" >&2
      exit 1
      ;;
  esac

  mkdir -p "$(dirname "$relative_path")"
  curl -fsSL "$KIT_BASE_URL/$relative_path" -o "$relative_path"
done < SHA256SUMS

sha256sum -c SHA256SUMS

1. 先区分三种“目录权限”
#

“想做目录权限”这句话至少有三种意思。混在一起选方案,很容易装完插件才发现目标不对。

需求合适的做法能解决什么不能解决什么
某目录必须由指定人员审批Code Owners按路径计算负责人和 Submit 门禁不限制读取和 fetch
改到某路径时追加一个固定 Labelfile: Submit Requirement不装插件也能做路径条件没有分层 OWNERS 和负责人发现
不同团队不能读取彼此代码拆分 Gerrit Project独立 Read、Push 和审计边界跨仓变更成本更高
flowchart LR
    A["需要目录保密"] --> B["拆分项目"]
    C["需要目录负责人"] --> D["使用 Code Owners"]
    E["只需路径条件"] --> F["使用 file 谓词"]

1.1 Gerrit 核心 ACL 为什么做不到
#

Gerrit 的访问规则挂在 Ref 上,例如:

[access "refs/heads/*"]
  read = group Registered Users
  push = group Developers

Git clone 和 fetch 以仓库对象为单位。即使界面隐藏某个目录,客户端已经拿到相关对象,也谈不上保密。这里不能照搬 SVN 的路径授权模型。

1.2 Code Owners 管的是 Submit
#

启用插件后,Change 会多出 Code-Owners Submit Requirement。插件检查本次变更涉及的文件,沿目录向上查找 OWNERS,再判断这些文件是否获得有效审批。

Code Owners 与 Verified 分开计算:

Code-Review = SATISFIED
Verified    = SATISFIED
Code-Owners = UNSATISFIED

上面这种状态仍不能 Submit。Jenkins 通过,只说明自动检查通过,不代表业务负责人同意目录里的修改。

2. 分阶段启用
#

启用过程分成四个 Change,每一步都保留可恢复状态。

顺序Change内容此时门禁状态
1安全配置Override Label、配置分支豁免、强制校验仍禁用
2OWNERS 基线根目录、frontend、backend 负责人仍禁用
3项目启用disabled=true 改为 false开始生效
4跨目录实验同时修改 frontend 和 backend验证阻塞与放行

2.1 全局默认禁用
#

安装脚本先写 Gerrit 全局配置,再放入 JAR:

[plugin "code-owners"]
  disabled = true
  backend = find-owners
  requiredApproval = Code-Review+1
  overrideApproval = Owners-Override+1
  fallbackCodeOwners = NONE
  enableImplicitApprovals = FALSE
  mergeCommitStrategy = ALL_CHANGED_FILES
  allowedEmailDomain = example.internal

disabled=true 是 opt-in 策略。插件加载后,只有项目自己的 code-owners.config 明确写 disabled=false 才会启用。

2.2 不给未覆盖文件安排“万能负责人”
#

本次使用:

fallbackCodeOwners = NONE

缺少 OWNERS 的路径会直接阻塞,启用阶段可以据此补齐覆盖范围。

2.3 保留管理员恢复通道
#

项目新增 Owners-Override Label:

[access "refs/heads/*"]
  label-Owners-Override = 0..+1 group Administrators

[label "Owners-Override"]
  function = NoBlock
  defaultValue = 0
  value = 0 No score
  value = +1 Override

Owners-Override 只用于 OWNERS 邮箱失效、目录搬迁或紧急修复,由管理员恢复提交能力。

2.4 配置分支不参加目录门禁
#

项目配置包含:

disabledBranch = refs/meta/config

这样 refs/meta/config 仍受原有 Code-Review 和 Submit 权限约束,但不要求 Code Owners。否则 OWNERS 配错后,修复 code-owners.config 的 Change 也可能被同一个错误挡住。

3. 安装 Code Owners 插件
#

3.1 固定制品和校验值
#

本次使用的制品:

PLUGIN_URL='https://gerrit-ci.gerritforge.com/job/plugin-code-owners-bazel-stable-3.14/6/artifact/bazel-bin/plugins/code-owners/code-owners.jar'
PLUGIN_SHA256='3d7f275efcee227eee8d5b6ae3cb564d62e4b9f4a3981666c868ce81bdb1a9f7'

先下载到临时文件,校验通过后再原子移动到 plugins 目录。不要把在线 URL 直接交给启动脚本,也不要用一个会漂移的 latest 地址。

3.2 执行安装脚本
#

在 Gerrit compose.yaml 所在目录运行:

ALLOWED_EMAIL_DOMAIN=example.internal \
  bash /path/to/files/scripts/install-code-owners.sh

脚本完成以下操作:

  1. 检查 curldockersha256sum
  2. 确认 Gerrit Compose 服务正在运行;
  3. 下载并校验 JAR;
  4. 备份 gerrit.config
  5. 先写全局禁用配置;
  6. 原子放入插件 JAR;
  7. 重启 Gerrit 并等待 HTTP 版本接口;
  8. 再次校验容器内 JAR;
  9. 检查插件加载错误。

健康循环同时检查容器状态和 /config/server/version,避免 Java 进程尚未提供 HTTP 服务时就判定安装完成。

3.3 验证插件状态
#

docker compose ps
docker compose logs --since=5m gerrit | grep -E 'code-owners|ERROR|Exception'
docker compose exec -T gerrit \
  sha256sum /var/gerrit/plugins/code-owners.jar

Gerrit 管理页面应显示:

字段结果
Plugin Namecode-owners
Versionb419796519
API Version3.14.2-SNAPSHOT
StatusEnabled

Gerrit 插件列表中的 Code Owners 版本与启用状态

插件已加载,但全局 disabled=true,此时尚未给任何项目增加提交门禁。

4. 先提交项目安全配置
#

4.1 获取 refs/meta/config
#

项目配置不是普通 main 文件。先拉取专用 Ref:

git fetch origin \
  refs/meta/config:refs/remotes/origin/meta-config

git switch -c feat/code-owners-bootstrap \
  origin/meta-config

将现有 project.config 与附件内容合并,不要直接覆盖原有 Verified、权限继承或 Submit Requirement。

4.2 准备期 code-owners.config
#

第一版配置仍然禁用门禁:

[codeOwners]
  disabled = true
  disabledBranch = refs/meta/config
  backend = find-owners
  requiredApproval = Code-Review+1
  overrideApproval = Owners-Override+1
  fallbackCodeOwners = NONE
  enableImplicitApprovals = FALSE

[validation "refs/heads/*"]
  enableValidationOnCommitReceived = FORCED
  enableValidationOnSubmit = FORCED
  rejectNonResolvableCodeOwners = true

准备期使用 FORCED,目的是在门禁尚未启用时就检查后续提交的 OWNERS。这样可以先修正邮箱、语法和无法解析的负责人,再打开 Submit Requirement。

准备期配置保持 disabled 为 true,并配置恢复通道和强制校验

4.3 通过评审提交配置
#

git add project.config code-owners.config groups
git commit -m "feat: prepare Code Owners safeguards"
git push origin HEAD:refs/for/refs/meta/config

配置 Change 通过原有 Code-Review 后再 Submit。此时可以在 refs/meta/config 中看到文件,但普通代码 Change 还不会出现 Code Owners 阻塞。

5. 提交 OWNERS 基线
#

5.1 根目录负责人
#

仓库根目录创建:

root-owner@example.internal

如果子目录没有自己的 OWNERS,插件会向上找到这里。

5.2 frontend 和 backend 各自负责
#

frontend/OWNERS

set noparent

frontend-owner@example.internal

backend/OWNERS

set noparent

backend-owner@example.internal

两个子目录都使用 set noparent,frontend 的审批不会继承到 backend。

5.3 OWNERS 里写邮箱,不写 Gerrit Group
#

find-owners backend 解析的是 Gerrit 账号邮箱。以下写法不是这套 backend 的组授权语法:

Developers
Backend-Team

负责人邮箱必须能解析到 Gerrit 账号,还要符合 allowedEmailDomain。本次遇到过同一 LDAP 目录中账号对象属性不一致的问题,所以启用前先逐个确认邮箱,而不是根据用户名猜地址。

5.4 在门禁仍禁用时合并基线
#

git switch main
git pull --ff-only

git switch -c feat/code-owners-baseline
mkdir -p frontend backend
# Copy and edit the three OWNERS files here.

git add OWNERS frontend/OWNERS backend/OWNERS
git commit -m "feat: add directory ownership baseline"
git push origin HEAD:refs/for/main

服务端接收时应报告 OWNERS 校验无问题。先把这条 Change 合并,再进入下一步。

6. 只为实验项目启用门禁
#

回到 refs/meta/config 分支,将:

disabled = true

改成:

disabled = false

同时把准备期的强制校验切换到启用后的常规校验:

enableValidationOnCommitReceived = TRUE
enableValidationOnSubmit = TRUE

完整文件见 code-owners.config

项目配置把 Code Owners 从禁用切换为启用

上传方式不变:

git add code-owners.config
git commit -m "feat: enable Code Owners for review project"
git push origin HEAD:refs/for/refs/meta/config

这次实验里,我曾尝试在 push option 中附带 Owners-Override+1。Gerrit 拒绝了,因为该 Label 只授权给 refs/heads/*,没有授权给 refs/meta/config。这是预期行为。配置分支已经被 Code Owners 排除,不需要 Override。

7. 用跨目录 Change 验证门禁
#

7.1 创建实验 Change
#

新 Change 同时修改:

frontend/README.md
backend/README.md
git switch main
git pull --ff-only
git switch -c feat/cross-directory-review

printf '\nFrontend review evidence.\n' >> frontend/README.md
printf '\nBackend review evidence.\n' >> backend/README.md

git add frontend/README.md backend/README.md
git commit -m "docs: update both owned components"
git push origin HEAD:refs/for/main%topic=code-owners-lab

Jenkins 收到 patchset-created 后完成检查并投 Verified +1。管理员也是 frontend owner,显式投了 Code-Review +2。由于 enableImplicitApprovals=FALSE,上传者身份本身不算批准,必须有实际 Label 投票。

7.2 第一次检查:backend 没有负责人批准
#

此时 Submit Requirements 为:

Requirement状态
Code-ReviewSATISFIED
VerifiedSATISFIED
Code-OwnersUNSATISFIED
No-Unresolved-CommentsNOT_APPLICABLE

文件级状态:

{
  "frontend/README.md": "APPROVED",
  "backend/README.md": "INSUFFICIENT_REVIEWERS"
}

Code-Review 和 Verified 已满足,但 backend 缺少 Code Owner

红叉只落在 backend/README.md。这说明 frontend/OWNERS 没有越过 set noparent 批准 sibling 目录。

插件 REST API 可以返回相同的文件级状态:

export GERRIT_HTTP_USER='review-admin'
export GERRIT_HTTP_PASSWORD='replace-with-http-password'

curl -fsS \
  --user "$GERRIT_HTTP_USER:$GERRIT_HTTP_PASSWORD" \
  'https://review.example.internal/a/changes/review-project~45/revisions/current/code_owners.status' \
  | sed '1d' \
  | jq .

Gerrit 的 /a/ REST 路径要求 HTTP Basic 凭据。浏览器已经登录并不等于这条 curl 能复用 Web Session。

7.3 backend owner 投票
#

切换到 backend owner 账号,对同一个 Patch Set 投:

Code-Review +1

再次查询后,两条文件状态都变为 APPROVEDCode-Owners 也变为 SATISFIED

backend owner 投票后 Code Owners 和两个文件全部通过

sequenceDiagram
    participant A as review-admin
    participant G as Gerrit
    participant J as Jenkins
    participant B as backend-owner

    A->>G: upload cross-directory change
    J-->>G: Verified +1
    A->>G: Code-Review +2
    G-->>A: submit blocked
    B->>G: Code-Review +1
    G-->>A: submit allowed
    A->>G: Submit

7.4 合并并复核 main
#

管理员 Submit 后:

status: MERGED
revision: 9d0a166d616a15412ac034e77281ecb40225f05a
subject: docs: update both owned components

Code Owners 实验 Change 已合并

合并状态、Submitted 时间和审批记录同时保留,方便后续审计。

8. Code Owners 和 Verified 怎么配合
#

两套门禁回答不同问题:

门禁回答的问题本次投票者
Code-Review代码评审是否通过人工 Reviewer
Verified自动检查是否通过Jenkins 服务账号
Code-Owners涉及的路径是否得到负责人批准路径 Owner

Code Owners 用 Code-Review+1 作为文件审批。项目仍可保留整体 Code-Review+2 的 Submit Requirement,owner 的 +1 不会代替整体 +2

Jenkins 只判断自动检查结果,目录修改仍由对应 owner 批准。

9. 实测故障
#

9.1 push 账号和 commit author 不一致
#

本地仓库残留了另一个测试账号的 user.email。使用管理员 SSH 账号 push 时,Gerrit 因缺少 Forge Author 权限拒绝提交。

修复前先检查:

git config user.name
git config user.email
git show -s --format='author=%an <%ae>%ncommitter=%cn <%ce>' HEAD

确认后再 amend:

git config user.name 'review-admin'
git config user.email 'admin@example.internal'
git commit --amend --reset-author --no-edit

不要为了省一次 amend 给普通用户开放 Forge Author。

9.2 Override Label 的 Ref 范围
#

Owners-Override 只授权在 refs/heads/*。把它作为 refs/meta/config push option 会被拒绝。

这不是插件故障。Label 权限和 Change 目标 Ref 必须匹配。配置分支已经用 disabledBranch 排除,不应再扩大 Override 范围。

9.3 OWNERS 邮箱无法解析
#

常见原因有:

  • 邮箱不属于任何 Gerrit 账号;
  • 账号邮箱不可见;
  • 邮箱域不在 allowedEmailDomain
  • LDAP 同步到 Gerrit 的邮箱和预期不一致;
  • 把 Gerrit Group 名称写进了 OWNERS。

启用前保留:

rejectNonResolvableCodeOwners = true

让错误在 push 时出现,比等到 Submit 才发现路径无人负责省事。

9.4 装完插件后所有项目突然被卡住
#

先检查全局配置:

disabled = true

安装顺序也可能反了。JAR 已加载,而全局 opt-out 还没写入,插件会按默认值处理已有项目。安装脚本必须先配置,再放 JAR。

9.5 Code Owners 通过了,仍然不能 Submit
#

逐项查看 Submit Requirements。实验项目还要求:

Code-Review +2
Verified +1

Code Owners 只负责文件归属;Jenkins 成功不能绕过 owner 审批,缺少 Code-Review +2 也无法 Submit。

10. 日常维护与回滚
#

10.1 日常检查
#

# Gerrit health
docker compose ps
curl -fsS https://review.example.internal/config/server/version

# Plugin checksum
docker compose exec -T gerrit \
  sha256sum /var/gerrit/plugins/code-owners.jar

# Plugin errors
docker compose logs --since=24h gerrit \
  | grep -E 'code-owners.*(ERROR|Exception)|Failed to load plugin'

OWNERS 和 code-owners.config 都在 Git 历史中。每次修改都走 Change,可以回看谁改了负责人和门禁。

10.2 项目级停用
#

首选回滚是修改 refs/meta/config

[codeOwners]
  disabled = true

因为 refs/meta/config 被排除,这条恢复 Change 不需要 Code Owners。它仍然受项目原有评审规则约束。

10.3 插件导致 Gerrit 无法启动
#

停止 Gerrit,恢复安装前的 gerrit.config,从持久化 plugins 目录移除 code-owners.jar,再启动并检查版本接口。不要在服务运行中直接删除 JAR 后假设类加载状态已经清理。

需要备份的内容至少包括:

data/gerrit_etc/
data/gerrit_plugins/
data/gerrit_git/

10.4 什么时候应该拆仓库
#

以下情况不要继续堆 OWNERS:

  • 两个团队必须拥有不同 Read 权限;
  • 代码存在合同或合规隔离;
  • 发布周期和版本边界完全独立;
  • 大量 Change 总要跨目录找多个团队审批,单仓库收益已经消失。

Code Owners 适合“代码可以一起存放,但修改要找对人”的仓库。它不是保密方案。

参考资料
#

相关文章

从 Gerrit 3.14.2 到 Jenkins Verified:代码评审门禁实战
··2910 字·14 分钟
CI/CD LDAP DevOps Gerrit Jenkins +2
Jenkins 动态参数流水线:模块版本选择、GitLab 触发与结果回填
·785 字·4 分钟
CI/CD 动态参数 DevOps Jenkins Groovy +2
Jira Webhook Integration Jenkins
·23 字·1 分钟
CI/CD Jira 自动化 DevOps Jenkins Webhook