This is a guest post by Liam Newman, Technical Evangelist at CloudBees.

Declare Your Pipelines! Declarative Pipeline 1.0 is here! This is the fourth post in a series showing some of the cool features of Declarative Pipeline.

In the previous post, we integrated several notification services into a Declarative Pipeline. We kept our Pipeline clean and easy to understand by using a shared library to make a custom step called sendNotifications that we called at the start and end of our Pipeline.

In this blog post, we’ll start by translating the Scripted Pipeline in the sample project I worked with in "Browser-testing with Sauce OnDemand and Pipeline" and "xUnit and Pipeline" to Declarative. We’ll make our Pipeline clearer by adding an environment directive to define some environment variables, and then moving some code to a shared library. Finally, we’ll look at using the when directive to add simple conditional behavior to our Pipeline.

Setup

The setup for this post uses the same repository as the two posts above, my fork of the JS-Nightwatch.js sample project. I’ve once again created a branch specifically for this blog post, this time called blog/declarative/sauce.

Like the two posts above, this Pipeline will use the xUnit and Sauce OnDemand plugins. The xUnit plugin only needs to be installed, the Sauce OnDemand needs additional configuration. Follow Sauce Labs' configuration instructions to create an account with Sauce Labs and add your Sauce Labs credentials to Jenkins. The Sauce OnDemand plugin will automatically install Sauce Connect for us when we call it from our Pipeline.

Be sure to you have the latest version of the Sauce OnDemand plugin (1.160 or newer). It has several fixes required for this post.

For a shared library, I’ve still got the one from the previous post. To set up this "Global Pipeline Library," navigate to "Manage Jenkins" → "Configure System" in the Jenkins web UI. Once there, under "Global Pipeline Libraries", add a new library. Then set the name to bitwiseman-shared, point it at my repository, and set the default branch for the library to master.

Global Pipeline Library

Reducing Complexity with Declarative

If you’ve been following along through this series, this first step will be quite familiar by now. We’ll start from the Pipeline we had at the end of the xUnit post and translate it to Declarative.

Jenkinsfile (Declarative Pipeline)
pipeline {
    agent any
    options {
        // Nightwatch.js supports color ouput, so wrap add his option
        ansiColor colorMapName: 'XTerm'
    }
    stages {
        stage ("Build") {
            steps {
                // Install dependencies
                sh 'npm install'
            }
        }
        stage ("Test") {
            steps {
                // Add sauce credentials
                sauce('f0a6b8ad-ce30-4cba-bf9a-95afbc470a8a') {
                    // Start sauce connect
                    sauceconnect() {
                        // Run selenium tests using Nightwatch.js
                        // Ignore error codes. The junit publisher will cover setting build status.
                        sh "./node_modules/.bin/nightwatch -e chrome,firefox,ie,edge --test tests/guineaPig.js || true"
                    }
                }
            }
            post {
                always {
                    step([$class: 'XUnitBuilder',
                        thresholds: [
                            [$class: 'SkippedThreshold', failureThreshold: '0'],
                            // Allow for a significant number of failures
                            // Keeping this threshold so that overwhelming failures are guaranteed
                            //     to still fail the build
                            [$class: 'FailedThreshold', failureThreshold: '10']],
                        tools: [[$class: 'JUnitType', pattern: 'reports/**']]])

                    saucePublisher()
                }
            }
        }
    }
Blue Ocean Run
SauceLabs Test Report
Blue Ocean doesn’t support displaying SauceLabs test reports yet (see JENKINS-42242). To view the report above, I had to switch back to the stage view of this run.

Elevating Settings using environment

Each time we’ve moved a project from Scripted Pipeline to Declarative, we’ve found the cleaner format of Declarative Pipeline highlights the less clear parts of the existing Pipeline. In this case, the first thing that jumps out at me is that the parameters of the Saucelabs and Nightwatch execution are hardcoded and buried down in the middle of our Pipeline. This is a relatively short Pipeline, so it isn’t terribly hard to find them, but as this pipeline grows and changes it would be better if those values were kept separate. In Scripted, we’d have defined some variables, but Declarative doesn’t allow us to define variables in the usual Groovy sense.

The environment directive let’s us set some environment variables and use them later in our pipeline. As you’d expect, the environment directive is just a set of name-value pairs. Environment variables are accessible in Pipeline via env.variableName (or just variableName) and in shell scripts as standard environment variables, typically $variableName.

Let’s move the list of browsers, the test filter, and the sauce credential string to environment variables.

Jenkinsfile
    environment {
        saucelabsCredentialId = 'f0a6b8ad-ce30-4cba-bf9a-95afbc470a8a'
        sauceTestFilter = 'tests/guineaPig.js'
        platformConfigs = 'chrome,firefox,ie,edge'
    }
    stages {
        /* ... unchanged ... */
        stage ("Test") {
            steps {
                // Add sauce credentials
                sauce(saucelabsCredentialId) {
                    // Start sauce connect
                    sauceconnect() {
                        // Run selenium tests using Nightwatch.js
                        // Ignore error codes. The junit publisher will cover setting build status.
                        sh "./node_modules/.bin/nightwatch -e ${env.platformConfigs} --test ${env.sauceTestFilter} || true" (1)
                    }
                }
            }
            post { /* ... unchanged ... */ }
        }
    }
}
1 This double-quoted string causes Groovy to replace the variables with their literal values before passing to sh. This could also be written using singe-quotes: sh './node_modules/.bin/nightwatch -e $platformConfigs --test $sauceTestFilter || true'. With a single quoted string, the string is passed as written to the shell, and then the shell does the variable substitution.

