If you have a team of developers, having instant feedback about broken and restored builds is invaluable. Most build servers send emails by default, but if you can make it do an HTTP request with build status after every build, you can forget those emails and do things more agile by making Hubot tell about broken builds in developer chatroom.
You will learn how to do this with , which is one of the most popular continuous integration servers available.
If you’re not using any build server in your company and do not have automated builds of the software you make, you should really start doing it. There is just no excuse for that.
Jenkins doesn’t have a capability to make HTTP requests with build status updates right out of the box, but there is that is easy to install and configure. There is also script available for Hubot, that is designed to handle these notifications. You can simply install and use it, but for the sake of learning we will roll our own version and use Hubot PubSub for more flexible notification routing.
After installing Jenkins Notification Plugin, open your build configuration page and find Job Notifications section. There you should configure it like this:
Format: JSON Protocol: HTTP URL: http://botserv:8080/jenkins/status
If you would analyze the payload that comes from jenkins, it would look something like this:
{
"name"
:
"test"
,
"url"
:
"job/test/"
,
"build"
:
{
"full_url"
:
"http://jenkins/job/test/13/"
,
"number"
:
13
,
"phase"
:
"FINISHED"
,
"status"
:
"SUCCESS"
,
"url"
:
"job/test/13/"
}
}
Phase can be STARTED
, COMPLETED
or FINISHED
, and status can be SUCCESS
, UNSTABLE
or FAILURE
. All we have to do is handle these different statuses.
The script itself is pretty simple:
scripts/jenkins-pubsub-notifier.coffee
# Description:
# An HTTP Listener that notifies about new Jenkins build failures
#
# Dependencies:
# "hubot-pubsub": "1.0.0"
#
# URLS:
# POST /jenkins/status
#
# Authors:
# spajus
module.exports =
(robot) ->
robot
.
router
.
post
"/jenkins/status"
,
(req, res) ->
@failing
||=
[]
res
.
end
(
''
)
data =
req
.
body
return
unless
data
.
build
.
phase
==
'FINISHED'
if
data
.
build
.
status
==
'FAILURE'
||
data
.
build
.
status
==
'UNSTABLE'
if
data
.
name
in
@failing
broke =
'still broken'
else
broke =
'just broke'
@failing
.
push
data
.
name
message =
"
#{
broke
}
#{
data
.
name
}
"
+
"
#{
data
.
build
.
display_name
}
(
#{
data
.
build
.
full_url
}
)"
if
data
.
build
.
status
==
'SUCCESS'
if
data
.
name
in
@failing
index =
@failing
.
indexOf
data
.
name
@failing
.
splice
index
,
1
if
index
isnt
-
1
message =
"restored
#{
data
.
name
}
"
+
"
#{
data
.
build
.
display_name
}
(
#{
data
.
build
.
full_url
}
)"
if
message
event =
"build.
#{
data
.
build
.
status
}
"
robot
.
emit
'pubsub:publish'
,
event
,
message
Script source available at
Now subscribe a room to build
events and try to break something:
Tomas V. hubot subscribe build Hubot Subscribed 585164 to build events Hubot build.FAILURE: just broke test #11 (http://jenkins/job/test/11/) Hubot build.FAILURE: still broken test #12 (http://jenkins/job/test/12/) Hubot build.SUCCESS: restored test #13 (http://jenkins/job/test/13/)
It’s easy to make Hubot trigger Jenkins builds. First, you have to enable remote build triggering in your Jenkins job. To do that, go to job configuration and under “Build Triggers” select “Trigger builds remotely (e.g., from scripts)” option, then enter the “Authentication Token” - it can be any string. For our script to work universally across all Jenkins builds, use the same Authentication Token for all your builds. Note that if you omit the “Authentication Token”, Jenkins will not save “Trigger Builds Remotely” as checked.
Under the “Authentication Token” there will be instructions of how to trigger the build:
Use the following URL to trigger build remotely: JENKINS_URL/job/test/build?token=TOKEN_NAME or /buildWithParameters?token=TOKEN_NAME Optionally append &cause=Cause+Text to provide text that will be included in the recorded build cause.
Then test if it works:
hubot@botserv:~$
curl -v "http://jenkins/job/test/build?token=test"
>
GET /job/test/build?token=
test
HTTP/1.1 >
User-Agent: curl/7.30.0 >
Host: jenkins >
Accept: */* >
< HTTP/1.1 201 Created
< Date: Fri, 21 Feb 2014 04:11:24 GMT
< Content-Length: 0
< Connection: keep-alive
< Location: http://jenkins/queue/item/792/
As you can see, Jenkins added build to queue and returned it in Location
header along with response. You can use Jenkins API to query the information:
hubot@botserv:~$
curl "http://jenkins/queue/item/792/api/json"
There are two possible response types, one when job is enqueued:
{
"actions"
:
[{
"causes"
:
[{
"shortDescription"
:
"Started by remote host 127.0.0.1\
"
}]}],
"blocked"
:
false
,
"buildable"
:
false
,
"id"
:
795
,
"inQueueSince"
:
1392957015732
,
"params"
:
""
,
"stuck"
:
false
,
"task"
:
{
"name"
:
"test"
,
"url"
:
"http://jenkins/job/test/"
,
"color"
:
"blue"
},
"url"
:
"queue/item/795/"
,
"why"
:
"In the quiet period. Expires in 4.2 sec"
,
"timestamp"
:
1392957035732
}
And one when job execution starts right away:
{
"actions"
:
[{
"causes"
:
[{
"shortDescription"
:
"Started by remote host
127.0.0.1
"}]}],
"blocked"
:
false
,
"buildable"
:
false
,
"id"
:
806
,
"inQueueSince"
:
1392958094707
,
"params"
:
""
,
"stuck"
:
false
,
"task"
:
{
"name"
:
"test"
,
"url"
:
"http://jenkins/job/test/"
,
"color"
:
"blue_anime"
},
"url"
:
"queue/item/795/"
,
"why"
:
null
,
"cancelled"
:
false
,
"executable"
:
{
"number"
:
30
,
"url"
:
"http://jenkins/job/test/30/"
}}
There are some subtle differences, so we will have to be sure to cover both scenarios in our scripts.
It’s also possible to query all Jenkins queue items:
hubot@botserv:~$
curl "https://jenkins.vinted.net/queue/api/json"
{"items":[...]}
We can remove build from the queue using POST
request to /queue/cancelItem?id=<item_id>
:
hubot@botserv:~$
curl -X POST "http://jenkins/queue/cancelItem?id=792"
It’s a pretty powerful API, so let’s befriend it with Hubot.
To begin with, let’s write a script that allows triggering Jenkins builds from chatroom by providing build name:
scripts/jenkins-builder.coffee
# Description
# Triggers Jenkins jobs from chatroom
#
# Configuration:
# HUBOT_JENKINS_URI - Base Jenkins URI
# HUBOT_JENKINS_BUILD_TOKEN - Token for triggering Jenkins builds
#
# Commands:
# hubot build <job> - build Jenkins job by name
#
# Author:
# spajus
module.exports =
(robot) ->
jenkins_uri =
process
.
env
.
HUBOT_JENKINS_URI
build_token =
process
.
env
.
HUBOT_JENKINS_BUILD_TOKEN
robot
.
respond
/build (.+)/i
,
(msg) ->
job =
msg
.
match
[
1
]
url =
"
#{
jenkins_uri
}
/job/
#{
encodeURI
(
job
)
}
/build"
msg
.
robot
.
http
(
url
).
query
(
token:
build_token
).
get
()
(err, res, body) ->
item_url =
res
.
headers
.
location
msg
.
robot
.
http
(
"
#{
item_url
}
api/json"
).
get
()
(err, res, body) ->
data =
JSON
.
parse
body
if
data
.
executable
msg
.
send
"Building
#{
data
.
task
.
name
}
(
#{
data
.
executable
.
url
}
)"
else
if
data
.
task
msg
.
send
"Added
#{
data
.
task
.
name
}
(
#{
data
.
task
.
url
}
) to build queue\
:
#{
data
.
why
}
"
else
msg
.
send
"Building
#{
data
.
name
}
(
#{
data
.
url
}
)"
You will have to set HUBOT_JENKINS_URI
and HUBOT_JENKINS_BUILT_TOKEN
in hubot.conf
first, or just provide it right in the script:
jenkins_uri =
process
.
env
.
HUBOT_JENKINS_URI
||
'http://your-jenkins.net'
build_token =
process
.
env
.
HUBOT_JENKINS_BUILD_TOKEN
||
'secret12345'
The execution works like this:
Tomas V. hubot build test Hubot Building test (http://jenkins/job/test/33/) Tomas V. hubot build test Hubot Added test (http://jenkins/job/test/) to build queue: In the quiet period. Expires in 19 sec
To get more flexibility, you can pass parameters to Jenkins builds. That becomes handy if you, for example, want to specify the branch you want to build. Let’s make a first. To do that, in Jenkins job configuration check “This build is parameterized” and click “Add Parameter” button. Create a “String parameter” with following settings:
Name: branch Default Value: master Description: Git branch to build
You can now refer to this parameter using $branch
anywhere in your job configuration. Go ahead to “Source Code Management” and set “Branches to build” value to $branch
.
Save the job configuration, and you should notice that “Build” link is now named “Build with Parameters”. If you click it, build doesn’t start instantly - you have to specify the branch, witch is prefilled with “master”. Try if it builds the right branch first, and if you got everything right, move on to Hubot script.
We will add a new command to the script we just created. This is how it will look like: hubot build job-name branch=test
.
You may notice, that Hubot already responds to /build (.+)/i
, that will match hubot build job-name branch=test
, so we have to change the regex of our first command. Jobs names are usually alphanumeric with dashes or underscores, so this should work:
robot
.
respond
/build ([\w_-]+)$/i
,
(msg) ->
Tip: to quickly test if a piece of Hubot script works as you expect it to, run coffee
from command line and you will get an interactive shell where you can do this:
hubot@botserv
:~/
campfire$
coffee
coffee
>
command =
"hubot build Some_Job-1"
'hubot build Some_Job-1'
coffee
>
command
.
match
/build ([\w_-]+)$/i
[
'build Some_Job-1'
,
'Some_Job-1'
,
index:
6
,
input:
'hubot build Some_Job-1'
]
coffee
>
command =
"hubot build job-name foo=bar"
'hubot build job-name foo=bar'
coffee
>
command
.
match
/build ([\w_-]+)$/i
null
Now we need a regular expression that would match parameterized jobs. Let’s continue to use the coffee shell to create one and try it out:
coffee
>
command =
"hubot build job-name foo=bar branch=master"
'hubot build job-name foo=bar branch=master'
coffee
>
matches =
command
.
match
/build ([\w_-]+) (.+)$/i
[
'build job-name foo=bar branch=master'
,
'job-name'
,
'foo=bar branch=master'
,
index:
6
,
input:
'hubot build job-name foo=bar branch=master'
]
coffee
>
matches
[
1
]
'job-name'
coffee
>
matches
[
2
]
'foo=bar branch=master'
We can see that /build ([\w_-]+) (.+)$/i
gives us job name in matches[1]
and all the parameters in matches[2]
. We can split the parameters on whitespace and build a dictionary by splitting each piece by =
. Let’s put our new Hubot command together:
robot
.
respond
/build ([\w_-]+) (.+)$/i
,
(msg) ->
job =
msg
.
match
[
1
]
params =
msg
.
match
[
2
].
split
/\s+/
query =
{
token:
build_token
}
for
param
in
params
[
k
,
v
]
=
param
.
split
'='
query
[
k
]
=
v
url =
"
#{
jenkins_uri
}
/job/
#{
encodeURI
(
job
)
}
/buildWithParameters"
msg
.
robot
.
http
(
url
).
query
(
query
).
get
()
(err, res, body) ->
item_url =
res
.
headers
.
location
msg
.
robot
.
http
(
"
#{
item_url
}
api/json"
).
get
()
(err, res, body) ->
data =
JSON
.
parse
body
if
data
.
executable
msg
.
send
"Building
#{
data
.
task
.
name
}
(
#{
data
.
executable
.
url
}
)"
else
if
data
.
task
msg
.
send
"Added
#{
data
.
task
.
name
}
(
#{
data
.
task
.
url
}
) to build queue\
:
#{
data
.
why
}
"
else
msg
.
send
"Building
#{
data
.
name
}
(
#{
data
.
url
}
)"
You will notice a considerable amount of code duplication between this new command and the one that triggers a job without parameters. Try combining these two actions into one to eliminate the duplication.
If you are eager to start deploying your app with hubot deploy
, wait until you read this. While it’s faily simple to make deployments directly from Hubot script, you should make Jenkins run your deployments instead, and then trigger Jenkins job with Hubot.
Why the extra step? Jenkins gives you several advantages:
How deployment can be implemented with Jenkins depends heavily on your technology stack and infrastructure, this is why I cannot give you a ready to use recipe for it. You should be able to build it yourself using the knowledge from this book.
Good news is that everything you can do manually over SSH connection, Jenkins can do it for you - in “Build” section add a step called “Execute shell” and you can do things like this:
Command: ssh deployer.your.org "STARTED_BY=$user BRANCH=$branch /opt/deploy.sh"
Now you can use Hubot to invoke this parameterized Jenkins build. Use msg.message.user.name
to get the name of person who started the deployment.
To make it fully integrated, implement feedback from your deployment script back to Hubot. You can find some ideas how to do that in “Monitoring With Hubot” chapter of this book. Just trigger pubsub messages over HTTP to notify Hubot about deployment start, end, and possible failures.