41

I'd like to leverage the existing Mailer plugin from Jenkins within a Jenkinsfile that defines a pipeline build job. Given the following simple failure script I would expect an email on every build.

stage 'Test'
node {
    try {
        sh 'exit 1'
    } finally {
        step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: 'me@me.com', sendToIndividuals: true])
    }
}

The output from the build is:

Started by user xxxxx
[Pipeline] stage (Test)
Entering stage Test
Proceeding
[Pipeline] node
Running on master in /var/lib/jenkins/jobs/rpk-test/workspace
[Pipeline] {
[Pipeline] sh
[workspace] Running shell script
+ exit 1
[Pipeline] step
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: script returned exit code 1
Finished: FAILURE

As you can see, it does record that it performs the pipeline step immediately after the failure, but no emails get generated.

Emails in other free-style jobs that leverage the mailer work fine, its just invoking via pipeline jobs.

This is running with Jenkins 2.2 and mailer 1.17.

Is there a different mechanism by which I should be invoking failed build emails? I don't need all the overhead of the mail step, just need notifications on failures and recoveries.

Mike
  • 14,010
  • 29
  • 101
  • 161
rkeilty
  • 413
  • 1
  • 4
  • 6
  • Is this helful? http://stackoverflow.com/questions/36948606/jenkins-notifying-error-by-sending-mail-in-pipeline-former-known-as-workflow – Daniel Hernández May 11 '16 at 23:50
  • Very close to what I was trying to do, but refers more to the use of the mail plugin after a failure. The missing piece is detailed in the answer below in how the status is set during the pipeline process, which is required for plugins that utilize a non-pending build state. – rkeilty May 13 '16 at 17:09

3 Answers3

66

In Pipeline failed sh doesn't immediately set the currentBuild.result to FAILURE whereas its initial value is null. Hence, build steps that rely on the build status like Mailer might work seemingly incorrect.

You can check it by adding a debug print:

stage 'Test'
node {
    try {
        sh 'exit 1'
    } finally {
        println currentBuild.result  // this prints null
        step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: 'me@me.com', sendToIndividuals: true])
    }
}

This whole pipeline is wrapped with exception handler provided by Jenkins that's why Jenkins marks the build as failed in the the end.

So if you want to utilize Mailer you need to maintain the build status properly. For instance:

stage 'Test'
node {
    try {
        sh 'exit 1'
        currentBuild.result = 'SUCCESS'
    } catch (any) {
        currentBuild.result = 'FAILURE'
        throw any //rethrow exception to prevent the build from proceeding
    } finally {
        step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: 'me@me.com', sendToIndividuals: true])
    }
}

If you don't need to re-throw the exception, you can use catchError. It is a Pipeline built-in which catches any exception within its scope, prints it into console and sets the build status. For instance:

stage 'Test'
node {
    catchError {
        sh 'exit 1'
    } 
    step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: 'me@me.com', sendToIndividuals: true])
}
Mike
  • 14,010
  • 29
  • 101
  • 161
izzekil
  • 5,781
  • 2
  • 36
  • 38
  • This is a fantastic and detailed answer, and got me back on the right track. Thanks for this! – rkeilty May 13 '16 at 17:07
  • How can I make sure that a successful build after a failed (or unstable) build triggers a new e-mail? – asmaier Dec 05 '16 at 13:17
  • @asmaier AFAIK Mailer plugin doesn't provide this functionality. You might either script it with groovy (check the previous build status) or take a look into [Email Ext plugin](https://wiki.jenkins-ci.org/display/JENKINS/Email-ext+plugin). – izzekil Dec 05 '16 at 14:05
  • 1
    I figured it out myself: To make sure that a successful build after a failed (or unstable) build triggers a new e-mail add `currentBuild.result = 'SUCCESS'` at the end of the try block (after sh 'exit 1'). – asmaier Dec 05 '16 at 14:06
  • @izzekil Mailer plugin explicitly has this functionality, see point two under Usage on https://wiki.jenkins-ci.org/display/JENKINS/Mailer. – asmaier Dec 05 '16 at 14:08
  • @asmaier yep, you are correct. I missed that point when read the wiki page. – izzekil Dec 05 '16 at 17:20
  • Is it possible to send email **every** build not only after failed one? – ipeacocks Apr 24 '18 at 13:10
31

In addition to izzekil's excellent answer, you may wish to choose email recipients based on the commit authors. You can use email-ext to do this (based on their pipeline examples):

