An real-life example Jenkinsfile

This How-To shows an example of a Jenkinsfile used in an actual pipeline. It took me days to finish it (including writing additional script files), so in order to decrease the pain when writing the next one, I want to document the knowledge I gained while writing it.

For this purpose, the file is split in smaller sections, which include the literal content of the file and a detailed explanations of the meaning of each file.

Global variables

This section defines global variables, which are visible within the entire Jenkinsfile.

 1@Library('tts_shared') _
 2
 3// --------------------------------------------------------------------------------------------------------------------
 4//
 5// Constants
 6//
 7
 8final String ARTIFACTORY_SOURCE_REPO = 'tts-release/com/cerence'
 9final String CREDENTIAL_KEY = 'tts_rd_jenkins_writer'
10
11final String PRODUCT_METAPACKAGE_REPO = "cerence_neural_tts"
12final String PRODUCT_METAPACKAGE_REPO_URL = 'https://aac-srv-bitbucket.coretech.cerence.net/scm/trng21/cerence_neural_tts.git'
13
14final String PRODUCT_CHOICES = [
15    "CNTTS",
16    "CNTTS-VVT"
17].join("\n")
18
19final def PRODUCT_CHOICES_TO_PREFIX_SUFFIX_MAP = [
20    "CNTTS": "cntts",
21    "CNTTS-VVT": "vvt"
22]
23
24// separate multiple email recipients with ';'
25// use the empty string '' to avoid sending email notifications
26final String NOTIFY_ON_FAILURE = 'alberto.pettarin@cerence.com;stefaan.willems@cerence.com;rahul.ranjan@cerence.com'
27final String NOTIFY_ON_SUCCESS = ''
28
29// --------------------------------------------------------------------------------------------------------------------
line 1:

Defines the import of an external library. To be able to import such a library, it must be added to the Jenkins installation’s configuration, where it can be retrieved from an SCM (such as a git repository, where it is kept) and a name is defined (here: tts_shared) under which is can be imported.

See External Libraries for further information.

line 8 - 12:

Definition of constant values, which are available for the entire Jenkinsfile. As the Jenkinsfile follows the Groovy syntax (which itself derives from the Java syntax), variables at the top-level must be declared in the following manner:

final String VARIABLE_NAME = "some_value"

The final keyword defines, that this variable a constant.

The String keyword defines the type as String. Using the def keyword defines a variable without a type, which is defined at runtime.

All basic types: https://pledbrook.github.io/groovy-cheat-sheet/guide/index.html#_type_list

line 14-17:

