跳过正文
  1. 博客文章/

从 Gerrit 3.14.2 到 Jenkins Verified:代码评审门禁实战

··2910 字·14 分钟·
DevOps CI/CD Gerrit Jenkins Gerrit Trigger Code Review LDAP
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录
这篇文章记录我搭建 Gerrit 3.14.2 评审环境的全过程。目标不是让服务“能打开”,而是验证一条真正可用的评审链路:开发者把提交推到 refs/for/main,Gerrit 创建 Change,Jenkins 收到 patchset-created,检出准确的 Patch Set,执行检查,再由 ci-cd 服务账号回写 Verified +1-1。只有 Code-Review 和 Verified 同时满足,Change 才能提交。
文中的域名、IP、DN、邮箱和账号均使用示例值。随文部署包不包含密码、Token、SSH 私钥或真实 Gerrit Group UUID。本文只写 Jenkins 全新安装和 Gerrit Trigger 配置,不记录 Jenkins 迁移过程。

先看最终结果
#

这次实验最后得到以下状态:

项目验证结果
Gerrit3.14.2,容器健康,Git 和配置均持久化
登录LDAP 用户首次登录时自动创建 Gerrit 账号
Git 访问HTTP 可使用 LDAP 密码;SSH 使用用户公钥
邮件SMTP 真实发送成功,不只停留在端口连通
人工评审普通用户只能 Code-Review -1/+1,管理员可以 +2 和 Submit
CI 身份ci-cdService Users 成员,可读取事件并投 Verified -1/+1
JenkinsGerrit 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 日验证过的组合:

组件版本说明
Gerrit3.14.2官方 gerritcodereview/gerrit 镜像
Jenkins2.555.1-jdk21官方 Jenkins LTS 镜像
Gerrit Trigger3.1983.v57096fe9923c要求 Jenkins Core 2.479.3 或更高
Java21Jenkins 镜像内置 Temurin 21
Docker24+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 附带一套脱敏后的可执行示例:

下载后先校验完整性:

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-Idcommit 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 根据三项内容匹配已有评审:

  1. Change-Id;
  2. Repository;
  3. 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-createdcomment-addedchange-merged 等事件,并在构建完成后写回 Label。对一个学习环境和少量项目,它是更短的路径。

2. 用 Docker Compose 部署 Gerrit 3.14.2
#

2.1 主机和端口
#

这次环境是一台 Debian 12 主机。原计划使用 8080,但该端口已被占用,因此改为:

用途容器端口主机端口
Gerrit Web80808081
Gerrit SSH2941829418

主机上同时运行其他服务,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 镜像在这次环境中出现了两条与主流程无关、但会污染日志的错误:

  1. plugin-manager-preloader 访问外部 GerritForge Jenkins 服务,响应解析失败;
  2. delete-project 插件把默认路径值误当成时间值解析。

我在 gerrit.config 中加入:

[plugin "plugin-manager"]
  preload = false
[plugin "delete-project"]
  deleteTrashFoldersMaxAllowedTime = 10 minutes

应用后重启,日志中的对应 ERRORExceptionFailed 消失。随文 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 时曾遇到一个很隐蔽的问题:用户条目中明明存在 uidldapcompare 也返回 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 0600secure.config。它不会把密码输出到日志。

3.4 首个登录用户是初始管理员
#

空 Gerrit 初始化后,第一个成功登录的用户会成为初始管理员。LDAP 切换前要确定谁完成第一次登录,否则可能把管理权交给错误账号。

后续 LDAP 用户无需手工创建 Gerrit 账号。只要目录中存在必需属性并能成功认证,第一次登录会自动创建 Registered User。

3.5 SSH 不接受 LDAP 密码
#

这是使用中最常见的误解之一:

协议可用认证方式
Web UILDAP 用户名和密码
Git over HTTPLDAP 密码或 HTTP Token,取决于 gitBasicAuthPolicy
Gerrit SSHSSH 公钥,或单独配置的 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.configLDAP、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 升级原则
#

升级前至少完成:

  1. 阅读目标版本 release notes;
  2. 冷备份并验证恢复;
  3. 在副本上跑 init 和 reindex;
  4. 检查插件与目标 Gerrit 版本兼容性;
  5. 记录当前镜像 digest 和回退边界;
  6. 预留停机窗口,不把 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-ReviewSubmit
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.configgroups 后,不直接 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-ReviewSATISFIED
VerifiedUNSATISFIED
Submit不可用

Code-Review 已满足而 Verified 尚无投票时,Gerrit 仍阻止提交

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 权限也要单独配置。

流程为:

  1. 让 LDAP 中的 ci-cd 首次登录 Gerrit,完成 JIT 账号创建;
  2. 生成专用 Ed25519 Key;
  3. ci-cd 添加公钥;
  4. 把账号加入 Service Users
  5. 确认 Service Users 具有 Stream Events Global Capability;
  6. 确认项目允许该组读取 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 添加:

