GitLab CI Discord Webhook и Jenkins: уведомления о пайплайнах
GitLab CI Discord webhook и Jenkins: job в .gitlab-ci.yml с masked-переменными и on_failure, блок post{} в Jenkinsfile, цвет embed по статусу, меньше шума.
На этой странице
- Прячем URL вебхука в секрет
- GitLab CI: стадия notify с on_success и on_failure
- Алерт о падении конкретного джоба через after_script
- Jenkins: блок post ${} в декларативном пайплайне
- Плагин HTTP Request вместо curl
- Плагин Discord Notifier
- Цвет по статусу
- Убираем шум: одно сообщение на пайплайн
- Частые ошибки
- FAQ
- Может ли GitLab писать в Discord вообще без YAML?
- Как упомянуть человека или роль при падении пайплайна?
- Можно ли приложить к сообщению лог тестов или отчёт о покрытии?
- Работает ли это в scripted-пайплайнах Jenkins?
- Итог
Зелёный пайплайн может молчать, красный обязан достучаться до человека за минуту. И GitLab CI, и Jenkins умеют доставить этот сигнал в канал Discord одним curl и URL вебхука. В статье разберём .gitlab-ci.yml (masked-переменные, джобы on_success/on_failure, ссылка на пайплайн внутри embed), блок post {} в Jenkins (curl, httpRequest или плагин Discord Notifier), цвет по статусу и способ не получать сорок сообщений от пайплайна из сорока джобов.
Прячем URL вебхука в секрет
Создайте вебхук (настройки канала → Интеграции → Вебхуки) и скопируйте URL вида https://discord.com/api/webhooks/{id}/{token}. Токен — это и есть весь секрет: любой, у кого есть URL, может писать в канал, поэтому никогда не коммитьте его в .gitlab-ci.yml или Jenkinsfile.
GitLab: Settings → CI/CD → Variables → Add variable. Ключ DISCORD_WEBHOOK_URL, значение — URL, галочка Mask variable, чтобы в логах джобов вместо значения печаталось [MASKED]. Protect variable ставьте только если джобы с уведомлениями бегут исключительно на защищённых ветках и тегах — иначе пайплайны feature-веток получат пустую переменную.
У GitLab есть ограничения на маскируемое значение (одна строка, минимальная длина, ограниченный набор символов — см. документацию GitLab). Если форма отказывается маскировать URL, храните его в base64 и декодируйте в джобе:
# на своей машине
echo -n 'https://discord.com/api/webhooks/ID/TOKEN' | base64 -w0
# в джобе
script:
- export DISCORD_WEBHOOK_URL=$(echo "$DISCORD_WEBHOOK_B64" | base64 -d)
Jenkins: Manage Jenkins → Credentials → Add Credentials → тип Secret text, ID discord-webhook-url. В пайплайне его читает withCredentials, он же маскирует значение в консольном логе.
GitLab CI: стадия notify с on_success и on_failure
Самая чистая схема — финальная стадия notify с двумя джобами: один выполняется, когда все предыдущие стадии прошли, второй — когда что-то упало. Общий скрипт лежит в скрытом шаблоне, а JSON собирает jq, чтобы кавычка в заголовке коммита не сломала payload.
stages:
- build
- test
- notify
build:
stage: build
script:
- npm ci && npm run build
test:
stage: test
script:
- npm test
.discord_notify:
stage: notify
image: alpine:3.20
before_script:
- apk add --no-cache curl jq
script:
- |
PAYLOAD=$(jq -n \
--arg title "$STATUS_TEXT $CI_PROJECT_NAME" \
--arg url "$CI_PIPELINE_URL" \
--arg desc "$CI_COMMIT_TITLE" \
--arg branch "$CI_COMMIT_REF_NAME" \
--arg sha "$CI_COMMIT_SHORT_SHA" \
--arg author "$GITLAB_USER_NAME" \
--arg pipeline "$CI_PIPELINE_ID" \
--argjson color "$STATUS_COLOR" \
'{
username: "GitLab CI",
embeds: [{
title: $title,
url: $url,
description: $desc,
color: $color,
fields: [
{ name: "Ветка", value: $branch, inline: true },
{ name: "Коммит", value: $sha, inline: true },
{ name: "Запустил", value: $author, inline: true }
],
footer: { text: ("Пайплайн #" + $pipeline) },
timestamp: (now | todate)
}]
}')
curl -sS -f --retry 3 \
-H "Content-Type: application/json" \
-d "$PAYLOAD" "$DISCORD_WEBHOOK_URL"
notify_success:
extends: .discord_notify
when: on_success
variables:
STATUS_TEXT: '✅ Пайплайн прошёл:'
STATUS_COLOR: '5763719'
notify_failure:
extends: .discord_notify
when: on_failure
variables:
STATUS_TEXT: '❌ Пайплайн упал:'
STATUS_COLOR: '15548997'
Что здесь важно:
when: on_failureзапускает джоб, только если упал джоб на одной из предыдущих стадий;when: on_success(значение по умолчанию) — только если всё прошло. За пайплайн срабатывает не больше одного из двух; у отменённого пайплайна не запустится ни один.urlв embed превращает заголовок в ссылку на пайплайн.CI_PIPELINE_URL,CI_COMMIT_TITLE,CI_COMMIT_SHORT_SHAиGITLAB_USER_NAME— предопределённые переменные, настраивать ничего не нужно.--argjson colorпревращает строковую переменную в число:colorдолжен быть десятичным целым, а не"#57F287".curl -fроняет джоб при ответе 4xx/5xx вместо тихого «зелёного»;--retry 3покрывает временные ошибки, включая 429.
Алерт о падении конкретного джоба через after_script
Если нужно знать, какой именно джоб упал, но добавлять стадию не хочется, используйте after_script. Там доступна CI_JOB_STATUS со значениями success, failed или canceled:
.alert_on_fail:
after_script:
- |
if [ "$CI_JOB_STATUS" = "failed" ]; then
curl -sS -H "Content-Type: application/json" \
-d "{\"content\": \"❌ **$CI_JOB_NAME** упал на \`$CI_COMMIT_REF_NAME\` — [открыть джоб]($CI_JOB_URL)\"}" \
"$DISCORD_WEBHOOK_URL"
fi
test:
extends: .alert_on_fail
script:
- npm test
Jenkins: блок post {} в декларативном пайплайне
У декларативного пайплайна есть секция post, которая выполняется после всех стадий. Вместо того чтобы копировать curl в success, failure и unstable, сопоставьте currentBuild.currentResult с цветом в одной функции и вызывайте её из always:
pipeline {
agent any
stages {
stage('Build') {
steps { sh 'npm ci && npm run build' }
}
stage('Test') {
steps { sh 'npm test' }
}
}
post {
always {
discordNotify(currentBuild.currentResult)
}
}
}
def discordNotify(String result) {
def colors = [SUCCESS: 5763719, FAILURE: 15548997, UNSTABLE: 16705372, ABORTED: 9807270]
def icons = [SUCCESS: '✅', FAILURE: '❌', UNSTABLE: '⚠️', ABORTED: '⏹️']
def payload = groovy.json.JsonOutput.toJson([
username: 'Jenkins',
embeds: [[
title: "${icons[result]} ${env.JOB_NAME} #${env.BUILD_NUMBER}: ${result}",
url: env.BUILD_URL,
color: colors[result],
fields: [
[name: 'Ветка', value: env.GIT_BRANCH ?: 'n/a', inline: true],
[name: 'Длительность', value: currentBuild.durationString.replace(' and counting', ''), inline: true]
]
]]
])
writeFile file: 'discord-payload.json', text: payload
withCredentials([string(credentialsId: 'discord-webhook-url', variable: 'DISCORD_WEBHOOK_URL')]) {
sh 'curl -sS -f -H "Content-Type: application/json" -d @discord-payload.json "$DISCORD_WEBHOOK_URL"'
}
}
Почему именно так:
JsonOutput.toJsonэкранирует кавычки и переводы строк, аwriteFile+-d @fileполностью выводит payload из-под кавычек shell.withCredentialsподставляет секрет и маскирует его в консоли: даже эхо отset -xпокажет****; строкаshв одинарных кавычках оставляет подстановку$DISCORD_WEBHOOK_URLшеллу, а не Groovy.currentBuild.durationStringзаканчивается на « and counting», пока сборка идёт, а внутриpostона всегда ещё идёт — отсюдаreplace.
Плагин HTTP Request вместо curl
Если на агенте нет curl, тот же запрос делает плагин HTTP Request — внутри того же блока withCredentials:
httpRequest url: env.DISCORD_WEBHOOK_URL, httpMode: 'POST',
contentType: 'APPLICATION_JSON', requestBody: payload, validResponseCodes: '200:204'
Плагин пишет URL запроса в консоль, но внутри withCredentials фильтр логов его замаскирует.
Плагин Discord Notifier
Если JSON не хочется вовсе, плагин Discord Notifier добавляет шаг discordSend, который сам раскрашивает embed по результату сборки:
post {
always {
withCredentials([string(credentialsId: 'discord-webhook-url', variable: 'DISCORD_WEBHOOK_URL')]) {
discordSend(
webhookURL: env.DISCORD_WEBHOOK_URL,
title: "${env.JOB_NAME} #${env.BUILD_NUMBER}",
link: env.BUILD_URL,
result: currentBuild.currentResult,
description: "Ветка: ${env.GIT_BRANCH}\nДлительность: ${currentBuild.durationString}",
footer: 'Jenkins'
)
}
}
}
С curl вы владеете всем payload (до 25 полей, картинки, несколько embed); с плагином — тем, что выведено в его параметры.
Цвет по статусу
color — десятичное целое; небольшой палитры хватает на все состояния CI:
| Состояние | Hex | Decimal |
|---|---|---|
| Прошёл | #57F287 | 5763719 |
| Упал | #ED4245 | 15548997 |
| Unstable / предупреждение | #FEE75C | 16705372 |
| Выполняется | #5865F2 | 5793266 |
| Отменён | #95A5A6 | 9807270 |
В shell hex переводится в decimal через echo $((16#ED4245)); больше оттенков — в статье про цвета embed в Discord. Дублируйте статус словом в заголовке: коллеги с нарушением цветовосприятия и превью push-уведомления на телефоне боковую полоску не видят.
Убираем шум: одно сообщение на пайплайн
Двенадцать джобов, двенадцать зелёных сообщений — типичная беда. Четыре правила её лечат.
Уведомляйте из одного места. Финальная стадия notify в GitLab, post на уровне пайплайна в Jenkins. Не по джобу и не по стадии — исключение только для алертов о падениях.
Успех — только там, где он важен. Ветка по умолчанию и теги получают и успех, и падения; feature-ветки — только падения; пайплайны merge request — ничего, их статус и так виден в виджете MR:
notify_success:
extends: .discord_notify
variables:
STATUS_TEXT: '✅ Пайплайн прошёл:'
STATUS_COLOR: '5763719'
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
when: on_success
notify_failure:
extends: .discord_notify
variables:
STATUS_TEXT: '❌ Пайплайн упал:'
STATUS_COLOR: '15548997'
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
- when: on_failure
В Jenkins — по факту изменения. failure плюс fixed дают красное сообщение и ровно одно зелёное, когда сборка починилась, вместо зелёного на каждую сборку:
post {
failure { discordNotify('FAILURE') }
fixed { discordNotify('SUCCESS') }
}
Редактируйте вместо повторной отправки. Отправьте сообщение «выполняется» с ?wait=true, сохраните id из ответа (в GitLab запишите его в артефакт dotenv, чтобы следующие джобы его видели), а в конце сделайте PATCH этого сообщения с итоговым payload. Одна строка на пайплайн, всегда актуальная:
MSG_ID=$(curl -sS -H "Content-Type: application/json" -d "$RUNNING_PAYLOAD" \
"$DISCORD_WEBHOOK_URL?wait=true" | jq -r .id)
# позже, в джобе notify:
curl -sS -f -X PATCH -H "Content-Type: application/json" -d "$PAYLOAD" \
"$DISCORD_WEBHOOK_URL/messages/$MSG_ID"
Полный цикл — в статье про редактирование и удаление сообщений вебхука. Какой бы путь вы ни выбрали, монорепозиторий, где постит каждый джоб, быстро упрётся в 429 — см. лимиты Discord webhook.
Частые ошибки
Джоб зелёный, а в Discord пусто. Без -f curl завершается с кодом 0 даже на 400 или 404. Добавьте -f -sS (или --fail-with-body, curl 7.76+: джоб падает, но тело ответа всё равно печатается) либо печатайте статус через -w '%{http_code}', затем читайте тело ответа: Discord называет невалидное поле. Голый -f тело скрывает.
400 Bad Request после конкретного коммита. В заголовке коммита была кавычка или обратный слеш, и он попал прямо в JSON-строку. Собирайте payload через jq --arg или JsonOutput.toJson, а не вручную в shell.
{"message": "Unknown Webhook", "code": 10015}. URL неверный или вебхук удалён в Discord. Если же curl завершается с (3) URL using bad/illegal format or missing URL, переменная здесь пуста: protected-переменная не видна незащищённым веткам, переменная с environment scope — вне своего окружения. Безопасная проверка — echo ${#DISCORD_WEBHOOK_URL}: длина 0 значит, что переменной нет.
notify_failure не запускается, хотя джоб красный. У красного джоба стоит allow_failure: true, и GitLab считает пайплайн успешным. Падения таких джобов on_failure не запускают.
post { failure } в Jenkins молчит при упавших тестах. junit помечает сборку как UNSTABLE, а не FAILED. Обрабатывайте unstable тоже или используйте always с currentBuild.currentResult, как выше.
No such DSL method 'httpRequest'. Плагин не установлен на этом контроллере: поставьте его или вернитесь к sh 'curl …'.
Токен засветился в логе. GitLab маскирует только точное значение переменной: выведите через echo декодированную или изменённую копию — и токен виден. Отзовите вебхук (удалите и создайте заново в Discord) и почините скрипт. То же правило в Jenkins вне withCredentials.
429 Too Many Requests. Слишком много джобов постят одновременно: уважайте retry_after и переходите на одну стадию notify.
FAQ
Может ли GitLab писать в Discord вообще без YAML?
Да. В GitLab есть интеграция Discord Notifications (Settings → Integrations → Discord Notifications): вставляете URL вебхука, и она шлёт события пайплайнов, пушей и merge request. Для простых пингов достаточно; джоб с curl даёт свой макет embed и цвета.
Как упомянуть человека или роль при падении пайплайна?
Поместите <@&ROLE_ID> в content и явно разрешите упоминание: "allowed_mentions": {"roles": ["ROLE_ID"]}. Без allowed_mentions Discord парсит все упоминания в content, и случайный @everyone из заголовка коммита пингует весь сервер; явный список ролей ограничивает пинг только этой ролью. Подробности — в статье про упоминания через вебхук.
Можно ли приложить к сообщению лог тестов или отчёт о покрытии?
Да, переключитесь на multipart/form-data: curl -F 'payload_json={"content":"Лог тестов"}' -F 'files[0][email protected]' "$DISCORD_WEBHOOK_URL". До 10 файлов по 10 МБ по умолчанию. См. отправку файлов через вебхук.
Работает ли это в scripted-пайплайнах Jenkins?
Да. Блока post там нет, поэтому оберните стадии в try { … } catch (e) { currentBuild.result = 'FAILURE'; throw e } finally { discordNotify(currentBuild.currentResult) }.
Итог
Постите из одного места, собирайте JSON настоящим сериализатором, красьте по статусу и оставляйте «успех» только для веток, за которыми следят. Чтобы спроектировать embed до того, как писать скрипт, откройте конструктор Discord Webhook, соберите поля и цвета визуально и экспортируйте JSON прямо в .gitlab-ci.yml или Jenkinsfile.
Смежные статьи: