Jenkins Lesson 20 – Pipeline Notifications | Dataplexa
Section II · Lesson 20

Pipeline Notifications

A pipeline that fails silently is almost worse than no pipeline at all. Notifications close the loop — the right person hears about a failure the moment it happens, not when a customer reports a bug three hours later.

This lesson covers

Slack notifications → Email notifications → Notification strategy — who gets what and when → Writing useful alert messages → Avoiding notification fatigue

Most teams set up notifications wrong. Either they notify on every build — which everyone learns to ignore within a week — or they notify on nothing, which means failures go unnoticed. The right approach is surgical: notify when something changes, notify the right people, and make the message actually useful.

The Analogy

A good notification system is like a good smoke alarm. It stays quiet when everything is fine. It makes noise the moment something is wrong. And crucially — it doesn't go off every time you make toast. Notifications that fire on every passing build are the smoke alarm that goes off for toast. Everyone disables it.

Notification Strategy — Who Gets What and When

Before writing a single line of notification code, decide what you actually want to communicate. The answer for most teams looks like this:

Event Who to notify Channel Notify?
Build passes (was already passing) Nobody No
Build fails (was passing) Developer who pushed + team channel Slack + Email Yes — immediately
Build recovers (was failing, now passes) Team channel Slack Yes — good news
Production deploy succeeds Team + release manager Slack Yes — always
Production deploy fails Team + release manager + on-call Slack + PagerDuty Yes — urgently

Slack Notifications

Slack is the most common notification channel for engineering teams. Jenkins integrates with it two ways — through the Slack Notification plugin (configured globally in Jenkins) or through a direct webhook URL (no plugin required, just a curl command). Both work. The plugin gives you richer formatting. The webhook approach works anywhere.

The scenario:

You're a DevOps engineer at a fintech company. The payments team has a #deployments Slack channel where they track all pipeline activity. They want: silence on passing builds, an immediate alert when a build breaks, a recovery message when it fixes itself, and a distinct notification every time code goes to production.

New terms in this code:

  • slackSend() — a step provided by the Slack Notification plugin. Sends a message to a Slack channel. Requires the plugin installed and a Slack token configured in Manage Jenkins → Slack.
  • color — the coloured sidebar on the Slack message. 'good' = green, 'warning' = yellow, 'danger' = red. You can also pass a hex colour like '#1d4ed8'.
  • currentBuild.previousBuild — a reference to the previous build object. Used to check if the last build failed and this one passed — the "fixed" state.
  • currentBuild.previousBuild?.result — the ? is Groovy's safe navigation operator. If previousBuild is null (first ever build), this returns null instead of throwing an error.
  • env.BUILD_URL — a Jenkins built-in environment variable containing the full URL of the current build. Include this in notifications so the team can click straight to the console output.
  • env.GIT_COMMIT — the Git commit SHA that triggered this build. Useful for correlating a notification to a specific commit in GitHub.
  • env.GIT_AUTHOR_NAME — the name from the Git commit. Not always set by default — depends on the Git plugin version and configuration.
