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、团队协作 复杂逻辑、自定义需求
调试方式 可视化代码重放 日志 + 重放
Pipeline 两种风格对比 声明式 Pipeline 结构约束 + 内置块 pipeline { agent any } stages { stage('Checkout') } steps { git url... } post { always { junit } } ✅ 可读性高 / 易维护 脚本式 Pipeline 自由编写 Groovy 代码 node { stage('...') } def branches = ['master','dev'] branches.each { ... } try { ... } catch(e) { ... } ⚠️ 灵活但容易翻车 💡 选哪个?看团队水平 + 流水线复杂度。大部分回归场景优先用声明式。

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 我的选择公式:一个简单的决策流程

你想想看,这个问题其实可以降维处理。我个人一般这样判断:

  1. 流水线是线性的、没有动态分支? → 无脑选声明式。
  2. 需要动态生成 stage、循环、复杂条件? → 用脚本式,但把复杂逻辑封装到 Shared Library 里,Jenkinsfile 保持精简。
  3. 团队平均水平? 如果大家 Groovy 不熟,别冒险,声明式 + Shared Library 也能实现大部分动态需求。
  4. 历史遗留? 如果已有脚本式流水线在跑且稳定,别重写,先跑着。但新项目一律声明式。

说到底,我见过太多团队在脚本式里写面条代码,最后不得不重构。声明式即使写长了,结构也是清晰的: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 之前先想三秒。