step([$class: 'Mailer',
      notifyEveryUnstableBuild: true,
      recipients: emailextrecipients([[$class: 'CulpritsRecipientProvider'],
                                      [$class: 'RequesterRecipientProvider']])])

If you're using a recent email-ext (2.50+), you can use that in your pipeline:

emailext(body: '${DEFAULT_CONTENT}', mimeType: 'text/html',
         replyTo: '$DEFAULT_REPLYTO', subject: '${DEFAULT_SUBJECT}',
         to: emailextrecipients([[$class: 'CulpritsRecipientProvider'],
                                 [$class: 'RequesterRecipientProvider']]))

If you're not using a declarative Jenkinsfile, you will need to put checkout scm so Jenkins can find the committers. See JENKINS-46431.

If you're still on an older version of email-ext, you'll hit JENKINS-25267. You could roll your own HTML email:

def emailNotification() {
    def to = emailextrecipients([[$class: 'CulpritsRecipientProvider'],
                                 [$class: 'DevelopersRecipientProvider'],
                                 [$class: 'RequesterRecipientProvider']])
    String currentResult = currentBuild.result
    String previousResult = currentBuild.getPreviousBuild().result

    def causes = currentBuild.rawBuild.getCauses()
    // E.g. 'started by user', 'triggered by scm change'
    def cause = null
    if (!causes.isEmpty()) {
        cause = causes[0].getShortDescription()
    }

    // Ensure we don't keep a list of causes, or we get
    // "java.io.NotSerializableException: hudson.model.Cause$UserIdCause"
    // see http://stackoverflow.com/a/37897833/509706
    causes = null

    String subject = "$env.JOB_NAME $env.BUILD_NUMBER: $currentResult"

    String body = """
<p>Build $env.BUILD_NUMBER ran on $env.NODE_NAME and terminated with $currentResult.
</p>

<p>Build trigger: $cause</p>

<p>See: <a href="$env.BUILD_URL">$env.BUILD_URL</a></p>

"""

    String log = currentBuild.rawBuild.getLog(40).join('\n')
    if (currentBuild != 'SUCCESS') {
        body = body + """
<h2>Last lines of output</h2>
<pre>$log</pre>
"""
    }

    if (to != null && !to.isEmpty()) {
        // Email on any failures, and on first success.
        if (currentResult != 'SUCCESS' || currentResult != previousResult) {
            mail to: to, subject: subject, body: body, mimeType: "text/html"
        }
        echo 'Sent email notification'
    }
}
Wilfred Hughes
  • 29,846
  • 15
  • 139
  • 192
22

I think a better way to send mail notifications in jenkins pipelines is to use the post section of a pipeline as described in the jenkins docs instead of using try catch:

pipeline {
  agent any
    stages {
      stage('whatever') {
        steps {
          ...
        }
      }
    }
    post {
        always {
          step([$class: 'Mailer',
            notifyEveryUnstableBuild: true,
            recipients: "example@example.com",
            sendToIndividuals: true])
        }
      }
    }
  }
}
Andi
  • 1,172
  • 1
  • 11
  • 16
  • 1
    I've tried your technique, but this does not send "Back to normal" notifications. – Baptiste Wicht Jun 02 '17 at 12:29
  • how to set `currentBuild.result` in post.always section? – prat0318 Jun 16 '17 at 02:46
  • @prat0318 i think the `currentBuild.result` is fix at this point as it is a post action. You can only read the result. – Andi Aug 03 '17 at 06:53
  • For currentBuild.result is null in always section. It's not fix that this point. – Ankush T Sep 07 '17 at 06:44
  • didn't work for me, had to do ` post { always { mail bcc: '', body: 'this is the body', cc: '', from: '', replyTo: '', subject: 'This is a test', to: 'test@uk.com' } }` – Tom Sep 21 '17 at 17:32
  • @BaptisteWicht To send Back to normaI messages I have had success with adding `script { if (currentBuild.result == null) { currentBuild.result = 'SUCCESS' } }` just before the `Mailer` step. – mmm444 Nov 10 '17 at 10:47
  • @TomH Do you know how to send attachments along with mail body?? – Yogesh Jilhawar Aug 23 '18 at 06:24
  • @Andi try catch is the only method available in a scripted pipeline. What you have there is a declarative pipeline. It confused me at the start too but scripted pipelines do not start with pipeline { .. amongst other things – Peter Moore Nov 22 '19 at 14:38