Notifications, Checks, Commit Status, Pull Request Feedback, Chat Integrations, and Release Communication: Guided Hands-On Workflow and Core Operations
Publish build feedback to a disposable local HTTP sink, preserve delivery evidence, simulate SCM status/check semantics, deduplicate retries, and notify only meaningful state transitions.
Learning objectives
- Run a local notification sink that records event ID, source SHA and build identity.
- Create a canonical feedback payload without secrets.
- Publish with bounded retry and verify response state.
- Simulate success, failure and recovery notifications without external provider credentials.
- Separate status/check publication from email/chat rendering.
1. Scenario and safe boundaries
Use a disposable Pipeline job jenkins-labs/feedback on
agent label feedback-lab. A Python server bound to
127.0.0.1:18080 acts as the external notification
endpoint. The sink deliberately stores only safe JSON under a
temporary lab directory.
| Identity | Value | Why |
|---|---|---|
| Job | jenkins-labs/feedback |
Exact retained Jenkins history. |
| Agent | feedback-lab |
No routine build on controller. |
| Sink | http://127.0.0.1:18080/events |
No internet or provider token. |
| Check name | ci/jenkins/feedback-lab |
Stable simulated external status identity. |
| Event key | job#build:event:state |
Supports duplicate detection. |
| Evidence directory | feedback-evidence/ |
Archives request/response without secrets. |
2. Create the local notification sink
The sink accepts POSTs, validates a small schema, deduplicates by
event_id, and returns an application-level delivery ID.
It intentionally ignores authorization headers because the lab
requires no secret.
# ci/mock_feedback_sink.py
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import json, hashlib
STORE = Path("feedback-sink")
STORE.mkdir(exist_ok=True)
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/events":
self.send_error(404)
return
try:
n = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(n))
required = {"event_id", "job_full_name", "build_number", "source_sha", "state", "summary"}
missing = sorted(required - payload.keys())
if missing:
self._json(422, {"accepted": False, "missing": missing})
return
if len(str(payload["source_sha"])) != 40:
self._json(422, {"accepted": False, "error": "source_sha must be full SHA-1 text for this lab"})
return
event_id = str(payload["event_id"])
delivery_id = hashlib.sha256(event_id.encode()).hexdigest()[:16]
path = STORE / f"{delivery_id}.json"
duplicate = path.exists()
if not duplicate:
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
self._json(200, {"accepted": True, "delivery_id": delivery_id, "duplicate": duplicate})
except Exception as exc:
self._json(400, {"accepted": False, "error": type(exc).__name__})
def log_message(self, fmt, *args):
print("sink", self.address_string(), fmt % args)
def _json(self, status, obj):
body = (json.dumps(obj, sort_keys=True) + "\n").encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
ThreadingHTTPServer(("127.0.0.1", 18080), Handler).serve_forever()
set -euo pipefail
mkdir -p ci feedback-evidence
python3 ci/mock_feedback_sink.py >feedback-evidence/sink.log 2>&1 &
echo $! > feedback-evidence/sink.pid
sleep 1
curl --fail --silent --show-error http://127.0.0.1:18080/not-found || true
The final curl should receive 404; that proves the server is reachable without pretending a valid event was accepted.
3. Build a canonical event from explicit Jenkins state
# ci/build_feedback_event.py
import json, os, subprocess, sys
from pathlib import Path
state = sys.argv[1]
allowed = {"success", "failure", "recovery"}
if state not in allowed:
raise SystemExit(f"state must be one of {sorted(allowed)}")
sha = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
job = os.environ.get("JOB_NAME", "local/feedback")
build = int(os.environ.get("BUILD_NUMBER", "0"))
build_url = os.environ.get("BUILD_URL", "http://jenkins.invalid/local/0/")
event_kind = "quality-result"
event = {
"schema": "devops-academy.feedback/v1",
"event_id": f"{job}#{build}:{event_kind}:{state}",
"job_full_name": job,
"build_number": build,
"build_url": build_url,
"source_sha": sha,
"change_id": os.environ.get("CHANGE_ID", "none"),
"check_name": "ci/jenkins/feedback-lab",
"state": state,
"summary": {
"success": "Quality evidence accepted.",
"failure": "Quality evidence failed; inspect retained Jenkins evidence.",
"recovery": "Quality evidence recovered after the previous failed build."
}[state],
"artifact_digest": os.environ.get("LAB_ARTIFACT_DIGEST", "none"),
}
Path("feedback-evidence/event.json").write_text(json.dumps(event, indent=2, sort_keys=True) + "\n")
print(json.dumps(event, sort_keys=True))
Notice what is absent: no token, no credential ID value that could be confused with a secret, no environment dump and no raw test log.
4. Publish with bounded retry and application-level verification
#!/usr/bin/env bash
# ci/publish_feedback.sh
set -euo pipefail
endpoint="${1:?endpoint required}"
event_file="${2:?event file required}"
response_file="${3:?response file required}"
for attempt in 1 2 3; do
http_code="$(curl --silent --show-error \
--output "$response_file" \
--write-out '%{http_code}' \
--header 'Content-Type: application/json' \
--data-binary "@$event_file" \
"$endpoint" || true)"
if [ "$http_code" = "200" ] && python3 - "$response_file" <<'PYRESP'
import json, sys
obj=json.load(open(sys.argv[1]))
raise SystemExit(0 if obj.get("accepted") is True and obj.get("delivery_id") else 1)
PYRESP
then
printf 'feedback accepted attempt=%s response=%s\n' "$attempt" "$response_file"
exit 0
fi
printf 'feedback attempt=%s failed http=%s\n' "$attempt" "$http_code" >&2
sleep "$attempt"
done
exit 75
This retry scope repeats only the notification request. It does not rebuild, republish an artifact or rerun deployment. Because the event ID is stable, an accepted first attempt followed by a lost response can be retried safely against this lab sink and recorded as a duplicate rather than a second logical event.
5. Exercise success, duplicate and failure states locally
set -euo pipefail
mkdir -p feedback-evidence
python3 ci/build_feedback_event.py success
bash ci/publish_feedback.sh \
http://127.0.0.1:18080/events \
feedback-evidence/event.json \
feedback-evidence/response-1.json
# Same event again: sink should return duplicate=true.
bash ci/publish_feedback.sh \
http://127.0.0.1:18080/events \
feedback-evidence/event.json \
feedback-evidence/response-2.json
cat feedback-evidence/response-1.json
cat feedback-evidence/response-2.json
The second response should preserve the same delivery ID and report
duplicate=true. That is concrete evidence that retry
identity is working.
6. Jenkinsfile: feedback is a post-result side effect
pipeline {
agent { label 'feedback-lab' }
parameters {
choice(name: 'SCENARIO', choices: ['success', 'failure'], description: 'Synthetic build outcome')
}
options { timestamps(); disableConcurrentBuilds() }
stages {
stage('Checkout + identity') {
steps {
checkout scm
sh '''
set -euo pipefail
mkdir -p feedback-evidence
git rev-parse HEAD | tee feedback-evidence/source-sha.txt
printf 'job=%s\nbuild=%s\nnode=%s\n' \
"$JOB_NAME" "$BUILD_NUMBER" "$NODE_NAME" | tee feedback-evidence/build.txt
'''
}
}
stage('Synthetic quality decision') {
steps {
script {
if (params.SCENARIO == 'failure') {
error('Synthetic quality failure for Chapter 34 lab')
}
sh 'printf "quality=success\\n" > feedback-evidence/quality.txt'
}
}
}
}
post {
success {
sh '''
set -euo pipefail
python3 ci/build_feedback_event.py success
bash ci/publish_feedback.sh http://127.0.0.1:18080/events \
feedback-evidence/event.json feedback-evidence/delivery.json
'''
}
failure {
sh '''
set -euo pipefail
python3 ci/build_feedback_event.py failure
bash ci/publish_feedback.sh http://127.0.0.1:18080/events \
feedback-evidence/event.json feedback-evidence/delivery.json
'''
}
always {
archiveArtifacts artifacts: 'feedback-evidence/**', allowEmptyArchive: true, fingerprint: true
}
}
}
The notification outcome should not erase the original build outcome. In production, decide explicitly whether a feedback-delivery failure should fail the whole Pipeline, mark it unstable, or create a separate operational alert. That policy depends on whether the feedback mechanism is advisory or a required integration.
7. Recovery means a state transition, not merely another success
A useful recovery message is sent when a previously failing signal
becomes healthy. It should identify both current build and the
relevant prior failure. The mandatory lab simulates this by creating
a recovery event after one controlled failed build and
one later successful build. In a production design, use Jenkins
build history or an external incident/notification record rather
than guessing from “last build” under concurrency.
8. Optional provider translations
| Internal event field | GitHub check/status | Email/chat |
|---|---|---|
source_sha |
Target commit/check-run SHA | Include short SHA and repository link. |
check_name |
Check name or status context | Human-readable signal name. |
state |
queued/in-progress/completed conclusion or pending/success/failure/error status | Success/failure/recovery wording. |
build_url |
details/target URL | Primary link back to retained Jenkins evidence. |
event_id |
External ID/update key where supported | Deduplication key stored by publisher/sink. |
summary |
Short output/description | Redacted actionable message. |
Provider APIs differ. Do not mechanically map every internal state to every external enum without current documentation.
9. Challenge: choose the right layer
The Jenkins build failed correctly, but the local sink is down. Should you rerun the whole build? No. Preserve the original build/source evidence, repair or restore the feedback endpoint, and retry only the feedback event with the same ID if your policy permits. Rebuilding could create a different build identity and repeat unrelated side effects.
Knowledge check
Answer before revealing the explanation.
1. Why does the sink return a delivery ID in addition to HTTP 200?
It gives application-level evidence that the logical event was accepted and lets Jenkins correlate retries with one external record.
2. Why does the lab use a stable event ID?
So retried delivery is distinguishable from a new notification and can be deduplicated.
3. If the feedback endpoint is unavailable after a failed build, should you rerun tests?
Not by default. Preserve the failed build evidence and retry only the feedback side effect when safe.
4. What does the local sink simulate accurately?
Identity, payload validation, application-level acceptance, delivery IDs and deduplication. It does not prove provider-specific permissions, rate limits or UI behavior.
5. Why archive request and response evidence?
To prove what safe payload Jenkins attempted to send and what the receiver reported without exposing credentials.
Official references and version notes
Notification plugins and provider APIs change independently. Verify current primary documentation before applying these patterns to a real organization.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.