使用 Jenkinsfile

使用 Jenkinsfile

Table of Contents
  • 创建 Jenkinsfile

    • 构建
    • 测试
    • 部署
  • 编写和使用 Jenkinsfile

    • 使用环境变量

      • 设置环境变量
      • 动态设置环境变量
    • 处理凭据

      • 密文、用户名与密码以及秘密文件

        • 密文
        • 用户名与密码
        • 秘密文件
      • 其他凭据类型

        • 在同一步骤中组合凭据
    • 字符串插值

      • 敏感环境变量的插值
      • 通过插值注入命令
    • 处理参数
    • 处理失败

      • 错误处理步骤
    • 使用多个代理
    • 可选的步骤参数
    • 高级脚本式流水线

      • 并行执行

本节在“流水线入门”的基础上介绍更多实用步骤和常见模式,并展示一些较复杂的 Jenkinsfile 示例。

创建 Jenkinsfile 并将其纳入源代码管理 [1],可以立即带来以下好处:

  • 对流水线进行代码评审和迭代。

  • 保留流水线的审计记录。

  • 为流水线建立单一事实来源 [2],项目中的多个成员可以共同查看和编辑。

流水线支持两种语法:声明式流水线(在 Pipeline 2.5 中引入)和脚本式流水线。两者都能构建持续交付流水线,也都可以在网页界面或 Jenkinsfile 中定义流水线。不过,通常推荐创建 Jenkinsfile 并将其提交到源代码仓库。

创建 Jenkinsfile

如“在 SCM 中定义流水线”所述,Jenkinsfile 是包含 Jenkins 流水线定义、并纳入源代码管理的文本文件。下面的流水线实现了一个基本的三阶段持续交付流程。

Jenkinsfile(声明式流水线)

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                echo 'Building..'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

node {
    stage('Build') {
        echo 'Building....'
    }
    stage('Test') {
        echo 'Testing....'
    }
    stage('Deploy') {
        echo 'Deploying....'
    }
}

并非所有流水线都采用这三个阶段,但对大多数项目来说,这是一种合适的起点。下面将演示如何在用于测试的 Jenkins 安装中创建并执行简单流水线。

这里假设项目已经建立源代码仓库,并按照相关说明在 Jenkins 中定义了流水线。

使用文本编辑器,在项目根目录新建 Jenkinsfile;最好选择支持 Groovy 语法高亮的编辑器。

上面的声明式流水线示例包含实现持续交付流水线所需的最小结构。必需的 agent 指令告诉 Jenkins 为流水线分配执行器和工作区。没有 agent 指令,声明式流水线不仅无效,也无法执行任何工作。默认情况下,agent 指令会确保检出源代码仓库,使后续阶段的步骤可以使用这些代码。

有效的声明式流水线也必须包含 stages 和 steps 指令,它们告诉 Jenkins 要执行什么,以及在哪个阶段执行。

对于更高级的脚本式流水线,上例中的 node 是关键的第一步,因为它为流水线分配执行器和工作区。没有 node,流水线就无法开展工作。在 node 内,首先要 checkout 项目的源代码。由于 Jenkinsfile 本身直接来自源代码管理系统,流水线提供了一种快捷方式来访问正确的代码修订版本。

Jenkinsfile(脚本式流水线)

node {
    checkout scm (1)
    /* .. snip .. */
}
1 checkout 步骤从源代码管理系统检出代码;scm 是一个特殊变量,它告诉 checkout 步骤克隆触发本次流水线运行的特定修订版本。

构建

许多项目的流水线从构建阶段开始实际工作。在此阶段通常会组装、编译或打包源代码。Jenkinsfile 并不替代 GNU/Make、Maven、Gradle 等现有构建工具,而是把项目开发生命周期中的构建、测试和部署等多个阶段连接起来。

Jenkins 提供了许多插件,可以调用几乎所有常用构建工具。本例直接通过 shell 步骤 sh 调用 make。sh 假定运行环境是 Unix/Linux;Windows 系统可改用 bat。

Jenkinsfile(声明式流水线)

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                sh 'make' (1)
                archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true (2)
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