pipeline {
    agent { label 'linux' }

    environment {
        APP_NAME     = 'payments-service'
        SLACK_CHANNEL = '#deployments'
        // Slack webhook stored securely in the Jenkins credential store
        SLACK_WEBHOOK = credentials('slack-webhook-url')
    }

    stages {

        stage('Checkout') {
            steps { checkout scm }
        }

        stage('Test') {
            steps { sh './gradlew test' }
            post {
                always { junit 'build/test-results/**/*.xml' }
            }
        }

        stage('Build') {
            steps { sh './gradlew bootJar' }
        }

        stage('Deploy to Production') {
            when { branch 'main' }
            steps {
                sh "./deploy.sh production ${BUILD_NUMBER}"
                // Notify on every successful production deploy — always useful signal
                slackSend(
                    channel: env.SLACK_CHANNEL,
                    color: 'good',
                    message: """
                        ✅ *${APP_NAME}* deployed to *production*
                        Build: <${env.BUILD_URL}|#${BUILD_NUMBER}>
                        Commit: ${env.GIT_COMMIT?.take(7) ?: 'unknown'}
                    """.stripIndent().trim()
                )
            }
        }

    }

    post {

        // 'failure' fires when any stage above failed
        failure {
            script {
                def message = """
                    ❌ *${APP_NAME}* build *FAILED*
                    Branch: ${env.BRANCH_NAME}
                    Build: <${env.BUILD_URL}|#${BUILD_NUMBER}>
                    Check the console output for details.
                """.stripIndent().trim()

                // Send via the Slack plugin step
                slackSend(
                    channel: env.SLACK_CHANNEL,
                    color: 'danger',
                    message: message
                )
            }
        }

        // 'fixed' is not a built-in post condition — we simulate it
        // by checking if the previous build failed and this one succeeded
        success {
            script {
                def previousResult = currentBuild.previousBuild?.result
                if (previousResult == 'FAILURE' || previousResult == 'UNSTABLE') {
                    // Previous build was broken — this build fixed it
                    slackSend(
                        channel: env.SLACK_CHANNEL,
                        color: 'good',
                        message: """
                            ✅ *${APP_NAME}* build *FIXED*
                            Branch: ${env.BRANCH_NAME}
                            Build: <${env.BUILD_URL}|#${BUILD_NUMBER}>
                        """.stripIndent().trim()
                    )
                }
                // If previous was already passing — stay silent (no notification)
            }
        }

        always { cleanWs() }
    }

}

Where to practice: Install the Slack Notification plugin from Manage Jenkins → Plugin Manager. Then go to Manage Jenkins → Configure System → Slack — paste in your workspace name and a Bot token. Create a test Slack app at api.slack.com/apps to get a token. Alternatively, use an incoming webhook URL directly in a curl command and skip the plugin entirely for quick testing.

Started by GitHub push by dev-miguel
[Pipeline] Start of Pipeline
[Pipeline] node (agent-linux-01)
[Pipeline] { (Checkout) } [Pipeline] checkout
> git checkout main — HEAD: a3f2b1c
[Pipeline] { (Test) }
+ ./gradlew test
BUILD SUCCESSFUL — 47 tests completed, 0 failed
[Pipeline] { (Build) }
+ ./gradlew bootJar
BUILD SUCCESSFUL in 22s
[Pipeline] { (Deploy to Production) }
+ ./deploy.sh production 58
Deploying payments-service 58 to production... done.
[Pipeline] slackSend
Slack message sent to #deployments
[Pipeline] post (success)
[Pipeline] script
previousBuild.result = FAILURE
Sending 'FIXED' notification to #deployments
[Pipeline] slackSend
Slack message sent to #deployments
[Pipeline] cleanWs
[Pipeline] End of Pipeline
Finished: SUCCESS

What just happened?

  • Two Slack messages were sent in one build — one for the production deploy success and one for the "fixed" state. Both are useful and non-noisy because the fixed message only fires when the previous build was broken.
  • previousBuild.result = FAILURE — the script block read the previous build's result and found it was FAILURE. That triggered the "FIXED" notification. If the previous build had been SUCCESS, nothing would have been sent from the success post block.
  • env.GIT_COMMIT?.take(7) — the ?. safe navigation means "if GIT_COMMIT is not null, take the first 7 characters". This produces a short commit hash like a3f2b1c — the standard format used in GitHub URLs and Slack messages.
  • The failure post block did not run — all stages passed, so the failure condition was skipped. This is the correct behaviour — no false alarms.
  • Build: <${env.BUILD_URL}|#58> — this is Slack's link formatting. The team sees "#58" as a clickable link that opens the Jenkins build page directly — they go straight from the Slack notification to the console output without searching through Jenkins.

What the Slack Messages Actually Look Like

