Книга: Automation and Monitoring with Hubot: For DevOps and Developers
Назад: GitLab Integration
Дальше: Invoking Chef’s Knife With Hubot

Building And Deploying With Hubot And Jenkins

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.

Notifying About Broken Jenkins Builds

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/) 

Executing Jenkins Builds

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 

Passing Parameters To Jenkins Builds

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.

Deploying With Jenkins

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:

  1. Live console output. If you tried to output your build logs directly to chatroom though Hubot, it could get ugly. Build logs can easily grow large, and chatroom is no place for such things. With Jenkins build you get to have a link that displays live console output and keeps it archived next to every build.
  2. A history of all your deployments, including console output of each and every one of them. Combine it with good performance monitoring tool (like , and you will be able to pinpoint and eliminate performance bottlenecks right away, minutes after they are rolled out.
  3. Build trends view. It will show you how deployment duration changes over time, and how often your builds tend to fail. This is good, because you will notice when you start going down the hill with poor speed and quality.

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.

Назад: GitLab Integration
Дальше: Invoking Chef’s Knife With Hubot