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: """ STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':
Check console output at " ${env.JOB_NAME} [${env.BUILD_NUMBER}]"""",
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: """ SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':
Check console output at " ${env.JOB_NAME} [${env.BUILD_NUMBER}]"""",
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: """ FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':
Check console output at " ${env.JOB_NAME} [${env.BUILD_NUMBER}]"""",
recipientProviders: [[$class: 'DevelopersRecipientProvider']]
)
}
}
}
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.
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 //
#!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
}
}
}
// Scripted //
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 = """ ${buildStatus}: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':
Check console output at " ${env.JOB_NAME} [${env.BUILD_NUMBER}]""""
// 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']]
)
}
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.
Links
Declarative Pipeline plugin
Declarative Pipeline Syntax Reference
Shared Library reference
Pipeline source for this post
Pipeline Shared Library source for this post