A CI pipeline that fails silently is half-built. The team needs to know that a build broke — immediately, in a channel they're already watching, with enough context to act. But the inverse problem is just as damaging: a notification system that sends alerts on every green build, every skipped step, and every scheduled run trains the team to ignore all alerts. The craft of CI notification design is sending the right information, to the right channel, at the right moment.
GitHub status checks — the primary notification
Before wiring up Slack or email, remember that GitHub already provides a notification channel built into every PR: status checks. Every workflow job that runs against a PR branch posts a result — ✅ green or ❌ red — directly on the PR conversation. Developers see it the moment they look at their PR; reviewers see it before approving; the merge button is gated on it.
For most teams, status checks are the primary notification for PR-level failures. They require zero configuration beyond having the workflow run — no Slack webhook, no email address, no token. Start here before adding anything else.
For PR failures where status checks are visible: use status checks. For nightly regression failures that aren't tied to a PR: use Slack. For formal records or escalation: use email.
Slack notifications in GitHub Actions
Slack notifications require a webhook URL. Create one in your Slack workspace (App settings → Incoming Webhooks) and store it as a repository secret named SLACK_WEBHOOK_URL.
Failure-only notification (most teams' choice):
- uses: slackapi/slack-github-action@v1
if: failure()
with:
payload: |
{
"text": "❌ *${{ github.workflow }}* failed on `${{ github.ref_name }}`",
"attachments": [{
"color": "danger",
"fields": [
{ "title": "Repository", "value": "${{ github.repository }}", "short": true },
{ "title": "Triggered by", "value": "${{ github.actor }}", "short": true },
{ "title": "Details", "value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" }
]
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOKStatus change notification (alert only when the build changes state — first failure or first recovery — to minimise noise):
- name: Notify on state change
if: |
(failure() && github.event.workflow_run.conclusion != 'failure') ||
(success() && github.event.workflow_run.conclusion == 'failure')
uses: slackapi/slack-github-action@v1
with:
payload: |
{ "text": "${{ job.status == 'success' && '✅ Build recovered' || '❌ Build broken' }}: ${{ github.workflow }}" }
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOKState-change notifications are the highest signal-to-noise option: you're alerted when something breaks and when it's fixed, but not for every passing build.
Slack notifications in Jenkins
Jenkins Slack notifications were covered in Chapter 3, but the channel routing strategy applies everywhere:
post {
success {
slackSend channel: '#qa-builds', color: 'good',
message: "✅ *${env.JOB_NAME}* #${env.BUILD_NUMBER} passed in ${currentBuild.durationString}"
}
failure {
slackSend channel: '#qa-builds', color: 'danger',
message: "❌ *${env.JOB_NAME}* #${env.BUILD_NUMBER} FAILED — ${env.BUILD_URL}"
}
unstable {
slackSend channel: '#qa-builds', color: 'warning',
message: "⚠️ *${env.JOB_NAME}* #${env.BUILD_NUMBER} unstable — ${env.BUILD_URL}"
}
}The unstable condition matters for Jenkins QA pipelines — it fires when tests fail but the pipeline script itself succeeded. That's exactly the state where a Slack notification is most valuable: tests are failing, and without the alert, nobody would notice until they manually check the dashboard.
Email notifications
Email is slower, creates more friction, and is easier to filter into a folder nobody reads. Use it selectively:
- uses: dawidd6/action-send-mail@v3
if: failure()
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
to: qa-team@company.com
subject: 'Build Failed: ${{ github.workflow }} on ${{ github.ref_name }}'
body: |
Build ${{ github.run_id }} failed.
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
Details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}Appropriate email uses: weekly regression summary reports, release build notifications to stakeholders outside the Slack workspace, formal audit trails for regulated environments.
Channel routing strategy
Route notifications by severity and audience, not by tool:
| Event | Channel | Why |
|---|---|---|
| PR test failure | GitHub status check (built-in) | Developer sees it on their own PR immediately |
| Nightly regression failure | #qa-builds Slack (failure only) | Team-wide awareness for systemic issues |
| Production deployment failure | #dev-alerts + #releases Slack | Broader visibility, faster response |
| Weekly coverage report | Email to qa-team@company.com | Formal record, stakeholder visibility |
| Production incident | @channel in #incidents | Emergency — escalate aggressively |
The most common mistake is routing everything to one channel with the same message format. #general with every CI result makes #general useless. #qa-builds with every passing nightly makes #qa-builds useless. Separate channels by audience; send to each only what that audience needs to act on.
README status badges
A status badge is a small image in your repository's README that reflects the current state of a workflow:


Green badges signal a healthy project to developers visiting the repository, to open-source contributors evaluating whether to contribute, and to your team during onboarding. Red badges are visible immediately, prompting someone to fix the failure before it's forgotten.
Add one badge per significant workflow: PR tests, nightly regression, and coverage. More than four badges becomes visual noise.
Notification channels: right audience, right event
GitHub Status Checks
Audience: PR author and reviewers
Visible on the PR — zero extra setup
Best for: PR-level test failures
Immediate, in-context, actionable
Fires on: every workflow run on a PR
No configuration — automatic
Limitation: no visibility outside GitHub
Team members not watching the PR miss it
Slack
Audience: entire team or specific channel
Best signal-to-noise with failure-only or state-change
Best for: nightly failures, deploy alerts
Breaks not tied to a specific PR
Fires on: failure, or state change
Configure with if: failure() or state-change logic
Limitation: easy to create notification fatigue
Over-notification trains the team to ignore it
Audience: stakeholders outside Slack
Managers, auditors, external partners
Best for: formal records, weekly summaries
Regulated environments, compliance evidence
Fires on: failures, release builds, reports
Use sparingly — daily emails are ignored
Limitation: slow, lower urgency perceived
Never use email for production incidents
⚠️ Common mistakes
- Sending success notifications for every run. A Slack message for every green build adds nothing — the team already knows the build passed because it was green. Silence on success; alert on failure. The exception is nightly regression, where a "nightly passed ✅" message once a day confirms the safety net is working.
- Using
@channelin general notification jobs.@channelnotifies everyone in the channel including people offline. Reserve it for production incidents and breaking main branch builds. For individual PR failures, send to#qa-buildswithout@channel. - No Slack notification for nightly regression failures. Nightly failures aren't attached to a PR, so they have no status check. Without a Slack notification, they go unnoticed until someone manually checks the pipeline — sometimes for days. Every scheduled workflow should send a notification on failure.
🎯 Practice task
Set up the notification stack for your project — 25 minutes.
- Add a README badge for your PR test workflow. Push. Confirm the badge updates from green to red when you push a failing test, and back to green when you fix it.
- Slack: create a free Slack workspace (if you don't have one), create an incoming webhook, store it as
SLACK_WEBHOOK_URL. Add theslackapi/slack-github-actionstep withif: failure()to your nightly workflow. Trigger a nightly failure manually (workflow_dispatch) and confirm the message appears. - Add a second Slack step with
if: success()only to the nightly workflow. Confirm the success message appears when the nightly passes, but not on PR runs. - Stretch: implement a state-change notification — only alert when the build status changes from passing to failing or vice versa. This requires comparing
github.event.workflow_run.conclusionwith the current job status.
You've completed Chapter 5. Chapter 6 brings all of these pieces together: a complete CI/CD pipeline for a real e-commerce application, built from scratch across four capstone lessons.