Better Slack Notifications for Jenkins Builds


In an effort to improve unit and integration test feedback, I sought a way to post information about builds to a #builds Slack channel. Jenkins is known for its extensive third party plugin library, spanning thousands of plugins. Unfortunately, the Slack plugin that Jenkins offers, slackSend, is limited in functionality; it can only be used to write to the message area.

slackSend color: 'good', message: 'Test message!', channel: '#builds'

Slack Plugin Post
Leaves a bit to be desired

Using a Slack incoming webhook and Jenkins Pipeline (née workflow), I was able to achieve my goal of providing rich unit and integration test feedback, including the commit author’s name, build status, branch information, test results, commit message, and the test failures’ stacktraces (if applicable).

Failed Build
A Failed Build

Successful Build
A Successful Build

In this post, I will walk you through how I achieved this.

Slack Webhook Setup

First, we need to setup a webhook in Slack that will respond to our curl POST request. Visit http://[your Slack team name].slack.com/apps, search for “incoming webhooks”, and click on the first result.

Next, choose the channel that the webhook should post to and then click “Add Incoming WebHooks Integration.”

Side note: the capitalization and spelling of “webhooks” seems to be inconsistent; I have seen “web hook,” “webhook,” “Webhook,” and “WebHook.”

Webhook Settings
Add Webhook Integration

The next page will show the webhook URL. Copy this somewhere (you will use it soon), and then click on the “Save Settings” button at the bottom of the page. There is no need to customize the name or icon, since the curl request we will use to post to #builds will contain all of the information.

If you visit the #builds channel in Slack, you should see a confirmation that the webhook integration was added.

Webhook Confirmation
Success!

Install Jenkins (if you haven’t already)

I’m running Jenkins in a Docker container, which makes it trivial to run. If you don’t have Docker, you can download it here and install it. Jenkins is available on the Docker hub, which allows you to docker pull the image directly. but you can get it up and accessible via port 8080 using the following commands.

$ docker pull jenkins                               # gets the latest Jenkins
$ docker run -p 8080:8080 -p 50000:50000 jenkins    # run Jenkins on port 8080

Access Jenkins by opening your web browser and visiting localhost:8080. The first administrator password should be displayed in the docker run output. It can also be found by running the command below. If you don’t know the container’s name, run docker ps; the name will be in the last column.

docker exec [CONTAINER_NAME] cat /var/jenkins_home/secrets/initialAdminPassword

Create Jenkins Pipeline Project

Click “New Item” in the top left corner of the Jenkins interface. Give your job a name, select Multibranch Pipeline, and click on the OK Button.

Next, click on the “Add Source” button and enter your repository information. The pipeline is stored in a file named Jenkinsfile in the project’s root, which will be pulled from your repository before each build.

Test Connectivity

Add Jenkinsfile to the root of your project with the following contents, replacing the [CHANNEL_NAME] and [SLACK_WEBHOOK_URL] placeholders with applicable information.

Jenkinsfile:

#!/usr/bin/env groovy

import groovy.json.JsonOutput

def slackNotificationChannel = '[CHANNEL_NAME]'     // ex: = "builds"

def notifySlack(text, channel, attachments) {
    def slackURL = '[SLACK_WEBHOOK_URL]'
    def jenkinsIcon = 'https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png'

    def payload = JsonOutput.toJson([text: text,
        channel: channel,
        username: "Jenkins",
        icon_url: jenkinsIcon,
        attachments: attachments
    ])

    sh "curl -X POST --data-urlencode \'payload=${payload}\' ${slackURL}"
}

node {
    stage("Post to Slack") {
        notifySlack("Success!", slackNotificationChannel, [])
    }
}

If all went well, Slack should have pinged you on the channel you specified. So far, we have replicated slackSend‘s functionality.

Success Message

The empty attachments parameter, [], in the notifySlack() call above is where we will include all of the pertinent build information. We will retrieve this information from Jenkins’ provided environment variables, the sh step to execute and store the results of git bash commands, and test results, provided by AbstractTestResultAction.

Jenkins Environment Variables

Jenkins provides environment variables, such as env.GIT_BRANCH, that you can use to populate messages.

echo "The branch is ${env.GIT_BRANCH}."

A complete list of env‘s fields can be found here. In my pipeline, I use the following environment variables:

  • env.JOB_NAME
  • env.BUILD_NUMBER
  • env.BUILD_URL
  • env.GIT_BRANCH

Implement Shell-Based Attachments

Unfortunately, other information we need, such as the git author and the test result information, isn’t as accessible. To retrieve these values, we can include helper methods in our Jenkinsfile that set global variables.

git Author

This can be determined with a git bash command, which we can invoke with Jenkins Pipeline’s sh command.

def author = "";

def getGitAuthor = {
    def commit = sh(returnStdout: true, script: 'git rev-parse HEAD')
    author = sh(returnStdout: true, script: "git --no-pager show -s --format='%an' ${commit}").trim()
}

Last Commit Message

def message = "";

def getLastCommitMessage = {
    message = sh(returnStdout: true, script: 'git log -1 --pretty=%B').trim()
}

Implement @NonCPS Attachments

The @NonCPS annotation should be used when the method uses objects that aren’t serializable, and you would like to execute it anyway. Jenkins sandboxes the scripts for security, but this annotation allows you to bypass it. The first execution will fail; you must explicitly allow the method to execute. I’ll get to that later.

The next two attachments, test summary and failed tests, require the project to be built before they can be queried, which makes sense–how could we get build results prior to building? We can define a new “Build” stage to accomplish this.

stage("Build") {
    sh "./gradlew clean build"
    step $class: 'JUnitResultArchiver', testResults: '**/TEST-*.xml'

    ...
}

Once this step is complete, the test results can be queried via the AbstractTestResultAction object (API Reference).

def testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)

Since we need to access rawBuild, this method requires use of the @NonCPS annotation. We will use the testResultAction object to get information about test results.

Test Summary

@NonCPS
def getTestSummary = { ->
    def testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
    def summary = ""

    if (testResultAction != null) {
        total = testResultAction.getTotalCount()
        failed = testResultAction.getFailCount()
        skipped = testResultAction.getSkipCount()

        summary = "Passed: " + (total - failed - skipped)
        summary = summary + (", Failed: " + failed)
        summary = summary + (", Skipped: " + skipped)
    } else {
        summary = "No tests found"
    }
    return summary
}

Failed Tests

@NonCPS
def getFailedTests = { ->
    def testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
    def failedTestsString = "```"

    if (testResultAction != null) {
        def failedTests = testResultAction.getFailedTests()

        if (failedTests.size() > 9) {
            failedTests = failedTests.subList(0, 8)
        }

        for(CaseResult cr : failedTests) {
            failedTestsString = failedTestsString + "${cr.getFullDisplayName()}:\n${cr.getErrorDetails()}\n\n"
        }
        failedTestsString = failedTestsString + "```"
    }
    return failedTestsString
}

Whitelisting Secured Methods

The first time you run this method, you will get an error message similar to the following:

org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use method org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper getRawBuild

To resolve this, visit http://jenkinsHost:port/scriptApproval/, and select the Approve option.

Please understand that this allows pipelines to bypass security requirements allowing scripts to run. I imagine this shouldn’t be an issue for most, as Jenkins and the code repository shouldn’t be accessible from outside of the network or VPN.

Success Message

After the first approval, getRawBuild, you need to continue to run the build to approve each child method.

Putting it all together

400: Invalid request

Further reading