运维知识
悠悠
2026年4月6日

Jenkins Pipeline 脚本踩坑记:我是如何被两种语法折磨并最终选择的

最近在折腾公司的 CI/CD 流水线,想把原来那套老掉牙的构建脚本升级一下。本以为 Jenkins Pipeline 挺简单的,结果一上手就懵了——竟然有两种写法!这不是逼死选择困难症吗?

我当时的内心OS:这玩意儿就像去饭店点菜,服务员问你要清汤还是麻辣的,你以为选完就完事了,结果又问你要不要加香菜、要不要加醋... 选择太多反而不知道怎么选了。

Pipeline 的两种"口味"

Jenkins Pipeline 主要有两种写法:Declarative Pipeline(声明式流水线)和 Scripted Pipeline(脚本式流水线)。

Declarative Pipeline 就像是傻瓜相机,你只需要按快门就行,其他的都帮你调好了。它用的是一种 DSL(Domain Specific Language),说白了就是专门为 Pipeline 设计的语言。整个 Pipeline 必须包在 pipeline 块里面,然后用 stagessteps 来组织你的构建步骤。

而 Scripted Pipeline 呢,更像是单反相机,功能强大但你得自己调参数。它直接用的是 Groovy 语言,灵活性爆表,但也意味着你得有点 Groovy 基础。整个脚本是在 node 块里执行的[4]。

我的踩坑之旅

第一坑:语法差异

刚开始我用的是 Declarative Pipeline,写起来确实简单:

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                echo 'Building...'
            }
        }
    }
}

看着挺清爽的对吧?结构一目了然。

后来有个需求,需要在构建过程中动态判断一些条件,我就想着用 if-else。结果发现 Declarative Pipeline 里不能直接写 if 语句!你得用 when 指令:

stage('Deploy') {
    when {
        branch 'master'
    }
    steps {
        echo 'Deploying to production...'
    }
}

这时候我就想,要不试试 Scripted Pipeline?切换过去一看:

node {
    stage('Build') {
        echo 'Building...'
        if (env.BRANCH_NAME == 'master') {
            echo 'This is master branch'
        }
    }
}

嘿,这下自由了!想怎么写就怎么写。但问题又来了...

第二坑:错误处理

Declarative Pipeline 的错误处理特别规范,有专门的 post 块:

pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                sh 'make test'
            }
        }
    }
    post {
        always {
            echo 'This will always run'
        }
        success {
            echo 'Only on success'
        }
        failure {
            echo 'Only on failure'
            mail to: 'team@example.com',
                 subject: 'Build Failed'
        }
    }
}

但在 Scripted Pipeline 里,你得自己写 try-catch:

node {
    try {
        stage('Test') {
            sh 'make test'
        }
        echo 'Success!'
    } catch (e) {
        echo 'Failed!'
        throw e
    } finally {
        echo 'Cleanup'
    }
}

说实话,写多了 try-catch 真的很烦。有一次我忘了写 finally 块,结果构建失败后一些临时文件没清理,磁盘差点爆了。

第三坑:并行执行

这个是我觉得两种 Pipeline 差异最大的地方。

Declarative Pipeline 的并行执行写法很直观:

pipeline {
    agent any
    stages {
        stage('Parallel Stage') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        echo 'Running unit tests...'
                    }
                }
                stage('Integration Tests') {
                    steps {
                        echo 'Running integration tests...'
                    }
                }
            }
        }
    }
}

而 Scripted Pipeline 的写法... 说实话第一次看到我是懵的:

node {
    stage('Parallel Stage') {
        parallel(
            "Unit Tests": {
                echo 'Running unit tests...'
            },
            "Integration Tests": {
                echo 'Running integration tests...'
            }
        )
    }
}

这种 Map 的写法,不知道为啥总让我想起 JavaScript 的对象字面量。

实战经验分享

经过这么多坑,我总结了一些实战经验。

