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 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 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.