Jenkins: Creating a custom pipeline step in your library
Pipelines in Jenkins, especially the declarative kind, can be very powerful and expressive at the same time.
As a caveat, it is also very easy to get overly verbose by (mis)using the
script
-tag to write all business logic not provided by Jenkins by default or by a plugin.
This post is part of a Series(Verwijst naar een externe website) in which I elaborate on my best practices for running Jenkins at scale which might benefit Agile teams and CI/CD efforts.
The Jenkins Shared Library has a solution for this, by enabling you to write your own custom pipeline steps, without creating a plugin.
Creating your custom step
Shared Libraries can define global variables which behave similarly to built-in steps, like
sh
or
git
. These Global variables defined in Shared Libraries must be named with all lower-case or “camelCased” in order to be loaded properly by a pipeline. If you fail to comply with this, you’ll be greeted by a
org.codehaus.groovy.control.MultipleCompilationErrorsException
.
Remember the layout of your library?
Your custom step needs to be in the folder
vars
. Including the other folders, you should have a layout like this:
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── resources
├── settings.gradle
├── src
├── test
└── vars
For example, to define your custom step
logError
, the file
vars/logError.groovy
should be created and should implement a
call
method. The
call
method allows the global variable to be invoked in a manner similar to a step:
// vars/logError.groovy
def call(String message) {
// Any valid steps can be called from this code, just like
// in a Scripted Pipeline
echo "[ERROR]: ${message}"
}
A pipeline would be able to invoke this custom step like it would any regular step:
steps {
logError 'Something went awfully wrong here.'
}
Creating a wrapper
The ability to define Global Variables in your Shared Library can also be extended to create a wrapper. The requirements are similar, but you’ll have to call the function with a block. The
call
method will receive a(Verwijst naar een externe website)
closure
. The type should be defined explicitly to clarify the intent of the step, for example:
// vars/ubuntu.groovy
def call(Closure body) {
node('ubuntu-linux') {
body()
}
}
The pipeline can then use this wrapper like any built-in step which accepts a block:
ubuntu {
sh "cat /etc/*-release"
}
Creating a module
A Global Variable can also be used to create a ‘module’ of sorts, where you can have a set of functions to call from a pipeline. An example of a module containing a collection of utility functions could be:
// vars/module_Utilities.groovy
import groovy.json.JsonSlurperClassic
Map parseJSONString(String json) {
def jsonSlurper = new JsonSlurperClassic()
return jsonSlurper.parseText(json) as Map
}
Calling a function within such a module differs slightly from what we’ve seen before as it needs to be wrapped by a
script
block:
steps {
script {
module_Utilities.parseJSONString("{foo: 'bar'}")
}
}
Next steps
Now that you know how to create a Global Variable in your library which can act as a pipeline step, a wrapper, or even a set of globally available functions, the next step is to start validating the functionality within your library. The code in the library is much like any other code: it performs certain tasks and should, therefore, be subject to testing, to minimize the risk of issues occurring due to the introduction of bugs.