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核心执行流程 脚本式Pipeline入口 Groovy脚本加载与执行 node 分配执行节点与工作空间 stage 定义可视化构建阶段 try-catch 异常捕获与错误恢复 DSL方法 checkout / sh / archive / withCredentials 等 构建完成 成功 / 失败 结果处理 所有节点内文字 均为白色 (#ffffff) 深色底 + 白字 = 高对比
一点经验:脚本式Pipeline的入口就是一个Groovy脚本块,不需要像声明式那样套一层 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' 而不是直接失败
  • 发通知或者写告警
注意:脚本式Pipeline中,如果 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里,结果不小心推到公开仓库——嗯,那场面挺尴尬的。

关于 timeout:回归测试经常因为某个case卡死导致整个流水线挂起。给 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 里先发通知再重新抛出异常,让构建状态正确。