Jenkins APP Today at 2:34 PM

✅ payments-service deployed to production

Build: #58

Commit: a3f2b1c

Jenkins APP Today at 11:17 AM

❌ payments-service build FAILED

Branch: feature/add-retry-logic

Build: #57 — check console output

Email Notifications

Email is less immediate than Slack but still useful — especially for scheduled builds, formal release notifications, or teams without a shared messaging platform. Jenkins has a built-in email step and the more powerful Email Extension (Email-ext) plugin for formatted HTML emails.

pipeline {
    agent { label 'linux' }

    environment {
        APP_NAME = 'inventory-service'
    }

    stages {
        stage('Build and Test') {
            steps {
                sh './gradlew clean test bootJar'
            }
            post {
                always { junit 'build/test-results/**/*.xml' }
            }
        }
    }

    post {

        failure {
            // emailext is from the Email Extension plugin — supports HTML and attachments
            emailext(
                // Who receives the failure email
                // $DEFAULT_RECIPIENTS pulls from the global Jenkins email config
                // You can also hardcode: 'team@acmecorp.com'
                to: '$DEFAULT_RECIPIENTS',

                // Email subject — keep it short and scannable
                subject: "[FAILED] ${APP_NAME} build #${BUILD_NUMBER} on ${env.BRANCH_NAME}",

                // HTML body — richer than plain text for email clients
                body: """
                    

Build Failed

Job: ${APP_NAME}

Build: #${BUILD_NUMBER}

Branch: ${env.BRANCH_NAME}

Console Output: ${env.BUILD_URL}console

Please check the console output and fix the build.

""", // Attach the console log so the recipient doesn't have to click a link attachLog: true, // Inline the test results in the email body compressLog: false ) } // Send a plain-text success email only when build recovers from failure success { script { if (currentBuild.previousBuild?.result == 'FAILURE') { mail( to: 'team@acmecorp.com', subject: "[FIXED] ${APP_NAME} build #${BUILD_NUMBER} is back to green", body: "The ${APP_NAME} pipeline has recovered.\n\nBuild: ${env.BUILD_URL}" ) } } } } }

Avoiding Notification Fatigue

The fastest way to make notifications useless is to send too many of them. Within a week, the team adds the channel to Do Not Disturb and the notifications become wallpaper.

🔕

Never notify on every passing build

Green builds are the expected state. Notifying on them is like your smoke alarm beeping to tell you the house is not on fire. Only notify on state changes — failure, recovery, and production deploys.

🎯

Send failures to a team channel, not individual DMs

Individual DMs create pressure on one person. A team channel makes the failure visible to everyone — whoever is available and knows the code can help, not just the person who happened to push last.

🔗

Always include a link to the build

A notification that says "build failed" with no link is barely better than nothing. Include ${env.BUILD_URL} in every message. One click from Slack to the console output is the standard.

📋

Include enough context to act without clicking

The message should answer: what failed, which branch, which build number. The team should be able to read the notification and immediately know whether they need to act — without opening Jenkins first.

Teacher's Note

The teams with the healthiest CI culture have notifications that actually mean something — because they're rare enough that when one arrives, people look at it.

Practice Questions

1. Which Jenkins build object reference lets you check the result of the build that ran before the current one?



2. In the slackSend() step, which color value produces a red sidebar on the Slack message?



3. Which Jenkins built-in environment variable holds the full URL of the current build — used in notifications so recipients can click straight to the console output?



Quiz

1. When should a Jenkins pipeline send a notification to avoid notification fatigue?


2. Declarative pipelines have no built-in "fixed" post condition. How do you simulate it?


3. Which Jenkins step should you use when you need to send an HTML-formatted email with the build log attached?


Up Next · Lesson 21

Pipeline with Git

Connecting Jenkins to Git properly — branch strategies, pull request builds, Multibranch pipelines, and how to structure a repo so Jenkins and your team both stay happy.