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
finalkeyword defines, that this variable a constant.The
Stringkeyword defines the type as String. Using thedefkeyword 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:
declarative 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
trueorfalseIt 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
falseand checked totrue.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
timeoutof five minutes is declared, which automatically fails the pipeline, if it hasn’t completed within that timeframe. Thetimestamps()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
deftype), 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
scriptoption. 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: trueensure, 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
$VARor${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 viaerror, which uses variable interpolation in its error string, which requires it to use double quotes.- line 106-109:
The
containsmethod checks whether the array contains the given value.- line 111:
The
replacemethod replaces a given target string (here:'.dev') with a substitute (here:''(empty)) within a string.- line 120:
The
replaceFirstmethod 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: trueoption is an optional option of the sh command. If set totrue, 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 stringNEW_PULL_REQUEST_URL, followed with the matching group which must not contain null, the followed by the stringEOL.- 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 viamatch_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.groovyfile defines a call() method, which accepts a custom typeMonitorTypes(an enum, which is eitherMonitorTypes.SUCCESSorMonitorTypes.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 emptyNOTIFY_ON_SUCCESSstring defined in line 29) and a failing pipeline (usesNOTIFY_ON_FAILUREdefined 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.