2. Pipeline 概述:声明式 vs 脚本式,什么时候选哪个?我踩过的坑
Pipeline 是 Jenkins 的魂。没有 Pipeline,你只能靠手动点按钮,或者写一堆零散的 Job。但一上来就面对两种写法——声明式(Declarative)和脚本式(Scripted)——很多人会懵。我刚开始也懵,觉得「写个脚本不就行了?搞那么复杂干啥」。结果……嗯,踩了坑才明白,选错风格,后期维护成本能让你怀疑人生。
一句话概括:声明式是「填空题」,脚本式是「作文题」。填空题简单、规范、易读,但灵活度低;作文题自由、强大,但写不好就是天书。
2.1 声明式 Pipeline 长什么样?
声明式 Pipeline 是 Jenkins 官方推荐的风格。它用 pipeline {} 包裹,内部按固定结构写:agent、stages、steps。有点像写配置文件。我个人习惯用声明式做大部分回归流水线,因为它清晰,团队成员一看就懂。
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git 'https://github.com/your/repo.git'
}
}
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
特点:
- 有严格的结构约束,写错了 Jenkins 直接报错。
- 内置
post、environment、options等块,开箱即用。 - 支持
when条件,比如只对 master 分支运行某些步骤。
2.2 脚本式 Pipeline 长什么样?
脚本式本质上是 Groovy 代码。它用 node {} 包裹,你可以写循环、写条件、写闭包、甚至动态生成 stage。自由度极高,但能看懂的人就少了。
node {
stage('Checkout') {
git 'https://github.com/your/repo.git'
}
stage('Build') {
sh 'mvn clean compile'
}
stage('Test') {
sh 'mvn test'
}
stage('Results') {
junit 'target/surefire-reports/*.xml'
}
}
看,是不是和声明式很像?但脚本式里你可以写这样的骚操作:
def branches = ['master', 'develop']
branches.each { branch ->
stage("Build ${branch}") {
node {
checkout scm: [$class: 'GitSCM', branches: [[name: "*/${branch}"]]]
sh 'mvn compile'
}
}
}
特点:
- 完全编程能力,想咋写咋写。
- 没有
post、options这种语法糖,很多东西要自己实现。 - 错误处理、重试逻辑用 Groovy 的 try/catch 写。
2.3 核心区别一览
| 维度 | 声明式 Pipeline | 脚本式 Pipeline |
|---|---|---|
| 书写难度 | 低,像填表格 | 中高,需要 Groovy 基础 |
| 可读性 | 高,结构一目了然 | 因人而异,容易写乱 |
| 灵活性 | 低,受限于预定义块 | 高,几乎无所不能 |
| 错误提示 | 好,语法检查严格 | 差,运行时才报错 |
| 内置功能 | 多(post、when、options) | 少,需自己造轮子 |
| 适合场景 | 标准化 CI/CD、团队协作 | 复杂逻辑、自定义需求 |
| 调试方式 | 可视化代码重放 | 日志 + 重放 |
2.4 什么时候选声明式?—— 我踩过的坑
我当初接手一个回归流水线,组里五个人,只有一个人会 Groovy。他用脚本式写了两千行的 Pipeline,里面还有动态生成 stage、嵌套循环、自定义类。结果他离职后,没人敢动那代码。每次要加一个测试步骤都要小心翼翼,怕改坏全局变量。这就是第一个坑:不要用脚本式为难团队。
后来我全部重写成声明式。回归流水线嘛,无非就是拉代码 → 编译 → 跑测试 → 归档报告。声明式完全够用。而且声明式有 when 条件,比如:
stage('Smoke Test') {
when { branch 'master' }
steps {
sh 'mvn test -Dtest=SmokeTestSuite'
}
}
你看,不用写 if(branch == 'master'),直接声明式内置。这让代码量减少 40%。
什么时候必须用声明式?
- 团队多人维护,新人多。
- 流水线流程标准、固定的回归、构建、测试。
- 要求代码审查简单,一眼能看出哪里出错。
- 需要利用 Jenkins Blue Ocean 的图形化编辑器(只支持声明式)。
2.5 什么时候选脚本式?—— 也是踩过的坑
当然,声明式不是万能的。有一次我需要做一个并行 + 动态流水线:根据 Git 仓库的 submodule 列表自动生成多个并行 stage。声明式不行,因为 stage 数量在 Jenkinsfile 运行前是未知的。这时候我只能用脚本式写 parallel 动态生成。
node {
def modules = ['module-a', 'module-b', 'module-c']
def parallelStages = [:]
modules.each { mod ->
parallelStages[mod] = {
stage("Build ${mod}") {
dir(mod) { sh 'mvn compile' }
}
}
}
parallel parallelStages
}
这脚本式代码也没多复杂,但关键是:你要保证自己或接手的人看得懂。我在这上面踩过更大的坑——写了一段用 input 阻塞等待人工确认的脚本式流水线,结果 input 在脚本式里会导致整个 node 被占用,别的 job 排不上队。后来才知道声明式的 input 步骤会自动释放 executor,脚本式不会。这就是没搞懂底层机制。
脚本式 Pipeline 的雷区:
- 没有
post块,你要手动 try/catch 处理失败通知。 - 变量作用域问题:在
node块外定义的变量可能不可用。 input会阻塞 executor(我之前就中招了)。- 无法使用 Blue Ocean 的可视化编辑。
2.6 我的选择公式:一个简单的决策流程
你想想看,这个问题其实可以降维处理。我个人一般这样判断:
- 流水线是线性的、没有动态分支? → 无脑选声明式。
- 需要动态生成 stage、循环、复杂条件? → 用脚本式,但把复杂逻辑封装到 Shared Library 里,Jenkinsfile 保持精简。
- 团队平均水平? 如果大家 Groovy 不熟,别冒险,声明式 + Shared Library 也能实现大部分动态需求。
- 历史遗留? 如果已有脚本式流水线在跑且稳定,别重写,先跑着。但新项目一律声明式。
说到底,我见过太多团队在脚本式里写面条代码,最后不得不重构。声明式即使写长了,结构也是清晰的:stage 块就是流程图上的每一个框。
2.7 我踩过的坑汇总(避坑指南)
- 坑 1: 脚本式里用
input阻塞 agent。声明式的input步骤是单独的stage,不会占用 executor。我那次线上 job 全卡住,被老板骂了一顿。 - 坑 2: 声明式里用
script { }块来写 Groovy 逻辑,但千万注意别在script块里定义外部变量后又在别的 stage 直接用——有时作用域会飘。我曾经定义了一个def buildNumber,在下一个 stage 里读不到,跑了一段时间才发现。 - 坑 3: 声明式的
parallel只能在stage内部使用,不能跨 stage。如果要做真正的并行流水线(多个分支同时构建),还得用脚本式或声明式的matrix块。 - 坑 4: 别把脚本式 Pipeline 的所有逻辑都塞在一个 Jenkinsfile 里。拆成 Shared Library 的方法,让 Jenkinsfile 只剩声明式的骨架。我见过一个脚本式 Jenkinsfile 超过 3000 行,打开就卡死。
一句话总结:90% 的回归自动化流水线用声明式就够了。剩下的 10% 用脚本式,但必须封装好、注释清楚、定期审查。别让 Pipeline 成为你团队的技术债务。
好了,这一节就到这。下节我们讲讲 Jenkinsfile 的环境管理和参数化构建——那个也容易踩坑。嗯,先记住今天的选择公式,写 Pipeline 之前先想三秒。