Code-Review +2、Jenkins Verified +1 和 Submit 串了起来。接下来遇到的问题更像单仓库团队的日常:frontend/ 应该由前端负责人批准,backend/ 应该由后端负责人批准,一个 Change 同时改两边时,缺任何一方都不能合并。我原先以为 Gerrit 能像 SVN 一样直接做目录 ACL。实际不是。Gerrit 核心权限落在 Project 和 Ref 上,用户只要能读取项目,就能 fetch 完整 Git 对象。Code Owners 解决的是“谁必须批准这些文件”,不是“谁能看见这些文件”。需要目录保密时,仍然要拆仓库。
实验结果#
实验环境沿用上一篇文章中的 Gerrit 和 Jenkins:
| 项目 | 实测结果 |
|---|---|
| Gerrit | 3.14.2,容器健康 |
| Code Owners | stable-3.14 build 6,版本 b419796519 |
| 插件制品 | SHA-256 校验通过,启动日志无加载错误 |
| 全局策略 | 默认 disabled=true,新项目不会自动套用门禁 |
| 实验项目 | gerrit-demo 单独启用 Code Owners |
| frontend owner | frontend-owner@example.internal |
| backend owner | backend-owner@example.internal |
| CI | Jenkins 对实验 Change 回写 Verified +1 |
| 阻塞结果 | backend 未批准时,Code-Owners 为 UNSATISFIED |
| 审批结果 | 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 版本契约#
| 组件 | 版本 | 说明 |
|---|---|---|
| Gerrit | 3.14.2 | 官方 Docker 镜像 |
| Code Owners | b419796519 | stable-3.14 build 6 |
| 插件 API | 3.14.2-SNAPSHOT | 以插件清单为准 |
| backend | find-owners | 从仓库中的 OWNERS 文件解析负责人 |
Code Owners 不是 Gerrit 核心模块。升级 Gerrit 时,不能默认旧 JAR 仍然兼容。先在测试环境加载对应 stable 分支的制品,检查 API 版本、启动日志和真实 Change,再碰生产实例。
0.3 随文配置包#
Page Bundle 附带本次实验的脱敏配置:
- 操作手册
- 版本与插件校验值
- 全局 Gerrit 配置示例
- 安装脚本
- 项目访问配置
- 准备期 Code Owners 配置
- 启用后的 Code Owners 配置
- Group 映射示例
- 根目录 OWNERS
- frontend OWNERS
- backend OWNERS
- 附件校验清单
下载后先检查文件:
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 |
| 改到某路径时追加一个固定 Label | file: 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、配置分支豁免、强制校验 | 仍禁用 |
| 2 | OWNERS 基线 | 根目录、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
脚本完成以下操作:
- 检查
curl、docker和sha256sum; - 确认 Gerrit Compose 服务正在运行;
- 下载并校验 JAR;
- 备份
gerrit.config; - 先写全局禁用配置;
- 原子放入插件 JAR;
- 重启 Gerrit 并等待 HTTP 版本接口;
- 再次校验容器内 JAR;
- 检查插件加载错误。
健康循环同时检查容器状态和 /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 Name | code-owners |
| Version | b419796519 |
| API Version | 3.14.2-SNAPSHOT |
| Status | Enabled |

插件已加载,但全局 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。

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。

上传方式不变:
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-Review | SATISFIED |
| Verified | SATISFIED |
| Code-Owners | UNSATISFIED |
| No-Unresolved-Comments | NOT_APPLICABLE |
文件级状态:
{
"frontend/README.md": "APPROVED",
"backend/README.md": "INSUFFICIENT_REVIEWERS"
}

红叉只落在 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
再次查询后,两条文件状态都变为 APPROVED,Code-Owners 也变为 SATISFIED。

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

合并状态、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 适合“代码可以一起存放,但修改要找对人”的仓库。它不是保密方案。