Moving Complex Code to Shared Libraries

Now that we have settings separated from the code, we can do some code clean up. Unlike the previous post, we don’t have any repeating code, but we do have some distractions. The nesting of sauce, sauceconnect, and sh nightwatch seems excessive, and that xUnit step is a bit ugly as well. Let’s move those into our shared library as custom steps with parameters. We’ll change the Jenkinsfile in our main project, and add the custom steps to a branch named blog/declarative/sauce in our library repository.

Jenkinsfile
@Library('bitwiseman-shared@blog/declarative/sauce') _

/* ... unchanged ... */

stage ("Test") {
    steps {
        sauceNightwatch saucelabsCredentialId,
            platformConfigs,
            sauceTestFilter
    }
    post {
        always {
            xUnitPublishResults 'reports/**',
                /* failWhenSkippedExceeds */ 0,
                /* failWhenFailedExceeds */ 10

            saucePublisher()
        }
    }
}
vars/sauceNightwatch.groovy
def call(String sauceCredential, String platforms = null, String testFilter = null) {
    platforms = platforms ? "-e '" + platforms + "'" : ''
    testFilter = testFilter ? "--test '" + testFilter + "'" : ''

    // Add sauce credentials
    sauce(sauceCredential) {
        // Start sauce connect
        sauceconnect() {
            // Run selenium tests using Nightwatch.js
            // Ignore error codes. The junit publisher will cover setting build status.
            sh "./node_modules/.bin/nightwatch ${platforms} ${testFilter} || true" (1)
        }
    }
}
1 In this form, this could not be written using a literal single-quoted string. Here, platforms and testFilter are groovy variables, not environment variables.
vars/xUnitPublishResults.groovy
def call(String pattern, Integer failWhenSkippedExceeds,
        Integer failWhenFailedExceeds) {
    step([$class: 'XUnitBuilder',
        thresholds: [
            [$class: 'SkippedThreshold', failureThreshold: failWhenSkippedExceeds.toString()],
            // Allow for a significant number of failures
            // Keeping this threshold so that overwhelming failures are guaranteed
            //     to still fail the build
            [$class: 'FailedThreshold', failureThreshold: failWhenFailedExceeds.toString()]],
        tools: [[$class: 'JUnitType', pattern: pattern]]])
}

Running Conditional Stages using when

This is a sample web testing project. We probably wouldn’t deploy it like we would production code, but we might still want to deploy somewhere, by publishing it to an artifact repository, for example. This project is hosted on GitHub and uses feature branches and pull requests to make changes. I’d like to use the same Pipeline for feature branches, pull requests, and the master branch, but I only want to deploy from master.

In Scripted, we’d wrap a stage in an if-then and check if the branch for the current run is named "master". Declarative doesn’t support that kind of general conditional behavior. Instead, it provides a when directive that can be added to stage sections. The when directive supports several types of conditions, including a branch condition, where the stage will run when the branch name matches the specified pattern. That is exactly what we need here.

Jenkinsfile
stages {
    /* ... unchanged ... */
    stage ('Deploy') {
        when {
            branch 'master'
        }
        steps {
             echo 'Placeholder for deploy steps.'
        }
    }
}

When we run our Pipeline with this new stage, we get the following outputs:

Log output for feature/test branch
...
Finished Sauce Labs test publisher
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Deploy)
Stage 'Deploy' skipped due to when conditional
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
...
Log output for master branch
...
Finished Sauce Labs test publisher
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Deploy)
[Pipeline] echo
Placeholder for deploy steps.
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
...

Conclusion

I have to say, our latest Declarative Pipeline turned out extremely well. I think someone coming from Freestyle jobs, with little to no experience with Pipeline or Groovy, would still be able to look at this Declarative Pipeline and make sense of what it is doing. We’ve added new functionality to our Pipeline while making it easier to understand and maintain.

I hope you’ve learned as much as I have during this blog series. I’m excited to see that even in the the short time since Declarative 1.0 was released, teams are already using it in make improvements similar to what those we’ve covered in this series. Thanks for reading!