This definition defines String which consists of an array of strings, which is joined into a single string with a new-line character as delimiter (join("\n"). This is a possible format needed to define the choices within the choice parameter, where all values must be separated via a new line character.

line 19-22:

The definition of a map, which contains two key-value pairs. The value of a key can be accessed via the get method (see https://docs.groovy-lang.org/latest/html/groovy-jdk/).

Agent, Parameters and Options

The agent, parameter and options sections are defined inside the pipeline section. They could also be defined in the Jenkins job configuration itself, but defining them inside the Jenkinsfile is a good practice, as changed are tracked and the Jenkinsfile can be applied to various pipeline jobs more easily.

31pipeline {
32
33    agent {
34        label 'ci-cntts-test'
35    }
36
37    parameters {
38        choice(
39            name: 'PRODUCT',
40            choices: "${PRODUCT_CHOICES}",
41            description: 'Product, whose product meta-package is promoted and uploaded to AF'
42        )
43        string(
44            name: 'SUBTASK_ID',
45            defaultValue: 'READER-XXXX',
46            description: 'Jira sub-task, which covers this release\'s promotion of the product metapackage (e.g. READER-1234)'
47        )
48        gitParameter(
49            branch: '',
50            branchFilter: 'origin/release/.*',
51            defaultValue: '',
52            description: 'Release branch (in cerence_neural_tts repo) to get development metapackage version from (default: NONE).',
53            name: 'PRODUCT_METAPACKAGE_REPO_CHECKOUT_SOURCE',
54            quickFilterEnabled: true,
55            selectedValue: 'DEFAULT',
56            sortMode: 'ASCENDING',
57            type: 'PT_BRANCH',
58            useRepository: "${PRODUCT_METAPACKAGE_REPO_URL}",
59            listSize: '10'
60        )
61    }
62
63    options {
64        timeout(time: 5, unit: 'MINUTES')
65        timestamps()
66    }
line 31:

The pipeline section starts a declarative pipeline. Jenkins supports two types of pipelines:

A declarative pipeline may contain scripted sections, which are equal to scripted pipelines. Declarative pipelines, though, have additional features (like restarting a pipelines from stage), but have limits in terms of what can be declared at which point.

line 33-35:

An agent, running on a Jenkins node, is defined via it’s label. During runtime, an agent, which has this label associated to it, is selected as the node, to run this pipeline on. If multiple agent have that same label, Jenkins selects one of them (preferably one, which currently does not execute any job, or which has the shorted queue).

line 37-61:

Declaration of the job’s input parameter. The parameter sections expect any available parameter, namely

  • string: a single line string

  • text: a single or multiline string

  • booleanParam: accepts true or false

    It may be used to not run certain stages, if the parameter is set to false:

    parameters {
        booleanParam(
            name: 'DO_SOMETHING',
            defaultValue: false,
            description: 'Select me, if you want to do something'
    }
    
    stage('Do something if not skipped') {
        when {
            expression { params.DO_SOMETHING == true }
        }
        steps {
            // ... do something
    

    In the Jenkins web UI’s job configuration, the parameter is a checkbox, where unchecked translates to false and checked to true.

  • choice: a drop-down list of predefined choices

  • password: a password string (designed for secrets, as it won’t be printed in the log output)

All parameter values can be accessed further down the Jenkinsfile via ${params.PARAMETER_NAME}.

Alternatively, parameters can be defined in a properties section. For this example:

properties([
    parameters([
        choice(
            name: 'PRODUCT',
            // ... other parameter settings
        ),
        string(
            name: 'SUBTASK_ID',
            // ... other parameter settings
        ),
        gitParameter(
            branch: '',
            // ... other parameter settings
        )
    ])
])

pipeline {
    // all other pipeline sections
}
line 48-60:

The Git Parameter plugin provides a gitParameter directive, which allows for assigning a git branch (used here), tag, pull request or a revision number as a build parameter.

In this case, the user is asked to select a branch (only branches matching the branchFilter expression are available) from the "${PRODUCT_METAPACKAGE_REPO_URL}" repository (defined as global variable in line 12) and assigns the selection to the PRODUCT_METAPACKAGE_REPO_CHECKOUT_SOURCE parameter variable.

line 63-66:

Pipeline options may be declared in the options section. A variety of options are available. Here, a timeout of five minutes is declared, which automatically fails the pipeline, if it hasn’t completed within that timeframe. The timestamps() option prepends all console outputs with a timestamp.

Stage - Checkout a git branch

Pipelines are divided into stages. Unless defined as parallel, all stages are executed in the specified order.

68stages {
69    stage('Checkout product repo release branch') {
70        steps {
71            script {
72                sh 'echo Checkout repository'
73                checkout([$class: 'GitSCM',
74                    branches: [[name: "${params.PRODUCT_METAPACKAGE_REPO_CHECKOUT_SOURCE}" ]],
75                    extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: "${PRODUCT_METAPACKAGE_REPO}"]],
76                    userRemoteConfigs: [[
77                        url: "${PRODUCT_METAPACKAGE_REPO_URL}",
78                        credentialsId: "${CREDENTIAL_KEY}"
79                    ]]
80                ])
81            }
82        }
83    }
line 68:

Start the stages section. It must contain at least one stage directive.

line 70-82:

Definition of a stage. It contains a steps section, which contains additional step directives defined in the Jenkins Steps Reference. It may also contain a script section, which contains a block of scripted pipeline, which allows for execution of a more complex series of steps.

In this case, it contains a script section. It first executes a sh command, which opens executes the shell script, in this case echo Checkout repository. The checkout checks out the sources of another git repository, specified via the URL ${PRODUCT_METAPACKAGE_REPO_URL}, wheras the specific branch ${params.PRODUCT_METAPACKAGE_REPO_CHECKOUT_SOURCE} is checked out (note that git checkout is done via the Git plugin).

Step - Configure environment variables

Pipelines may require some job or run-specific environment variables to be defined. If those are few and simple or already known at the very beginning of the pipeline, those can also be defined inside a environment directive, either at the above of the stages section or individual stage section (in case, those will only be needed for this single step).

As the definition becomes more complex or certain parameters are only known until checking out additional sources (as in our case), an extra stage, which globally defines those variables, becomes necessary. Note, that here, each environment variable must be added to the env object. Variables defined in the environment sections are automatically added to the env object.

 84stage("Configure environment variables") {
 85    steps {
 86        script {
 87            // Input validation
 88            if (!(params.SUBTASK_ID =~ /READER-\d+/)) {
 89                currentBuild.result = 'ABORTED'
 90                error "The entered SUBTASK_ID must match READER-[0-9]+ (e.g. READER-1234)"
 91            }
 92
 93            // Product variables
 94            env.NTTS_PRODUCT_PREFIX_SUFFIX = PRODUCT_CHOICES_TO_PREFIX_SUFFIX_MAP.get(params.PRODUCT)
 95
 96            env.CURRENT_METAPACKAGE_VERSION = sh(script: """
 97                cd ${WORKSPACE}/${PRODUCT_METAPACKAGE_REPO}/meta/${NTTS_PRODUCT_PREFIX_SUFFIX};
 98                python3 -c "from src.neuraltts.products.${NTTS_PRODUCT_PREFIX_SUFFIX}.__version__ import __version__;print(__version__)"
 99                """, returnStdout: true)
100
101            env.CURRENT_PRODUCT_VERSION = sh(script: """yq '.product.version' $WORKSPACE/$PRODUCT_METAPACKAGE_REPO/docker/"$NTTS_PRODUCT_PREFIX_SUFFIX"_build_deps.yaml""", returnStdout: true)
102            if (env.CURRENT_METAPACKAGE_VERSION != env.CURRENT_PRODUCT_VERSION) {
103                currentBuild.result = 'ABORTED'
104                error "The product version (${env.CURRENT_PRODUCT_VERSION}) and product metapackage version (${env.CURRENT_METAPACKAGE_VERSION}) are not identical. Make sure both versions match and then retry."""
105            }
106            if (!env.CURRENT_METAPACKAGE_VERSION.contains('dev')) {
107                currentBuild.result = 'ABORTED'
108                error "This release branch ($PRODUCT_METAPACKAGE_REPO_CHECKOUT_SOURCE) already uses a promoted (non-dev) metapackage version for ${env.PRODUCT}: ${env.CURRENT_METAPACKAGE_VERSION}. Pipeline run was aborted."
109            }
110
111            env.PROMOTED_METAPACKAGE_VERSION = "$CURRENT_METAPACKAGE_VERSION".replace('.dev', '').trim()
112
113            // Paths
114            env.MAKEFILE_DIR = "${WORKSPACE}/pipelines/common/promote_product_metapackage"
115            env.METAPACKAGE_SETUP_PY_FILE_PATH = "${WORKSPACE}/${PRODUCT_METAPACKAGE_REPO}/meta/${env.NTTS_PRODUCT_PREFIX_SUFFIX}/setup.py"
116            env.METAPACKAGE_VERSION_FILE_PATH ="${WORKSPACE}/${PRODUCT_METAPACKAGE_REPO}/meta/${env.NTTS_PRODUCT_PREFIX_SUFFIX}/src/neuraltts/products/${env.NTTS_PRODUCT_PREFIX_SUFFIX}/__version__.py"
117            env.PRODUCT_DESCRIPTORS_DIR_PATH = "${WORKSPACE}/${PRODUCT_METAPACKAGE_REPO}/docker"
118
119            // Git branches
120            env.PROMOTION_RELEASE_BRANCH_LOCAL = "$PRODUCT_METAPACKAGE_REPO_CHECKOUT_SOURCE".replaceFirst("origin/", "")
121            env.PROMOTION_PULL_REQUEST_BRANCH_LOCAL = "feature/${params.SUBTASK_ID}_promote_product_metapackage_${params.PRODUCT}_" + "$PROMOTED_METAPACKAGE_VERSION".replace('.', '-')
122        }
123    }
124}
line 94:

Defines the PRODUCT build parameter to the NTTS_PRODUCT_PREFIX_SUFFIX environment variables, by retrieving the associated value from the PRODUCT_CHOICES_TO_PREFIX_SUFFIX_MAP. The get() method returns the value of the given key (params.PRODUCT). It is a method provided by the Java layer, which Groovy builds upon. The type of the PRODUCT_CHOICES_TO_PREFIX_SUFFIX_MAP variable was not declared (using the def type), but assigned to the Map interface implementation during runtime.

line 96-99:

Defines the output of a shell script (sh) as the value of environment variable. The script itself must a string value assigned to the script option. If the string starts with triple single quotes ('''), the script allows for multiple lines without using line continuation (this is a requirement from Groovy). The same applies to using triple double-quotes (""").

For both, global or environmental variables put inside ${...} are interpolated by their value.

The returnStdout: true ensure, that the standard output value is returned and, in this case, assigned to a variable.

General rules

Use ''' specifically, if your script requires to use variables literally:

environment {
    VAR="global_value"

    script {
        sh '''
            VAR=local_value
            echo $VAR
        '''

which prints local_var, as $VAR is not replaced by the environmental value.

Use """ specifically, if your script requires variables to be interpolated by their actual value:

environment {
    VAR="global_value"

    script {
        sh """
            VAR=local_value
            echo $VAR
        """

which prints global_var, as $VAR is replaced by the environmental value.

This also applies for single quotes. Variables inside single quotes ('$VAR') are used literally, whereas inside double quotes ("$VAR") are replaced by their interpolated value.

In both single and double quotes script blocks, variables can be accessed both via $VAR or ${VAR}.

line 101-105:

Within the script block (which are executed in the default shell on the node, which is either bash on Linux or zsh on macOS), the environment variables inside the env object can be accessed via ${env.<VARIABLE_NAME>}. The block assigns the currentBuild global variable, settings it to 'ABORTED' and emitting and error signal via error, which uses variable interpolation in its error string, which requires it to use double quotes.

line 106-109:

The contains method checks whether the array contains the given value.

line 111:

The replace method replaces a given target string (here: '.dev') with a substitute (here: '' (empty)) within a string.

line 120:

The replaceFirst method replaces the first match of a given target (here: 'origin/' with a substitute (here: "" (empty)) within a string.

Stage - Execute Makefile target

More complex logic is advised to be moved into shell script. Furthermore, such shell script can be executed via a Makefile target.

128    stage("Get latest compatible metapackage dependencies from AF") {
129        steps {
130            dir("${MAKEFILE_DIR}") {
131                script {
132                    sh """
133                        CURRENT_METAPACKAGE_VERSION=${CURRENT_METAPACKAGE_VERSION} \
134                        NTTS_PRODUCT_PREFIX_SUFFIX=${NTTS_PRODUCT_PREFIX_SUFFIX} \
135                        ARTIFACTORY_SOURCE_REPO=${ARTIFACTORY_SOURCE_REPO} \
136                        JFROG_HOME=${JFROG_HOME} \
137                        make pull-python-dependencies
138                    """
139                }
140            }
141        }
142    }
line 130:

The dir block defines the working directory of all processes within the block (so from line 131 until line 139).

line 131 - 139:

A script section, which includes a sh command. As mentioned above, the script block contains a block of scripted pipeline. In this case, this includes the sh command, which opens executes the shell script. As the script opens with triple double-quotes (""") it is a multi-line shell script, which supports variable interpolation. As this multi-line script is actually a single line command, just broken into several lines for better readability, each line but the last requires the line continuation character \.

Stage - Run inline groovy script

Jenkins recommends to outsource more complex logic into separate Groovy script files, to keep a Jenkinsfile simpler and to allow reuse but other pipeline. Simpler scripts can still be defined within the Jenkinsfile itself.

It follows the Groovy syntax, while also supporting additional steps provided by Jenkins.

183    stage("Push promotion changes to remote and create pull request") {
184        steps {
185            dir("${WORKSPACE}/${PRODUCT_METAPACKAGE_REPO}") {
186                script {
187                    withCredentials([gitUsernamePassword(credentialsId: "${CREDENTIAL_KEY}")]) {
188                        def create_pull_request_log = sh(script: """
189                            PROMOTED_METAPACKAGE_VERSION=${PROMOTED_METAPACKAGE_VERSION} \
190                            NTTS_PRODUCT_PREFIX_SUFFIX=${NTTS_PRODUCT_PREFIX_SUFFIX} \
191                            PROMOTION_PULL_REQUEST_BRANCH_LOCAL=${PROMOTION_PULL_REQUEST_BRANCH_LOCAL} \
192                            PROMOTION_RELEASE_BRANCH_LOCAL=${PROMOTION_RELEASE_BRANCH_LOCAL} \
193                            PRODUCT_METAPACKAGE_PULL_REQUEST_URL=${PRODUCT_METAPACKAGE_PULL_REQUEST_URL} \
194                            SUBTASK_ID=${params.SUBTASK_ID} \
195                            CURRENT_BUILD_URL=${currentBuild.getAbsoluteUrl()} \
196                            MAKEFILE_DIR=${MAKEFILE_DIR} \
197                            make -f ${MAKEFILE_DIR}/Makefile push-promotion-changes-to-remote-and-create-pull-request
198                            """, returnStdout: true)
199
200                        /* Determine pull request URL and use as job description
201                        Note: an already existing PR is updated, if pipeline runs successfully
202                        another time against the same product release branch */
203                        def pull_request_url
204                        def new_pull_request_url_regex = /NEW_PULL_REQUEST_URL ((?!.*null).*) EOL/
205                        def existing_pull_request_url_regex = /EXISTING_PULL_REQUEST_URL ((?!.*null).*) EOL/
206
207                        def new_match = create_pull_request_log =~ new_pull_request_url_regex
208                        def existing_match = create_pull_request_log =~ existing_pull_request_url_regex
209
210                        if (new_match) {
211                            pull_request_url = new_match[0][1]
212                            currentBuild.description =
213                            """Pull request created: <a href="${pull_request_url}">${pull_request_url}</a>"""
214                        } else if (existing_match) {
215                            pull_request_url = existing_match[0][1]
216                            currentBuild.description =
217                            """Pull request updated: <a href="${pull_request_url}">${pull_request_url}</a>"""
218                        } else {
219                            currentBuild.description = "Cannot determine pull request URL - check logs"
220                        }
221                    }
222                }
223            }
224        }
225    }
line 187:

Opens a withCredentials block, which binds credentials, stored as a credentials set in Jenkins, to variables within this block. In this case, it calls the gitUsernamePassword(...) method, passing in the "${CREDENTIAL_KEY}" (defined in line 9) as credentialsID, referencing the credentials set predefined on the Jenkins instance under that same ID. Effectively, this sets the variables

  • $GIT_USERNAME

  • $GIT_PASSWORD

  • $GIT_ASKPASS

for the duration of the block.

line 188 - 198:

Assigns the variables create_pull_request_log as the stdout (standard output) of the shells script in line 189 - 197. The returnStdout: true option is an optional option of the sh command. If set to true, the standard output of the command is returned as string (in this case, whatever the Makefile target returns).

line 204 - 205:

These two lines each define a regular expression. The regex’s syntax follows the one for Java. They must start and end with a \. For line 204, a match must start with the string NEW_PULL_REQUEST_URL, followed with the matching group which must not contain null, the followed by the string EOL.

line 207 - 208:

Here the pattern is applied to the output of the command in line 188 -198. The syntax is: def match_object = input_text =~ regex_pattern.

line 211 + 215:

The match object contains the matching string, which accessed via match_string = match_object[0][1] in this case, as the entire matching text is accessed via match_object[0] and the [1] defines the match of the first capturing group, which is the wanted text.

Post section

The post section contains all steps, which are executed after the pipeline or it’s steps are completed.

228post {
229    success {
230        script {
231            nttsMonitorEmail nttsMonitorEmail.SUCCESS()
232        }
233    }
234    failure {
235        script {
236            nttsMonitorEmail nttsMonitorEmail.FAILURE()
237        }
238    }
239}
line 229:

This opens a success condition block, which is only executed, if the pipeline is marked as success.

line 231 + 236:

The nttsMonitorEmail is the name of an imported Groovy script (line 1). The nttsMonitorEmail.groovy file defines a call() method, which accepts a custom type MonitorTypes (an enum, which is either MonitorTypes.SUCCESS or MonitorTypes.FAILURE) which executes differently depending on the given argument. In this case, different recipients are contacted in case of a successful (none, as it uses the empty NOTIFY_ON_SUCCESS string defined in line 29) and a failing pipeline (uses NOTIFY_ON_FAILURE defined in line 28).

More on shared libraries: https://www.jenkins.io/doc/book/pipeline/shared-libraries/

line 234:

This opens a failure condition block, which is only executed, if the pipeline is marked as failure.