4、脚本式Pipeline语法:node、stage、script、并行执行

脚本式Pipeline,说白了就是用 Groovy 代码直接写编排逻辑。 我刚开始接触 Jenkins 时,第一反应是「声明式多香啊,为啥要用脚本式?」 后来遇到一个需要动态判断并行分支的场景,才意识到脚本式的灵活度有多重要。

这一节,我们就来看脚本式 Pipeline 的核心原语:node、stage、script 和并行执行。 你如果能把这四个零件组装明白,90% 的 CI/CD 管线都能手到擒来。

4.1 脚本式 vs 声明式——一张表说清区别

先来个快速对比,帮你定位什么时候该用脚本式。

对比维度声明式 Pipeline脚本式 Pipeline
语法结构pipeline { ... } 块状声明node { ... } 纯 Groovy 代码
灵活性受限于预定义指令可写任意逻辑、循环、异常处理
并行控制parallel 指令 + 有限动态支持动态生成 parallel 分支
学习曲线较低需要 Groovy 基础
适合场景标准流程、团队统一规范复杂条件、动态分支、自研工具集成

我个人习惯是:能用声明式就用声明式,但一旦碰到「我需要根据分支名称决定跑多少测试机」或者「并行数量在运行时才确定」,马上切脚本式。

4.2 node:分配执行节点

node 是脚本式 Pipeline 的入口。它告诉 Jenkins:「我要在这台机器(或容器)上干活」。 我一般会把它当做一个执行上下文包裹器。

node('linux-agent') {
    // 这里的代码都会在名为 linux-agent 的节点上运行
    stage('Checkout') {
        checkout scm
    }
}

注意几点:

  • 节点标签可以写死(如 'linux-agent'),也可以用表达式动态选择。我在做多架构构建时,常常用 node(params.BUILD_NODE) 让用户选。
  • 多个 node 块可以嵌套,但要注意 Agent 资源——别嵌套太多,否则容易死锁。
  • 如果没指定任何标签,默认会在任意可用节点上跑。我建议在重要管线里显式写标签,避免跑到奇怪的环境。
我的习惯: 每个主线 Pipeline 只用一个顶层 node,把资源声明放在最外面,内部只分 stage。这样代码清晰,也方便后面加并行。

4.3 stage:定义阶段

在脚本式里,stage 没有声明式那么多魔法——它就是个可视化阶段的标签。 Jenkins 的 Blue Ocean 和 Stage View 全靠它来分段展示。

node('docker-builder') {
    stage('编译') {
        sh 'make build'
    }
    stage('单元测试') {
        sh 'make test'
    }
    stage('打包镜像') {
        sh 'docker build -t myapp:${BUILD_NUMBER} .'
    }
}

你可能会问:「那我能不能不写 stage?直接用 sh 命令?」 嗯,技术上可以,但我强烈建议你写。因为不写 stage,Pipeline 页面就只剩一个灰色长条,失败了你都不知道哪一步出问题。 我曾经接手过一个遗留管线,里面 200 行 Groovy 没有任何 stage,排查问题全靠加 println —— 那酸爽,不想再体验第二次。

4.4 script:嵌入任意脚本

script 块是脚本式 Pipeline 的「万能胶」。

声明式 Pipeline 里很多语法限制,比如不能直接写 if/else、try/catch,但用 script 包一层就可以绕过。 在纯脚本式里,其实 script 不是必须的,因为外面就是 Groovy 上下文。不过为了保持整洁,我还是习惯把复杂逻辑单独放进 script { } 里。

node {
    stage('动态选参') {
        script {
            def branch = env.BRANCH_NAME
            if (branch.startsWith('release/')) {
                currentBuild.displayName = "Release ${branch}"
                // 这里可以设置额外变量
                env.DEPLOY_ENV = 'prod'
            } else {
                env.DEPLOY_ENV = 'staging'
            }
        }
    }
    stage('部署') {
        sh "deploy.sh --env ${env.DEPLOY_ENV}"
    }
}
踩坑提醒: 不要在 script 里写太长的业务逻辑。把大段代码拆成函数或共享库,否则维护起来想哭。

