把代码提交和构建流水线绑在一起最省心的做法是让 Gerrit 当发令枪、Jenkins 当执行手——开发往 Gerrit 推一个 patchsetJenkins 自动拉取对应版本编译、跑测试再把结果以 Verified 标签回写到那个 change 上通过就 1失败就 -1。这样一来评审的人在页面上就能直接看到这次改动到底能不能编过、测试跑没跑通不用再靠人工问一句你本地编过了吗。整套链路涉及 Gerrit 的 SSH 事件流、refs/changes 引用规则、Jenkins 的凭据管理、Gerrit Trigger 插件的触发与回写配置每一样都有坑尤其是权限和 refspec 这两块配错一个字符就是提交了但没反应。这篇内容就是把这套东西从头到尾捋一遍从环境准备、插件选型、Gerrit 账号与权限配置到 Jenkins 全局配置、流水线写法、Verified 回写、钉钉通知最后把我在实际搭建中踩过的十几个坑整理成速查表。适合正准备给团队搭 CI 的同学也适合已经搭了一半、卡在触发不生效或权限不足上的朋友。下面按实际搭建顺序来你可以照着一步步抄。1. 整体方案设计与链路选型思路1.1 为什么让 Gerrit 主动推事件而不是 Jenkins 定时轮询很多人第一反应是让 Jenkins 用 SCM 轮询poll SCM去扫仓库变化配置简单五分钟就能跑起来。但这套方案在 Gerrit 场景下很快会暴露三个问题一是轮询只能看分支看不到 patchset。Gerrit 的代码改动是挂在refs/changes/下的临时引用不在refs/heads/里轮询根本扫不到二是延迟不可控轮询间隔设短了浪费资源设长了开发提交完得干等三是没法精确对应哪一次改动触发了哪一次构建回写 Verified 标签时容易张冠李戴。所以正经做法是走事件驱动Gerrit 在 patchset 创建、草稿发布、评论添加、ref 更新这些时机通过 SSH 的 event stream 把事件推出来Jenkins 侧监听到之后立刻触发构建并且事件里自带 change number、patchset number、refspec、项目名这些信息构建脚本直接拿来用。这套机制的好处是明确、实时、可追溯代价是需要在 Gerrit 侧开一个长期连接的账号并配好权限。1.2 组件拆解与网络关系梳理整套链路其实只有三个角色Gerrit 服务端、Jenkins 服务端、构建执行节点。Gerrit 需要开放两个入口一个是 SSH默认 29418给 Jenkins 监听事件和回写标签用另一个是 HTTP默认 8080给 Gerrit Trigger 插件调 REST API 补充信息用。Jenkins 这边需要一个能访问 Gerrit 29418 和 8080 的网络路径以及一个专门用于对接的 Gerrit 账号。我一般会画一张简单的表来对齐这些信息避免配置的时候端口写错组件用途默认端口/路径配置位置Gerrit SSH事件流监听、review 回写29418Jenkins 全局 Gerrit Trigger 配置Gerrit HTTPREST API 查询 change 详情8080或反向代理 80/443Gerrit Trigger 的 Gerrit REST API 段Git 仓库拉取源码通常复用 29418 或独立 9418/HTTP任务里的 SCM 配置Gerrit 网页人工查看 Verified 标签8080无需配置用于验证结果这里有个容易被忽略的点Jenkins 里配置 SCM 拉代码时用的 Git URL 和 Gerrit Trigger 用的 SSH 地址是两回事。前者是ssh://cigerrit.example.com:29418/project.git后者在插件里只填主机名和端口。很多人第一次配的时候把带路径的 URL 全塞进插件的 Host 字段结果连接直接失败。1.3 三种触发链路对比与选型建议实际可选的路子有三条我列个表对比一下你自己按团队规模挑方案实现方式优点缺点适用场景Gerrit Trigger 插件Jenkins 装插件长连 Gerrit SSH 监听事件开箱即用自动回写 Verified支持动态触发插件与 Jenkins 版本有兼容要求事件多了连接易断中小团队标准用法Gerrit Webhook 通用 Webhook 插件Gerrit 配置 webhook 回调 Jenkins解耦Jenkins 重启不影响 Gerrit需要额外处理认证与回写逻辑已有 webhook 基础设施的团队自建 SSH 监听脚本自己写脚本接gerrit stream-events再调 Jenkins API完全可控灵活维护成本高回写要自己实现有特殊定制需求的团队我的建议是除非有硬性约束否则直接用 Gerrit Trigger 插件。它的Verified回写、动态触发按文件路径、分支、项目过滤这些能力都是现成的自建脚本省下来的那点灵活性维护成本早就吃回去了。后面章节也全部按插件方案展开。2. Jenkins 环境准备与安装配置要点2.1 系统资源规划与端口预留Jenkins 本身不挑机器但一旦跑起编译任务资源消耗会陡增。我一般给的单机建议是4 核 CPU、8G 内存起步如果构建 Java Web 项目、还要在容器里跑集成测试直接上 8 核 16G磁盘至少留 100G 给工作空间和历史构建。工作空间目录JENKINS_HOME/workspace的磁盘增长非常快尤其是每次构建都要拉全量代码的场景建议单独挂一块盘。端口方面Jenkins 默认 8080如果 Gerrit 也在同一台机器上不推荐但测试环境常见必须把其中一个改掉。改 Jenkins 端口最稳妥的方式不是改启动参数而是改/etc/default/jenkins或 systemd unit 里的HTTP_PORT这样重启服务不会丢配置。注意不要用 root 直接跑 Jenkins 主进程。用专用的 jenkins 用户工作空间目录的属主也交给它否则构建脚本里读文件会出现莫名其妙的权限拒绝。2.2 安装方式选择与初始化避坑安装有四种路子包管理器apt/yum、WAR 包手动部署、Docker 容器、离线安装包。我按场景说内网环境、无法访问外部仓库 → 用 WAR 包或离线安装包把jenkins.war和插件目录一起打包带进去需要快速起测试环境 → Docker注意把JENKINS_HOME挂出来否则容器一删全没了长期稳定运行 → 包管理器安装配 systemd 管理初始化阶段最容易卡的是插件下载慢。首次启动会问你要装哪套插件直接跳过进主页之后去Manage Jenkins → Plugins → Advanced改升级站点地址。国内可以用清华或华为的镜像把https://updates.jenkins.io/update-center.json换成镜像地址速度能从几 KB 提到几 MB。离线安装的话插件是.hpi文件从有网机器上Manage Jenkins → Plugins → Advanced → Download下载好拷到$JENKINS_HOME/plugins/目录重启生效。注意插件之间有依赖关系单独装 Gerrit Trigger 往往不够会提示缺structs、ssh-credentials、credentials这些建议在有网环境把依赖一起下全。提示修改升级站点后记得点Check now刷新一次再回插件列表看可用更新。有时候缓存不刷新你会以为镜像没生效。2.3 必备插件清单与安装顺序对接 Gerrit 的最小插件集合是这几个插件作用是否必需Gerrit Trigger监听事件、触发构建、回写 Verified必需Git plugin拉取 Gerrit 仓库代码必需Credentials Binding在流水线里安全引用凭据必需Pipeline写 Jenkinsfile强烈建议SSH Agent / SSH CredentialsSSH 密钥类凭据支持必需DingTalk或 Generic Webhook构建结果通知可选Workspace Cleanup构建前后清理工作空间可选但推荐安装顺序上先装依赖插件再装 Gerrit Trigger能少踩一次重启。装完之后Manage Jenkins → Global Tool Configuration里把 JDK、Maven、Git 的可执行路径配好或者用自动安装。老版本 Jenkins 的JDK自动安装经常因为下载源问题失败我一般直接填服务器上已经装好的路径稳。3. Gerrit 侧配置账号、密钥与权限3.1 创建 Jenkins 专用账号与 SSH 密钥第一步是给 Jenkins 建一个独立的 Gerrit 账号不要复用任何人的账号。原因是这个账号会有Label Verified的回写权限混用会导致审计困难而且人员离职时容易误删。账号建好之后用这个账号登录 Gerrit 网页进入Settings → SSH Keys把 Jenkins 侧的 SSH 公钥加进去。公钥在 Jenkins 服务器上这样生成ssh-keygen -t rsa -b 4096 -C jenkins-ciexample.com -f /var/lib/jenkins/.ssh/gerrit_ci -N -N 表示不设密码因为 Jenkins 非交互式调用没法输密码。生成完把gerrit_ci.pub的内容粘到 Gerrit。私钥gerrit_ci的内容后面要填进 Jenkins 的凭据里注意是全文包括头尾的-----BEGIN/END-----行。验证连通性在 Jenkins 机器上执行ssh -p 29418 -i /var/lib/jenkins/.ssh/gerrit_ci ci-botgerrit.example.com gerrit version能打印出版本号就说明密钥和账号都对上了。这一步千万别跳后面插件连不上你回头查会花十倍时间。3.2 项目权限配置与 Verified 标签授权权限是 Gerrit 最绕的部分。核心思路是CI 账号需要读代码和给 Verified 打分两种能力其他一律不给。配置在项目的Access页面或者直接编辑project.config。先在All-Projects层面确认有Verified这个 label。如果是新装的 Gerrit默认 label 只有Code-Review需要手动加。编辑All-Projects的project.config[label Verified] function MaxWithBlock value -1 Fails value 0 No score value 1 Verifiedfunction MaxWithBlock的含义是取所有投票里的最大值且 -1 会直接阻塞提交。这个设置对 CI 很关键构建失败给 -1整个 change 就无法合入形成硬门禁。然后给 CI 账号授权。在项目 Access 里添加refs/*的Read权限给 CI 账号读代码refs/heads/*的Label Verified范围设为-1..1给 CI 账号refs/for/refs/*的Push给开发者这个本来就有refs/heads/*的Submit给评审人或有权限的人注意Label Verified的范围如果只给0..1那构建失败时 CI 就没法打 -1门禁就形同虚设。一定要给到-1..1。3.3 事件流机制与触发时机选择Gerrit 的事件是通过gerrit stream-events命令输出的 JSON 流插件在后台维持这个 SSH 连接。理解事件类型配置触发条件时才有底事件类型触发时机典型用途patchset-created新 patchset 上传每次提交自动构建最常用draft-published草稿 change 发布团队用草稿流程时comment-added有人在 change 上评论用评论关键词触发重跑ref-updated分支引用更新合并后构建主干change-mergedchange 被合并触发部署流水线最常见的组合是patchset-createdcomment-added关键词比如recheck。前者保证每次提交都跑一遍后者给开发一个手动重试的入口避免因为环境抖动导致的偶发失败必须重新推一个 patchset。配置comment-added触发时正则要写严一点比如(?i)^recheck$只认整行就一个 recheck 的评论。写松了会导致随便一句评论都触发构建队列瞬间爆炸。4. Jenkins 侧对接 Gerrit 完整实操4.1 Gerrit Trigger 全局配置逐项填写进入Manage Jenkins → Gerrit Trigger → Add New Server这是整个对接的核心。字段含义和填写要点如下字段填写内容说明Namegerrit-main自定义任务里会引用这个名字Gerrit Hostgerrit.example.com只填主机名不带协议和端口Gerrit SSH Server Port29418默认值改过就填实际值Gerrit SSH Server Usernameci-bot3.1 建的账号SSH Private Key File上传的凭据 ID选 SSH Username with private key 类型Gerrit REST API URLhttp://gerrit.example.com:8080用于查询 change 详情Gerrit HTTP Usernameci-botREST API 认证Gerrit HTTP PasswordHTTP 密码在 Gerrit Settings → HTTP Password 生成提示Gerrit 的 HTTP 密码不是登录密码要去 Gerrit 网页的Settings → HTTP Password → Generate new password生成。很多人填了登录密码结果 REST API 一直 401。配完之后点Test Connection两个按钮分别测 SSH 和 REST。SSH 通了、REST 通了全局配置才算完。如果 SSH 报认证失败八成是私钥格式问题把私钥重新导出一次确保没有多余空格。4.2 凭据配置的三种类型与踩坑Jenkins 里的凭据分三种对接 Gerrit 会用到其中两种SSH Username with private key给 Gerrit Trigger 和 Git 拉代码用。Username 填ci-botPrivate Key 选Enter directly粘贴私钥全文或者选From a file on Jenkins master指定路径。Username with password给 REST API 用用户名ci-bot密码是 HTTP 密码。Secret text给钉钉 webhook token 之类的用。踩坑最多的是 SSH 私钥格式。如果你是从 Windows 用 PuTTY 生成的.ppk文件Jenkins 不认必须转成 OpenSSH 格式再上传。转换用puttygen key.ppk -O private-openssh -o id_rsa_openssh另一个坑是 Git 拉代码时用的凭据和 Gerrit Trigger 用的可以不同。我习惯统一用同一个 SSH 密钥减少管理成本但如果团队规定 CI 账号和拉代码账号分离那就各配各的。4.3 任务触发配置与动态过滤新建任务类型选 Pipeline 或 Freestyle。以 Pipeline 为例在Build Triggers里勾选Gerrit event然后配置触发器Trigger On勾Patchset Created、勾Comment Added并填正则Gerrit Project填Plain模式值写项目的project.config里的 name比如platform/web-portalBranches可以填Plain精确匹配或者Path模式用正则比如refs/heads/(master|release/.*)File Paths可选按改动文件过滤比如只改动src/main/**才触发这里有个非常实用的功能叫Dynamic Trigger开启后可以在同级目录里维护一个配置文件把项目、分支、触发条件的映射写在文件里不用改任务配置。项目多了之后比如几十个仓库共用一个 CI 任务这个配置方式能省下大量重复劳动。注意Gerrit Project字段必须和 Gerrit 里的项目全名完全一致大小写敏感。写错了不会报错就是永远不触发排查起来很费劲。4.4 Verified 回写配置与失败门禁回写是插件自动完成的但要配几个地方。在 Gerrit Trigger 全局配置的Advanced里Gerrit Reporting Values配置成功时Verified 1、失败时Verified -1的默认消息Gerrit Verified Commands插件内部通过gerrit review命令回写可以自定义命令模板如果构建过程中途被中断比如超时、节点掉线插件会走unstable或notbuilt分支对应打分可以单独设。我一般把超时也设成-1因为超时本身就是问题不该放过。流水线里也可以手动调用回写用于更精细的控制stage(Report) { steps { script { if (currentBuild.currentResult SUCCESS) { gerritReview labels: [Verified: 1], message: 构建通过 } else { gerritReview labels: [Verified: -1], message: 构建失败请查看日志 } } } }5. 流水线与构建脚本实战5.1 Jenkinsfile 整体结构设计一个标准的 Gerrit CI 流水线我一般拆成五段准备、编译、测试、归档、回写。下面是一个可直接复用的骨架语言标注为 groovypipeline { agent any options { timeout(time: 30, unit: MINUTES) buildDiscarder(logRotator(numToKeepStr: 30)) skipDefaultCheckout(true) } stages { stage(Checkout) { steps { checkout([$class: GitSCM, branches: [[name: env.GERRIT_REFSPEC ?: refs/heads/master]], userRemoteConfigs: [[ url: ssh://ci-botgerrit.example.com:29418/platform/web-portal.git, credentialsId: gerrit-ci-ssh, refspec: refs/changes/*:refs/changes/* ]] ]) } } stage(Build) { steps { sh mvn -B -DskipTests clean package } } stage(Test) { steps { sh mvn -B test } } stage(Archive) { steps { archiveArtifacts artifacts: target/*.jar, fingerprint: true junit target/surefire-reports/*.xml } } } post { success { gerritReview labels: [Verified: 1] } failure { gerritReview labels: [Verified: -1], message: 构建失败 } } }skipDefaultCheckout(true)很重要它会跳过 Jenkins 自动生成的 checkout 阶段改由我们自己控制拉取逻辑这样才能精确拉到指定的 patchset。5.2 refspec 机制怎么拉到指定的那一次提交Gerrit 的 change 不在分支上而是挂在refs/changes/XX/YYYY/Z这个引用下。三个数字的含义是XXchange number 的后两位不足两位前面补零。比如 change number 是 7就写07YYYY完整的 change numberZpatchset 序号从 1 开始举例change number 为1234第 3 个 patchset引用就是refs/changes/34/1234/3。Jenkins 触发时环境变量GERRIT_REFSPEC就是这个完整引用GERRIT_BRANCH是目标分支如masterGERRIT_CHANGE_NUMBER是 change 号。所以流水线里直接用env.GERRIT_REFSPEC作为拉取目标就能精确取到触发的那一次改动。要在 URL 里配refspec: refs/changes/*:refs/changes/*否则 Git 不会把远端这些临时引用同步下来checkout 就会失败报Couldnt find remote ref。提示GERRIT_REFSPEC变量只在由 Gerrit 触发时才有值。手动点立即构建时它是空的所以骨架里写了?: refs/heads/master兜底否则手动构建会因为空 ref 直接报错。5.3 构建产物归档与测试报告聚合归档不只是留个文件它和评审体验直接相关。把target/*.jar用archiveArtifacts归档后评审人可以直接从构建页面下载产物做验证不用自己编译。fingerprint: true会记录文件指纹将来追溯这个包是哪次提交产出的时非常有用。单元测试报告用junit步骤聚合Jenkins 会把target/surefire-reports/*.xml解析成趋势图挂在任务首页。构建不稳定有测试失败但不阻塞时currentBuild.currentResult会是UNSTABLE这时候回写策略要单独想清楚我一般把 UNSTABLE 也当成 -1因为不准的测试比没有测试更危险。日志量大是常态尤其是 Java 项目跑集成测试。建议在流水线里加日志截断只保留关键行或者在logRotator里限制保留份数不然磁盘一个月就满了。5.4 构建结果通知到钉钉通知不是必需品但团队协作少不了。钉钉自定义机器人的做法是群里添加机器人拿到 webhook 地址和一个 access_token然后用 curl 发消息。流水线里这样写post { always { script { def status currentBuild.currentResult def msg 【${env.JOB_NAME}】${status}\nchange: ${env.GERRIT_CHANGE_NUMBER}\n地址: ${env.BUILD_URL} sh curl -s -H Content-Type: application/json \ -d {msgtype:text,text:{content:${msg}}} \ https://oapi.dingtalk.com/robot/send?access_token你的token } } }安全上加两层一是机器人设置里开启自定义关键词消息里必须包含该关键词才会发出比如让所有消息都带项目名二是用加签方式把 timestamp 和 sign 拼进 URL这样 token 泄露了别人也没法伪造。token 本身放 Jenkins 凭据里别硬编码在 Jenkinsfile 中。s和sh的引号嵌套容易出错上面的写法用双引号包 shell、双引号包 JSON 是不对的实际要改成单引号包 shell 脚本或者用 Groovy 的writeFile先生成 JSON 文件再用curl -d file。这个细节不注意消息发出去就是格式错误。5.5 多仓库共用流水线的组织方式项目一多每个仓库建一个 Jenkins 任务会累死运维。两种解法一种是多分支流水线 共享库。把公共逻辑拉取、编译、回写、通知抽到 Shared Library各仓库的 Jenkinsfile 只写差异部分比如用哪个 Maven profile、跑哪些测试。Jenkins 配置里指定共享库的 Git 地址和版本任务里Library(ci-lib) _引入。另一种是单任务 动态触发。一个 Jenkins 任务绑定 Gerrit 的多个项目用 Dynamic Trigger 的配置文件控制每个项目的触发条件然后在流水线里根据GERRIT_PROJECT变量走不同的构建分支。这种方式任务数量最少但 Jenkinsfile 里的分支逻辑会变复杂。我的经验是仓库数量在 10 个以内用第一种清晰超过 20 个且构建流程高度相似用第二种好维护。两种也可以混用核心仓库单独建任务边缘仓库走通用任务。6. 常见问题与排查技巧实录6.1 触发不生效的排查路径提交了代码Jenkins 没反应是对接阶段最高频的问题。按这个顺序查看 Gerrit Trigger 全局配置的Test Connection是不是都通过SSH 和 REST 有一个不通就不行看 Jenkins 任务的Gerrit event触发条件项目名和分支是否和实际提交完全一致在 Gerrit 网页看事件是否产生People → Events里能看到最近的事件记录看 Jenkins 系统日志Manage Jenkins → System Log加一个com.sonyericsson.hudson.plugins.gerrit的日志记录器级别调到 FINE能看到插件收到的事件详情看 CI 账号在 Gerrit 里的Read权限有没有覆盖到目标项目的目标分支第 5 条最隐蔽。很多团队给 CI 账号只配了refs/heads/master的读权限结果 release 分支的提交完全不触发还以为是插件坏了。现象高频原因处理方式完全无触发CI 账号读权限不足补refs/*的 Read 权限部分分支不触发分支匹配模式写错检查 Branches 字段模式触发但拉不到代码refspec 未配置补refs/changes/*:refs/changes/*事件时有时无SSH 连接被防火墙断开调大 keepalive 或检查网络设备会话超时6.2 权限与 SSH 报错的典型表现Permission denied (publickey)是最常见的一类原因无非三种密钥没加到 Gerrit、私钥格式不对、账号用户名写错。逐条排除即可。还有一种报错是missing Change-Id in commit message footer这个是开发提交时没装 commit-msg hook。让开发执行curl -Lo .git/hooks/commit-msg http://gerrit.example.com:8080/tools/hooks/commit-msg chmod x .git/hooks/commit-msg回写失败报not permitted或label Verified is restricted说明 CI 账号没有Label Verified的-1..1权限回到 3.2 检查授权范围。6.3 构建阶段的报错处理容器化构建时经常遇到镜像拉取失败报错长这样docker: error response from daemon: get https://registry-1.docker.io/v2/: net/http: request canceled这是拉取公共镜像慢或超时。配置镜像加速即可编辑/etc/docker/daemon.json{ registry-mirrors: [https://你的加速地址], max-concurrent-downloads: 5 }改完systemctl daemon-reload systemctl restart docker。另外max-concurrent-downloads调小反而更稳因为并发太高容易触发限流。工作空间残留导致的构建失败也很典型比如上次构建留下的target目录里有个被锁的文件。在流水线开头加清理stage(Clean) { steps { cleanWs() } }cleanWs()比deleteDir()更彻底它会把工作空间和相关的元数据一起清掉。6.4 离线环境与升级站点问题内网环境装插件走离线包。流程是外网机器装一个同版本 Jenkins把需要的插件在插件管理里下载.hpi连同依赖一起拷到内网机的$JENKINS_HOME/plugins/重启。常见坑是版本不匹配插件要求的 Jenkins 核心版本高于当前版本装上直接启动失败。解决办法是先在插件页面看清楚要求的核心版本必要时升级 Jenkins 主程序。升级站点慢的问题除了换镜像还可以直接用离线 update-center 文件把update-center.json下载到本地改配http://内网地址/update-center.json插件列表从本地读速度就上去了。注意升级 Jenkins 主版本前务必备份$JENKINS_HOME整个目录至少备份config.xml、jobs/、plugins/、credentials.xml这几项。主版本跨度过大时插件兼容问题会成片出现回滚是常态。7. 实际搭建中的经验与优化建议7.1 构建速度与并发控制流水线跑得慢八成慢在依赖下载和全量编译。几个实操手段Maven 本地仓库做成持久化卷不要每次构建都重新下用mvn -o走离线模式前提是依赖已经缓存过把测试拆成单元测试和集成测试两段单元测试必跑集成测试只在特定分支或加标签的 change 上跑。并发方面同一个 change 的多次 patchset 会排队。我的做法是开启 Jenkins 的同一任务并发执行但给每个 change 加锁用lock步骤按 change number 加锁保证同一个 change 不会有两个构建同时跑不同 change 之间互不影响。7.2 稳定运行与长期维护要点Jenkins 和 Gerrit 的 SSH 长连接容易因为网络设备的会话超时被断开表现是用了一段时间后突然不触发了。解决办法有两个一是在 Jenkins 全局配置里调大连接重试和 keepalive 参数二是加一个定时任务每隔几分钟检查一次 Gerrit Trigger 的状态页发现断连就发告警。另外构建历史会持续膨胀buildDiscarder(logRotator(numToKeepStr: 30))这类策略要在每个任务上都配上别指望全局默认值。归档产物也设个大小上限大文件归档几次就能把磁盘吃满。日志方面建议把 Gerrit Trigger 的日志级别平时设成WARNING出问题临时调到FINE查完调回去。常年开着FINE会让日志文件以肉眼可见的速度增长。我自己维护这套流程两年多最大的体会是配置本身不难难的是把每个环节的隐性前提明确下来——CI 账号的权限范围、refspec 的同步规则、事件类型和触发时机的对应关系。把这些写进团队文档新人接手时就不会在为什么不触发上反复折腾。最后再分享一个小习惯每次改动触发配置或权限配置后拿一个测试项目做一次端到端验证从提交到看到 Verified 标签走完一遍比事后排查便宜太多。