Back to blog

Declarative Pipeline: Notifications and Shared Libraries

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

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

In the previous post, we converted a Scripted Pipeline to a Declarative Pipeline, adding descriptive stages and post sections. In one of those post blocks, we included a placeholder for sending notifications.

In this blog post, we’ll repeat what I did in "Sending Notifications in Pipeline but this time in Declarative Pipeline. First we’ll integrate calls to notification services Slack, HipChat, and Email into our Pipeline. Then we’ll refactor those calls into a single Step in a Shared Library, which we’ll reuse as needed, keeping our Jenkinsfile concise and understandable.

Setup

The setup for this post is almost the same as my previous Declarative Pipeline post. I’ve used a new branch in my fork of the Hermann project: blog/declarative/notifications. I’d already set up a Multibranch Pipeline and pointed it at my repository, so the new branch will be picked up and built automatically.

I still have my notification targets (where we’ll send notifications) that I created for the "Sending Notifications in Pipeline" blog post. Take a look at that post to review how I setup the Slack, HipChat, and Email-ext plugins to use those channels.

Adding Notifications

We’ll start from the same Pipeline we had at the end of the previous post.

This Pipeline works quite well, except it doesn’t print anything at the start of the run, and that final always directive only prints a message to the console log. Let’s start by getting the notifications working like we did in the original post. We’ll just copy-and-paste the three notification steps (with different parameters) to get the notifications working for started, success, and failure.

pipeline {
  /* ... unchanged ... */
  stages {
    stage ('Start') {
      steps {
        // send build started notifications
        slackSend (color: '#FFFF00', message: "STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")

        // send to HipChat
        hipchatSend (color: 'YELLOW', notify: true,
            message: "STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})"
          )

        // send to email
        emailext (
            subject: "STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'",
            body: """<p>STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
              <p>Check console output at &QUOT;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&QUOT;</p>""",
            recipientProviders: [[$class: 'DevelopersRecipientProvider']]
          )
      }
    }
    /* ... unchanged ... */
  }
  post {
    success {
      slackSend (color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")

      hipchatSend (color: 'GREEN', notify: true,
          message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})"
        )

      emailext (
          subject: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'",
          body: """<p>SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
            <p>Check console output at &QUOT;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&QUOT;</p>""",
          recipientProviders: [[$class: 'DevelopersRecipientProvider']]
        )
    }

    failure {
      slackSend (color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")

      hipchatSend (color: 'RED', notify: true,
          message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})"
        )

      emailext (
          subject: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'",
          body: """<p>FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
            <p>Check console output at &QUOT;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&QUOT;</p>""",
          recipientProviders: [[$class: 'DevelopersRecipientProvider']]
        )
    }
  }
}
Blue Ocean Run with Notifications

Moving Notifications to Shared Library

This new Pipeline works and our Declarative Pipeline sends notifications; however, it is extremely ugly. In the original post using Scripted Pipeline, I defined a single method that I called at both the start and end of the pipeline. I’d like to do that here as well, but Declarative doesn’t support creating methods that are accessible to multiple stages. For this, we’ll need to turn to Shared Libraries.

Shared Libraries, as the name suggests, let Jenkins Pipelines share code instead of copying it to each new project. Shared Libraries are not specific to Declarative; they were released in their current form several months ago and were useful in Scripted Pipeline. Due to Declarative Pipeline’s lack of support for defining methods, Shared Libraries take on a vital role. They are the only supported way within Declarative Pipeline to define methods or classes that we want to use in more than one stage.

The lack of support for defining methods that are accessible in multiple stages, is a known issue, with at least two JIRA tickets: JENKINS-41335 and JENKINS-41396. For this series, I chose to stick to using features that are fully supported in Declarative Pipeline at this time. The internet has plenty of hacked together solutions that happen to work today, but I wanted to highlight current best practices and dependable solutions.

Setting up a Shared Library

I’ve created a simple shared library repository for this series of posts, called jenkins-pipeline-shared. The shared library functionality has too many configuration options to cover in one post. I’ve chosen to configure this library as a "Global Pipeline Library," accessible from any project on my Jenkins controller. To setup a "Global Pipeline Library," I navigated to "Manage Jenkins" → "Configure System" in the Jenkins web UI. Once there, under "Global Pipeline Libraries", I added a new library. I then set the name to bitwiseman-shared, pointed it at my repository, and set the default branch for the library to master, but I’ll override that in my Jenkinsfile.

Global Pipeline Library

Moving the Code to the Library