字段示例值
Namegerrit-stu
Hostreview.example.internal
SSH Port29418
Userci-cd
SSH Key/var/jenkins_home/.ssh/gerrit-ci-cd
Frontend URLhttp://review.example.internal:8081/
Notification LevelNONE

保存前执行 Test Connection。连接建立后,日志应出现:

Ready to receive data from Gerrit: gerrit-stu

Jenkins Gerrit Trigger Server 使用 SSH 连接 Gerrit 且 Test Connection 成功

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

配置
Servergerrit-stu
ProjectPlain: gerrit-demo
BranchPlain: main
Event 1Patchset Created
Event 2Comment Added Contains: (?i)^recheck\s*$
SuccessVerified +1
FailedVerified -1
UnstableVerified -1
Code-Review全部为 0

Jenkins Job 同时监听 Patchset Created 和 recheck 评论,并限定 gerrit-demo main

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_HOSTGERRIT_PORT 来自触发事件的 Gerrit Provider。五个参数先统一去掉首尾空白,端口还必须是 165535 的整数;同一个模板才能在跟随 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

Jenkins Build 4 成功检出 Gerrit Patch Set 并完成仓库检查

Build #4 的两个固定阶段均成功,触发来源已脱敏为 review.example.internal

修正 Review Command 后,ci-cd 写入:

Verified +1
Tag: autogenerated:jenkins-gerrit-trigger

Gerrit 随后显示:

Requirement状态
Code-ReviewSATISFIED
VerifiedSATISFIED
No-Unresolved-CommentsNOT_APPLICABLE
ChangeNEW,可提交

Jenkins 写回 Verified 加一后 Gerrit 的两个提交要求均已满足

Patch Set 2:ci-cd 写回 Verified +1 后,Change 进入可人工 Submit 状态。

Change 仍是 NEW,因为我没有配置自动 Submit。门禁负责给事实,合并动作仍由人发起。

8.3 闭环成立的证据
#

一条可靠的闭环至少要同时具备:

  1. Jenkins Build Cause 显示 Gerrit 事件;
  2. Build 使用正确的 GERRIT_REFSPEC
  3. checkout SHA 等于 GERRIT_PATCHSET_REVISION
  4. Jenkins 结果是预期状态;
  5. Gerrit 上的投票人是 ci-cd
  6. 投票落在当前 Patch Set;
  7. Submit Requirement 从 UNSATISFIED 变为 SATISFIED;
  8. 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 必须重新投票。这正是质量门禁应该有的行为。

同一 Gerrit Change 的两个 Patch Set 及 Jenkins Verified 写回历史

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 没投票
#

检查四项:

  1. Service Usersrefs/heads/* 是否有 Label Verified -1..+1
  2. 项目是否真正定义 Verified Label;
  3. Jenkins 日志里的 gerrit review stderr;
  4. 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 账号搜索;
  • 用户对象是否真的有 mailcn 和登录属性;
  • 不同用户是否使用了不同 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 邮件服务可达但没有通知
#

检查顺序:

  1. sendemail.enable=true
  2. smtpEncryption 与端口是否匹配;
  3. SMTP 授权码是否写进 secure.config
  4. 发件地址是否被 SMTP 服务允许;
  5. Gerrit 日志是否有认证或 TLS 错误;
  6. 用真实 Change 评论触发通知;
  7. 检查收件箱和垃圾邮件目录。

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 有三个好处:

  1. 容易观察 Label 和 Requirement 是否按预期变化;
  2. CI 误判时不会直接合并;
  3. 人工 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,并定期验证备份恢复和事件补偿。

学习资源
#

优先级项目适合解决的问题
Sdocker-gerrit部署、配置、LDAP 和持久化
Sgerrit-trigger-pluginGerrit 事件驱动 Jenkins 与投票
Sgerrit-mcp-serverAgent/MCP 操作 Gerrit
Agit-repoAndroid 与多仓库工作流
Acookbook-pluginGerrit 插件开发示例
Areplication仓库镜像和灾备
Ak8s-gerritKubernetes Operator 部署
BGerrit Core源码、内部机制和扩展点

官方参考
#

相关文章

Jira Webhook Integration Jenkins
·23 字·1 分钟
CI/CD Jira 自动化 DevOps Jenkins Webhook
Jenkins 动态参数流水线:模块版本选择、GitLab 触发与结果回填
·785 字·4 分钟
CI/CD 动态参数 DevOps Jenkins Groovy +2
Outline 自用部署:Keycloak、MinIO 和 OpenResty
·1020 字·5 分钟
MinIO LDAP DevOps Outline Keycloak +4