node {
    stage('Build') {
        sh 'make' (1)
        archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true (2)
    }
}
1 sh 步骤调用 make 命令,只有命令返回零退出码时才继续。任何非零退出码都会使流水线失败。
2 archiveArtifacts 收集符合包含模式 **/target/*.jar 的构建文件,并将其保存到 Jenkins 控制器,以便之后提取。

归档构建产物不能替代 Artifactory 或 Nexus 等外部制品仓库,只应将其用于基本报告和文件归档。

测试

运行自动化测试是成功实现持续交付的关键组成部分。Jenkins 通过多个插件提供测试记录、报告和可视化功能。测试失败时,应让 Jenkins 记录失败信息,以便在网页界面中报告和展示。下例使用 JUnit 插件提供的 junit 步骤。

下例中,如果测试失败,流水线会被标记为“不稳定”,网页界面用黄色圆球表示。Jenkins 还可以根据记录的测试报告分析并展示历史趋势。

Jenkinsfile(声明式流水线)

pipeline {
    agent any

    stages {
        stage('Test') {
            steps {
                /* `make check` returns non-zero on test failures,
                * using `true` to allow the Pipeline to continue nonetheless
                */
                sh 'make check || true' (1)
                junit '**/target/*.xml' (2)
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

node {
    /* .. snip .. */
    stage('Test') {
        /* `make check` returns non-zero on test failures,
         * using `true` to allow the Pipeline to continue nonetheless
         */
        sh 'make check || true' (1)
        junit '**/target/*.xml' (2)
    }
    /* .. snip .. */
}
1 通过内联 shell 条件 sh 'make check || true',可以确保 sh 步骤始终获得零退出码,让 junit 步骤有机会收集和处理测试报告。后面的“处理失败”一节会详细介绍其他方法。
2 junit 收集并关联符合包含模式 **/target/*.xml 的 JUnit XML 文件。

部署

部署所包含的步骤取决于项目或组织的需求,可能是将构建产物发布到 Artifactory 服务器,也可能是将代码推送到生产系统。

在示例流水线执行到此处时,构建和测试阶段都已成功执行。部署阶段只有在前面的阶段成功完成后才会运行,否则流水线会提前退出。

Jenkinsfile(声明式流水线)

pipeline {
    agent any

    stages {
        stage('Deploy') {
            when {
              expression {
                currentBuild.result == null || currentBuild.result == 'SUCCESS' (1)
              }
            }
            steps {
                sh 'make publish'
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

node {
    /* .. snip .. */
    stage('Deploy') {
        if (currentBuild.result == null || currentBuild.result == 'SUCCESS') { (1)
            sh 'make publish'
        }
    }
    /* .. snip .. */
}
1 访问 currentBuild.result 变量,可以判断流水线中是否出现测试失败;如果出现,该值为 UNSTABLE。

假设示例 Jenkins 流水线中的所有步骤都成功执行,那么每次成功运行都会在 Jenkins 中保存相关构建产物、测试报告和完整控制台输出。

脚本式流水线可以包含上述条件判断、循环、try/catch/finally 代码块,甚至函数。下一节会进一步介绍这些高级语法。

编写和使用 Jenkinsfile

以下各节详细说明如何处理:

  • Jenkinsfile 中的特定流水线语法;

  • 构建应用或流水线项目所必需的流水线语法特性与功能。

使用环境变量

Jenkins 流水线通过全局变量 env 暴露环境变量,Jenkinsfile 中的任何位置都可以访问它。可用环境变量的完整列表见 ${YOUR_JENKINS_URL}/pipeline-syntax/globals#env,其中包括:

BUILD_ID

当前构建 ID。对于 Jenkins 1.597 及以后版本创建的构建,它与 BUILD_NUMBER 相同。

BUILD_NUMBER

当前构建编号,例如 153。

BUILD_TAG

格式为 jenkins-${JOB_NAME}-${BUILD_NUMBER} 的字符串,适合放入资源文件、JAR 文件等位置,便于识别。

BUILD_URL

查看本次构建结果的 URL,例如 http://buildserver/jenkins/job/MyJobName/17/。

EXECUTOR_NUMBER

在同一台机器的执行器中,标识当前执行此构建的执行器的唯一编号。它与“构建执行器状态”中显示的编号对应,但从 0 而不是 1 开始。

JAVA_HOME

如果任务配置为使用特定 JDK,该变量就设为该 JDK 的 JAVA_HOME。设置此变量时,PATH 也会更新,以包含 JAVA_HOME 下的 bin 子目录。

JENKINS_URL

Jenkins 的完整 URL,例如 https://example.com:port/jenkins/。注意:只有在系统配置中设置了 Jenkins URL,该变量才可用。

JOB_NAME

本次构建所属项目的名称,例如 foo 或 foo/bar。

NODE_NAME

当前构建运行所在节点的名称。对于 Jenkins 控制器,该值为 master。

WORKSPACE

