Skip to content

Commit

Permalink
Reorganized to avoid non-declarative hacks
Browse files Browse the repository at this point in the history
  • Loading branch information
bitwiseman committed Feb 15, 2017
1 parent 669b41b commit 126bcdf
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ link:/blog/2017/02/10/declarative-html-publisher/[my previous Declarative Pipeli
I've used a new branch in
link:https://github.com/bitwiseman/hermann[my fork] of the
link:https://github.com/reiseburo/hermann[Hermann project]:
link:https://github.com/bitwiseman/hermann/tree/blog/declarative/notifications[`blog/add-declarative/notifications`].
link:https://github.com/bitwiseman/hermann/tree/blog/declarative/notifications[`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.

Expand All @@ -55,15 +55,15 @@ and plugin:email-ext[Email-ext]
plugins to use those channels.


== Preparing for Notifications
== Adding Notifications

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

It works quiet well, but only only has that final `always` directive that prints a message to the console log,
but doesn't print anything at the start of the run.
The Scripted Pipeline in the original blog post called the same method twice,
once at the start of the run and once at the end.
Let's start by replicating that here as well.
It works quiet well, but only only has that final `always` directive
which prints a message to the console log, and it doesn't print anything at the start of the run.
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 for started, success, and failure working.

[source, groovy]
----
Expand All @@ -73,132 +73,99 @@ pipeline {
stage ('Start') {
steps {
// send build started notifications
sendNotifications 'STARTED'
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 {
always {
sendNotifications currentBuild.result
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']]
)
}
}
}
def sendNotifications(String buildStatus = 'STARTED') {
echo "Send notifications for run: ${buildStatus}"
}
----

// TODO: still re-editing this section.
.Declarative is still Jenkins Pipeline
****
Looking at the method definition above, some of you will be saying,
"Wait a minute, wasn't Declarative Pipeline supposed to get me away from
Groovy, function definitions, and conditional logic?"
When I first saw this, that's certainly what I said, only with a few more expletives.
Once I got past the original shock and started working with it,
I found that Declarative still being Groovy was actually a huge advantage.
Declarative Pipeline - everything inside the `pipeline {}` section -
is a
link:https://en.wikipedia.org/wiki/Domain-specific_language[domain specific language]
that uses Groovy syntax,
and outside the `pipeline {}` section
we can still access all the facilities of Groovy and Scripted Pipeline.
Jenkins Declarative Pipeline is still *Jenkins Pipeline*.
This meant, as an expert in Scripted Pipeline, I was already familiar with the
basic syntax of Declarative Pipeline.
For users entirely new to Jenkins Pipeline, I think this the same will be true in the other direction -
the basic syntax of Declarative Pipeline is good introduction to the Groovy syntax that is
used throughout Pipeline.
As we'll see in the remainder of this post, Declarative and Scripted Pipeline both being Groovy
makes building up from simple to more complex Pipelines much smoother.
Declarative Pipeline provides a structured way of *declaring* the overall flow of Steps in our pipeline.
The complex logic and technology-specific work is handled by Steps.
At the same time, Declarative doesn't care about how those Steps are implemented.
Many Steps come from plugins, but here we've essentially created a Step called
`sendNotifications` that takes a parameter and does some work based on that.
When reviewing the overall structure of our Pipeline,
we don't need to know the implementation details of Step.
****

== Adding Notifications
Next, I'll pull in the code for the `sendNotification` method from the original blog post.
I wrote the method to only depend on the one parameter and the Steps from the
three notification plugins.
Like I said, Declarative is still Pipeline -
we'll just copy and paste this code in and,
since I'm running on the same master with the same configuration,
it should just work.
failure {
slackSend (color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
[source, groovy]
----
pipeline {
/* ... unchanged ... */
}
hipchatSend (color: 'RED', notify: true,
message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})"
)
def sendNotifications(String buildStatus = 'STARTED') {
// build status of null means successful
buildStatus = buildStatus ?: 'SUCCESSFUL'
// 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>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>"""
// Override default values based on build status
if (buildStatus == 'STARTED') {
color = 'YELLOW'
colorCode = '#FFFF00'
} else if (buildStatus == 'SUCCESSFUL') {
color = 'GREEN'
colorCode = '#00FF00'
} else {
color = 'RED'
colorCode = '#FF0000'
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']]
)
}
}
// 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']]
)
}
----

