1
0
mirror of https://github.com/firstBitMarksistskaya/jenkins-lib.git synced 2025-01-05 13:10:28 +02:00

Добавление отправки результатов сборки в почту и телеграм

This commit is contained in:
Nikita Fedkin 2022-05-20 20:49:35 +03:00
parent 2822b6c323
commit 25facc91fc
No known key found for this signature in database
GPG Key ID: E7AE91471C6FFE04
23 changed files with 910 additions and 10 deletions

View File

@ -39,6 +39,7 @@
1. Запуск валидации проекта средствами EDT и конвертация отчета в формате generic issues.
1. Запуск статического анализа для SonarQube.
1. Публикация результатов junit и Allure в интерфейс Jenkins.
1. Рассылка результатов сборки на почту и в Telegram.
1. Конфигурирование логгера запускаемых oscript-приложений.
## Подключение
@ -109,9 +110,11 @@ pipeline1C()
* Исходники конфигурации ожидаются в каталоге `src/cf` (`srcDir`).
* Формат исходников - выгрузка из Конфигуратора (`sourceFormat`).
* Ветка по умолчанию (для комбинированного режима загрузки конфигурации) - "main" (`defaultBranch`).
* Имена "секретов" (jenkins credentials, `secrets`) по умолчанию высчитываются из пути к git-репозиторию (без учета домена, с заменой `/` на `_`) с прибавлением ключа секрета. Например, для репозитория https://github.com/firstBitSemenovskaya/jenkins-lib секрет с адресом хранилища будет выглядеть как `firstBitSemenovskaya_jenkins-lib_STORAGE_PATH`. Ключи секретов:
* Имена большинства "секретов" (jenkins credentials, `secrets`) по умолчанию высчитываются из пути к git-репозиторию (без учета домена, с заменой `/` на `_`) с прибавлением ключа секрета. Например, для репозитория https://github.com/firstBitSemenovskaya/jenkins-lib секрет с адресом хранилища будет выглядеть как `firstBitSemenovskaya_jenkins-lib_STORAGE_PATH`. Ключи секретов:
* `STORAGE_PATH` - путь к хранилищу конфигурации (для `secrets` -> `storagePath`);
* `STORAGE_USER` - параметры авторизации в хранилище вида "username with password" (для `secrets` -> `storage`).
* `TELEGRAM_CHAT_ID` - идентификатор чата Telegram для рассылки уведомлений и результате сборки вида "secret text" (для `secrets` -> `telegramChatId`).
* Секрет `TELEGRAM_BOT_TOKEN` задается глобально на весь сервер Jenkins, либо может быть переопределен (`secrets` -> `telegramBotToken`)
* Все "шаги" по умолчанию выключены (`stages`).
* Если в корне репозитория существует файл `packagedef`, то в шагах, работающих с информационной базой, будет выполнена попытка установки локальных зависимостей средствами `opm`.
* Если после установки локальных зависимостей в каталоге `oscript_modules/bin` существует файл `vrunner`, то для выполнения команд работы с информационной базой будет использоваться он, а не глобально установленный `vrunner` из `PATH`.
@ -150,3 +153,16 @@ pipeline1C()
* Шаг анализа не дожидается окончания фонового задания на сервере SonarQube и не анализирует результат прохождения Порога качества (`sonarqube` -> `waitForQualityGate`).
Для этого необходимо заполнить параметр (`sonarqube` -> `infoBaseUpdateModuleName`). Если параметр не заполнен, версия передается из корня конфигурации.
* Если выполнялась валидация EDT, результаты валидации в формате `generic issues` передаются утилите `sonar-scanner` как значение параметра `sonar.externalIssuesReportPaths`.
* Рассылка уведомлений:
* Электронная почта:
* Для отправки используется плагин [`email-ext`](https://plugins.jenkins.io/email-ext). Шаблоны сообщений конфигурируются в настройках плагина.
* Уведомления о результатах сборки по умолчанию рассылаются только при полном падении сборочной линии (`notifications` -> `email` -> `onAlways`, `onFailure`, `onUnstable`, `onSuccess`).
* Лог сборки прикладывается к письму при полном падении сборочной линии и при отправке в режиме "всегда отправлять" (`notifications` -> `email` -> `*options` -> `attachLog`).
* В качестве получателей писем (`notifications` -> `email` -> `*options` -> `recipientProviders`) в различных режимах отправки используются:
* всегда - разработчики и запустивший сборку;
* при падении - разработчики, запустивший сборку и подозреваемый в причине падения сборки;
* при успехе - разработчики и запустивший сборку;
* при нестабильной сборке (упавшие тесты) - разработчики и запустивший сборку.
* Прямые получатели уведомлений не заполнены (`notifications` -> `email` -> `*options` -> `directRecipients`).
* Telegram:
* Уведомления о результатах сборки по умолчанию рассылаются всегда (`notifications` -> `telegram` -> `onAlways`, `onFailure`, `onUnstable`, `onSuccess`).

View File

@ -80,6 +80,7 @@ sharedLibrary {
dependency("org.jenkins-ci.plugins", "pipeline-build-step", "2.12")
dependency("org.jenkins-ci.plugins", "pipeline-utility-steps", "2.8.0")
dependency("org.jenkins-ci.plugins", "git", "4.4.4")
dependency("org.jenkins-ci.plugins", "http_request", "1.15")
dependency("org.6wind.jenkins", "lockable-resources", "2.7")
dependency("ru.yandex.qatools.allure", "allure-jenkins-plugin", "2.28.1")
val declarativePluginsVersion = "1.6.0"
@ -87,5 +88,6 @@ sharedLibrary {
dependency("org.jenkinsci.plugins", "pipeline-model-declarative-agent", "1.1.1")
dependency("org.jenkinsci.plugins", "pipeline-model-definition", declarativePluginsVersion)
dependency("org.jenkinsci.plugins", "pipeline-model-extensions", declarativePluginsVersion)
dependency("io.jenkins.blueocean", "blueocean-pipeline-api-impl", "1.25.3")
}
}

View File

@ -7,7 +7,9 @@
"defaultBranch": "main",
"secrets": {
"storagePath": "UNKNOWN_ID",
"storage": "UNKNOWN_ID"
"storage": "UNKNOWN_ID",
"telegramBotToken": "UNKNOWN_ID",
"telegramChatId": "UNKNOWN_ID"
},
"stages": {
"initSteps": false,
@ -15,7 +17,9 @@
"bdd": false,
"syntaxCheck": false,
"edtValidate": false,
"smoke": false
"smoke": false,
"email": false,
"telegram": false
},
"timeout": {
"smoke": 240,
@ -77,5 +81,52 @@
"removeSupport": true,
"supportLevel": 0
},
"notifications": {
"email": {
"onAlways": false,
"onFailure": true,
"onUnstable": false,
"onSuccess": false,
"alwaysOptions": {
"attachLog": true,
"directRecipients": [],
"recipientProviders": [
"developers",
"requestor"
]
},
"failureOptions": {
"attachLog": true,
"directRecipients": [],
"recipientProviders": [
"developers",
"requestor",
"brokenBuildSuspects"
]
},
"successOptions": {
"attachLog": false,
"directRecipients": [],
"recipientProviders": [
"developers",
"requestor"
]
},
"unstableOptions": {
"attachLog": false,
"directRecipients": [],
"recipientProviders": [
"developers",
"requestor"
]
}
},
"telegram": {
"onAlways": true,
"onFailure": false,
"onUnstable": false,
"onSuccess": false
}
},
"logosConfig": ""
}

