Last year I got introduced to the new features 2nd generation Jenkins at a workshop of CloudBees . This year I got also the opportunity to discover them in practice.
One of the most interesting and often challenging part of working with scripted pipelines is the gap between the list of plugins supported in Jenkinsfiles vs. only effective from the GUI. Even though the gap is very small, sometimes it involves an operation crucial or at least very much appreciated in your pipeline. Some kind of drawback of the Groovy-like, indulgent pipeline scripts is when a plugin has a well defined API and a syntax, which is fully tolerated when you actually try to run your job, however, falling in the gap, it may simply do nothing or do something else than you expected.
Last time I encountered the opposite. It was the active choices plugin actually with no positively propagated support for pipeline scripts. This plugin does a great job as we can define the parameter values as a list returned from an embedded script in Groovy or Scripter.
When I adapted the parameters I created for my job with the GUI to my Jenkinsfile, fortunately, they showed up in the job correctly and they also worked properly when initiating a build.
The Case
The purpose of the pipeline was to build, publish and and release a (large) piece of software sourced in Git repository App on a Bitbucket server. Having a quite monolithic code with about 5 scrum teams working on it, an excessive branching strategy, using feature, release and hotfix branches, is applied to guarantee maximal consistency.
For operational and legacy reasons, the release pipeline must have been driven from a different repository Gen. WIth other words “Gen” is the location of our Jenkinsfile.
The release process knows 3 scenarios based on the actual phase of the software version:
- Development phase
- Publishing and delivering an intermediate release version at the end of each sprint
- This version is managed in a sprint branch created from the actual state of the main branch
- After a successful delivery, the branch is merged back to the main branch
- Acceptance test phase
- Publishing and delivering a release version ready for acceptance at the end of each 12 sprints
- This version is managed in a numbered release branch
- Code fixes resulted by acceptance test findings are released as patches
- Production phase
- Publishing and delivering fixes based on findings on the live system
- This version is managed in a hotfix branch
For our case the important part of each phase is that the pipeline job to run:
- Has to determine in which phase we are
- Collect (filter) the applicable branches into a choice parameter of the App Git repository before the build is started
- The filtered collection of the branches must be based on the phase
The Solution
There are a couple of possibilities to make life simpler:
- Trusting the user filling in the correct branch name. The choice parameter is not needed.
- Defining the choices manually. We have to keep track on the repository and update the job regularly.
- Using the scripted choice parameter provided by the plugin in the GUI, the Jenkinsfile just take care of the build.
In this case, the choice (Groovy) script has to be modified only when the filtering criteria changes. However, our goal is to source-control as much as possible including the job parameters. A multi-branch pipelineis not to do as it scans all SCM objects configured in it. In each branch the corresponding Jenkinsfile must be present to execute the build. In our case the SCM of the branches and the SCM of the script are different.
OK. Let’s go for the fully scripted, active choice parameter of the branch.
Assumption: The actual build stages and steps are already programmed in the Jenkinsfile. We concentrate now only on the parameters.
Making the job “rebootable” and showing the dynamic parameter
Below is the “boot” function of the job. This is not a working script, just demonstrates the necessary elements.
- Import SecureGroovyScript class to use it for the CascadeChoiceParameter definition
- Propagate the Phase parameter to the inline Groovy scripts dynBranch and dynBranchFB
- In the script dynBranch the procedural invocation of Git command takes care of filtering the list of branches. Similar command can be used to collect e.g. tags.
- The SecureGroovyScript constructor has a boolean argument indicating Groovy sandbox. It can be left true unless certain static functions are invoked, when Jenkins rejects the sandbox usage. In this case it was reverse() method for the output list res.
- In order to pick up possible changes in repository name, branch selection criteria or description, the last parameter RegenerateJob has to be set true
- By checking the existence of RegenerateJob, we make sure at the very first build all parameters are added to the job
import groovy.transform.Field import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript ... @Field def repoApp = 'https://<Git repository URL of App>' @Field def props = [] @Field def newParams = [] ... setNewProps() ... // Setup job properties void setNewProps() { //Parameters are unknown at first load try { regenerateJob = (params.RegenerateJob == null) ? true : params.RegenerateJob } catch (MissingPropertyException e) { regenerateJob = true } if (regenerateJob) { def dynBranchFB = new SecureGroovyScript(""" def brMap = ['sprint':'main','acc':'release','prod':'hotfix'] def brs = brMap[Phase] String rS = "No \${brs} branch found" return [rS] """, true) def dynBranch = new SecureGroovyScript(""" def repurl = '${repoApp}' def brMap = ['sprint':['main'],'acc':['release*'],'prod':['hotfix*']] String[] brList = [] brMap[Fase].each { def proc = "git ls-remote -h \${repurl} \${it}".execute() String[] out = proc.text.split('\\n') brList += (out - '') } if (brList.size() < 1) throw RuntimeException("1") def res = brList.collect {elem -> elem.split('heads/')[1]} return res.reverse() """, false) println "Jenkins job ${env.JOB_NAME} gets updated." currentBuild.displayName = "#" + Integer.toString(currentBuild.number) + ": Initialize job" newParams += [$class: 'ChoiceParameterDefinition', name: 'Phase', choices: ['sprint', 'acc', 'prod']] newParams += [$class: 'CascadeChoiceParameter', name: 'Branch', referencedParameters: 'Phase', script: [ $class: 'GroovyScript', script: dynBranch, fallbackScript: dynBranchFB ], choiceType: 'PT_SINGLE_SELECT', description: 'De name of the branch.' ] newParams += [$class: 'BooleanParameterDefinition', name: 'RegenerateJob', defaultValue: false] props += [$class: 'ParametersDefinitionProperty', parameterDefinitions: newParams] properties(properties: props) } }
Invoke the pipeline job
When the job is created, it stands with no parameters.
After the first build, the job is regenerated and picked up its new parameters
The main branch is not found because I did not use a real Git repository to find the branches. Therefore the plugin activated the fallback script dynBranchFB.