说明:声明式流水线写得顺手之后,总会碰到几个绕不过去的问题——跑得太慢、要在机器之间传文件、并发构建互相打架、上线前要人等拍板、命令偶发失败。这篇把六个高频技巧逐个拆开,讲清楚每个参数是干什么的、什么时候用、坑在哪。
总览:六大技巧定位
| 技巧 | 解决的核心问题 | 一句话记忆 |
|---|---|---|
parallel | 执行慢——串行太耗时 | 「同时跑,省时间」 |
stash / unstash | 跨节点/跨阶段传文件 | 「打包暂存,换机器取」 |
lock / milestone | 并发冲突、旧构建抢跑 | 「上锁防撞,里程碑淘汰旧的」 |
input | 需要人拍板 | 「停下来等人批准」 |
retry / timeout / waitUntil | 不稳定、卡死、依赖未就绪 | 「重试、限时、等到就绪」 |
script | 声明式表达力不够 | 「钻进 Groovy 随便写」 |
这六个不是互相独立的。真实的生产流水线通常就是「parallel 跑测试 + lock 占住环境 + input 等人审批 + timeout 兜底超时」,最后用 script 处理声明式写不出的动态逻辑。结尾有一份组合实战。
一、parallel 并行
把原本串行执行的多个 stage 同时跑,显著缩短流水线总时长。常用于互不依赖的测试套件、多平台构建。
1.1 参数
声明式里 parallel 是一个阶段容器,本身没有「参数」参数,但它内部每个分支都是标准 stage,并且有一个关键开关:
| 元素 | 必填 | 作用 |
|---|---|---|
stage('分支名'){...} | 必填 | parallel 内的每个并行分支,必须是一个独立 stage;分支名会显示在 Blue Ocean / 阶段图上 |
failFast: true | 可选 | 写在 parallel { } 内的第一行。true = 任一分支失败,立即中止其余分支(适合「快速失败」);false(默认)= 某分支失败,其余分支仍跑完(适合「攒齐所有测试结果」) |
agent {...}(各分支内) | 可选 | 每个并行分支可指定不同 agent(如一个跑 Linux、一个跑 Windows),不写则继承外层 agent |
脚本式(Scripted)用 parallel 步骤,形态不同:
parallel(
failFast: true, // 脚本式把开关放第一个键值
'单元测试': { node { sh 'make unit' } },
'端到端': { node { sh 'make e2e' } }
)
| 元素 | 作用 |
|---|---|
failFast | 同上,放参数首位 |
键: 闭包 | 每个键是分支名,值是要执行的 Groovy 闭包(可含 node{}、steps 等) |
1.2 场景与示例
场景 A(快速失败):提交前的多重校验,任一不过就别浪费时间跑其他的。
场景 B(攒齐结果):跑多组测试,希望一次看到哪组挂了,而不是第一组挂了就停。
stage('质量门禁') {
parallel(
failFast: true, // A:任一失败立刻停
'单元测试': { stage('Unit'){ steps{ sh 'make unit' } } },
'代码扫描': { stage('Scan'){ steps{ sh 'make sonar' } } },
'构建产物': { stage('Build'){ steps{ sh 'make build' } } }
)
}
stage('全量测试') {
parallel { // B:默认 failFast:false,跑完所有
stage('Web E2E'){ steps{ sh 'make e2e-web' } }
stage('API E2E'){ steps{ sh 'make e2e-api' } }
}
}
注意坑:parallel 内的 stage 不能再嵌套
parallel(声明式限制);且 parallel 默认不共享工作区,需要共享文件请用下面的stash。另外并行分支越多,占用的 executor 越多,注意节点并发上限。
二、stash / unstash 跨节点传文件
Jenkins 的 workspace 是按节点隔离的:在节点 A 构建出的产物,节点 B 默认看不到。stash 把文件打包暂存到 Controller 端(内存/磁盘临时区),unstash 在另一处恢复。它是「临时传递」,区别于持久化的 archiveArtifacts。
2.1 stash 参数
| 参数 | 必填 | 类型/默认 | 作用 |
|---|---|---|---|
name | 必填 | String | 暂存包的唯一标识,unstash 靠它取回。建议语义化,如 'dist' |
includes | 二选一 | String(Ant 风格) | 要收纳的文件模式,如 'dist/**, target/*.jar'。与 excludes 至少给一个 |
excludes | 二选一 | String(Ant 风格) | 排除模式,如 '**/*.log' |
allowEmpty | 可选 | bool,默认 false | true = 没有匹配到任何文件也不报错;false = 匹配为空直接失败(防止漏打包还以为成功了) |
useDefaultExcludes | 可选 | bool,默认 true | 是否自动排除 SCM 元数据(如 .git)。一般保持默认即可 |
2.2 unstash 参数
| 参数 | 必填 | 作用 |
|---|---|---|
name | 必填 | 对应 stash 时的 name |
includes / excludes | 可选 | 恢复时进一步缩小范围(只拿出需要的子集) |
2.3 场景与示例
场景:构建机(Maven 节点)编译出 jar,部署机(deploy 节点,无 JDK)需要这个 jar。或者并行分支 A 产出、分支 B 之后要消费。
stage('Build') {
agent { label 'maven' }
steps {
sh 'mvn package'
stash name: 'app-jar', includes: 'target/*.jar', allowEmpty: false
}
}
stage('Deploy') {
agent { label 'deploy' } // 另一个节点
steps {
unstash 'app-jar' // 把 jar 恢复到本节点 workspace
sh 'scp target/*.jar server:/opt/app/'
}
}
stash vs archiveArtifacts:stash 是「流水线内部短时传递」,不长久保留、不显示在构建页;
archiveArtifacts会把产物保存下来供下载和历史追溯。要留存交付物用后者,要跨节点搬运用前者。
三、lock / milestone 并发控制
当多个构建可能同时操作同一资源(同一个生产环境、同一个共享许可证)时,需要「互斥」;当新构建已经排队、旧的还在等人审批时,需要「让新的赢」。两者配合是生产发布的标准姿势。
3.1 lock 参数
| 参数 | 必填 | 默认 | 作用 |
|---|---|---|---|
resource | 必填 | — | 锁的名字(字符串)。同名锁互斥,同一时刻只有一个构建能持有 |
quantity | 可选 | 1 | 该资源允许同时持有的数量。>1 用于「资源池」场景,如 5 个许可证席位 → quantity: 5,第 6 个才排队 |
variable | 可选 | — | 把锁的持有信息(如被授予的编号)绑定到一个环境变量名,供脚本读取 |
label | 可选 | — | 给人看的队列标签,出现在「锁队列」展示里,便于排查谁在等 |
用法:可放在 options { } 里(整段加锁),也可放在 steps 里用 lock(resource:'x'){...} 局部加锁。
3.2 milestone 参数
| 参数 | 必填 | 作用 |
|---|---|---|
ordinal | 必填 | 整数序号。含义:序号更大的 milestone 一旦通过,所有还卡在更早 milestone 的旧构建会被取消。通常用递增数字(1,2,3…) |
label | 可选 | 显示标签 |
3.3 场景与示例
场景 A(lock):两条流水线都可能部署到 prod-cluster,绝不能同时进行,否则互相覆盖。
场景 B(milestone):构建 #10 还在 input 等人审批,构建 #11 已经跑完并准备部署。应该让 #11 赢,取消 #10 的审批——否则 #10 被批准后会拿旧代码上线。
stage('Deploy Prod') {
options {
lock(resource: 'prod-cluster', quantity: 1, label: '生产集群')
milestone(1) // 走到这就标记"我是最新的"
}
steps {
input message: '确认发布生产?', submitter: 'ops-lead'
milestone(2) // 审批通过后再标记一次
sh 'deploy.sh'
}
}
经验:把
milestone放在input前后各一个。前一个起「新的抢跑」作用,后一个确保「批准通过的才算数」。配合lock杜绝并发部署。
四、input 人工审批
流水线运行到某处主动暂停,弹出表单等人操作,通过后才继续。是「人在回路」的关键,几乎所有生产发布都需要。
4.1 参数
| 参数 | 必填 | 默认 | 作用 |
|---|---|---|---|
message | 必填 | — | 提示文案,显示在审批弹窗和日志里,说明「要人决定什么」 |
id | 可选 | 随机 | 唯一标识。用于「可恢复构建」(restart from stage)时精确对齐。复用 Jenkinsfile 建议显式指定 |
ok | 可选 | 'Proceed' | 确认按钮上的文字,如中文 '批准' |
submitter | 可选 | 所有人 | 逗号分隔的允许审批的用户/组,如 'alice,bob,ops-team'。不在列表里的人看不到/点不了 |
submitterParameter | 可选 | — | 把「实际是谁点的批准」写入这个环境变量,用于审计留痕 |
parameters | 可选 | 无 | 额外的表单字段,让用户填写(如发布版本、回滚原因)。语法同流水线 parameters{} |
注意坑:
input本身没有 timeout 参数。要限时必须用timeout包裹:timeout(time:30,unit:'MINUTES'){ input ... },超时则按 timeout 的默认行为(中断)。否则审批窗口会一直开着,executor 也被占着。
4.2 两种写法
写法一:作为 stage 指令(声明式推荐,结构清晰)
stage('发布审批') {
input {
message '确认发布到生产?'
id 'prod-approve'
ok '批准'
submitter 'alice,bob'
submitterParameter 'APPROVER'
parameters {
string(name: 'REASON', defaultValue: '', description: '发布原因')
}
}
steps { sh 'deploy.sh' }
}
写法二:作为 step(放在 steps 内,可嵌逻辑)
steps {
def decision = input(
message: '是否继续?',
ok: '继续',
parameters: [booleanParam(name: 'FORCE', defaultValue: false)]
)
if (decision.FORCE) { sh 'force-deploy.sh' }
}
4.3 场景与示例
场景:生产发布前强制二级审批,且只让运维负责人能批,并记录谁批的、为什么批,满足合规审计。
stage('生产发布门禁') {
input {
message: '即将发布到 PROD,确认?'
ok: '确认发布'
submitter: 'ops-lead,sre'
submitterParameter: 'APPROVED_BY'
parameters: [ string(name: 'TICKET', description: '变更单号') ]
}
steps {
echo "由 ${APPROVED_BY} 批准,变更单 ${TICKET}"
sh 'deploy-prod.sh'
}
}
五、retry / timeout / waitUntil 容错三件套
流水线跑在真实环境里,会遇到:偶发失败(flaky)、卡死、依赖服务还没起来。这三个步骤分别兜底。
5.1 retry 参数
| 参数 | 必填 | 默认 | 作用 |
|---|---|---|---|
count | 必填 | — | 最大重试次数(不含首次)。retry(3) = 最多跑 4 次(1 次原始 + 3 次重试) |
conditions | 可选 | 无 | 重试触发条件列表,如 [nonresumable, unstable]。默认任何失败都重试;可限定只在特定状态重试 |
5.2 timeout 参数
| 参数 | 必填 | 默认 | 作用 |
|---|---|---|---|
time | 必填 | — | 超时阈值数值 |
unit | 可选 | MINUTES | 单位:NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS |
activity | 可选 | false | true = 只要构建有任意活动(输出日志)就重置计时,适合「长任务但一直在动」;false = 从包裹开始就固定倒计时 |
options { timeout(...) }用在阶段级可设整体上限;timeout(time:..){ steps... }用在 steps 内可只包裹某个易卡死的命令。
5.3 waitUntil 参数
| 参数 | 必填 | 默认 | 作用 |
|---|---|---|---|
timeout | 可选 | 无(一直等) | 最长等待时间,超时则失败。建议必填,否则可能永久阻塞 |
initialRecurrencePeriod | 可选 | 内部默认 | 首次检查前的初始等待(毫秒级),用于避免「立刻轮询」的惊群 |
waitUntil 内放一个返回布尔值的闭包,Jenkins 周期性执行,直到返回 true 或超时。
5.4 场景与示例
场景:① 集成测试偶发超时但重跑就过 → retry;② 某命令可能死循环 → timeout 兜底;③ 刚启动的服务端口还没监听 → waitUntil 轮询就绪。
steps {
// 1) 重试偶发失败的集成测试
retry(3) { sh 'make integration-test' }
// 2) 给可能卡死的步骤上锁死时间
timeout(time: 10, unit: 'MINUTES') {
sh './long-migration.sh'
}
// 3) 轮询等待服务就绪(最多等 5 分钟)
timeout(time: 5, unit: 'MINUTES') {
waitUntil {
script { return sh(returnStatus: true, script: 'curl -sf http://svc/health') == 0 }
}
}
}
注意坑:
retry会重跑整个闭包。如果闭包里有「副作用」(如已发了通知、已建了资源),重试会重复执行——务必保证被重试的逻辑是幂等的。
六、script 逃生舱
声明式流水线语法简洁但有边界:不能用任意循环、动态生成 stage、复杂条件分支。script 块让你在声明式内部「切回 Groovy 全部能力」。
用法与变量作用域
stage('动态逻辑') {
steps {
script {
def targets = ['web','api','worker'] // 块内可见
def results = [:]
targets.each { svc ->
results[svc] = buildJob(svc) // 调共享库/任意方法
}
// 可写复杂 if/循环/异常捕获
if (results.values().any { it == 'FAIL' }) {
error('存在失败服务,中止')
}
}
}
}
| 要点 | 说明 |
|---|---|
| 无参数 | script 只接受一个 Groovy 闭包,没有独立参数 |
| 返回值 | 闭包的最后一个表达式值就是 script{} 的返回值,可赋给外层变量 |
| 作用域 | def 变量仅在该 script 块内有效;要跨 stage 共享,赋值给 env. 或声明在 pipeline 顶层 |
6.1 场景与示例
场景 A:根据 Git 变更的文件动态决定要构建哪些模块(声明式 when 写不出这种循环)。
场景 B:根据参数 + 外部 API 返回,动态拼出要执行的 stage 列表。
stage('按需构建') {
steps {
script {
def changed = sh(returnStdout: true, script: 'git diff --name-only HEAD~1')
.split('\n').toList()
def modules = changed.collect { it.split('/')[0] }.unique()
if (modules.isEmpty()) { modules = ['web','api'] }
modules.each { m ->
echo "构建模块: ${m}"
build job: "build-${m}", wait: true
}
}
}
}
原则:能用声明式指令(
when/parallel等)就别用 script;只有当「声明式真的写不出」时才进script,并保持块小而清晰,便于维护与排错。

48

被折叠的 条评论
为什么被折叠?