View File

@ -35,6 +35,14 @@
"storage" : {
"type" : "string",
"description" : "Данные авторизации в хранилище конфигурации"
},
"telegramChatId" : {
"type" : "string",
"description" : "Идентификатор telegram-чата для отправки уведомлений"
},
"telegramBotToken" : {
"type" : "string",
"description" : "Токен авторизации telegram-бота для отправки уведомлений"
}
}
},
@ -66,6 +74,14 @@
"bdd" : {
"type" : "boolean",
"description" : "Запуск BDD сценариев включен"
},
"email" : {
"type" : "boolean",
"description" : "Выполнять рассылку результатов сборки на email"
},
"telegram" : {
"type" : "boolean",
"description" : "Выполнять рассылку результатов сборки в telegram"
}
}
},
@ -257,6 +273,93 @@
}
}
},
"notifications" : {
"type" : "object",
"id" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:NotificationsOptions",
"description" : "Настройки рассылки результатов сборки",
"properties" : {
"email" : {
"type" : "object",
"id" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:notification:EmailNotificationOptions",
"description" : "Настройки рассылки результатов сборки через email",
"properties" : {
"onAlways" : {
"type" : "boolean",
"description" : "Отправлять всегда"
},
"onSuccess" : {
"type" : "boolean",
"description" : "Отправлять при успешной сборке"
},
"onFailure" : {
"type" : "boolean",
"description" : "Отправлять при падении сборки"
},
"onUnstable" : {
"type" : "boolean",
"description" : "Отправлять при нестабильной сборке"
},
"alwaysOptions" : {
"type" : "object",
"id" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:notification:email:EmailExtConfiguration",
"properties" : {
"attachLog" : {
"type" : "boolean"
},
"directRecipients" : {
"type" : "array",
"items" : {
"type" : "string"
}
},
"recipientProviders" : {
"type" : "array",
"items" : {
"type" : "string",
"enum" : [ "developers", "requestor", "brokenBuildSuspects", "brokenTestsSuspects" ]
}
}
}
},
"successOptions" : {
"type" : "object",
"$ref" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:notification:email:EmailExtConfiguration"
},
"failureOptions" : {
"type" : "object",
"$ref" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:notification:email:EmailExtConfiguration"
},
"unstableOptions" : {
"type" : "object",
"$ref" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:notification:email:EmailExtConfiguration"
}
}
},
"telegram" : {
"type" : "object",
"id" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:notification:TelegramNotificationOptions",
"description" : "Настройки рассылки результатов сборки через telegram",
"properties" : {
"onAlways" : {
"type" : "boolean",
"description" : "Отправлять всегда"
},
"onSuccess" : {
"type" : "boolean",
"description" : "Отправлять при успешной сборке"
},
"onFailure" : {
"type" : "boolean",
"description" : "Отправлять при падении сборки"
},
"onUnstable" : {
"type" : "boolean",
"description" : "Отправлять при нестабильной сборке"
}
}
}
}
},
"logosConfig" : {
"type" : "string",
"description" : "Конфигурация библиотеки logos. Применяется перед запуском каждой стадии сборки"

View File

@ -1,7 +1,11 @@
package ru.pulsar.jenkins.library
import jenkins.plugins.http_request.HttpMode
import jenkins.plugins.http_request.MimeType
import jenkins.plugins.http_request.ResponseContentSupplier
import org.jenkinsci.plugins.pipeline.utility.steps.fs.FileWrapper
import org.jenkinsci.plugins.workflow.support.actions.EnvironmentAction
import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper
interface IStepExecutor {
@ -69,7 +73,9 @@ interface IStepExecutor {
def catchError(Closure body)
def httpRequest(String url, String outputFile, String responseHandle, boolean wrapAsMultipart)
ResponseContentSupplier httpRequest(String url, String outputFile, String responseHandle, boolean wrapAsMultipart)
ResponseContentSupplier httpRequest(String url, HttpMode httpMode, MimeType contentType, String requestBody, String validResponseCodes, boolean consoleLogResponseBody)
def error(String errorMessage)
@ -78,4 +84,16 @@ interface IStepExecutor {
def junit(String testResults, boolean allowEmptyResults)
def installLocalDependencies()
def emailext(String subject, String body, String to, List recipientProviders, boolean attachLog)
def developers()
def requestor()
def brokenBuildSuspects()
def brokenTestsSuspects()
RunWrapper currentBuild()
}

View File

@ -1,7 +1,11 @@
package ru.pulsar.jenkins.library
import jenkins.plugins.http_request.HttpMode
import jenkins.plugins.http_request.MimeType
import jenkins.plugins.http_request.ResponseContentSupplier
import org.jenkinsci.plugins.pipeline.utility.steps.fs.FileWrapper
import org.jenkinsci.plugins.workflow.support.actions.EnvironmentAction
import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper
import ru.yandex.qatools.allure.jenkins.config.ResultsConfig
class StepExecutor implements IStepExecutor {
@ -153,10 +157,22 @@ class StepExecutor implements IStepExecutor {
}
@Override
def httpRequest(String url, String outputFile, String responseHandle = 'NONE', boolean wrapAsMultipart = false) {
ResponseContentSupplier httpRequest(String url, String outputFile, String responseHandle = 'NONE', boolean wrapAsMultipart = false) {
steps.httpRequest responseHandle: responseHandle, outputFile: outputFile, url: url, wrapAsMultipart: wrapAsMultipart
}
@Override
ResponseContentSupplier httpRequest(String url, HttpMode httpMode, MimeType contentType, String requestBody, String validResponseCodes, boolean consoleLogResponseBody) {
steps.httpRequest(
url: url,
httpMode: httpMode,
contentType: contentType,
requestBody: requestBody,
validResponseCodes: validResponseCodes,
consoleLogResponseBody: consoleLogResponseBody
)
}
@Override
def error(String errorMessage) {
steps.error errorMessage
@ -183,4 +199,40 @@ class StepExecutor implements IStepExecutor {
def installLocalDependencies() {
steps.installLocalDependencies()
}
@Override
def emailext(String subject, String body, String to, List recipientProviders, boolean attachLog) {
steps.emailext(
subject: subject,
body: body,
to: to,
recipientProviders: recipientProviders,
attachLog: attachLog,
)
}
@Override
def developers() {
steps.developers()
}
@Override
def requestor() {
steps.requestor()
}
@Override
def brokenBuildSuspects() {
steps.brokenBuildSuspects()
}
@Override
def brokenTestsSuspects() {
steps.brokenTestsSuspects()
}
@Override
RunWrapper currentBuild() {
steps.currentBuild
}
}

View File

@ -6,8 +6,11 @@ import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.commons.beanutils.BeanUtilsBean
import org.apache.commons.beanutils.ConvertUtilsBean
import ru.pulsar.jenkins.library.IStepExecutor
import ru.pulsar.jenkins.library.configuration.notification.email.EmailExtConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import static java.util.Collections.emptySet
class ConfigurationReader implements Serializable {
private static ObjectMapper mapper
@ -62,12 +65,21 @@ class ConfigurationReader implements Serializable {
"sonarQubeOptions",
"smokeTestOptions",
"syntaxCheckOptions",
"resultsTransformOptions"
"resultsTransformOptions",
"notificationsOptions",
"emailNotificationOptions",
"alwaysEmailOptions",
"successEmailOptions",
"failureEmailOptions",
"unstableEmailOptions",
"recipientProviders",
"telegramNotificationOptions"
).toSet()
mergeObjects(baseConfiguration, configurationToMerge, nonMergeableSettings)
mergeInitInfoBaseOptions(baseConfiguration.initInfoBaseOptions, configurationToMerge.initInfoBaseOptions);
mergeBddOptions(baseConfiguration.bddOptions, configurationToMerge.bddOptions);
mergeInitInfoBaseOptions(baseConfiguration.initInfoBaseOptions, configurationToMerge.initInfoBaseOptions)
mergeBddOptions(baseConfiguration.bddOptions, configurationToMerge.bddOptions)
mergeNotificationsOptions(baseConfiguration.notificationsOptions, configurationToMerge.notificationsOptions)
return baseConfiguration;
}
@ -84,10 +96,16 @@ class ConfigurationReader implements Serializable {
}
nonMergeableSettings.forEach({ key ->
if (!baseObject.hasProperty(key)) {
return
}
if (objectToMerge == null) {
return
}
mergeObjects(
baseObject[key],
objectToMerge[key],
Collections.emptySet()
nonMergeableSettings
)
})
}
@ -107,4 +125,53 @@ class ConfigurationReader implements Serializable {
}
baseObject.vrunnerSteps = objectToMerge.vrunnerSteps.clone()
}
private static void mergeNotificationsOptions(NotificationsOptions baseObject, NotificationsOptions objectToMerge) {
if (objectToMerge == null) {
return
}
if (objectToMerge.telegramNotificationOptions != null) {
mergeObjects(
baseObject.telegramNotificationOptions,
objectToMerge.telegramNotificationOptions,
emptySet()
)
}
def emailNotificationOptionsBase = baseObject.emailNotificationOptions
def emailNotificationOptionsToMerge = objectToMerge.emailNotificationOptions
if (emailNotificationOptionsToMerge != null) {
mergeEmailExtConfiguration(
emailNotificationOptionsBase.successEmailOptions,
emailNotificationOptionsToMerge.successEmailOptions
)
mergeEmailExtConfiguration(
emailNotificationOptionsBase.failureEmailOptions,
emailNotificationOptionsToMerge.failureEmailOptions
)
mergeEmailExtConfiguration(
emailNotificationOptionsBase.unstableEmailOptions,
emailNotificationOptionsToMerge.unstableEmailOptions
)
mergeEmailExtConfiguration(
emailNotificationOptionsBase.alwaysEmailOptions,
emailNotificationOptionsToMerge.alwaysEmailOptions
)
}
}
@NonCPS
private static void mergeEmailExtConfiguration(EmailExtConfiguration baseObject, EmailExtConfiguration objectToMerge) {
if (objectToMerge != null && objectToMerge.recipientProviders != null) {
baseObject.recipientProviders = objectToMerge.recipientProviders.clone()
}
if (objectToMerge != null && objectToMerge.directRecipients != null) {
baseObject.directRecipients = objectToMerge.directRecipients.clone()
}
}
}

View File

@ -59,6 +59,10 @@ class JobConfiguration implements Serializable {
@JsonPropertyDescription("Настройки трансформации результатов анализа")
ResultsTransformOptions resultsTransformOptions;
@JsonProperty("notifications")
@JsonPropertyDescription("Настройки рассылки результатов сборки")
NotificationsOptions notificationsOptions;
@JsonProperty("logosConfig")
@JsonPropertyDescription("Конфигурация библиотеки logos. Применяется перед запуском каждой стадии сборки")
String logosConfig;
@ -81,6 +85,7 @@ class JobConfiguration implements Serializable {
", syntaxCheckOptions=" + syntaxCheckOptions +
", smokeTestOptions=" + smokeTestOptions +
", resultsTransformOptions=" + resultsTransformOptions +
", notificationOptions=" + notificationsOptions +
", logosConfig='" + logosConfig + '\'' +
'}';
}

View File

@ -0,0 +1,31 @@
package ru.pulsar.jenkins.library.configuration
import com.cloudbees.groovy.cps.NonCPS
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import ru.pulsar.jenkins.library.configuration.notification.EmailNotificationOptions
import ru.pulsar.jenkins.library.configuration.notification.TelegramNotificationOptions
@JsonIgnoreProperties(ignoreUnknown = true)
class NotificationsOptions implements Serializable {
@JsonProperty("email")
@JsonPropertyDescription("Настройки рассылки результатов сборки через email")
EmailNotificationOptions emailNotificationOptions;
@JsonProperty("telegram")
@JsonPropertyDescription("Настройки рассылки результатов сборки через telegram")
TelegramNotificationOptions telegramNotificationOptions;
@Override
@NonCPS
String toString() {
return "NotificationOptions{" +
"emailNotificationOptions=" + emailNotificationOptions +
", telegramNotificationOptions=" + telegramNotificationOptions +
'}';
}
}

View File

@ -15,12 +15,20 @@ class Secrets implements Serializable {
@JsonPropertyDescription("Данные авторизации в хранилище конфигурации")
String storage
@JsonPropertyDescription("Идентификатор telegram-чата для отправки уведомлений")
String telegramChatId
@JsonPropertyDescription("Токен авторизации telegram-бота для отправки уведомлений")
String telegramBotToken
@Override
@NonCPS
String toString() {
return "Secrets{" +
"storagePath='" + storagePath + '\'' +
", storage='" + storage + '\'' +
", telegramChatId='" + telegramChatId + '\'' +
", telegramBotToken='" + telegramBotToken + '\'' +
'}';
}
}

View File

@ -24,6 +24,12 @@ class StageFlags implements Serializable {
@JsonPropertyDescription("Запуск BDD сценариев включен")
Boolean bdd
@JsonPropertyDescription("Выполнять рассылку результатов сборки на email")
Boolean email
@JsonPropertyDescription("Выполнять рассылку результатов сборки в telegram")
Boolean telegram
@Override
@NonCPS
String toString() {
@ -34,6 +40,8 @@ class StageFlags implements Serializable {
", smoke=" + smoke +
", initSteps=" + initSteps +
", bdd=" + bdd +
", email=" + email +
", telegram=" + telegram +
'}';
}

View File

@ -0,0 +1,46 @@
package ru.pulsar.jenkins.library.configuration.notification
import com.cloudbees.groovy.cps.NonCPS
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import ru.pulsar.jenkins.library.configuration.notification.email.EmailExtConfiguration
@JsonIgnoreProperties(ignoreUnknown = true)
class EmailNotificationOptions implements Serializable {
@JsonPropertyDescription("Отправлять всегда")
Boolean onAlways
@JsonPropertyDescription("Отправлять при успешной сборке")
Boolean onSuccess
@JsonPropertyDescription("Отправлять при падении сборки")
Boolean onFailure
@JsonPropertyDescription("Отправлять при нестабильной сборке")
Boolean onUnstable
@JsonProperty("alwaysOptions")
EmailExtConfiguration alwaysEmailOptions
@JsonProperty("successOptions")
EmailExtConfiguration successEmailOptions
@JsonProperty("failureOptions")
EmailExtConfiguration failureEmailOptions
@JsonProperty("unstableOptions")
EmailExtConfiguration unstableEmailOptions
@Override
@NonCPS
String toString() {
return "EmailNotificationOptions{" +
"onAlways=" + onAlways +
", onSuccess=" + onSuccess +
", onFailure=" + onFailure +
", onUnstable=" + onUnstable +
", alwaysEmailOptions=" + alwaysEmailOptions +
", successEmailOptions=" + successEmailOptions +
", failureEmailOptions=" + failureEmailOptions +
", unstableEmailOptions=" + unstableEmailOptions +
'}';
}
}

View File

@ -0,0 +1,31 @@
package ru.pulsar.jenkins.library.configuration.notification
import com.cloudbees.groovy.cps.NonCPS
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonPropertyDescription
@JsonIgnoreProperties(ignoreUnknown = true)
class TelegramNotificationOptions implements Serializable {
@JsonPropertyDescription("Отправлять всегда")
Boolean onAlways
@JsonPropertyDescription("Отправлять при успешной сборке")
Boolean onSuccess
@JsonPropertyDescription("Отправлять при падении сборки")
Boolean onFailure
@JsonPropertyDescription("Отправлять при нестабильной сборке")
Boolean onUnstable
@Override
@NonCPS
String toString() {
return "TelegramNotificationOptions{" +
"onAlways=" + onAlways +
", onSuccess=" + onSuccess +
", onFailure=" + onFailure +
", onUnstable=" + onUnstable +
'}';
}
}

View File

@ -0,0 +1,21 @@
package ru.pulsar.jenkins.library.configuration.notification.email
import com.cloudbees.groovy.cps.NonCPS
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class EmailExtConfiguration implements Serializable {
Boolean attachLog;
String[] directRecipients
RecipientProvider[] recipientProviders
@Override
@NonCPS
String toString() {
return "EmailExtConfiguration{" +
"attachLog=" + attachLog +
", directRecipients=" + directRecipients +
", recipientProviders=" + recipientProviders +
'}';
}
}

View File

@ -0,0 +1,15 @@
package ru.pulsar.jenkins.library.configuration.notification.email
import com.fasterxml.jackson.annotation.JsonProperty
enum RecipientProvider {
@JsonProperty("developers")
DEVELOPERS,
@JsonProperty("requestor")
REQUESTOR,
@JsonProperty("brokenBuildSuspects")
BROKEN_BUILD_SUSPECTS,
@JsonProperty("brokenTestsSuspects")
BROKEN_TESTS_SUSPECTS
}

View File

@ -0,0 +1,95 @@
package ru.pulsar.jenkins.library.steps
import hudson.model.Result
import ru.pulsar.jenkins.library.IStepExecutor
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.configuration.notification.email.EmailExtConfiguration
import ru.pulsar.jenkins.library.configuration.notification.email.RecipientProvider
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.utils.Logger
import ru.pulsar.jenkins.library.utils.StringJoiner
class EmailNotification implements Serializable {
private final JobConfiguration config;
EmailNotification(JobConfiguration config) {
this.config = config
}
def run() {
Logger.printLocation()
if (!config.stageFlags.email) {
Logger.println("Email notifications are disabled")
return
}
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
def options = config.notificationsOptions.emailNotificationOptions
def currentBuild = steps.currentBuild()
def currentResult = Result.fromString(currentBuild.getCurrentResult())
EmailExtConfiguration configuration = null;
if (options.onAlways) {
configuration = options.alwaysEmailOptions
} else if (options.onFailure && (currentResult == Result.FAILURE || currentResult == Result.ABORTED)) {
configuration = options.failureEmailOptions
} else if (options.onUnstable && currentResult == Result.UNSTABLE) {
configuration = options.unstableEmailOptions
} else if (options.onSuccess && currentResult == Result.SUCCESS) {
configuration = options.successEmailOptions
}
sendEmail(configuration)
}
private static void sendEmail(EmailExtConfiguration configuration) {
if (configuration == null) {
Logger.println("Unknown build result. Can't send an email!")
return
}
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
String subject = '$DEFAULT_SUBJECT'
String body = '$DEFAULT_CONTENT'
StringJoiner toJoiner = new StringJoiner(",")
configuration.directRecipients.each {
toJoiner.add(it)
}
String to = toJoiner.toString()
List recipientProviders = new ArrayList();
configuration.recipientProviders.each {
switch (it) {
case RecipientProvider.BROKEN_BUILD_SUSPECTS:
recipientProviders.add(steps.brokenBuildSuspects())
break
case RecipientProvider.BROKEN_TESTS_SUSPECTS:
recipientProviders.add(steps.brokenTestsSuspects())
break
case RecipientProvider.DEVELOPERS:
recipientProviders.add(steps.developers())
break
case RecipientProvider.REQUESTOR:
recipientProviders.add(steps.requestor())
break
}
}
steps.emailext(
subject,
body,
to,
recipientProviders,
configuration.attachLog
)
}
}

View File

@ -17,10 +17,16 @@ class PublishAllure implements Serializable {
}
def run() {
steps = ContextRegistry.getContext().getStepExecutor()
Logger.printLocation()
if (config == null) {
Logger.println("jobConfiguration is not initialized")
return
}
steps = ContextRegistry.getContext().getStepExecutor()
safeUnstash('init-allure')
safeUnstash('bdd-allure')
if (config.stageFlags.smoke && config.smokeTestOptions.publishToAllureReport) {

View File

@ -0,0 +1,30 @@
package ru.pulsar.jenkins.library.steps
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.utils.Logger
class SendNotifications implements Serializable {
private final JobConfiguration config;
SendNotifications(JobConfiguration config) {
this.config = config
}
def run() {
Logger.printLocation()
if (config == null) {
Logger.println("jobConfiguration is not initialized")
return
}
def emailNotification = new EmailNotification(config);
emailNotification.run()
def telegramNotification = new TelegramNotification(config);
telegramNotification.run();
}
}

View File

@ -0,0 +1,251 @@
package ru.pulsar.jenkins.library.steps
import com.cloudbees.groovy.cps.NonCPS
import com.fasterxml.jackson.databind.ObjectMapper
import hudson.model.Result
import hudson.scm.ChangeLogSet
import io.jenkins.blueocean.rest.impl.pipeline.FlowNodeWrapper
import io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeGraphVisitor
import io.jenkins.blueocean.rest.model.BlueRun
import io.jenkins.cli.shaded.org.apache.commons.lang.time.DurationFormatUtils
import jenkins.plugins.http_request.HttpMode
import jenkins.plugins.http_request.MimeType
import org.jenkinsci.plugins.workflow.actions.TimingAction
import org.jenkinsci.plugins.workflow.graph.BlockStartNode
import org.jenkinsci.plugins.workflow.job.WorkflowRun
import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper
import ru.pulsar.jenkins.library.IStepExecutor
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.configuration.Secrets
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.utils.Logger
import ru.pulsar.jenkins.library.utils.RepoUtils
import ru.pulsar.jenkins.library.utils.StringJoiner
import static ru.pulsar.jenkins.library.configuration.Secrets.UNKNOWN_ID
class TelegramNotification implements Serializable {
private final JobConfiguration config;
TelegramNotification(JobConfiguration config) {
this.config = config
}
def run() {
Logger.printLocation()
if (!config.stageFlags.telegram) {
Logger.println("Telegram notifications are disabled")
return
}
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
def options = config.notificationsOptions.telegramNotificationOptions
def currentBuild = steps.currentBuild()
def currentResult = Result.fromString(currentBuild.getCurrentResult())
String message = getMessage(currentBuild)
if (options.onAlways) {
sendMessage(message)
} else if (options.onFailure && (currentResult == Result.FAILURE || currentResult == Result.ABORTED)) {
sendMessage(message)
} else if (options.onUnstable && currentResult == Result.UNSTABLE) {
sendMessage(message)
} else if (options.onSuccess && currentResult == Result.SUCCESS) {
sendMessage(message)
} else {
Logger.println("Unknown build result! Can't send a message to telegram")
}
}
private void sendMessage(message) {
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
def env = steps.env();
String repoSlug = RepoUtils.getRepoSlug()
Secrets secrets = config.secrets
String telegramChatIdCredentials = secrets.telegramChatId == UNKNOWN_ID ? repoSlug + "_TELEGRAM_CHAT_ID" : secrets.telegramChatId
String telegramBotTokenCredentials = secrets.telegramBotToken == UNKNOWN_ID ? "TELEGRAM_BOT_TOKEN" : secrets.telegramBotToken
steps.withCredentials([
steps.string(telegramBotTokenCredentials, 'TOKEN'),
steps.string(telegramChatIdCredentials, 'CHAT_ID')
]) {
def mapper = new ObjectMapper()
def body = [
chat_id : env.CHAT_ID,
text : message,
disable_web_page_preview: true,
parse_mode : 'MarkdownV2'
]
def bodyString = mapper.writeValueAsString(body)
String url = "https://api.telegram.org/bot${env.TOKEN}/sendMessage"
steps.echo(message)
steps.echo(bodyString)
steps.httpRequest(
url,
HttpMode.POST,
MimeType.APPLICATION_JSON_UTF8,
bodyString,
'200',
true
)
}
}
private static String getMessage(RunWrapper currentBuild) {
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
def env = steps.env();
def currentResult = Result.fromString(currentBuild.getCurrentResult())
def messageJoiner = new StringJoiner('\n\n')
def displayName = escapeStringForMarkdownV2(currentBuild.fullDisplayName)
String header = "[$displayName]($env.BUILD_URL)"
messageJoiner.add(header)
String result = ""
if (currentResult == Result.SUCCESS) {
result = "✅ Сборка прошла успешно!"
} else if (currentResult == Result.FAILURE) {
result = "❌ Сборка завершилась с ошибкой!"
} else if (currentResult == Result.ABORTED) {
result = "🛑 Сборка прервана!"
} else if (currentResult == Result.UNSTABLE) {
result = "💩 Есть упавшие тесты!"
}
result = escapeStringForMarkdownV2(result)
messageJoiner.add(result)
String stageResults = getStageResultsMessage(currentBuild)
if (stageResults.length() > 0) {
stageResults = escapeStringForMarkdownV2(stageResults)
messageJoiner.add(stageResults)
}
def duration = "Длительность сборки: ${currentBuild.getDurationString()}".replace(" and counting", "")
duration = escapeStringForMarkdownV2(duration)
messageJoiner.add(duration)
def changeSet = getChangeSet(currentBuild)
steps.echo(changeSet)
if (changeSet.length() > 0) {
changeSet = 'Изменения с последней сборки:\n\n' + changeSet
messageJoiner.add(changeSet)
}
String buildUrl = "[Лог сборки](${env.BUILD_URL}console)"
messageJoiner.add(buildUrl)
steps.echo(messageJoiner.toString())
return messageJoiner.toString()
}
@NonCPS
private static String getChangeSet(RunWrapper currentBuild) {
String changeSetText = ''
int counter = 0
currentBuild.changeSets.each { changeSet ->
changeSetText += "Набор изменений \\#${++counter}:\n"
changeSet.items.each { ChangeLogSet.Entry entry ->
String commit = ''
def commitId = entry.commitId;
if (commitId != null) {
if (isValidSHA1(commitId)) {
commitId = commitId.substring(0, 7)
}
def link = changeSet.browser?.getChangeSetLink(entry)
if (link != null) {
commit = "[$commitId]($link)"
} else {
commit = commitId
}
}
def author = escapeStringForMarkdownV2(entry.author.displayName)
def authorLink = entry.author.absoluteUrl
def message = escapeStringForMarkdownV2(entry.getMsgAnnotated())
changeSetText += "\\* $commit $message \\([$author]($authorLink)\\)\n"
}
changeSetText += '\n'
}
return changeSetText.trim()
}
@NonCPS
private static String getStageResultsMessage(RunWrapper currentBuild) {
def visitor = new PipelineNodeGraphVisitor(currentBuild.rawBuild as WorkflowRun)
def stages = visitor.pipelineNodes.findAll { it.type != FlowNodeWrapper.NodeType.STEP }
def stageResultMessage = ""
for (FlowNodeWrapper stage in stages) {
if (stage.status.result == BlueRun.BlueRunResult.SUCCESS || stage.status.result == BlueRun.BlueRunResult.NOT_BUILT) {
continue
}
long duration
def endNode = stage.node.getExecution().getEndNode(stage.node as BlockStartNode)
if (endNode != null) {
def startTime = TimingAction.getStartTime(stage.node)
def endTime = TimingAction.getStartTime(endNode)
duration = endTime - startTime
} else {
duration = stage.timing.totalDurationMillis
}
def time = DurationFormatUtils.formatDuration(duration, "H:mm:ss")
stageResultMessage += "$stage.displayName: $stage.status.result, затрачено времени $time \n"
}
return stageResultMessage.trim()
}
@NonCPS
private static String escapeStringForMarkdownV2(String incoming) {
return incoming.replace('_', '\\-')
.replace('*', '\\*')
.replace('[', '\\[')
.replace(']', '\\]')
.replace('(', '\\(')
.replace(')', '\\)')
.replace('~', '\\~')
.replace('`', '\\`')
.replace('>', '\\>')
.replace('#', '\\#')
.replace('+', '\\+')
.replace('-', '\\-')
.replace('=', '\\=')
.replace('|', '\\|')
.replace('{', '\\{')
.replace('}', '\\}')
.replace('.', '\\.')
.replace('!', '\\!')
}
@NonCPS
private static boolean isValidSHA1(String s) {
return s.matches('^[a-fA-F0-9]{40}$');
}
}

View File

@ -63,6 +63,18 @@ class ConfigurationReaderTest {
assertThat(jobConfiguration.getTimeoutOptions().getBdd()).isEqualTo(120);
assertThat(jobConfiguration.getTimeoutOptions().getZipInfoBase()).isEqualTo(123);
assertThat(jobConfiguration.getNotificationsOptions().getEmailNotificationOptions().getOnAlways()).isTrue();
assertThat(jobConfiguration.getNotificationsOptions().getEmailNotificationOptions().getOnSuccess()).isFalse();
assertThat(jobConfiguration.getNotificationsOptions().getEmailNotificationOptions().getAlwaysEmailOptions().getAttachLog()).isTrue();
assertThat(jobConfiguration.getNotificationsOptions().getEmailNotificationOptions().getAlwaysEmailOptions().getRecipientProviders()).hasSize(2);
assertThat(jobConfiguration.getNotificationsOptions().getEmailNotificationOptions().getAlwaysEmailOptions().getDirectRecipients()).hasSize(2);
assertThat(jobConfiguration.getNotificationsOptions().getEmailNotificationOptions().getFailureEmailOptions().getDirectRecipients()).isEmpty();
assertThat(jobConfiguration.getNotificationsOptions().getEmailNotificationOptions().getFailureEmailOptions().getRecipientProviders()).hasSize(1);
assertThat(jobConfiguration.getNotificationsOptions().getTelegramNotificationOptions().getOnAlways()).isFalse();
assertThat(jobConfiguration.getNotificationsOptions().getTelegramNotificationOptions().getOnFailure()).isTrue();
}
@Test

View File

@ -31,5 +31,26 @@
"publishToAllureReport": false,
"publishToJUnitReport": true
},
"notifications": {
"email": {
"onAlways": true,
"alwaysOptions": {
"attachLog": true,
"directRecipients": [
"1@1.com",
"2@1.com"
]
},
"failureOptions": {
"recipientProviders": [
"developers"
]
}
},
"telegram": {
"onAlways": false,
"onFailure": true
}
},
"logosConfig": "logger.rootLogger=DEBUG"
}

View File

@ -238,6 +238,7 @@ void call() {
always {
node('agent') {
saveResults config
sendNotifications(config)
}
}
}

View File

@ -0,0 +1,10 @@
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.steps.SendNotifications
def call(JobConfiguration config) {
ContextRegistry.registerDefaultContext(this)
def sendNotifications = new SendNotifications(config)
sendNotifications.run()
}