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') {

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.

Houd jij je kennis graag up to date?

Mis niets meer van onze kennisdocumenten, events, blogs en cases: ontvang als eerste het laatste nieuws in je inbox!

Fijn dat we je op de hoogte mogen houden!