工作区的绝对路径。

可以像访问 Groovy Map 中的键一样引用或使用这些环境变量,例如:

Jenkinsfile(声明式流水线)

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

node {
    echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
}

设置环境变量

在 Jenkins 流水线中设置环境变量的方法,取决于采用声明式还是脚本式流水线。

声明式流水线支持 environment 指令,脚本式流水线则必须使用 withEnv 步骤。

Jenkinsfile(声明式流水线)

pipeline {
    agent any
    environment { (1)
        CC = 'clang'
    }
    stages {
        stage('Example') {
            environment { (2)
                DEBUG_FLAGS = '-g'
            }
            steps {
                sh 'printenv'
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

node {
    /* .. snip .. */
    withEnv(["PATH+MAVEN=${tool 'M3'}/bin"]) {
        sh 'mvn -B verify'
    }
}
1 在顶层 pipeline 块中使用 environment 指令,会影响流水线中的所有步骤。
2 在某个 stage 内定义的 environment 指令,只会将指定环境变量应用于该阶段中的步骤。

动态设置环境变量

环境变量可以在运行时设置,并供 shell 脚本 sh、Windows 批处理脚本 bat 和 PowerShell 脚本 powershell 使用。每个脚本都可以使用 returnStatus 或 returnStdout。更多信息参见脚本文档。

下例在声明式流水线中使用 sh,同时展示 returnStatus 和 returnStdout。

Jenkinsfile(声明式流水线)

pipeline {
    agent any (1)
    environment {
        // Using returnStdout
        CC = """${sh(
                returnStdout: true,
                script: 'echo "clang"'
            )}""" (2)
        // Using returnStatus
        EXIT_STATUS = """${sh(
                returnStatus: true,
                script: 'exit 1'
            )}"""
    }
    stages {
        stage('Example') {
            environment {
                DEBUG_FLAGS = '-g'
            }
            steps {
                sh 'printenv'
            }
        }
    }
}
1 必须在流水线顶层设置 agent。如果设为 agent none,此例会失败。
2 使用 returnStdout 时,返回字符串末尾会附加空白字符;使用 .trim() 可将其移除。

处理凭据

可以在流水线中直接使用 Jenkins 已配置的凭据。更多信息见“使用凭据”。

在 Jenkins 中正确处理凭据

原文视频

密文、用户名与密码以及秘密文件

Jenkins 声明式流水线提供 credentials() 辅助方法,在 environment 指令中使用,支持密文、用户名和密码,以及秘密文件凭据。若要处理其他类型,请参见“其他凭据类型”。

密文

下面的代码演示如何使用环境变量,在流水线中使用密文凭据。

本例将两个密文凭据分配给不同的环境变量,用于访问 Amazon Web Services(AWS)。这些凭据应已在 Jenkins 中配置,其 ID 分别为 jenkins-aws-secret-key-id 和 jenkins-aws-secret-access-key。

Jenkinsfile(声明式流水线)

pipeline {
    agent {
        // Define agent details here
    }
    environment {
        AWS_ACCESS_KEY_ID     = credentials('jenkins-aws-secret-key-id')
        AWS_SECRET_ACCESS_KEY = credentials('jenkins-aws-secret-access-key')
    }
    stages {
        stage('Example stage 1') {
            steps {
                // (1)
            }
        }
        stage('Example stage 2') {
            steps {
                // (2)
            }
        }
    }
}
1 在此阶段的 steps 中,可以通过 $AWS_ACCESS_KEY_ID 和 $AWS_SECRET_ACCESS_KEY 引用流水线 environment 指令定义的两个凭据环境变量。例如,可使用分配给这些变量的密文凭据向 AWS 认证。为降低敏感信息泄露到控制台和日志的风险,如果任务在流水线中显示凭据变量的值,例如 echo $AWS_SECRET_ACCESS_KEY,Jenkins 只会返回 ****;凭据 ID 中的敏感信息(如用户名)也会在运行输出中显示为 ****。这只能降低意外泄露风险,无法阻止恶意用户通过其他手段获取凭据值。使用凭据的流水线也能泄露凭据。不要允许不可信的流水线任务使用可信凭据。
2 本例中,分配给两个 AWS_… 环境变量的凭据在整个流水线中全局有效,因此也可以在此阶段的步骤中使用。如果将 environment 指令移入某个特定阶段,如后面的用户名与密码示例所示,那么这些 AWS_… 环境变量就只在该阶段的步骤中有效。
在 Jenkins 凭据中保存静态 AWS 密钥并不十分安全。如果 Jenkins 本身(至少其代理)可以运行于 AWS,最好为计算机或 EKS 服务账户使用 IAM 角色,也可以使用 Web 身份联合。
用户名与密码

以下代码片段演示如何通过环境变量,在流水线中使用用户名与密码凭据。

本例把用户名和密码凭据分配给环境变量,以访问组织公共账户或团队下的 Bitbucket 仓库;这些凭据应已在 Jenkins 中配置,ID 为 jenkins-bitbucket-common-creds。

在 environment 指令中设置凭据环境变量时:

environment {
    BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
}

实际会设置以下三个环境变量:

  • BITBUCKET_COMMON_CREDS:包含用冒号分隔的用户名和密码,格式为 username:password。

  • BITBUCKET_COMMON_CREDS_USR:额外变量,仅包含用户名。

  • BITBUCKET_COMMON_CREDS_PSW:额外变量,仅包含密码。

按照惯例,环境变量名通常全部大写,并使用下划线分隔单词。不过,也可以使用合法的小写变量名。注意,credentials() 方法创建的额外环境变量始终以 _USR 和 _PSW 结尾,即一个下划线加三个大写字母。

下面是该示例的完整流水线:

Jenkinsfile(声明式流水线)

pipeline {
    agent {
        // Define agent details here
    }
    stages {
        stage('Example stage 1') {
            environment {
                BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
            }
            steps {
                // (1)
            }
        }
        stage('Example stage 2') {
            steps {
                // (2)
            }
        }
    }
}
1 下列凭据环境变量定义于此流水线的 environment 指令中,可在此阶段的步骤内使用,引用语法如下:

  • $BITBUCKET_COMMON_CREDS

  • $BITBUCKET_COMMON_CREDS_USR

  • $BITBUCKET_COMMON_CREDS_PSW

例如,可以使用分配给这些变量的用户名和密码向 Bitbucket 认证。如果任务在流水线中显示凭据变量的值,Jenkins 对用户名与密码变量的保护行为,与上面的密文示例相同。这只能降低意外泄露风险,不能阻止恶意用户通过其他方式取得凭据值。使用凭据的流水线也能泄露凭据。不要允许不可信的流水线任务使用可信凭据。

2 本例中的三个 BITBUCKET_COMMON_CREDS… 环境变量仅在 Example stage 1 内有效,因此无法在 Example stage 2 的步骤中使用。如果将 environment 指令移到 pipeline 块直接内部,如上面的密文示例所示,它们就会在整个流水线中全局有效,可供任意阶段使用。
秘密文件

秘密文件是一种保存在文件中、并上传到 Jenkins 的凭据。它适用于以下凭据:

  • 内容过于繁杂,不便直接输入 Jenkins;以及/或者

  • 采用二进制格式,例如 GPG 文件。

本例使用一个 Kubernetes 配置文件,该文件已经配置为名为 my-kubeconfig 的秘密文件凭据。

Jenkinsfile(声明式流水线)

pipeline {
    agent {
        // Define agent details here
    }
    environment {
        // The MY_KUBECONFIG environment variable will be assigned the value of a temporary file.
        // For example:
        //   /home/user/.jenkins/workspace/cred_test@tmp/secretFiles/546a5cf3-9b56-4165-a0fd-19e2afe6b31f/kubeconfig.txt
        MY_KUBECONFIG = credentials('my-kubeconfig')
    }
    stages {
        stage('Example stage 1') {
            steps {
                sh("kubectl --kubeconfig $MY_KUBECONFIG get pods")
            }
        }
    }
}

其他凭据类型

如果流水线需要处理密文、用户名与密码、秘密文件之外的其他凭据,例如 SSH 密钥或证书,请使用 Jenkins 经典界面中的代码片段生成器。

进入流水线项目的代码片段生成器:

  1. 在 Jenkins 仪表板中选择流水线项目名称。

  2. 在左侧导航栏选择 Pipeline Syntax,并确认导航栏顶部提供 Snippet Generator。

  3. 在 Sample Step 字段选择 withCredentials: Bind credentials to variables。

  4. 在 Bindings 下点击 Add,然后从下拉列表选择:

    • SSH User Private Key:处理 SSH 公钥/私钥对凭据,可设置:

      • Key File Variable:绑定此凭据的环境变量名称。Jenkins 实际将此临时变量设为私钥文件的安全存储位置,该文件用于 SSH 密钥对认证。

      • Passphrase Variable(可选):绑定 SSH 公钥/私钥对口令的环境变量名称。

      • Username Variable(可选):绑定 SSH 公钥/私钥对关联用户名的环境变量名称。

      • Credentials:选择保存在 Jenkins 中的 SSH 公钥/私钥凭据。此字段的值是凭据 ID,Jenkins 会将其写入生成的代码片段。

    • Certificate:处理 PKCS#12 证书,可设置:

      • Keystore Variable:绑定此凭据的环境变量名称。Jenkins 将此临时变量设为证书密钥库文件的安全存储位置,该文件用于证书认证。

      • Password Variable(可选):绑定证书密码的环境变量名称。

      • Alias Variable(可选):绑定证书唯一别名的环境变量名称。

      • Credentials:选择保存在 Jenkins 中的证书凭据。此字段的值是凭据 ID,Jenkins 会将其写入生成的代码片段。

    • Docker client certificate:处理 Docker 主机证书认证。

  5. 点击 Generate Pipeline Script,Jenkins 会针对指定凭据生成 withCredentials(…) { … } 流水线步骤片段,可复制到声明式或脚本式流水线中。注意:

    • 上面的 Credentials 字段显示 Jenkins 中配置的凭据名称;点击 Generate Pipeline Script 后,这些值会转换为凭据 ID。

    • 若要在一个 withCredentials(…) { … } 步骤中组合多个凭据,参见下方“在同一步骤中组合凭据”。

SSH User Private Key 示例

withCredentials(bindings: [sshUserPrivateKey(credentialsId: 'jenkins-ssh-key-for-abc', \
                                             keyFileVariable: 'SSH_KEY_FOR_ABC', \
                                             passphraseVariable: '', \
                                             usernameVariable: '')]) {
  // some block
}

最终流水线代码可以删除可选的 passphraseVariable 和 usernameVariable 定义。

Certificate 示例

withCredentials(bindings: [certificate(aliasVariable: '', \
                                       credentialsId: 'jenkins-certificate-for-xyz', \
                                       keystoreVariable: 'CERTIFICATE_FOR_XYZ', \
                                       passwordVariable: 'XYZ-CERTIFICATE-PASSWORD')]) {
  // some block
}

最终流水线代码可以删除可选的 aliasVariable 和 passwordVariable 定义。

下面展示实现上述 SSH User Private Key 与 Certificate 片段的完整流水线:

Jenkinsfile(声明式流水线)

pipeline {
    agent {
        // define agent details
    }
    stages {
        stage('Example stage 1') {
            steps {
                withCredentials(bindings: [sshUserPrivateKey(credentialsId: 'jenkins-ssh-key-for-abc', \
                                                             keyFileVariable: 'SSH_KEY_FOR_ABC')]) {
                  // (1)
                }
                withCredentials(bindings: [certificate(credentialsId: 'jenkins-certificate-for-xyz', \
                                                       keystoreVariable: 'CERTIFICATE_FOR_XYZ', \
                                                       passwordVariable: 'XYZ-CERTIFICATE-PASSWORD')]) {
                  // (2)
                }
            }
        }
        stage('Example stage 2') {
            steps {
                // (3)
            }
        }
    }
}
1 在此步骤中,通过 $SSH_KEY_FOR_ABC 引用凭据环境变量。例如,可以使用已配置的 SSH 公钥/私钥对凭据向 ABC 应用认证,其中 SSH 用户私钥文件的位置赋给了 $SSH_KEY_FOR_ABC。
2 在此步骤中,通过 $CERTIFICATE_FOR_XYZ 和 $XYZ-CERTIFICATE-PASSWORD 引用凭据环境变量。例如,可以使用已配置的证书凭据向 XYZ 应用认证;证书的密钥库文件和密码分别赋给这两个变量。
3 本例中的 $SSH_KEY_FOR_ABC、$CERTIFICATE_FOR_XYZ 和 $XYZ-CERTIFICATE-PASSWORD 环境变量,只在各自的 withCredentials(…) { … } 步骤内有效,因此 Example stage 2 中不能使用它们。

如果在这些 withCredentials(…) { … } 步骤内尝试输出凭据变量的值,SSH 密钥对和证书变量也会采用上面密文示例所述的保护行为。这只能降低意外泄露风险,不能阻止恶意用户通过其他方式取得凭据值。使用凭据的流水线也能泄露凭据。不要允许不可信的流水线任务使用可信凭据。

  • 在代码片段生成器的 Sample Step 字段使用 withCredentials: Bind credentials to variables 时,各个 Credentials 列表只允许选择当前流水线项目有权访问的凭据。虽然可以像上面的示例一样手工编写 withCredentials(…) { … },但推荐使用生成器,避免指定超出项目权限范围的凭据,否则步骤运行时会失败。

  • 也可以使用代码片段生成器生成 withCredentials(…) { … },处理密文、用户名与密码以及秘密文件。不过,如果只需要这些凭据类型,建议采用上节的对应方法,以提高流水线代码的可读性。

  • 上面的 Groovy 脚本定义使用单引号,而不是双引号;该脚本是 sh 的隐式参数。单引号会让 shell 将秘密值作为环境变量展开。双引号可能更不安全,因为 Groovy 会先插入秘密值,导致常见的操作系统进程列表意外暴露它:

node {
  withCredentials([string(credentialsId: 'mytoken', variable: 'TOKEN')]) {
    sh /* WRONG! */ """
      set +x
      curl -H "Token: $TOKEN" https://some.api/
    """
    sh /* CORRECT */ '''
      set +x
      curl -H "Token: $TOKEN" https://some.api/
    '''
  }
}
在同一步骤中组合凭据

通过代码片段生成器,可以按以下步骤在单个 withCredentials(…) { … } 中提供多个凭据:

  1. 在 Jenkins 仪表板中选择流水线项目名称。

  2. 在左侧导航栏选择 Pipeline Syntax,并确认导航栏顶部提供 Snippet Generator。

  3. 在 Sample Step 字段选择 withCredentials: Bind credentials to variables。

  4. 在 Bindings 下点击 Add。

  5. 从下拉列表选择要加入 withCredentials(…) { … } 步骤的凭据类型。

  6. 填写凭据的 Bindings 详情;相关字段说明见上面的“其他凭据类型”步骤。

  7. 每增加一项或一组凭据,都从上面的“点击 Add”开始重复操作。

  8. 选择 Generate Pipeline Script,生成最终的 withCredentials(…) { … } 步骤片段。

字符串插值

Jenkins 流水线的字符串插值规则与 Groovy 相同。Groovy 的插值机制可能让初学者困惑。虽然可以用单引号或双引号声明字符串,例如:

def singlyQuoted = 'Hello'
def doublyQuoted = "World"

但只有双引号字符串支持基于美元符号 $ 的字符串插值,例如:

def username = 'Jenkins'
echo 'Hello Mr. ${username}'
echo "I said, Hello Mr. ${username}"

输出结果为:

Hello Mr. ${username}
I said, Hello Mr. Jenkins

掌握字符串插值,对于使用流水线的一些高级功能至关重要。

敏感环境变量的插值

绝不要对凭据使用 Groovy 字符串插值。

Groovy GString(双引号或三双引号字符串)会在命令发送给代理之前展开 ${TOKEN}。秘密值因此被复制到进程参数中,可通过 ps 等工具看到;秘密值或用户可控值中的 shell 元字符也会在命令执行时生效。应始终向 sh、bat、powershell 或 pwsh 传递字面命令字符串,让 shell 从环境中读取秘密值;withCredentials 或 environment 块已经将这些值写入环境。

Jenkinsfile(声明式流水线)

pipeline {
    agent any
    environment {
        API_TOKEN = credentials('example-token-id')
    }
    stages {
        stage('Example') {
            steps {
                /* WRONG */
                    sh "curl -H 'Authorization: Bearer ${API_TOKEN}' https://example.com"
                """
            }
        }
    }
}

Jenkinsfile(声明式流水线)

pipeline {
    agent any
    environment {
        API_TOKEN = credentials('example-token-id')
    }
    stages {
        stage('Example') {
            steps {
                /* CORRECT */
                sh 'curl -H "Authorization: Bearer ${API_TOKEN}" https://example.com'
            }
        }
    }
}

任何避免插值的 Groovy 写法,例如 sh(script: ‘curl … $API_TOKEN’, label: ‘call API’),都符合这一原则;关键是让秘密值远离 GString,只由 shell 展开它们。

通过插值注入命令

Groovy 字符串插值可能借助特殊字符向命令解释器注入恶意命令。

还要注意:在 sh、bat、powershell 或 pwsh 等把参数交给命令解释器的步骤中,对用户可控变量使用 Groovy 插值,可能引发类似 SQL 注入的问题。用户可控变量通常是环境变量,常见来源是传给构建的参数。当含有 /、反斜杠、$、&、%、^、>、<、|、; 等特殊字符的值经 Groovy 插值传入这些步骤时,就可能发生问题。例如:

Jenkinsfile(声明式流水线)

pipeline {
  agent any
  parameters {
    string(name: 'STATEMENT', defaultValue: 'hello; ls /', description: 'What should I say?')
  }
  stages {
    stage('Example') {
      steps {
        /* WRONG! */
        sh("echo ${STATEMENT}")
      }
    }
  }
}

本例中,sh 的参数由 Groovy 求值,STATEMENT 被直接插入参数,等同于在流水线中写了 sh('echo hello; ls /')。代理执行时,不会原样输出 hello; ls /,而是先输出 hello,再列出代理的整个根目录。任何能够控制被插值变量的用户,都可能让 sh 在代理上运行任意代码。为避免这个问题,对引用参数或其他用户可控环境变量的 sh、bat 等步骤参数使用单引号,避免 Groovy 插值。

Jenkinsfile(声明式流水线)

pipeline {
  agent any
  parameters {
    string(name: 'STATEMENT', defaultValue: 'hello; ls /', description: 'What should I say?')
  }
  stages {
    stage('Example') {
      steps {
        /* CORRECT */
        sh('echo ${STATEMENT}')
      }
    }
  }
}

包含特殊字符的凭据经 Groovy 插值传给步骤时,还可能发生凭据变形。变形后的值既不再有效,也不再会被控制台日志遮蔽。

Jenkinsfile(声明式流水线)

pipeline {
  agent any
  environment {
    EXAMPLE_KEY = credentials('example-credentials-id') // Secret value is 'sec%ret'
  }
  stages {
    stage('Example') {
      steps {
          /* WRONG! */
          bat "echo ${EXAMPLE_KEY}"
      }
    }
  }
}

这里 bat 接收到 echo sec%ret,Windows 批处理 shell 会去掉 %,输出 secret。由于与原凭据相差一个字符,secret 不会被遮蔽。虽然它并非实际凭据的完整值,仍然构成严重的敏感信息泄露。使用单引号也可以避免此问题。

Jenkinsfile(声明式流水线)

pipeline {
  agent any
  environment {
    EXAMPLE_KEY = credentials('example-credentials-id') // Secret value is 'sec%ret'
  }
  stages {
    stage('Example') {
      steps {
          /* CORRECT */
          bat 'echo %EXAMPLE_KEY%'
      }
    }
  }
}

处理参数

声明式流水线原生支持参数,可通过 parameters 指令在运行时接收用户指定的参数。脚本式流水线使用 properties 步骤配置参数,可在代码片段生成器中找到它。

如果通过 Build with Parameters 配置流水线接受参数,就可以通过 params 变量的成员访问这些参数。

假设 Jenkinsfile 中配置了名为 Greeting 的字符串参数,可以通过 ${params.Greeting} 访问:

Jenkinsfile(声明式流水线)

pipeline {
    agent any
    parameters {
        string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
    }
    stages {
        stage('Example') {
            steps {
                echo "${params.Greeting} World!"
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

properties([parameters([string(defaultValue: 'Hello', description: 'How should I greet the world?', name: 'Greeting')])])

node {
    echo "${params.Greeting} World!"
}

处理失败

声明式流水线默认通过 post 节提供完善的失败处理,可以声明 always、unstable、success、failure 和 changed 等不同的后置条件。具体用法见流水线语法文档。

Jenkinsfile(声明式流水线)

pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                sh 'make check'
            }
        }
    }
    post {
        always {
            junit '**/target/*.xml'
        }
        failure {
            mail to: team@example.com, subject: 'The Pipeline failed :('
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

node {
    /* .. snip .. */
    stage('Test') {
        try {
            sh 'make check'
        }
        finally {
            junit '**/target/*.xml'
        }
    }
    /* .. snip .. */
}

脚本式流水线则依赖 Groovy 内置的 try/catch/finally 语义,处理流水线执行期间的失败。

前面的测试示例通过 sh 'make check || true',使 sh 永不返回非零退出码。此方法有效,但意味着后续阶段必须检查 currentBuild.result,才能知道是否发生测试失败。

另一种方法是使用 try/finally 块:既保留流水线失败时提前退出的行为,又让 junit 有机会收集测试报告:

错误处理步骤

Jenkins 流水线提供专用步骤进行灵活的错误处理,控制流水线如何响应错误和警告。这些步骤可以在 Jenkins 中清楚地呈现错误与警告,并决定流水线失败、继续执行,还是仅报告警告。更多信息参见:

使用多个代理

前面的示例都只使用一个代理。这意味着 Jenkins 会在可用位置分配执行器,不论其标签或配置如何。此行为可以覆盖;流水线也允许在同一个 Jenkinsfile 中使用 Jenkins 环境中的多个代理,适用于跨平台构建或测试等高级场景。

下例在一个代理上执行构建,然后在测试阶段由两个后续代理复用构建结果;这两个代理分别具有 linux 和 windows 标签。

Jenkinsfile(声明式流水线)

pipeline {
    agent none
    stages {
        stage('Build') {
            agent any
            steps {
                checkout scm
                sh 'make'
                stash includes: '**/target/*.jar', name: 'app' (1)
            }
        }
        stage('Test on Linux') {
            agent { (2)
                label 'linux'
            }
            steps {
                unstash 'app' (3)
                sh 'make check'
            }
            post {
                always {
                    junit '**/target/*.xml'
                }
            }
        }
        stage('Test on Windows') {
            agent {
                label 'windows'
            }
            steps {
                unstash 'app'
                bat 'make check' (4)
            }
            post {
                always {
                    junit '**/target/*.xml'
                }
            }
        }
    }
}

Toggle Scripted Pipeline
(Advanced)

Jenkinsfile(脚本式流水线)

stage('Build') {
    node {
        checkout scm
        sh 'make'
        stash includes: '**/target/*.jar', name: 'app' (1)
    }
}

stage('Test') {
    node('linux') { (2)
        checkout scm
        try {
            unstash 'app' (3)
            sh 'make check'
        }
        finally {
            junit '**/target/*.xml'
        }
    }
    node('windows') {
        checkout scm
        try {
            unstash 'app'
            bat 'make check' (4)
        }
        finally {
            junit '**/target/*.xml'
        }
    }
}
1 stash 步骤收集符合包含模式 **/target/*.jar 的文件,以便在同一次流水线中复用。流水线执行结束后,暂存文件会从 Jenkins 控制器删除。
2 agent/node 中的参数可以是任何有效的 Jenkins 标签表达式。更多信息见流水线语法文档。
3 unstash 将指定名称的暂存内容从 Jenkins 控制器取回到流水线当前工作区。
4 bat 可以在 Windows 平台执行批处理脚本。

可选的步骤参数

流水线遵循 Groovy 约定,允许省略方法参数两侧的括号。

许多流水线步骤还使用命名参数语法,作为创建 Groovy Map 的简写;Map 语法为 [key1: value1, key2: value2]。因此,下面两条语句功能相同:

git url: 'git://example.com/amazing-project.git', branch: 'master'
git([url: 'git://example.com/amazing-project.git', branch: 'master'])

为方便起见,调用只接受一个参数或只有一个必需参数的步骤时,可以省略参数名,例如:

sh 'echo hello' /* short form  */
sh([script: 'echo hello'])  /* long form */

高级脚本式流水线

脚本式流水线是基于 Groovy 的领域专用语言 [3];大多数 Groovy 语法无需修改就能使用。

并行执行

上例在两个不同平台上依次运行测试。实际使用中,如果 make check 需要 30 分钟,那么整个测试阶段就要 60 分钟。

流水线内置并行执行脚本式流水线部分代码的功能,由 parallel 步骤实现。

将上面的示例改为使用 parallel:

Jenkinsfile(脚本式流水线)

stage('Build') {
    /* .. snip .. */
}

stage('Test') {
    parallel linux: {
        node('linux') {
            checkout scm
            try {
                unstash 'app'
                sh 'make check'
            }
            finally {
                junit '**/target/*.xml'
            }
        }
    },
    windows: {
        node('windows') {
            /* .. snip .. */
        }
    }
}

只要 Jenkins 环境具有所需容量,带 linux 和 windows 标签的节点就会并行执行测试,而不再依次执行。

1. en.wikipedia.org/wiki/Source_control_management
2. en.wikipedia.org/wiki/Single_Source_of_Truth
3. en.wikipedia.org/wiki/Domain-specific_language

正文相关链接

来源与许可

原文:Using a Jenkinsfile。本页为中文翻译与排版整理,示例源码保留原样,权利归原作者及维护者所有。

源码核对备注:原文的部分示例带有用于对应说明表格的编号,并包含明确标为 WRONG 的反例。敏感变量插值的 WRONG 示例有一个孤立的三双引号;失败处理示例的 mail 参数中 team@example.com 未加引号。这里按原文保留,不能将这些展示片段直接视为已验证可运行的脚本。

Jenkins 官方用户手册;著作权归 Jenkins 项目及相应贡献者所有。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容