Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 19: Xây dựng cơ chế thông báo trạng thái Build (Discord/Telegram/Slack Webhooks)
Sau 18 bài học, cỗ máy Jenkins của chúng ta đã có thể tự động kiểm thử, quét bảo mật, phân tích SonarQube và đóng gói với tốc độ chỉ vài phút nhờ Caching. Tuy nhiên, chẳng lẽ mỗi khi lập trình viên gõ lệnh git push xong, họ lại phải mở tab trình duyệt Jenkins lên ngồi F5 liên tục để canh xem bản build Xanh hay Đỏ?
Trong môi trường làm việc thực tế với hệ thống Monolith — nơi nhiều thành viên cùng đẩy code vào một nhánh chung — việc tích hợp thông báo tự động (ChatOps Notification) về Telegram, Discord hoặc Slack là mảnh ghép bắt buộc để giải phóng thời gian cho đội ngũ kỹ thuật.
1. Tại sao nên dùng HTTP Webhook (curl) thay vì cài đặt Plugin?
Trên kho ứng dụng của Jenkins có rất nhiều plugin như Slack Notification, Telegram Bot, Discord Notifier. Tuy nhiên, kinh nghiệm xương máu của các kỹ sư DevOps lâu năm là: Hãy hạn chế cài đặt các plugin thông báo bên thứ ba, mà hãy dùng trực tiếp lệnh curl gọi HTTP Webhook ngay trong Jenkinsfile!
Lý do thực chiến:
-
Không lo lỗ hổng bảo mật & lỗi tương thích: Các plugin thông báo nhỏ lẻ thường ít được cập nhật. Khi bạn nâng cấp Jenkins Core lên bản mới, các plugin này rất dễ bị lỗi hoặc cảnh báo lỗ hổng bảo mật.
-
Tùy biến giao diện tin nhắn 100%: Dùng
curlgửi payload JSON giúp bạn toàn quyền thiết kế định dạng tin nhắn (Markdown, HTML, màu sắc khung Embed, gắn thẻ@mentiontrực tiếp người vừa làm gãy bản build). -
Độ trễ bằng 0 và cực kỳ nhẹ: Mọi máy chủ Linux hoặc Docker Agent đều có sẵn
curl, không tốn thêm bất kỳ MB RAM nào của Jenkins Controller.
2. Một bản tin thông báo CI/CD chuẩn cho hệ thống Monolith cần chứa những gì?
Một thông báo gửi về nhóm chat không nên chỉ ghi cụt lủn "Build #15 Failed". Để lập trình viên nhìn vào điện thoại là nắm bắt được ngay tình hình mà chưa cần mở máy tính, tin nhắn cần trích xuất tự động 6 thông tin sau từ Git và Jenkins:
-
Trạng thái & Màu sắc:
✅ SUCCESS(Xanh),⚠️ UNSTABLE(Vàng), hoặc❌ FAILURE(Đỏ). -
Tên dự án & Phiên bản (
BUILD_VERSION): Đã tạo ở Bài 17 (ví dụ:v1.2.0-b45-a1b2c3d). -
Nhánh Git (
BRANCH_NAME): Để biết lỗi xảy ra ở nhánhdevelophay nhánhmain(Production). -
Tác giả Commit (
GIT_AUTHOR) & Nội dung Commit (GIT_MESSAGE): Biết chính xác ai vừa đẩy đoạn code đó lên để chủ động xử lý. -
Thời gian thực thi (
currentBuild.durationString): Ví dụ: 2 min 15 sec. -
Đường dẫn trực tiếp (Direct Links): Link bấm thẳng vào màn hình Console Output, Test Result hoặc SonarQube Dashboard.
3. Chuẩn bị Webhook / Bot Token và lưu vào Jenkins Credentials
Tùy thuộc vào công cụ giao tiếp mà công ty bạn đang sử dụng, hãy lấy thông tin kết nối và cất vào Jenkins Credentials (loại Secret text) như đã học ở Bài 9:
A. Đối với Telegram (Phổ biến nhất tại các team kỹ thuật Việt Nam)
-
Mở Telegram, chat với
@BotFather, gõ/newbotđể tạo bot và lấy chuỗi Bot Token (ví dụ:7123456:AAH_xyz...). -
Thêm Bot vào nhóm chat của dự án, lấy Chat ID của nhóm (thường bắt đầu bằng dấu trừ, ví dụ:
-1001987654321). -
Vào Jenkins Credentials, tạo 2 Secret text:
-
ID:
telegram-bot-token(Chứa Bot Token). -
ID:
telegram-chat-id(Chứa Chat ID).
-
B. Đối với Discord
-
Vào cài đặt kênh chat trên Discord (Edit Channel -> Integrations -> Webhooks -> New Webhook).
-
Copy đường dẫn Webhook URL (có dạng
[https://discord.com/api/webhooks/123456/abcdef](https://discord.com/api/webhooks/123456/abcdef)...). -
Vào Jenkins Credentials, tạo 1 Secret text với ID:
discord-webhook-url.
C. Đối với Slack
-
Tạo một Incoming Webhook trong Slack App của workspace.
-
Copy đường dẫn Webhook URL (có dạng
[https://hooks.slack.com/services/T000/B000/X000](https://hooks.slack.com/services/T000/B000/X000)). -
Vào Jenkins Credentials, tạo 1 Secret text với ID:
slack-webhook-url.
4. Xây dựng hàm Groovy gửi thông báo đa kênh dùng chung trong Jenkinsfile
Để tránh làm rác phần thân chính của Pipeline, cách viết code sạch nhất (Clean Code) trong Jenkinsfile là định nghĩa một hàm def sendNotification(String buildStatus) ở cuối tệp và gọi nó trong khối post.
Dưới đây là đoạn mã trích xuất thông tin Git và gửi thông báo cho cả Telegram, Discord và Slack (bạn dùng nền tảng nào thì giữ lại khối lệnh của nền tảng đó):
Groovy
// Hàm gửi thông báo đặt ở cuối file Jenkinsfile (ngoài khối pipeline {})
def sendNotification(String status) {
// 1. Trích xuất thông tin người vừa commit và nội dung commit từ Git
def gitAuthor = sh(script: "git log -1 --pretty=format:'%an'", returnStdout: true).trim()
def gitMessage = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim().replaceAll('"', "'")
def duration = currentBuild.durationString.replace(' and counting', '')
// 2. Thiết lập biểu tượng và mã màu theo trạng thái Build
def statusIcon = "✅"
def discordColor = "3066993" // Màu xanh lá (Decimal)
if (status == 'FAILURE') {
statusIcon = "❌"
discordColor = "15158332" // Màu đỏ
} else if (status == 'UNSTABLE') {
statusIcon = "⚠️"
discordColor = "16776960" // Màu vàng
}
// =========================================================================
// KÊNH 1: GỬI THÔNG BÁO QUA TELEGRAM (Định dạng HTML sắc nét)
// =========================================================================
withCredentials([
string(credentialsId: 'telegram-bot-token', variable: 'TG_TOKEN'),
string(credentialsId: 'telegram-chat-id', variable: 'TG_CHAT_ID')
]) {
sh """
curl -s -X POST "https://api.telegram.org/bot\${TG_TOKEN}/sendMessage" \\
-d chat_id="\${TG_CHAT_ID}" \\
-d parse_mode="HTML" \\
-d disable_web_page_preview=true \\
--data-urlencode text="${statusIcon} <b>[${status}] ${env.APP_NAME}</b>
━━━━━━━━━━━━━━━
🏷 <b>Version:</b> <code>${env.BUILD_VERSION}</code>
🌿 <b>Branch:</b> <code>${env.CLEAN_BRANCH}</code>
👤 <b>Author:</b> ${gitAuthor}
💬 <b>Commit:</b> <i>${gitMessage}</i>
⏱ <b>Duration:</b> ${duration}
━━━━━━━━━━━━━━━
🔗 <a href='${env.BUILD_URL}console'>Xem Console Log</a> | <a href='${env.BUILD_URL}testReport'>Báo cáo Test</a>" > /dev/null
"""
}
// =========================================================================
// KÊNH 2: GỬI THÔNG BÁO QUA DISCORD (Sử dụng Rich Embed có thanh màu bên trái)
// =========================================================================
/* Mở comment khối này nếu team bạn dùng Discord:
withCredentials([string(credentialsId: 'discord-webhook-url', variable: 'DISCORD_WEBHOOK')]) {
sh """
curl -s -H "Content-Type: application/json" -X POST "\${DISCORD_WEBHOOK}" -d '{
"username": "Jenkins Monolith Bot",
"embeds": [{
"title": "${statusIcon} [${status}] ${env.APP_NAME} - ${env.BUILD_VERSION}",
"url": "${env.BUILD_URL}",
"color": ${discordColor},
"fields": [
{"name": "Branch", "value": "`${env.CLEAN_BRANCH}`", "inline": true},
{"name": "Author", "value": "${gitAuthor}", "inline": true},
{"name": "Duration", "value": "${duration}", "inline": true},
{"name": "Commit Message", "value": "${gitMessage}", "inline": false},
{"name": "Quick Links", "value": "[Console Output](${env.BUILD_URL}console) | [Test Report](${env.BUILD_URL}testReport)", "inline": false}
]
}]
}' > /dev/null
"""
}
*/
}
Kỹ thuật an toàn với
--data-urlencodetrên Telegram: Nhiều lập trình viên khi gửi thông báo Telegram thường nối chuỗi trực tiếp vào URL hoặc JSON. Khi nội dung commit (gitMessage) của dev chứa các ký tự đặc biệt như dấu&,#,", hoặc xuống dòng, lệnhcurlsẽ bị vỡ cú pháp và không gửi được tin nhắn. Việc dùng cờ--data-urlencode text="..."giúpcurltự động mã hóa an toàn mọi ký tự đặc biệt trong commit message.
5. Chiến lược chống "Spam tin nhắn" (Notification Fatigue) với trạng thái fixed và regression
Nếu một ngày hệ thống Monolith có 50 lượt push code và cả 50 lượt đều báo ✅ SUCCESS ting ting liên tục trong nhóm chat chung, chỉ sau 1 tuần mọi người sẽ tắt thông báo (Mute) nhóm đó. Kết quả là khi có lỗi thật thì không ai để ý!
Trong Jenkins Declarative Pipeline, khối post cung cấp các điều kiện lọc thông minh giúp bạn thiết kế luồng thông báo chống làm phiền:
-
failure: Luôn báo ngay lập tức vào nhóm khi bản build bị gãy (❌ FAILURE). -
unstable: Báo khi có Unit Test bị fail hoặc vi phạm ngưỡng cảnh báo (⚠️ UNSTABLE). -
fixed: Cực kỳ hữu ích! Chỉ báo xanh (✅ FIXED) khi bản build trước đó bị lỗi (Đỏ/Vàng) và bản build hiện tại vừa sửa xong lỗi đó để trở lại bình thường. -
success: Nếu vẫn muốn lưu lịch sử mọi bản build thành công, bạn có thể đẩy vào một kênh phụ (ví dụ#ci-cd-logs), còn kênh chính của team (#backend-dev) chỉ nhận thông báofailure,unstablevàfixed.
6. Hoàn thiện Jenkinsfile tích hợp hệ thống Thông báo tự động
Dưới đây là tệp Jenkinsfile hoàn chỉnh ghép nối toàn bộ quy trình từ Bài 10 đến Bài 19:
Groovy
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '15', artifactNumToKeepStr: '5'))
timeout(time: 30, unit: 'MINUTES')
timestamps()
}
environment {
APP_NAME = "monolith-core-system"
CACHE_ROOT = "/var/jenkins_home/.cache"
COMPOSER_CACHE_DIR = "${CACHE_ROOT}/composer"
NPM_CONFIG_CACHE = "${CACHE_ROOT}/npm"
SONAR_USER_HOME = "${CACHE_ROOT}/sonar"
}
stages {
stage('1. Fast Checkout & Versioning') {
steps {
checkout([
$class: 'GitSCM',
branches: scm.branches,
extensions: [
[$class: 'CloneOption', depth: 50, noTags: false, shallow: true, timeout: 10]
],
userRemoteConfigs: scm.userRemoteConfigs
])
script {
sh "mkdir -p ${COMPOSER_CACHE_DIR} ${NPM_CONFIG_CACHE} ${SONAR_USER_HOME}"
env.GIT_SHORT_HASH = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
env.LATEST_SEMVER_TAG = sh(script: 'git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0"', returnStdout: true).trim()
env.CLEAN_BRANCH = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim().replaceAll('[^a-zA-Z0-9.-]', '-')
if (env.CLEAN_BRANCH == 'main' || env.CLEAN_BRANCH == 'master') {
env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-b${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
} else {
env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-${env.CLEAN_BRANCH}.${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
}
currentBuild.displayName = "${env.BUILD_VERSION}"
}
}
}
stage('2. Cached Install Dependencies') {
steps {
sh 'composer install --no-interaction --prefer-dist'
}
}
stage('3. Run Unit Tests & Coverage') {
environment {
APP_ENV = "testing"
DB_CONNECTION = "sqlite"
DB_DATABASE = ":memory:"
XDEBUG_MODE = "coverage"
}
steps {
sh '''
mkdir -p reports
./vendor/bin/phpunit \
--log-junit reports/junit.xml \
--coverage-clover reports/coverage-clover.xml
'''
}
post {
always {
junit testResults: 'reports/junit.xml', allowEmptyResults: false
}
}
}
stage('4. Security & SonarQube Gate') {
steps {
script {
sh "trivy fs --cache-dir ${CACHE_ROOT}/trivy --scanners vuln,secret --severity CRITICAL --exit-code 1 ."
def scannerHome = tool 'SonarScanner-CLI'
withSonarQubeEnv('SonarQube-Main-Server') {
sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectVersion=${env.BUILD_VERSION}"
}
}
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('5. Package Production Artifact') {
steps {
sh '''
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
tar --exclude='.git' --exclude='node_modules' --exclude='tests' --exclude='reports' \
-czf ${APP_NAME}-${BUILD_VERSION}.tar.gz .
'''
}
}
}
post {
success {
archiveArtifacts artifacts: "${APP_NAME}-${env.BUILD_VERSION}.tar.gz", fingerprint: true
script {
sendNotification('SUCCESS')
}
}
unstable {
script {
sendNotification('UNSTABLE')
}
}
failure {
script {
sendNotification('FAILURE')
}
}
always {
sh 'rm -rf reports/ .env'
}
}
}
// Định nghĩa hàm gửi thông báo Telegram dùng chung ở cuối file
def sendNotification(String status) {
def gitAuthor = sh(script: "git log -1 --pretty=format:'%an'", returnStdout: true).trim()
def gitMessage = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim().replaceAll('<', '<').replaceAll('>', '>')
def duration = currentBuild.durationString.replace(' and counting', '')
def statusIcon = (status == 'SUCCESS') ? "✅" : ((status == 'UNSTABLE') ? "⚠️" : "❌")
withCredentials([
string(credentialsId: 'telegram-bot-token', variable: 'TG_TOKEN'),
string(credentialsId: 'telegram-chat-id', variable: 'TG_CHAT_ID')
]) {
sh """
curl -s -X POST "https://api.telegram.org/bot\${TG_TOKEN}/sendMessage" \\
-d chat_id="\${TG_CHAT_ID}" \\
-d parse_mode="HTML" \\
-d disable_web_page_preview=true \\
--data-urlencode text="${statusIcon} <b>[${status}] ${env.APP_NAME}</b>
━━━━━━━━━━━━━━━
🏷 <b>Version:</b> <code>${env.BUILD_VERSION}</code>
🌿 <b>Branch:</b> <code>${env.CLEAN_BRANCH}</code>
👤 <b>Author:</b> ${gitAuthor}
💬 <b>Commit:</b> <i>${gitMessage}</i>
⏱ <b>Duration:</b> ${duration}
━━━━━━━━━━━━━━━
🔗 <a href='${env.BUILD_URL}console'>Console Log</a> | <a href='${env.BUILD_URL}testReport'>Test Report</a>" > /dev/null || true
"""
}
}
(Lưu ý kỹ thuật: Luôn thêm || true ở cuối lệnh curl gửi thông báo. Điều này đảm bảo rằng nếu máy chủ Telegram/Discord bị đứt cáp mạng quốc tế không gọi được API, nó cũng không làm gãy oan uổng bản build đã thành công của bạn!).
All rights reserved