Adding a Step to a library involves creating a file with the name of our Step, adding our code to a call() method inside that file, and replacing the appropriate code in our Jenkinsfile with the new Step calls. Libraries can be set to load "implicitly," making their default branch automatically available to all Pipelines, or they can be loaded manually using a @Library annotation. The branch for implicitly loaded libraries can also be overridden using the @Library annotation.

The minimal set of dependencies for sendNotifications means we can basically copy-and-paste the code from the original blog post. We’ll check this change into a branch in the library named blog/declarative/notifications, the same as my branch in the hermann repository. This will let us make changes on the master branch later without breaking this example. We’ll then use the @Library directive to tell Jenkins to use that branch’s version of the library with this Pipeline.

Jenkinsfile (Declarative Pipeline)
#!groovy
@Library('bitwiseman-shared@blog/declarative/notifications') _ (1)

pipeline {
  agent {
    // Use docker container
    docker {
      image 'ruby:2.3'
    }
  }
  options {
    // Only keep the 10 most recent builds
    buildDiscarder(logRotator(numToKeepStr:'10'))
  }
  stages {
    stage ('Start') {
      steps {
        // send build started notifications
        sendNotifications 'STARTED'
      }
    }
    stage ('Install') {
      steps {
        // install required bundles
        sh 'bundle install'
      }
    }
    stage ('Build') {
      steps {
        // build
        sh 'bundle exec rake build'
      }

      post {
        success {
          // Archive the built artifacts
          archive includes: 'pkg/*.gem'
        }
      }
    }
    stage ('Test') {
      steps {
        // run tests with coverage
        sh 'bundle exec rake spec'
      }

      post {
        success {
          // publish html
          publishHTML target: [
              allowMissing: false,
              alwaysLinkToLastBuild: false,
              keepAll: true,
              reportDir: 'coverage',
              reportFiles: 'index.html',
              reportName: 'RCov Report'
            ]
        }
      }
    }
  }
  post {
    always {
      sendNotifications currentBuild.result
    }
  }
}
1 The _ here is intentional. Java/Groovy Annotations such as @Library must be applied to an element. That is often a using statement, but that isn’t needed here so by convention we use an \_.
vars/sendNotifications.groovy
#!/usr/bin/env groovy

/**
 * Send notifications based on build status string
 */
def call(String buildStatus = 'STARTED') {
  // build status of null means successful
  buildStatus = buildStatus ?: 'SUCCESS'

  // Default values
  def colorName = 'RED'
  def colorCode = '#FF0000'
  def subject = "${buildStatus}: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'"
  def summary = "${subject} (${env.BUILD_URL})"
  def details = """<p>${buildStatus}: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
    <p>Check console output at &QUOT;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&QUOT;</p>"""

  // Override default values based on build status
  if (buildStatus == 'STARTED') {
    color = 'YELLOW'
    colorCode = '#FFFF00'
  } else if (buildStatus == 'SUCCESS') {
    color = 'GREEN'
    colorCode = '#00FF00'
  } else {
    color = 'RED'
    colorCode = '#FF0000'
  }

  // Send notifications
  slackSend (color: colorCode, message: summary)

  hipchatSend (color: color, notify: true, message: summary)

  emailext (
      to: 'bitwiseman@bitwiseman.com',
      subject: subject,
      body: details,
      recipientProviders: [[$class: 'DevelopersRecipientProvider']]
    )
}
Global Pipeline Library
HipChat and Slack Popups
MailCatcher List

Conclusion

In this post we added notifications to our Declarative Pipeline. We wanted to move our repetitive notification code into a method; however, Declarative Pipeline prevented us from defining a method in our Jenkinsfile. Instead, with the help of the Shared Library feature, we were able to define a sendNotifications Step that we could call from our Jenkinsfile. This maintained the clarity of our Pipeline and will let us easily reuse this Step in other projects. I was pleased to see how little the resulting Pipeline differed from where we started. The changes were restricted to the start and end of the file with no reformatting elsewhere.

In the next post, we’ll cover more about shared libraries and how to run Sauce OnDemand with xUnit Reporting in Declarative Pipeline.

About the author

Liam Newman

Liam Newman

Liam started his software career as a tester, which might explain why he’s such a fan of CI/CD and Pipeline as Code. He has spent the majority of his software engineering career implementing Continuous Integration systems at companies big and small. He is a Jenkins project contributor and an expert in Jenkins Pipeline, both Scripted and Declarative. Liam currently works as a Jenkins Evangelist at CloudBees. When not at work, he enjoys testing gravity by doing Aikido.