Migrating to declarative Jenkins pipelines

Last year I shared my thoughts on Jenkins pipelines and provided a walkthrough of how we’re using pipelines at Mozilla. Since then, the Pipeline Model Definition plugin has came out of beta, and we’ve been migrating our pipelines to the new declarative syntax with a shared library.

Shared libraries

As soon as you’re implementing similar steps in multiple Jenkins pipelines, it makes sense to consider writing a shared library. Our fxtest-jenkins-pipeline library allows us to centrally maintain custom steps such as IRC notifications, or creating variables files with desired capabilities for Selenium, rather than implementing these in every pipeline.

Pipeline options

In our original pipelines it was necessary to wrap steps in order to configure timeouts, ANSI colours, and timestamps. With declarative this is made so much better by allowing pipeline-specific options to be configured. The following snippet demonstrates enabling all three of these:

options {
  ansiColor('xterm')
  timestamps()
  timeout(time: 1, unit: 'HOURS')
}

Everything defined in one place, and with less nesting/indentation vastly improves the readability and maintainability of the pipelines.

Environment variables

Another huge improvement is the handling of environment variables variables and accessing credentials. Previously it was necessary to wrap steps in withEnv for environment variables, and withCredentials for credentials. This is what we previously would have needed:

withCredentials([[
  $class: 'StringBinding',
  credentialsId: 'SAUCELABS_API_KEY',
  variable: 'SAUCELABS_API_KEY']]) {
  withEnv(["PYTEST_ADDOPTS=" +
    "-n=${processes} " +
    "--driver=SauceLabs " +
    "--variables=capabilities.json " +
    "--color=yes"]) {
      // ...
}

With declarative, this can be replaced with the following:

environment {
  PYTEST_ADDOPTS =
    "-n=10 " +
    "--tb=short " +
    "--color=yes " +
    "--driver=SauceLabs " +
    "--variables=capabilities.json"
  SAUCELABS_API_KEY = credentials('SAUCELABS_API_KEY')
}

Note that there’s a regression in version 1.1.1 of the plugin that prevents strings from being split over multiple lines, but this is already fixed and will be included in the next release.

Post build steps

Possibly the most valuable addition to declarative piplines are the post build steps. It’s now possible to define steps to execute after each stage or pipeline depending on the current status of the build. Previously we were using try/catch/finally for our tests step ensure we reported our results, and a broader try/catch to send failure notifications.

We now have a post section immediately after our test stage that publishes artifacts, similar to the following snippet:

stage('Test') {
  steps {
    // ...
  }
  post {
    always {
      archiveArtifacts 'results/*'
      junit 'results/*.xml'
    }
  }
}

We also have a post section for the entire pipeline for notifications. This means that we send notifications when any stage fails, which is another improvement over our previous pipelines.

Conclusion

In my opinion declarative pipelines are a huge improvement over the original scripted pipelines. The new syntax is succinct and easier to read and maintain. As a result of migrating to declarative and our shared library, one of our pipelines had a 60% reduction in the lines of code but with more functionality!

Here’s a full example of one of our declarative Jenkins pipelines.

Leave a Reply

Your email address will not be published. Required fields are marked *