image::/images/post-images/2017-02-14/blueocean-notifications.png[Blue Ocean Run with Notifications, role="center", width=800]

== Moving Notifications to Shared Library

Our current pipeline is pretty good, we have our Declarative Pipeline sending notifications.
However, the notification method takes up about a third of our `Jenkinsfile`.
It is a bit of a distraction.
Also, as Jenkins admin, I'd like to share that notification method among all my projects
without copying it to each new project and then having maintain all the copies.
I want to move that method to a
link:/doc/book/pipeline/shared-libraries/[Shared Library].
Our current Pipeline works, our Declarative Pipeline sends notifications.
However, it is extremely ugly.
In the original post, using Scripted Pipeline, I defined a single method
which I called at the start and end of the pipeline.
I'd like to do that here as well, but Declarative doesn't support declaring methods
that are accessible to multiple stages.

Shared Libraries are not specific to Declarative. They were released in their
.Declarative is still Jenkins Pipeline
****
While it is true that Declarative doesn't *support* defining methods that are
accessible to multiple stages, that doesn't mean it can't be done.
At the time of this writing, the limitations of Declarative Pipeline only apply
inside the `pipeline {}` section of our `Jekinsfile`.
Outside the `pipeline {}` section we can still access all the facilities of Scripted Pipeline,
including defining methods. Those methods would then be accessible inside the
Declarative section of our `Jenkinsfile`.
For this series, I chose to stick to what is actually supported
*inside* Declarative Pipeline at this time.
The internet has plenty of hacked together solutions that *happen to work today*, but
I want to highlight current best practices and dependable solutions.
There are also at least two JIRA tickets for this area:
link:https://issues.jenkins-ci.org/browse/JENKINS-41335[JENKINS-41335] and
link:https://issues.jenkins-ci.org/browse/JENKINS-41396[JENKINS-41396].
****

For this, we'll need to turn to
link:/doc/book/pipeline/shared-libraries/[Shared Libraries].

Shared Libraries, as the name suggests,
let Jenkins Pipelines share code instead copying it to each new project.
They are not specific to Declarative. They were released in their
current form several months ago and were useful in Scripted Pipeline.
Since Declarative is still Pipeline, we can use them here as well.
While not every method like this one should go into a Shared Library,
Declarative's focus on overall pipeline flow makes it much clearer what
parts of a pipeline are candidates for moving to a shared library.
// TODO: Might be a good idea to put a short statement of how you judge what should and shouldn't go into a Shared Library
With Declarative Pipeline not supporting defining methods locally,
they become vital. They are the only way supported way within Pipeline
to declare methods or classes (such as this one) which we want to use in more than one stage.

=== Setting up a Shared Library

Expand All @@ -211,25 +178,24 @@ To setup a "Global Pipeline Library", navigated to "Manage Jenkins" -> "Configur
in the Jenkins web UI.
Then, under "Global Pipeline Libraries", I've added a new library.
I've the name `bitwiseman-shared`, pointed it at my repository,
and set the default branch for the library to `master` (we ),
and set the default branch for the library to `master`,
but I'll override that in my `Jenkinsfile`.

image::/images/post-images/2017-02-14/shared-library.png[Global Pipeline Library, role="center", width=800]
image::/images/post-images/2017-02-15/shared-library.png[Global Pipeline Library, role="center", width=800]

=== Moving the Code to the Library

Moving a method or class to a library involves creating a file with the name of our method,
Adding a Step or class to a library involves creating a file with the name of our method,
adding our method to that file as a `call()` method,
and removing the method from our local `Jenkinsfile`.
and replace the appropriate code in our local `Jenkinsfile` with Step calls.
We'll also have the option to add help text to the library.
Library's 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.

