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 资源——别嵌套太多,否则容易死锁。 - 如果没指定任何标签,默认会在任意可用节点上跑。我建议在重要管线里显式写标签,避免跑到奇怪的环境。
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 做阶段划分,在 stage 里用 script 写任意逻辑,最后用 parallel 把可以并行的步骤加速。
当你能把这张图刻在脑子里,写脚本式 Pipeline 就会像写普通函数一样自然。