如果你的项目比较简单,构建流程相对固定,强烈建议用 Declarative Pipeline。为啥?因为它:

  • 语法检查更严格,写错了立马报错
  • 结构清晰,新人上手快
  • 自带很多实用功能,比如 optionsparameterstriggers 等[3]

我们有个前端项目就是用的 Declarative:

pipeline {
    agent {
        docker {
            image 'node:14-alpine'
        }
    }
  
    options {
        timeout(time: 30, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }
  
    parameters {
        choice(name: 'ENVIRONMENT', choices: ['dev', 'test', 'prod'], description: 'Deploy environment')
    }
  
    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }
        stage('Build') {
            steps {
                sh "npm run build:${params.ENVIRONMENT}"
            }
        }
        stage('Deploy') {
            when {
                expression { params.ENVIRONMENT == 'prod' }
            }
            steps {
                input message: 'Deploy to production?'
                sh './deploy.sh'
            }
        }
    }
}

但如果你的构建逻辑很复杂,需要大量的条件判断、循环处理,或者要调用很多 Groovy 的高级特性,那 Scripted Pipeline 可能更合适。

我们有个数据处理的项目,需要根据不同的数据源动态生成构建步骤:

node {
    def dataSources = ['mysql', 'postgres', 'mongodb']
    def stages = [:]
  
    dataSources.each { source ->
        stages["Process ${source}"] = {
            stage("Process ${source}") {
                echo "Processing ${source} data..."
              
                def config = readJSON file: "${source}.json"
                config.tables.each { table ->
                    sh "python process.py --source ${source} --table ${table}"
                }
            }
        }
    }
  
    parallel stages
}

这种动态生成 stage 的需求,用 Declarative 就很难实现了。

性能和维护性对比

从性能角度看,两者其实差不多。Jenkins 最终都会把它们转换成同样的执行逻辑。

但从维护性来说,Declarative Pipeline 胜出太多了。我们团队之前有个项目用的 Scripted Pipeline,写得那叫一个自由奔放。后来换了个人维护,看着那些嵌套的闭包和各种 Groovy 黑魔法,直接崩溃了。最后花了一周时间重写成 Declarative 的。

我的最终选择

折腾了这么久,我的选择是:能用 Declarative 就用 Declarative,实在不行再用 Scripted。

为啥?因为大部分场景下,Declarative Pipeline 真的够用了。而且 Jenkins 官方也推荐优先使用 Declarative。它的限制反而是优点,能让团队的 Pipeline 风格保持一致。

当然,如果你遇到这些情况,可能真的需要 Scripted Pipeline:

  • 需要复杂的流程控制逻辑
  • 要动态生成大量的 stages
  • 需要使用 Groovy 的高级特性
  • 要集成一些特殊的 Jenkins 插件

一些实用技巧

不管用哪种 Pipeline,这些技巧都很有用:

  1. 使用共享库:把通用的逻辑抽取到共享库里,避免重复代码
  2. 参数化构建:灵活使用 parameters,让 Pipeline 更通用
  3. 合理使用 agent:不同的 stage 可以用不同的 agent,提高资源利用率
  4. 做好错误处理:该 catch 的要 catch,该重试的要重试
  5. 日志要清晰:多用 echo 输出关键信息,方便排查问题

总结

Jenkins Pipeline 的两种写法各有千秋。Declarative Pipeline 像是给你画好了框,你只需要往里面填东西;Scripted Pipeline 则是给你一张白纸,你想怎么画就怎么画。

我的建议是,如果你刚接触 Jenkins Pipeline,从 Declarative 开始。等你熟悉了,再根据实际需求决定是否需要 Scripted 的灵活性。记住,工具是为了解决问题的,不要为了炫技而选择复杂的方案。

最后,不管选择哪种,记得多写注释!相信我,三个月后的你会感谢现在的自己的。


如果这篇文章对你有帮助,欢迎关注 @运维躬行录,我会持续分享更多运维实战经验。遇到 Pipeline 的坑也欢迎留言交流,说不定我也踩过呢!

公众号:运维躬行录
个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码