There are a few limitations for methods in Shared Libraries, but they don't apply here.
The minimal set of dependencies for `sendNotifications` means we can once again
basically copy-and-paste the code across.
The minimal set of dependencies for `sendNotifications` means we can
basically copy-and-paste the code across 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.
Expand Down Expand Up @@ -331,7 +297,7 @@ def call(String buildStatus = 'STARTED') {
def colorCode = '#FF0000'
def subject = "${buildStatus}: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'"
def summary = "${subject} (${env.BUILD_URL})"
def details = """<p>STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
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
Expand Down Expand Up @@ -360,15 +326,34 @@ def call(String buildStatus = 'STARTED') {
}
----

// TODO: summary image of "everything working".
image::/images/post-images/2017-02-15/blueocean-notifications-finished.png[Global Pipeline Library, role="center", width=800]

image::/images/post-images/2017-02-15/popups.png[HipChat and Slack Popups, role="center"]

image::/images/post-images/2017-02-15/mailcatcher.png[MailCatcher List, role="center"]


== Conclusion
In this post, we added notifications to our Declarative Pipeline.
I was pleased to see how little we had to disrupt the existing flow of our pipeline to do so.
The changes were at the start and end of the file with no reformatting elsewhere.
Declarative Pipeline prevented us from defining a method in our `Jenkinsfile` to send notifications.
Then with the help of the Shared Library feature,
we moved the implementation of `sendNotifications` out of `Jenkinsfile`.
This will let us easily reuse that code in other projects and maintains the clarity of our Pipeline.
we were able to define a `sendNotifications` method that we could call from our `Jenkinsfile`.
This maintained the clarity of our Pipeline and will let us easily reuse that code in other projects.
I was pleased to see how little the resulting Pipeline differed from where we started.
The changes were at the start and end of the file with no reformatting elsewhere.

The is a great example of how Declarative Pipeline
can help us write clearer, more maintainable Pipelines.
The limitations of Declarative guided us naturally to separate our
our Pipeline into two levels: an overall flow and individual steps.
Declarative Pipeline provides a structured way of *declaring* the overall flow of Steps in our Pipeline.
The complex logic and technology-specific work is handled by Steps.
Many Steps come from plugins, but Declarative doesn't care about how those Steps are implemented.
Here we've created a Step using a groovy method called
`sendNotifications` that takes a parameter and does some work based on that.
This lets us manage the overall structure of our Pipeline with out
getting caught up in how individual Steps are implemented.

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

Expand All @@ -379,3 +364,36 @@ run Sauce OnDemand with xUnit Reporting in Declarative Pipeline.
* link:/doc/book/pipeline/shared-libraries/[Shared Library reference]
* link:https://github.com/bitwiseman/hermann/tree/blog/declarative/notifications[Pipeline source for this post]
* link:https://github.com/bitwiseman/jenkins-pipeline-shared/tree/blog/declarative/notifications[Pipeline Shared Library source for this post]


////
// TODO: still re-editing this section.
.Declarative is still Jenkins Pipeline
****
Looking at the method definition above, some of you will be saying,
"Wait a minute, wasn't Declarative Pipeline supposed to get me away from
Groovy, function definitions, and conditional logic?"
When I first saw this, that's certainly what I said, only with a few more expletives.
Once I got past the original shock and started working with it,
I found that Declarative still being Groovy was actually a huge advantage.
Declarative Pipeline - everything inside the `pipeline {}` section -
is a
link:https://en.wikipedia.org/wiki/Domain-specific_language[domain specific language]
that uses Groovy syntax,
and outside the `pipeline {}` section
we can still access all the facilities of Groovy and Scripted Pipeline.
Jenkins Declarative Pipeline is still *Jenkins Pipeline*.
This meant, as an expert in Scripted Pipeline, I was already familiar with the
basic syntax of Declarative Pipeline.
For users entirely new to Jenkins Pipeline, I think this the same will be true in the other direction -
the basic syntax of Declarative Pipeline is good introduction to the Groovy syntax that is
used throughout Pipeline.
As we'll see in the remainder of this post, Declarative and Scripted Pipeline both being Groovy
makes building up from simple to more complex Pipelines much smoother.
****
////
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 126bcdf

Please sign in to comment.