4、脚本式Pipeline语法:node、stage、try-catch、常用DSL方法
脚本式Pipeline,说白了就是直接用Groovy脚本写流水线。跟声明式那种"填空"风格不同,脚本式给你最大自由度。我刚开始接触Jenkins Pipeline时,其实是从声明式入手的,但后来发现有些复杂逻辑声明式根本绕不过去——比如动态并行、条件分支、循环。嗯,这时候脚本式就派上用场了。
这一章,我把脚本式Pipeline最核心的几个语法拆开来讲。你掌握了这些,就能写出健壮、可维护的回归自动化流水线。
4.1 脚本式Pipeline vs 声明式Pipeline
先看一个快速对比,你就知道两种风格的区别在哪。
| 对比维度 | 脚本式Pipeline | 声明式Pipeline |
|---|---|---|
| 语法基础 | Groovy脚本,完整编程能力 | DSL结构,预定义指令集 |
| 灵活度 | 高——可写循环、条件、异常处理 | 中——受限于指令和when条件 |
| 代码结构 | 自由,须自己组织node/stage | 强制pipeline→agent→stages→steps |
| 异常处理 | 原生try-catch-finally | 通过post块做条件处理 |
| 学习曲线 | 需要Groovy基础 | 低,接近声明式配置 |
| 适用场景 | 复杂回归、动态环境、多分支逻辑 | 标准CI/CD,简单流程 |
我个人习惯在回归自动化场景中用脚本式。为什么?回归测试往往需要动态生成测试矩阵、处理不同环境的重试逻辑、还要根据测试结果决定是否中断流水线——这些用声明式写起来很别扭。脚本式一句 try-catch 就搞定了。
下面这张图,是脚本式Pipeline的核心执行流程。你看一眼就能理解各个语法块是怎么串起来的。
pipeline { } 外壳。你直接从 node 开始写就行。
4.2 node:分配执行节点
node 是脚本式Pipeline的基石。它负责两件事:分配一个执行节点,以及在这个节点上创建临时工作空间。说白了,你的代码、编译、测试全在这个工作空间里跑。
基本用法:
node('linux-agent') {
// 在这个节点的工作空间内执行
echo "当前工作空间: ${env.WORKSPACE}"
}
括号里可以指定标签或节点名称。不指定的话,Jenkins 会随机分配一个可用的 agent。
我在项目中遇到过一个问题:回归流水线跑了一百多次,突然某次构建卡住了。查了半天,发现是 node 没有指定标签,构建被分配到了一个资源不足的节点上,磁盘直接打满。所以我现在写回归流水线,一定会给 node 加上明确的标签——比如 node('high-mem') 或 node('gpu-agent')。
node 块都会创建一个新的工作空间。如果你在同一个流水线里写了多个 node 块,它们之间默认不共享文件。想传文件?用 stash 和 unstash。
4.3 stage:定义可视化阶段
stage 的作用很简单——给你的流水线步骤打个标签。但这个标签非常重要。它会在 Jenkins 的 Pipeline 视图和 Blue Ocean 里生成可视化的阶段进度条。你看一眼就知道构建跑到哪了。
node('linux-agent') {
stage('代码检出') {
checkout scm
}
stage('编译构建') {
sh 'mvn clean compile -DskipTests'
}
stage('回归测试') {
sh 'mvn test'
}
}
上面这段代码,在 Jenkins 界面上会显示三个阶段——绿色通过、红色失败、黄色进行中。你想想看,回归测试跑几百个 case,要是没有 stage 做分段,你根本不知道是在编译挂了还是测试挂了。
我个人习惯:每个逻辑单元都包一个 stage,哪怕里面只有一行代码。别偷懒。因为 stage 不仅是给人看的,它还能配合 when、post 做条件判断和后置动作——虽然脚本式里这些要自己手写。
stage 的名字尽量用中文或英文缩写,保持简短。Blue Ocean 的界面空间有限,名字太长会被截断。
4.4 try-catch:异常处理机制
回归自动化最怕什么?半夜跑挂了,没人发现,第二天早上才知道。用 try-catch 可以优雅地处理异常——发通知、记录日志、甚至自动重试。
脚本式Pipeline直接使用Groovy原生的 try-catch-finally:
node('linux-agent') {
stage('回归测试') {
try {
sh 'mvn test'
} catch (Exception e) {
echo "回归测试失败: ${e.message}"
currentBuild.result = 'UNSTABLE'
// 发邮件通知
emailext(
subject: "回归测试异常: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "详情请查看构建日志: ${env.BUILD_URL}",
to: 'team@example.com'
)
} finally {
archiveArtifacts artifacts: 'target/surefire-reports/**/*.xml'
}
}
}
我曾经因为忘记加 try-catch 吃过一次大亏。那次回归流水线跑的是全量集成测试,中途一个网络超时直接让整个流水线闪退了。关键是——没有 catch,Jenkins 默认把构建标记为失败,但没有发送任何通知。第二天早上才发现,团队半天时间白白浪费。
所以现在我写回归流水线,一定会做到:
- 每个关键
stage至少包一层try-catch catch里设置currentBuild.result = 'UNSTABLE'而不是直接失败- 发通知或者写告警
catch 里不重新抛出异常,流水线会继续往下走。这跟声明式的 post { failure } 逻辑不同。你想要中断流水线?在 catch 里加一句 throw e 或者 error("强制失败")。
4.5 常用DSL方法
脚本式Pipeline内置了很多DSL方法,这里列几个你在回归流水线里高频会用到的:
| DSL方法 | 用途 | 常见用法示例 |
|---|---|---|
checkout |
从Git/SVN检出代码 | checkout scm 或 checkout([$class: 'GitSCM', ...]) |
sh |
在节点上执行shell命令 | sh 'mvn clean test' 或 sh script: 'deploy.sh', returnStdout: true |
withCredentials |
安全注入凭证(密码、、SSH密钥) | withCredentials([string(credentialsId: 'db-pass', variable: 'PASS')]) { ... } |
archiveArtifacts |
归档构建产物(测试报告、日志等) | archiveArtifacts artifacts: 'target/reports/**/*.xml', allowEmpty: true |
emailext |
发送邮件通知 | emailext subject: '构建通知', body: '详见日志', to: 'dev@example.com' |
timeout |
设置步骤超时时间 | timeout(time: 30, unit: 'MINUTES') { sh 'mvn test' } |
retry |
失败后自动重试 | retry(3) { sh 'flaky-test.sh' } |
我建议你把 withCredentials 作为所有敏感操作的标准配置。千万不要在脚本里硬编码密码或。我见过一个团队把数据库密码直接写在Jenkinsfile里,结果不小心推到公开仓库——嗯,那场面挺尴尬的。
sh 或测试步骤包一层 timeout,设定30分钟或1小时的上限,是个好习惯。宁可超时失败,也比无限等待强。
4.6 完整示例:回归自动化流水线
下面我把上面讲的语法全部揉到一起,写一个完整的回归自动化流水线脚本。你可以直接拿它当模板:
node('linux-agent') {
try {
stage('代码检出') {
checkout scm
echo "已从 ${env.BRANCH_NAME} 分支检出代码"
}
stage('编译构建') {
timeout(time: 15, unit: 'MINUTES') {
sh 'mvn clean compile -DskipTests --batch-mode'
}
}
stage('单元测试') {
timeout(time: 20, unit: 'MINUTES') {
sh 'mvn test --batch-mode'
}
}
stage('回归测试') {
timeout(time: 60, unit: 'MINUTES') {
retry(2) {
sh 'mvn verify -Pregression --batch-mode'
}
}
}
stage('归档报告') {
archiveArtifacts artifacts: 'target/surefire-reports/**/*.xml'
archiveArtifacts artifacts: 'target/failsafe-reports/**/*.xml'
junit 'target/surefire-reports/**/*.xml'
}
echo "回归流水线全部通过!"
} catch (Exception e) {
currentBuild.result = 'UNSTABLE'
echo "流水线异常: ${e.message}"
withCredentials([string(credentialsId: 'mail-password', variable: 'MAIL_PASS')]) {
emailext(
subject: "回归流水线异常: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "异常信息: ${e.message}\n\n构建日志: ${env.BUILD_URL}",
to: 'team@example.com',
mimeType: 'text/plain'
)
}
throw e // 重新抛出,让构建最终标记为失败
} finally {
stage('清理') {
cleanWs()
echo "工作空间已清理"
}
}
}
这个示例里,我把 node、stage、try-catch-finally、timeout、retry、withCredentials、archiveArtifacts、emailext 全用上了。你写回归流水线时,直接在这个框架上改就行了。
— 用
timeout 防止测试卡死;— 用
retry(2) 处理偶发的网络抖动或服务暂时不可用;— 用
finally 块做清理,保证工作空间不会被占满;—
catch 里先发通知再重新抛出异常,让构建状态正确。