4.5 并行执行:parallel 与动态分支

脚本式的并行比声明式灵活得多。你可以用 parallel 关键字直接写并行块,也可以用 Groovy 的集合批量生成并行分支。

先看最基本的 parallel 写法:

node('big-agent') {
    stage('并行测试') {
        parallel(
            '单元测试': {
                sh 'make test-unit'
            },
            '集成测试': {
                sh 'make test-integration'
            },
            '静态检查': {
                sh 'make lint'
            }
        )
    }
}

每个并行分支都是一个闭包。Jenkins 会尽可能同时启动它们。 要注意的是,并行分支默认共享同一个 node 的工作空间,所以如果有文件冲突,要小心处理。

更高级的用法是动态生成并行分支。比如你有几十个测试模块,写死太蠢了。可以用循环构造:

def modules = ['auth', 'payment', 'order', 'user']
def parallelTasks = [:]

for (mod in modules) {
    def moduleName = mod  // 闭包捕获需要用局部变量
    parallelTasks[moduleName] = {
        stage("测试 ${moduleName}") {
            sh "make test-${moduleName}"
        }
    }
}

parallel parallelTasks

这个技巧我在一次微服务批量构建中大杀四方——一次性跑 20+ 模块的构建和测试,时间从 40 分钟压到了 6 分钟。

4.6 实战:一个混合并行的部署管线

来一个我经常用的套路:先并行编译,然后串行部署到多环境。

node('linux-builder') {
    stage('准备') {
        checkout scm
    }

    // 并行编译
    stage('编译') {
        parallel(
            '前端': {
                sh 'cd frontend && npm run build'
            },
            '后端': {
                sh 'cd backend && mvn package'
            }
        )
    }

    // 依次部署到不同环境
    stage('部署') {
        script {
            for (envName in ['dev', 'staging', 'prod']) {
                stage("部署到 ${envName}") {
                    // 这里每个部署阶段会串行执行
                    sh "deploy.sh --env ${envName}"
                }
            }
        }
    }
}

注意最后那个 for 循环里的 stage——虽然循环内嵌了一个 stage,但它会变成真实的阶段,在 Stage View 里依次展示。这个技巧能让你清晰地看到每个环境的部署状态。

4.7 避坑总结

  • 节点死锁: 别在并行分支里嵌套 node,否则可能等着等着就永远等不到资源。我曾经在深夜被 on-call 电话吵醒,就因为一个工程师在并行里又申请了 node……导致所有 agent 都在等自己。
  • Groovy 闭包陷阱: 循环里生成并行任务时,一定要用局部变量 def localVar = loopVar,否则闭包捕获的是循环变量的最终值。
  • 日志管理: 并行分支太多时,Jenkins 的日志会混在一起。我会在每个分支开头加 echo "=== 分支名 ===",方便 grep。
  • 资源控制: 并行数不是越多越好,Agent 的 CPU/内存扛不住。建议限制最大并行数,比如用 parallelTasks.put(mod, ...) 后直接 parallel(parallelTasks);如果任务太多,可以分组分批。
一句话记住: node 管机器,stage 管可视化,script 管逻辑,parallel 管速度。这四个概念构成了脚本式 Pipeline 的骨架。
脚本式 Pipeline 核心语法结构 node (分配执行节点) stage 块(可视化的阶段分组) stage('编译') stage('测试') stage('部署') script { ... } 嵌入任意逻辑 parallel 并行执行(可动态生成分支) 并行分支 1 并行分支 2 并行分支 N 动态生成:for 循环 → parallelTasks 映射 → parallel

这个图帮你串一遍:最外层 node 提供执行环境,内部可以放多个 stage 做阶段划分,在 stage 里用 script 写任意逻辑,最后用 parallel 把可以并行的步骤加速。 当你能把这张图刻在脑子里,写脚本式 Pipeline 就会像写普通函数一样自然。