refs/for/main,Gerrit 创建 Change,Jenkins 收到 patchset-created,检出准确的 Patch Set,执行检查,再由 ci-cd 服务账号回写 Verified +1 或 -1。只有 Code-Review 和 Verified 同时满足,Change 才能提交。先看最终结果#
这次实验最后得到以下状态:
| 项目 | 验证结果 |
|---|---|
| Gerrit | 3.14.2,容器健康,Git 和配置均持久化 |
| 登录 | LDAP 用户首次登录时自动创建 Gerrit 账号 |
| Git 访问 | HTTP 可使用 LDAP 密码;SSH 使用用户公钥 |
| 邮件 | SMTP 真实发送成功,不只停留在端口连通 |
| 人工评审 | 普通用户只能 Code-Review -1/+1,管理员可以 +2 和 Submit |
| CI 身份 | ci-cd 是 Service Users 成员,可读取事件并投 Verified -1/+1 |
| Jenkins | Gerrit Trigger 收到 Patch Set 和 recheck 事件 |
| 自动投票 | 构建成功回写 Verified +1,失败回写 -1 |
| 提交门禁 | Code-Review 和 Verified 分别计算,缺一项就不能 Submit |
| 自动合并 | 未启用,保留人工 Submit |
完整链路如下:
sequenceDiagram
participant D as Developer
participant G as Gerrit 3.14.2
participant J as Jenkins
participant B as ci-cd Bot
participant R as Reviewer
D->>G: git push HEAD:refs/for/main
G-->>J: patchset-created via stream-events
J->>G: fetch GERRIT_REFSPEC
J->>J: run fixed quality checks
J->>B: build result
B->>G: gerrit review --label Verified=+1/-1
R->>G: Code-Review +2
G->>G: evaluate submit requirements
R->>G: Submit
0. 范围、版本与附件#
0.1 版本契约#
本文对应 2026 年 8 月 4 日验证过的组合:
| 组件 | 版本 | 说明 |
|---|---|---|
| Gerrit | 3.14.2 | 官方 gerritcodereview/gerrit 镜像 |
| Jenkins | 2.555.1-jdk21 | 官方 Jenkins LTS 镜像 |
| Gerrit Trigger | 3.1983.v57096fe9923c | 要求 Jenkins Core 2.479.3 或更高 |
| Java | 21 | Jenkins 镜像内置 Temurin 21 |
| Docker | 24+ | Compose v2 |
镜像在附件中同时固定了 tag 和多架构 manifest digest。tag 方便阅读,digest 避免同名镜像内容漂移。
0.2 本文覆盖什么#
本文覆盖:
- 单节点 Gerrit 的 Docker Compose 部署和资源限制;
- LDAP 登录、JIT 账号创建和 SMTP 通知;
- Change、Patch Set、Change-Id、Review、Submit 的实际操作;
- 项目权限继承、Verified Label 和 Submit Requirement;
- Jenkins 全新安装、Gerrit Trigger、服务账号和自动投票;
- 新 Patch Set 的重新验证、
recheck和常见故障。
本文不覆盖:
- Gerrit 或 Jenkins 高可用;
- Jenkins 历史数据迁移;
- 互联网入口的反向代理、WAF 和证书自动化;
- 大规模动态 Agent 调度;
- Gerrit 多站点复制和灾备编排。
0.3 随文部署包#
Page Bundle 附带一套脱敏后的可执行示例:
- 部署包说明
- 版本与镜像摘要
- 非敏感环境变量模板
- Gerrit Compose
- LDAP 配置脚本
- SMTP 配置脚本
- Gerrit 冷备份脚本
- Verified 项目配置
- Jenkins Dockerfile
- Jenkins Compose
- 固定质量检查 Pipeline
- Gerrit 3.14 兼容的 Review Commands
- 闭环 Smoke Check
- 文件校验清单
下载后先校验完整性:
KIT_BASE_URL="https://blog.treesir.pub/posts/gerrit-jenkins-verified-gate/files"
mkdir -p gerrit-jenkins-verified-lab
cd gerrit-jenkins-verified-lab
curl -fsSLO "$KIT_BASE_URL/SHA256SUMS"
while read -r _ relative_path; do
mkdir -p "$(dirname "$relative_path")"
curl -fsSL "$KIT_BASE_URL/$relative_path" -o "$relative_path"
done < SHA256SUMS
sha256sum -c SHA256SUMS
env、密码文件、SSH 私钥和运行时 data/ 不在校验清单中,也不应该提交到 Git。
1. 先把 Gerrit 的评审模型说清楚#
第一次接触 Gerrit,最容易把它理解成“另一个 GitLab”。这会在 push、分支和 CI 触发上不断遇到反直觉行为。Gerrit 的核心对象不是 Merge Request,而是 Change。
| 概念 | 含义 |
|---|---|
| Change | 一条评审记录,由项目、目标分支和 Change-Id 共同定位 |
| Patch Set | 同一个 Change 的某个代码版本 |
| Change-Id | commit message footer 中的稳定标识,不等于 commit SHA |
refs/for/main | 把提交送入 main 的评审队列 |
refs/heads/main | 真实分支;直推这里会绕过普通评审流程 |
| Label | 用户或机器人对 Change 的评分维度 |
| Submit Requirement | 判定 Change 是否允许提交的规则 |
1.1 为什么 push 的是 refs/for/main#
正常评审 push:
git push origin HEAD:refs/for/main
Gerrit 接收 commit 后,不会立即移动 refs/heads/main。它创建一个 Change,并把 commit 保存为该 Change 的 Patch Set。Change 满足提交条件并执行 Submit 后,目标分支才更新。
直接执行下面的 push 是另一条路径:
git push origin HEAD:refs/heads/main
这会直接更新真实分支。生产项目通常不给普通开发者这项权限,否则 Verified 门禁没有意义。
1.2 Change-Id 如何把多个 commit 归到同一个 Change#
Gerrit 根据三项内容匹配已有评审:
- Change-Id;
- Repository;
- Branch。
修改同一条评审时,保留 Change-Id:
git add .
git commit --amend
git push origin HEAD:refs/for/main
虽然 commit SHA 改了,Gerrit 仍会把它识别为原 Change 的新 Patch Set。删掉 Change-Id 或生成新的 Change-Id,则会创建新 Change。
1.3 为什么这里选 Jenkins,而不是直接套 GitLab CI#
Gerrit 的待评审提交位于 Gerrit 自己的 refs/changes/*,push 入口是 refs/for/*。这些提交不会自然出现在 GitLab 仓库的普通分支历史中,因此 GitLab Pipeline 不会自动知道 Gerrit 新增了 Patch Set。
要用 GitLab CI,需要自己维护一条桥:
Gerrit event -> GitLab Pipeline Trigger API -> build -> Gerrit Review API
Jenkins Gerrit Trigger 已经实现了这条事件链。插件通过 Gerrit SSH stream-events 持续接收 patchset-created、comment-added、change-merged 等事件,并在构建完成后写回 Label。对一个学习环境和少量项目,它是更短的路径。
2. 用 Docker Compose 部署 Gerrit 3.14.2#
2.1 主机和端口#
这次环境是一台 Debian 12 主机。原计划使用 8080,但该端口已被占用,因此改为:
| 用途 | 容器端口 | 主机端口 |
|---|---|---|
| Gerrit Web | 8080 | 8081 |
| Gerrit SSH | 29418 | 29418 |
主机上同时运行其他服务,Swap 也长期处于高占用状态。Gerrit JVM 因此固定为:
-Xms512m -Xmx2g
容器上限为 2 CPU、3 GiB 内存。这个限制不是性能模板,只是避免学习实例把共享主机拖进 OOM。
2.2 Compose 配置#
核心配置如下,完整文件见 files/gerrit/compose.yaml:
services:
gerrit:
image: docker.io/gerritcodereview/gerrit:3.14.2@sha256:c1a145a7700909af6a2327b95d8eaeee6a0447745d4364ca3e8f0b84d03ce17f
restart: unless-stopped
environment:
CANONICAL_WEB_URL: ${GERRIT_WEB_URL:?set GERRIT_WEB_URL}
JAVA_TOOL_OPTIONS: -Xms512m -Xmx2g
ports:
- "${GERRIT_BIND_ADDRESS:-127.0.0.1}:${GERRIT_HTTP_PORT:-8081}:8080"
- "${GERRIT_BIND_ADDRESS:-127.0.0.1}:${GERRIT_SSH_PORT:-29418}:29418"
volumes:
- ./data/gerrit_git:/var/gerrit/git
- ./data/gerrit_index:/var/gerrit/index
- ./data/gerrit_cache:/var/gerrit/cache
- ./data/gerrit_db:/var/gerrit/db
- ./data/gerrit_etc:/var/gerrit/etc
- ./data/gerrit_data:/var/gerrit/data
- ./data/gerrit_plugins:/var/gerrit/plugins
cpus: 2.0
mem_limit: 3g
我最终选择 bind mount,而不是 Docker named volume。这样备份、查看配置和迁移目录都更直观。代价是宿主机目录权限要自己负责。
默认模板只绑定 127.0.0.1。如果环境明确允许内网直连,再把 GERRIT_BIND_ADDRESS 改为内网地址或 0.0.0.0,同时限制防火墙来源。
2.3 启动和健康检查#
cp files/env.example files/env
chmod 600 files/env
${EDITOR:-vi} files/env
set -a
source files/env
set +a
docker compose --env-file files/env \
-f files/gerrit/compose.yaml config --quiet
docker compose --env-file files/env \
-f files/gerrit/compose.yaml up -d
docker compose --env-file files/env \
-f files/gerrit/compose.yaml ps
首次初始化会比普通重启慢。不要只看容器是 running,要等 healthcheck 通过:
curl -fsS "${GERRIT_WEB_URL%/}/config/server/version"
预期返回:
3.14.2
2.4 两条非致命启动错误#
官方 3.14.2 镜像在这次环境中出现了两条与主流程无关、但会污染日志的错误:
plugin-manager-preloader访问外部 GerritForge Jenkins 服务,响应解析失败;delete-project插件把默认路径值误当成时间值解析。
我在 gerrit.config 中加入:
[plugin "plugin-manager"]
preload = false
[plugin "delete-project"]
deleteTrashFoldersMaxAllowedTime = 10 minutes
应用后重启,日志中的对应 ERROR、Exception 和 Failed 消失。随文 configure-site.sh 已包含这两个设置。
这里的判断标准不是“服务能访问,所以错误不用管”。先确认错误不影响核心功能,再针对具体插件收口,避免真正的故障被旧噪声淹没。
3. 接入 LDAP 和 SMTP#
3.1 LDAP 登录链路#
这次 LDAP 目录已经被其他内部系统使用。Gerrit 的登录过程是:
sequenceDiagram
participant U as User
participant G as Gerrit
participant L as LDAP
U->>G: username + password
G->>L: bind with service DN
G->>L: search accountPattern
L-->>G: exact user DN
G->>L: re-bind as user with user password
L-->>G: authentication result
G->>G: JIT create/update account
关键配置为:
[auth]
type = LDAP
gitBasicAuthPolicy = HTTP_LDAP
[ldap]
server = ldap://ldap.example.internal:389
startTls = false
username = cn=gerrit,ou=services,dc=example,dc=internal
accountBase = ou=users,dc=example,dc=internal
accountPattern = (uid=${username})
accountFullName = cn
accountEmailAddress = mail
accountSshUserName = uid
groupBase = ou=groups,dc=example,dc=internal
groupPattern = (cn=${groupname})
groupMemberPattern = (uniqueMember=${dn})
LDAP 模式下,Bind DN 先搜索用户 DN,Gerrit 随后用用户自己的密码重新 bind。Bind 账号密码不是所有人的通用登录密码。
实验网络是受控内网,长期允许明文 LDAP,所以 startTls=false 是明确的网络约束,不是遗漏。其他环境应根据自己的目录和链路策略决定传输方式。
3.2 先验证搜索,再改 Gerrit#
配置 LDAP 时曾遇到一个很隐蔽的问题:用户条目中明明存在 uid,ldapcompare 也返回 true,但 (uid=username) 搜索返回 0 条。根因在 LDAP equality index,而不是 Gerrit。
因此先执行与 Gerrit 完全一致的查询:
ldapsearch -x \
-H "ldap://ldap.example.internal:389" \
-D "cn=gerrit,ou=services,dc=example,dc=internal" \
-W \
-b "ou=users,dc=example,dc=internal" \
"(uid=your-user)" dn uid cn mail
验收条件是返回唯一且正确的 DN。只证明“389 端口可达”或“Bind 成功”远远不够。
我一度把 accountPattern 临时改成 (cn=${username})。LDAP 索引修复并重新验证后,又恢复为 uid。不要让应用配置永久掩盖目录数据问题。
3.3 密码只写 secure.config#
LDAP Bind 密码不应出现在 Git、Compose、命令行参数或进程列表里。随文脚本从受限文件读取密码,经 stdin 送入容器:
install -m 600 /dev/null /secure/path/ldap-password
${EDITOR:-vi} /secure/path/ldap-password
./files/gerrit/configure-ldap.sh \
files/env \
/secure/path/ldap-password
脚本把非敏感项写进 gerrit.config,把密码写进 mode 0600 的 secure.config。它不会把密码输出到日志。
3.4 首个登录用户是初始管理员#
空 Gerrit 初始化后,第一个成功登录的用户会成为初始管理员。LDAP 切换前要确定谁完成第一次登录,否则可能把管理权交给错误账号。
后续 LDAP 用户无需手工创建 Gerrit 账号。只要目录中存在必需属性并能成功认证,第一次登录会自动创建 Registered User。
3.5 SSH 不接受 LDAP 密码#
这是使用中最常见的误解之一:
| 协议 | 可用认证方式 |
|---|---|
| Web UI | LDAP 用户名和密码 |
| Git over HTTP | LDAP 密码或 HTTP Token,取决于 gitBasicAuthPolicy |
| Gerrit SSH | SSH 公钥,或单独配置的 Kerberos GSSAPI |
因此“免 SSH Key 的 Git”应走 HTTPS。Jenkins Gerrit Trigger 需要 stream-events,必须使用 Gerrit SSH,所以 ci-cd 仍然要有专用 SSH Key。
3.6 SMTP 要验真实投递#
SMTP 配置同样把密码放在 secure.config:
[sendemail]
enable = true
smtpServer = smtp.example.com
smtpServerPort = 465
smtpEncryption = ssl
sslVerify = true
smtpUser = gerrit@example.com
from = Gerrit Code Review <gerrit@example.com>
应用脚本:
install -m 600 /dev/null /secure/path/smtp-password
${EDITOR:-vi} /secure/path/smtp-password
./files/gerrit/configure-smtp.sh \
files/env \
/secure/path/smtp-password
我在真实 Change 上发送评论通知,确认 Gerrit 出站日志没有 SMTP 错误,并在收件箱看到邮件。只用 openssl s_client 证明 TLS 握手成功,不能算邮件闭环通过。
4. Gerrit 的日常管理与数据边界#
4.1 配置文件#
主要配置位于:
| 文件 | 内容 | 备份级别 |
|---|---|---|
data/gerrit_etc/gerrit.config | 非敏感运行配置 | 必须 |
data/gerrit_etc/secure.config | LDAP、SMTP 等密码 | 必须,按凭据保护 |
data/gerrit_git/ | 项目 Git 数据和 NoteDb | 必须 |
data/gerrit_db/ | AccountPatchReviewDb | 建议 |
data/gerrit_data/ | 插件运行数据 | 建议 |
data/gerrit_plugins/ | 已安装插件 | 建议 |
data/gerrit_index/ | Lucene 索引 | 可重建 |
data/gerrit_cache/ | 缓存 | 可重建 |
Gerrit 3.x 的核心状态保存在 NoteDb,也就是 Git 仓库中的特殊 refs。Lucene 负责搜索索引。gerrit_db 中的 H2 只承载 AccountPatchReviewDb,例如用户对文件的 reviewed 标记,不是核心业务数据库。
单节点学习环境不需要再部署 MySQL 或 PostgreSQL。把外部数据库当成 Gerrit 核心数据源,是旧版本经验造成的误判。
4.2 常用管理命令#
# 状态
docker compose --env-file files/env \
-f files/gerrit/compose.yaml ps
# 最近日志
docker compose --env-file files/env \
-f files/gerrit/compose.yaml logs --tail=200 gerrit
# 重启
docker compose --env-file files/env \
-f files/gerrit/compose.yaml restart gerrit
# 停止但保留数据
docker compose --env-file files/env \
-f files/gerrit/compose.yaml stop gerrit
不要为了“清理”执行 docker compose down -v 或删除 data/。bind mount 目录就是实例本身。
4.3 冷备份#
随文脚本会在维护窗口停止 Gerrit,备份非重建数据,生成 SHA-256,再恢复服务:
./files/gerrit/backup.sh \
files/env \
/secure/backups/gerrit
备份不包含 Lucene index 和 cache。恢复时可以重新生成它们。归档包含 secure.config,必须按密码文件管理。
备份的最终验收不是“tar 文件存在”,而是在隔离目录中实际恢复、启动、查询 Change,并完成 reindex。
4.4 升级原则#
升级前至少完成:
- 阅读目标版本 release notes;
- 冷备份并验证恢复;
- 在副本上跑
init和 reindex; - 检查插件与目标 Gerrit 版本兼容性;
- 记录当前镜像 digest 和回退边界;
- 预留停机窗口,不把 schema 变更当成普通容器回滚。
5. 创建项目并走完一次人工评审#
5.1 创建 gerrit-demo#
可以从 Web UI 创建,也可以用管理员 SSH:
ssh -p 29418 review-admin@review.example.internal \
gerrit create-project --empty-commit gerrit-demo
项目默认继承 All-Projects。这次没有一开始就复制一整套权限,而是先使用继承,再只添加 Verified 所需的项目级规则。
5.2 安装 commit-msg Hook#
git clone \
ssh://developer@review.example.internal:29418/gerrit-demo
cd gerrit-demo
git config core.hooksPath .git/hooks
curl -fsSLo .git/hooks/commit-msg \
http://review.example.internal:8081/tools/hooks/commit-msg
chmod u+x .git/hooks/commit-msg
我的开发机全局设置了 core.hooksPath,它会覆盖仓库默认的 .git/hooks。因此先检查来源:
git config --show-origin --get core.hooksPath
再用仓库级配置明确恢复 .git/hooks。否则 Hook 文件虽然存在,Git 仍不会执行它,最终 push 会报 missing Change-Id。
5.3 创建 Change#
printf '# Gerrit demo\n' > README.md
git add README.md
git commit -m "docs: add project readme"
git show --format=fuller --stat HEAD
commit message footer 应出现:
Change-Id: I...
推送评审:
git push origin HEAD:refs/for/main
Gerrit 返回 Change URL。此时 main 尚未更新。
5.4 上传 Patch Set 2#
收到评审意见后修改原提交:
printf '\nThis project demonstrates Gerrit review.\n' >> README.md
git add README.md
git commit --amend --no-edit
git push origin HEAD:refs/for/main
因为 Change-Id 未变,Gerrit 把新 commit 放到原 Change 下,形成 Patch Set 2。旧 Patch Set 仍可比较,但提交条件只针对当前 Patch Set 计算。
5.5 验证 Code-Review 权限#
这次默认权限表现为:
| 身份 | Code-Review | Submit |
|---|---|---|
| Registered Users | -1..+1 | 否 |
| Administrators | -2..+2 | 是 |
普通用户投 +1 表示认可,但不能把 Change 变成可提交。管理员投 +2 后,Code-Review Requirement 才满足。最后执行 Submit,Change 合入 main。
这一步很重要:先证明 Gerrit 自身的人工评审模型正确,再引入 Jenkins。否则 CI 出错时,很难判断问题在基础权限还是自动化连接。
6. 增加 Verified Label 和 Submit Requirement#
6.1 为什么 Label 和 Submit Requirement 要分开#
Label 只是评分维度。它本身不一定阻止提交。Submit Requirement 才定义“什么条件算满足”。
Gerrit 3.14 推荐新 Label 使用 function = NoBlock,再用 Submit Requirement 表达门禁:
[label "Verified"]
function = NoBlock
defaultValue = 0
value = -1 Fails
value = 0 No score
value = +1 Verified
copyCondition = changekind:NO_CODE_CHANGE
[submit-requirement "Verified"]
description = Changes must pass verification before submission.
applicableIf = -branch:refs/meta/config
submittableIf = label:Verified=MAX AND -label:Verified=MIN
submittableIf 同时要求:
- 至少有一个
Verified +1; - 不存在
Verified -1。
6.2 为什么排除 refs/meta/config#
Submit Requirement 自己就存储在 refs/meta/config。如果配置错误,又要求配置 Change 先通过同一个 CI 门禁,就可能把修复通道锁死。
因此:
applicableIf = -branch:refs/meta/config
这不是为了让业务代码绕过 CI,而是保留配置修复路径。
6.3 投票权限只给管理员和服务账号#
项目配置为:
[access "refs/heads/*"]
label-Verified = -1..+1 group Administrators
label-Verified = -1..+1 group Service Users
完整 project.config 还包含 All-Projects 继承关系。groups 文件必须用当前 Gerrit 实例自己的 UUID 映射组名,不能复制其他环境的 UUID。
6.4 通过评审修改项目配置#
mkdir gerrit-demo-meta-config
cd gerrit-demo-meta-config
git init
git remote add origin \
ssh://review-admin@review.example.internal:29418/gerrit-demo
git fetch origin refs/meta/config
git checkout -b meta-config FETCH_HEAD
编辑 project.config 和 groups 后,不直接 push 到配置分支,而是送审:
git add project.config groups
git commit -m "feat: require Verified approval"
git push origin HEAD:refs/for/refs/meta/config
管理员 Code-Review +2 并 Submit 后,规则生效。
6.5 先证明门禁真的会阻断#
我创建一个普通 Change,只给 Code-Review +2,不投 Verified。结果:
| Requirement | 状态 |
|---|---|
| Code-Review | SATISFIED |
| Verified | UNSATISFIED |
| Submit | 不可用 |

Patch Set 1:人工评审已通过,Verified 尚无投票,Submit 仍不可用。
这证明 Verified 不是一个只展示在 UI 上的 Label,它已经进入提交判定。
7. 全新安装 Jenkins 并接入 Gerrit Trigger#
7.1 Jenkins 安装#
这里使用全新 Jenkins,不导入历史 JENKINS_HOME。镜像在构建时安装固定插件:
FROM jenkins/jenkins:2.555.1-jdk21@sha256:7004d07dbcdc5439fdad8853acdb029c5e3ab7a3d8190184fbf89bec66786c02
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --latest false --plugin-file /usr/share/jenkins/ref/plugins.txt
插件清单只有这条链路所需的顶层插件:
gerrit-trigger:3.1983.v57096fe9923c
git:5.10.1
ssh-credentials:372.va_250881b_08cd
workflow-aggregator:608.v67378e9d3db_1
--latest false 让依赖版本遵循插件声明的最低兼容版本,避免构建时自动追到最新传递依赖。顶层插件仍逐项固定版本。
启动:
mkdir -p files/jenkins/data/jenkins_home
sudo chown -R 1000:1000 files/jenkins/data/jenkins_home
chmod 750 files/jenkins/data/jenkins_home
docker compose --env-file files/env \
-f files/jenkins/compose.yaml build --pull
docker compose --env-file files/env \
-f files/jenkins/compose.yaml up -d
附件中的 Jenkins 只开放 Web 端口。这个质量门禁不需要 inbound agent,所以没有把 50000 暴露到宿主机。
7.2 为 Jenkins 准备 ci-cd 身份#
Gerrit 官方提供 Service Users 组,用于标识 CI 和机器人,但组成员身份不保证每个实例都已授予 Stream Events。需要在 All-Projects 的 Global Capabilities 中检查这项授权,项目上的 Read 和 Label 权限也要单独配置。
流程为:
- 让 LDAP 中的
ci-cd首次登录 Gerrit,完成 JIT 账号创建; - 生成专用 Ed25519 Key;
- 给
ci-cd添加公钥; - 把账号加入
Service Users; - 确认
Service Users具有Stream EventsGlobal Capability; - 确认项目允许该组读取 Change 并投
Verified -1/+1。
管理员添加公钥:
cat gerrit-ci-cd.pub | \
ssh -p 29418 review-admin@review.example.internal \
gerrit set-account --add-ssh-key - ci-cd
加入服务组:
ssh -p 29418 review-admin@review.example.internal \
"gerrit set-members --add ci-cd 'Service Users'"
在 Jenkins 运行环境验证:
ssh -i /var/jenkins_home/.ssh/gerrit-ci-cd \
-p 29418 ci-cd@review.example.internal \
gerrit version
timeout 5 ssh -i /var/jenkins_home/.ssh/gerrit-ci-cd \
-p 29418 ci-cd@review.example.internal \
gerrit stream-events
stream-events 没有输出时,不代表失败。空闲 Gerrit 本来就没有事件。正确表现是连接保持,直到 timeout 结束,而不是立即返回权限错误。
7.3 配置 Gerrit Server#
在 Manage Jenkins > Gerrit Trigger 添加:
| 字段 | 示例值 |
|---|---|
| Name | gerrit-stu |
| Host | review.example.internal |
| SSH Port | 29418 |
| User | ci-cd |
| SSH Key | /var/jenkins_home/.ssh/gerrit-ci-cd |
| Frontend URL | http://review.example.internal:8081/ |
| Notification Level | NONE |
保存前执行 Test Connection。连接建立后,日志应出现:
Ready to receive data from Gerrit: gerrit-stu

Server 配置使用专用 ci-cd 身份;截图中的地址和邮箱已替换为示例值。
7.4 Gerrit 3.14.2 与默认 Review Command 不兼容#
第一次 Jenkins Build 已经成功,Gerrit 却没有出现 Verified +1。Jenkins 日志给出真正原因:
fatal: "--verified" is not a valid option
Gerrit Trigger 3.1983 的默认 SSH 命令仍使用:
--verified <VERIFIED>
Gerrit 3.14.2 要求使用通用 Label 语法:
--label Verified=<VERIFIED>
我把 Successful、Unstable、Failed、Started、Not Built 和 Aborted 六条命令全部改为:
gerrit review --project <GERRIT_NAME> <CHANGE>,<PATCHSET> \
--message 'Build Successful <BUILDS_STATS>' \
--label Verified=<VERIFIED> \
--label Code-Review=<CODE_REVIEW> \
--tag autogenerated:jenkins-gerrit-trigger \
--notify NONE
六条完整值见 review-commands.xml。只改 Successful 不够;失败路径若仍使用旧参数,最需要阻断时反而无法写回 -1。
7.5 配置 Job Trigger#
创建 Pipeline Job gerrit-demo-verified:
| 配置 | 值 |
|---|---|
| Server | gerrit-stu |
| Project | Plain: gerrit-demo |
| Branch | Plain: main |
| Event 1 | Patchset Created |
| Event 2 | Comment Added Contains: (?i)^recheck\s*$ |
| Success | Verified +1 |
| Failed | Verified -1 |
| Unstable | Verified -1 |
| Code-Review | 全部为 0 |

Job 只匹配 gerrit-demo/main,新 Patch Set 和 recheck 都能触发同一条质量门禁。
Pipeline 不能检出 main 后就开始测试,它必须检出触发事件携带的 Patch Set,并拒绝缺失的连接参数:
def gerritProject = env.GERRIT_PROJECT?.trim()
def gerritRefSpec = env.GERRIT_REFSPEC?.trim()
def gerritRevision = env.GERRIT_PATCHSET_REVISION?.trim()
def gerritHost = env.GERRIT_HOST?.trim()
def gerritPort = env.GERRIT_PORT?.trim()
[
GERRIT_PROJECT: gerritProject,
GERRIT_REFSPEC: gerritRefSpec,
GERRIT_PATCHSET_REVISION: gerritRevision,
GERRIT_HOST: gerritHost,
GERRIT_PORT: gerritPort
].each { name, value ->
if (!value) {
error("Missing Gerrit Trigger variable: ${name}")
}
}
if (!(gerritPort ==~ /\d{1,5}/)) {
error('Invalid GERRIT_PORT: expected an integer from 1 to 65535')
}
def gerritPortNumber = gerritPort.toInteger()
if (gerritPortNumber < 1 || gerritPortNumber > 65535) {
error('Invalid GERRIT_PORT: expected an integer from 1 to 65535')
}
checkout([
$class: 'GitSCM',
branches: [[name: gerritRevision]],
userRemoteConfigs: [[
credentialsId: 'gerrit-ci-cd',
refspec: gerritRefSpec,
url: "ssh://ci-cd@${gerritHost}:${gerritPortNumber}/${gerritProject}"
]]
])
GERRIT_HOST 和 GERRIT_PORT 来自触发事件的 Gerrit Provider。五个参数先统一去掉首尾空白,端口还必须是 1 到 65535 的整数;同一个模板才能在跟随 Job 所选 Server 的同时,对格式错误给出明确失败。
随后核对:
test "$(git rev-parse HEAD)" = "$GERRIT_PATCHSET_REVISION"
如果不做参数和 SHA 核对,流水线可能连错 Gerrit,或对着目标分支、旧 workspace 测试,再把结果投给当前 Patch Set。
7.6 为什么示例 Pipeline 不读取仓库里的 Jenkinsfile#
附件中的 quality-gate.pipeline.groovy 由 Jenkins 管理员固定,只做:
test -f README.md
git diff-tree --check --root -r HEAD
git fsck --no-dangling
它运行在 Jenkins Controller 的 built-in executor 上,因此不能执行评审提交中提供的任意脚本。否则一个尚未审核的 Patch Set 就能在 Controller 权限下运行命令。
正式项目应把构建移到隔离的临时 Agent,并明确管理凭据边界、工作目录清理、网络权限和构建超时。Controller 只负责调度。
8. Verified 闭环验收#
8.1 验收矩阵#
我没有用一次绿色 Build 就宣布完成,而是按下面的矩阵检查:
| 场景 | 预期结果 |
|---|---|
Code-Review +2,无 CI 投票 | Verified UNSATISFIED,不能 Submit |
| Jenkins 成功 | ci-cd 写入 Verified +1 |
| Jenkins 失败 | ci-cd 写入 Verified -1 |
当前 Patch Set 评论 recheck | 同一 Job 再次运行并更新投票 |
| 上传代码变化的新 Patch Set | 旧 Verified 不复制,新事件重新构建 |
| Code-Review 与 Verified 都满足 | 可以人工 Submit |
| 构建成功后 | 不自动 Submit |
8.2 实际闭环#
在一个已经有 Code-Review +2、但 Verified 未满足的 Change 上评论:
recheck
Gerrit Trigger 收到 CommentAdded,Jenkins Build 成功完成:
Checkout Patch Set SUCCESS
Repository Checks SUCCESS
Finished SUCCESS

Build #4 的两个固定阶段均成功,触发来源已脱敏为 review.example.internal。
修正 Review Command 后,ci-cd 写入:
Verified +1
Tag: autogenerated:jenkins-gerrit-trigger
Gerrit 随后显示:
| Requirement | 状态 |
|---|---|
| Code-Review | SATISFIED |
| Verified | SATISFIED |
| No-Unresolved-Comments | NOT_APPLICABLE |
| Change | NEW,可提交 |

Patch Set 2:ci-cd 写回 Verified +1 后,Change 进入可人工 Submit 状态。
Change 仍是 NEW,因为我没有配置自动 Submit。门禁负责给事实,合并动作仍由人发起。
8.3 闭环成立的证据#
一条可靠的闭环至少要同时具备:
- Jenkins Build Cause 显示 Gerrit 事件;
- Build 使用正确的
GERRIT_REFSPEC; - checkout SHA 等于
GERRIT_PATCHSET_REVISION; - Jenkins 结果是预期状态;
- Gerrit 上的投票人是
ci-cd; - 投票落在当前 Patch Set;
- Submit Requirement 从 UNSATISFIED 变为 SATISFIED;
- Gerrit 和 Jenkins 日志没有被忽略的写回错误。
只看到 Build 绿色不够。第一次验证正是“构建成功、投票失败”,如果只看 Jenkins 首页就会得到错误结论。
9. 后续提交如何重新触发 Verified#
这是日常使用中最重要的一段。
9.1 更新同一个 Change#
保留原 Change-Id,amend 后再次推到 review ref:
git add .
git commit --amend
git push origin HEAD:refs/for/main
流程为:
flowchart LR
A[Amend commit
保留 Change-Id] --> B[Push refs/for/main]
B --> C[同一 Change 新 Patch Set]
C --> D[patchset-created]
D --> E[Jenkins 检出新 GERRIT_REFSPEC]
E --> F[重新运行检查]
F --> G[ci-cd 更新当前 Patch Set 的 Verified]
代码发生变化时,配置中的:
copyCondition = changekind:NO_CODE_CHANGE
不会复制旧 Verified +1。新 Patch Set 从未验证状态开始,Jenkins 必须重新投票。这正是质量门禁应该有的行为。

Change Log 保留 Patch Set 1、Patch Set 2、Build Started、Build Successful 和 Verified +1 的完整顺序。
9.2 创建一个新 Change#
正常创建新 commit,Hook 生成新 Change-Id:
git add .
git commit -m "feat: add another change"
git push origin HEAD:refs/for/main
Gerrit 创建新 Change,并发出独立的 patchset-created。每个 Change 都有自己的 Code-Review 和 Verified 状态。
9.3 不改代码,只重跑当前 Patch Set#
如果失败来自临时网络、依赖仓库或执行节点,在 Change 评论:
recheck
Comment Added Contains 触发同一个 Job。ci-cd 对同一 Label 的新评分会替换自己的旧评分,例如从 -1 更新为 +1。
recheck 不是跳过代码变化的捷径。代码有改动时仍应上传新 Patch Set,让 Gerrit 保存可审计的版本。
9.4 哪些操作不会触发这条流水线#
以下操作不会自然触发 patchset-created:
- 只在本地 commit,不 push;
- push 到普通 GitLab 分支;
- 直接 push 到
refs/heads/main; - 修改 Jenkins Job 但没有 Gerrit 事件;
- 在不匹配 Job 项目或分支规则的 Change 上评论。
因此权限上要限制直推真实分支,流程上统一使用 refs/for/<branch>。
10. 实际遇到的故障与定位方法#
10.1 Jenkins 没有触发#
按顺序检查:
# 1. 服务账号 SSH
ssh -i /path/to/key -p 29418 \
ci-cd@review.example.internal gerrit version
# 2. stream-events 能否保持连接
timeout 5 ssh -i /path/to/key -p 29418 \
ci-cd@review.example.internal gerrit stream-events
# 3. Jenkins 日志是否已监听
# 预期包含 Ready to receive data from Gerrit
然后核对 Job 的 Server、Project、Branch 和 Event。项目名或分支 Pattern 写错,比网络故障更常见。
10.2 Build 成功但 Gerrit 没投票#
检查四项:
Service Users对refs/heads/*是否有Label Verified -1..+1;- 项目是否真正定义 Verified Label;
- Jenkins 日志里的
gerrit reviewstderr; - Review Command 是否仍使用
--verified。
本次故障属于第 4 项。Build 本身没有问题,失败发生在 post-build 写回阶段。
10.3 Gerrit 报 --verified is not a valid option#
把六条命令都改为:
--label Verified=<VERIFIED> --label Code-Review=<CODE_REVIEW>
可先在 Jenkins 容器中查看 Gerrit 当前支持的参数:
ssh -p 29418 ci-cd@review.example.internal \
gerrit review --help
版本兼容应以目标 Gerrit 的 CLI Help 为准,不以插件默认模板为准。
10.4 Jenkins 启动出现 duplicate timer#
我曾用 Groovy Init Script 在 Jenkins 初始化阶段主动调用 Gerrit Server start()。Job 加载完成后,插件监听器又启动一次连接,日志出现:
Can't create two timers for the same Gerrit instance
Already started!
如果通过 UI 配置 Server,不要额外在 Init Script 中启动。必须自动化时,可以把 Server 设为 noConnectionOnStartup=true,再在 Job 加载完成后只调用一次 startConnection()。核心原则是让连接只有一个生命周期所有者。
10.5 Kubernetes 中 SSH Key 被改成 0660#
Kubernetes fsGroup 可能在挂载 PVC 时修改文件组权限。本次环境里,私钥从 0600 变成 0660,SSH 严格权限检查存在失败风险。
解决方式是在 Jenkins 启动前执行:
chmod 0600 /var/jenkins_home/.ssh/gerrit-ci-cd
Docker Compose 示例没有 fsGroup,但仍应把 0600 作为启动检查项,而不是只在首次创建 Key 时设置一次。
10.6 LDAP 某个用户搜不到#
不要先改 Gerrit 为模糊搜索。用完全相同的 Base DN 和 Filter 执行 ldapsearch:
ldapsearch -x -H "$LDAP_SERVER" \
-D "$LDAP_BIND_DN" -W \
-b "$LDAP_ACCOUNT_BASE" \
"(uid=problem-user)" dn uid cn mail objectClass
同时检查:
- equality index;
- 账号是否在正确 Base DN 下;
- ACL 是否允许 Bind 账号搜索;
- 用户对象是否真的有
mail、cn和登录属性; - 不同用户是否使用了不同 objectClass。
这次目录中用户 objectClass 并不完全一致,因此 (objectClass=inetOrgPerson) 会漏用户,(uid=*) 才能覆盖全部账号。
10.7 反向代理后出现 Page Not Found#
Gerrit 对 canonical URL 和代理路径比较敏感。Nginx proxy_pass 是否带末尾 / 会改变路径拼接。根路径代理通常应保持请求 URI,不要无意中删改路径段。
排查时同时对比:
CANONICAL_WEB_URL
httpd.listenUrl
X-Forwarded-Proto
X-Forwarded-Host
proxy_pass URI
10.8 邮件服务可达但没有通知#
检查顺序:
sendemail.enable=true;smtpEncryption与端口是否匹配;- SMTP 授权码是否写进
secure.config; - 发件地址是否被 SMTP 服务允许;
- Gerrit 日志是否有认证或 TLS 错误;
- 用真实 Change 评论触发通知;
- 检查收件箱和垃圾邮件目录。
11. 上线后的运行边界#
11.1 日常检查#
每次变更后至少看:
# Gerrit 版本与健康
curl -fsS http://review.example.internal:8081/config/server/version
# Jenkins 登录页
curl -fsS -o /dev/null -w '%{http_code}\n' \
http://jenkins.example.internal:8082/login
# Gerrit Trigger 连接
# Jenkins log 中应只有一次 Ready to receive data from Gerrit
还要定期核对:
ci-cd是否仍是Service Users成员;- 项目 Verified 权限是否被父项目或本地配置覆盖;
- SSH Key mode 是否为
0600; - Pipeline 是否检出当前 Patch Set;
- 失败路径能否写回
Verified -1; - 备份能否恢复,而不是只看定时任务退出码。
11.2 为什么暂时不自动 Submit#
自动投 Verified +1 和自动 Submit 是两项不同能力。第一阶段保留人工 Submit 有三个好处:
- 容易观察 Label 和 Requirement 是否按预期变化;
- CI 误判时不会直接合并;
- 人工 Reviewer 仍控制最终提交时机。
等失败路径、超时、重试、Patch Set 并发和权限都经过稳定观察,再单独评估自动提交。
11.3 扩展到真实项目之前#
学习 Job 只验证仓库结构。接入真实项目时需要补充:
- 项目自己的构建、单元测试、静态检查和制品校验;
- 临时 Jenkins Agent,而不是 Controller executor;
- 每个 Agent 的 CPU、内存、超时和网络限制;
- 凭据最小权限与构建日志脱敏;
- 多 Patch Set 并发时取消旧 Build;
- Jenkins 停机期间的事件补偿。
Gerrit Trigger 的 Missed Events Playback 需要 Gerrit events-log 插件和 Jenkins REST 配置。没有这两个前提,不能假设 Jenkins 重启后会自动补齐所有漏掉的事件。
12. 这次学习真正解决了什么#
搭建 Gerrit 的命令不多。耗时主要花在边界上:LDAP 搜索与认证不是一回事,SSH 不使用 LDAP 密码,Label 不等于 Submit Requirement,Build 成功不等于投票成功,新 Patch Set 也不应该继承代码变化前的 CI 结论。
最后保留下来的设计很克制:
- Gerrit 单节点运行,数据用 bind mount 持久化;
- LDAP 负责人员登录,SSH Key 负责机器人事件连接;
All-Projects提供基础权限,项目只增加自己的 Verified 规则;- Jenkins 只负责测试和投票,不自动合并;
- 每个新 Patch Set 都重新验证;
recheck只处理同一 Patch Set 的临时失败;- 配置分支排除 Verified,保留修复通道。
这套配置已经覆盖日常代码评审的最小闭环。下一步不是继续堆插件,而是把真实项目测试迁到隔离 Agent,并定期验证备份恢复和事件补偿。
学习资源#
| 优先级 | 项目 | 适合解决的问题 |
|---|---|---|
| S | docker-gerrit | 部署、配置、LDAP 和持久化 |
| S | gerrit-trigger-plugin | Gerrit 事件驱动 Jenkins 与投票 |
| S | gerrit-mcp-server | Agent/MCP 操作 Gerrit |
| A | git-repo | Android 与多仓库工作流 |
| A | cookbook-plugin | Gerrit 插件开发示例 |
| A | replication | 仓库镜像和灾备 |
| A | k8s-gerrit | Kubernetes Operator 部署 |
| B | Gerrit Core | 源码、内部机制和扩展点 |
