1
0
mirror of https://github.com/firstBitMarksistskaya/jenkins-lib.git synced 2025-01-20 11:54:30 +02:00

Merge branch 'feature-telegram' into develop

This commit is contained in:
Nikita Fedkin 2022-05-20 21:10:20 +03:00
commit c14d85c20f
No known key found for this signature in database
GPG Key ID: E7AE91471C6FFE04
28 changed files with 1006 additions and 42 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

@ -27,12 +27,12 @@ contributor(ctx) {
method(name: 'library', type: 'Object', namedParams: [parameter(name: 'identifier', type: 'java.lang.String'), parameter(name: 'changelog', type: 'java.lang.Boolean'), parameter(name: 'retriever', type: 'Map'),], doc: 'Load a shared library on the fly')
method(name: 'libraryResource', type: 'Object', params: [resource: 'java.lang.String'], doc: 'Load a resource file from a shared library')
method(name: 'libraryResource', type: 'Object', namedParams: [parameter(name: 'resource', type: 'java.lang.String'), parameter(name: 'encoding', type: 'java.lang.String'),], doc: 'Load a resource file from a shared library')
method(name: 'lock', type: 'Object', params: [resource: java.lang.String, body: 'Closure'], doc: 'Lock shared resource')
method(name: 'lock', type: 'Object', params: [resource: String, body: 'Closure'], doc: 'Lock shared resource')
method(name: 'lock', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'resource', type: 'java.lang.String'), parameter(name: 'extra', type: 'Map'), parameter(name: 'inversePrecedence', type: 'boolean'), parameter(name: 'label', type: 'java.lang.String'), parameter(name: 'quantity', type: 'int'), parameter(name: 'variable', type: 'java.lang.String'),], doc: 'Lock shared resource')
method(name: 'mail', type: 'Object', namedParams: [parameter(name: 'subject', type: 'java.lang.String'), parameter(name: 'body', type: 'java.lang.String'), parameter(name: 'bcc', type: 'java.lang.String'), parameter(name: 'cc', type: 'java.lang.String'), parameter(name: 'charset', type: 'java.lang.String'), parameter(name: 'from', type: 'java.lang.String'), parameter(name: 'mimeType', type: 'java.lang.String'), parameter(name: 'replyTo', type: 'java.lang.String'), parameter(name: 'to', type: 'java.lang.String'),], doc: 'Mail')
method(name: 'milestone', type: 'Object', params: [ordinal: 'java.lang.Integer'], doc: 'The milestone step forces all builds to go through in order')
method(name: 'milestone', type: 'Object', namedParams: [parameter(name: 'ordinal', type: 'java.lang.Integer'), parameter(name: 'label', type: 'java.lang.String'),], doc: 'The milestone step forces all builds to go through in order')
method(name: 'node', type: 'Object', params: [label: java.lang.String, body: 'Closure'], doc: 'Allocate node')
method(name: 'node', type: 'Object', params: [label: String, body: 'Closure'], doc: 'Allocate node')
method(name: 'nodesByLabel', type: 'Object', params: [label: 'java.lang.String'], doc: 'List of nodes by Label, by default excludes offline nodes.')
method(name: 'nodesByLabel', type: 'Object', namedParams: [parameter(name: 'label', type: 'java.lang.String'), parameter(name: 'offline', type: 'boolean'),], doc: 'List of nodes by Label, by default excludes offline nodes.')
method(name: 'properties', type: 'Object', params: [properties: 'Map'], doc: 'Set job properties')
@ -55,26 +55,30 @@ contributor(ctx) {
method(name: 'script', type: 'Object', params: [body: 'Closure'], doc: 'Run arbitrary Pipeline script')
method(name: 'sleep', type: 'Object', params: [time: 'int'], doc: 'Sleep')
method(name: 'sleep', type: 'Object', namedParams: [parameter(name: 'time', type: 'int'), parameter(name: 'unit', type: 'java.util.concurrent.TimeUnit'),], doc: 'Sleep')
method(name: 'stage', type: 'Object', params: [name: java.lang.String, body: 'Closure'], doc: 'Stage')
method(name: 'stage', type: 'Object', params: [name: String, body: 'Closure'], doc: 'Stage')
method(name: 'stage', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'name', type: 'java.lang.String'), parameter(name: 'concurrency', type: 'java.lang.Integer'),], doc: 'Stage')
method(name: 'options', type: 'Object', params: [body: 'Closure'], doc: 'Options')
method(name: 'timeout', type: 'Object', params: [time: int, body: 'Closure'], doc: 'Enforce time limit')
method(name: 'timeout', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'time', type: 'int'), parameter(name: 'activity', type: 'boolean'), parameter(name: 'unit', type: 'java.util.concurrent.TimeUnit'),], doc: 'Enforce time limit')
method(name: 'timeout', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'time', type: 'java.lang.Integer'), parameter(name: 'activity', type: 'boolean'), parameter(name: 'unit', type: 'java.util.concurrent.TimeUnit'),], doc: 'Enforce time limit')
method(name: 'timestamps', type: 'Object', params: [body: 'Closure'], doc: 'Timestamps')
method(name: 'tool', type: 'Object', params: [name: 'java.lang.String'], doc: 'Use a tool from a predefined Tool Installation')
method(name: 'tool', type: 'Object', namedParams: [parameter(name: 'name', type: 'java.lang.String'), parameter(name: 'type', type: 'java.lang.String'),], doc: 'Use a tool from a predefined Tool Installation')
method(name: 'unstable', type: 'Object', params: [message: 'java.lang.String'], doc: 'Set stage result to unstable')
method(name: 'waitUntil', type: 'Object', params: [body: 'Closure'], doc: 'Wait for condition')
method(name: 'warnError', type: 'Object', params: [message: java.lang.String, body: 'Closure'], doc: 'Catch error and set build and stage result to unstable')
method(name: 'warnError', type: 'Object', params: [message: String, body: 'Closure'], doc: 'Catch error and set build and stage result to unstable')
method(name: 'warnError', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'message', type: 'java.lang.String'), parameter(name: 'catchInterruptions', type: 'boolean'),], doc: 'Catch error and set build and stage result to unstable')
method(name: 'withCredentials', type: 'Object', params: [bindings: Map, body: 'Closure'], doc: 'Bind credentials to variables')
method(name: 'withCredentials', type: 'Object', params: [bindings: List, body: 'Closure'], doc: 'Bind credentials to variables')
method(name: 'string', type: 'Object', namedParams: [parameter(name: 'credentialsId', type: String), parameter(name: 'variable', type: String)], doc: 'Bind secret text credentials to variable')
method(name: 'withEnv', type: 'Object', params: [overrides: Map, body: 'Closure'], doc: 'Set environment variables')
method(name: 'ws', type: 'Object', params: [dir: java.lang.String, body: 'Closure'], doc: 'Allocate workspace')
method(name: 'ws', type: 'Object', params: [dir: String, body: 'Closure'], doc: 'Allocate workspace')
method(name: 'dockerFingerprintRun', type: 'Object', params: [containerId: 'java.lang.String'], doc: 'Advanced/Deprecated Record trace of a Docker image run in a container')
method(name: 'dockerFingerprintRun', type: 'Object', namedParams: [parameter(name: 'containerId', type: 'java.lang.String'), parameter(name: 'toolName', type: 'java.lang.String'),], doc: 'Record trace of a Docker image run in a container')
method(name: 'envVarsForTool', type: 'Object', namedParams: [parameter(name: 'toolId', type: 'java.lang.String'), parameter(name: 'toolVersion', type: 'java.lang.String'),], doc: 'Fetches the environment variables for a given tool in a list of \'FOO=bar\' strings suitable for the withEnv step.')
method(name: 'getContext', type: 'Object', params: [type: 'Map'], doc: 'Advanced/Deprecated Get contextual object from internal APIs')
method(name: 'podTemplate', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'label', type: 'java.lang.String'), parameter(name: 'name', type: 'java.lang.String'), parameter(name: 'activeDeadlineSeconds', type: 'int'), parameter(name: 'annotations', type: 'Map'), parameter(name: 'cloud', type: 'java.lang.String'), parameter(name: 'containers', type: 'Map'), parameter(name: 'envVars', type: 'Map'), parameter(name: 'idleMinutes', type: 'int'), parameter(name: 'imagePullSecrets', type: 'Map'), parameter(name: 'inheritFrom', type: 'java.lang.String'), parameter(name: 'instanceCap', type: 'int'), parameter(name: 'namespace', type: 'java.lang.String'), parameter(name: 'nodeSelector', type: 'java.lang.String'), parameter(name: 'nodeUsageMode', type: 'java.lang.String'), parameter(name: 'podRetention', type: 'Map'), parameter(name: 'serviceAccount', type: 'java.lang.String'), parameter(name: 'slaveConnectTimeout', type: 'int'), parameter(name: 'volumes', type: 'Map'), parameter(name: 'workingDir', type: 'java.lang.String'), parameter(name: 'workspaceVolume', type: 'Map'), parameter(name: 'yaml', type: 'java.lang.String'),], doc: 'Define a podTemplate to use in the kubernetes plugin')
method(name: 'withContext', type: 'Object', params: [context: java.lang.Object, body: 'Closure'], doc: 'Advanced/Deprecated Use contextual object from internal APIs within a block')
method(name: 'withContext', type: 'Object', params: [context: Object, body: 'Closure'], doc: 'Advanced/Deprecated Use contextual object from internal APIs within a block')
method(name: 'httpRequest', type: 'jenkins.plugins.http_request.ResponseContentSupplier', params: [url:'java.lang.String'], doc: 'Perform an HTTP Request and return a response object')
method(name: 'httpRequest', type: 'jenkins.plugins.http_request.ResponseContentSupplier', namedParams: [parameter(name: 'url', type: 'java.lang.String'), parameter(name: 'acceptType', type: 'Map'), parameter(name: 'authentication', type: 'java.lang.String'), parameter(name: 'consoleLogResponseBody', type: 'java.lang.Boolean'), parameter(name: 'contentType', type: 'jenkins.plugins.http_request.MimeType'), parameter(name: 'customHeaders', type: 'java.util.List'), parameter(name: 'formData', type: 'java.util.List'), parameter(name: 'httpMode', type: 'jenkins.plugins.http_request.HttpMode'), parameter(name: 'httpProxy', type: 'java.lang.String'), parameter(name: 'ignoreSslErrors', type: 'boolean'), parameter(name: 'multipartName', type: 'java.lang.String'), parameter(name: 'outputFile', type: 'java.lang.String'), parameter(name: 'proxyAuthentication', type: 'java.lang.String'), parameter(name: 'quiet', type: 'java.lang.Boolean'), parameter(name: 'requestBody', type: 'java.lang.String'), parameter(name: 'responseHandle', type: 'Map'), parameter(name: 'timeout', type: 'java.lang.Integer'), parameter(name: 'uploadFile', type: 'java.lang.String'), parameter(name: 'useNtlm', type: 'boolean'), parameter(name: 'useSystemProperties', type: 'java.lang.Boolean'), parameter(name: 'validResponseCodes', type: 'java.lang.String'), parameter(name: 'validResponseContent', type: 'java.lang.String'), parameter(name: 'wrapAsMultipart', type: 'boolean'), ], doc: 'Perform an HTTP Request and return a response object')
property(name: 'docker', type: 'org.jenkinsci.plugins.docker.workflow.DockerDSL')
property(name: 'pipeline', type: 'org.jenkinsci.plugins.pipeline.modeldefinition.ModelStepLoader')
property(name: 'env', type: 'org.jenkinsci.plugins.workflow.cps.EnvActionImpl.Binder')
@ -82,6 +86,17 @@ contributor(ctx) {
property(name: 'currentBuild', type: 'org.jenkinsci.plugins.workflow.cps.RunWrapperBinder')
property(name: 'scm', type: 'org.jenkinsci.plugins.workflow.multibranch.SCMVar')
}
//Steps that require a options context
def optionsCtx = context(scope: closureScope())
contributor(optionsCtx) {
def call = enclosingCall('options')
if (call) {
method(name: 'timestamps', type: 'Object', params: [], doc: 'Timestamps')
method(name: 'timeout', type: 'Object', namedParams: [parameter(name: 'time', type: 'java.lang.Integer'), parameter(name: 'activity', type: 'boolean'), parameter(name: 'unit', type: 'java.util.concurrent.TimeUnit'),], doc: 'Enforce time limit')
}
}
//Steps that require a node context
def nodeCtx = context(scope: closureScope())
contributor(nodeCtx) {
@ -92,7 +107,7 @@ contributor(nodeCtx) {
method(name: 'containerLog', type: 'Object', params: [name: 'java.lang.String'], doc: 'Get container log from Kubernetes')
method(name: 'containerLog', type: 'Object', namedParams: [parameter(name: 'name', type: 'java.lang.String'), parameter(name: 'limitBytes', type: 'int'), parameter(name: 'returnLog', type: 'boolean'), parameter(name: 'sinceSeconds', type: 'int'), parameter(name: 'tailingLines', type: 'int'),], doc: 'Get container log from Kubernetes')
method(name: 'deleteDir', type: 'Object', params: [:], doc: 'Recursively delete the current directory from the workspace')
method(name: 'dir', type: 'Object', params: [path: java.lang.String, body: 'Closure'], doc: 'Change current directory')
method(name: 'dir', type: 'Object', params: [path: String, body: 'Closure'], doc: 'Change current directory')
method(name: 'fileExists', type: 'Object', params: [file: 'java.lang.String'], doc: 'Verify if file exists in workspace')
method(name: 'findFiles', type: 'Object', params: [:], doc: 'Find files in the workspace')
method(name: 'findFiles', type: 'Object', namedParams: [parameter(name: 'excludes', type: 'java.lang.String'), parameter(name: 'glob', type: 'java.lang.String'),], doc: 'Find files in the workspace')
@ -122,7 +137,7 @@ contributor(nodeCtx) {
method(name: 'step', type: 'Object', params: [delegate: 'Map'], doc: 'General Build Step')
method(name: 'svn', type: 'Object', params: [url: 'java.lang.String'], doc: 'Subversion')
method(name: 'svn', type: 'Object', namedParams: [parameter(name: 'url', type: 'java.lang.String'), parameter(name: 'changelog', type: 'boolean'), parameter(name: 'poll', type: 'boolean'),], doc: 'Subversion')
method(name: 'tee', type: 'Object', params: [file: java.lang.String, body: 'Closure'], doc: 'Tee output to file')
method(name: 'tee', type: 'Object', params: [file: String, body: 'Closure'], doc: 'Tee output to file')
method(name: 'tm', type: 'Object', params: [stringWithMacro: 'java.lang.String'], doc: 'Expand a string containing macros')
method(name: 'touch', type: 'Object', params: [file: 'java.lang.String'], doc: 'Create a file (if not already exist) in the workspace, and set the timestamp')
method(name: 'touch', type: 'Object', namedParams: [parameter(name: 'file', type: 'java.lang.String'), parameter(name: 'timestamp', type: 'java.lang.Long'),], doc: 'Create a file (if not already exist) in the workspace, and set the timestamp')
@ -141,12 +156,12 @@ contributor(nodeCtx) {
method(name: 'zip', type: 'Object', namedParams: [parameter(name: 'zipFile', type: 'java.lang.String'), parameter(name: 'archive', type: 'boolean'), parameter(name: 'dir', type: 'java.lang.String'), parameter(name: 'glob', type: 'java.lang.String'),], doc: 'Create Zip file')
method(name: 'archive', type: 'Object', params: [includes: 'java.lang.String'], doc: 'Advanced/Deprecated Archive artifacts')
method(name: 'archive', type: 'Object', namedParams: [parameter(name: 'includes', type: 'java.lang.String'), parameter(name: 'excludes', type: 'java.lang.String'),], doc: 'Archive artifacts')
method(name: 'container', type: 'Object', params: [name: java.lang.String, body: 'Closure'], doc: 'Advanced/Deprecated Run build steps in a container')
method(name: 'container', type: 'Object', params: [name: String, body: 'Closure'], doc: 'Advanced/Deprecated Run build steps in a container')
method(name: 'container', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'name', type: 'java.lang.String'), parameter(name: 'shell', type: 'java.lang.String'),], doc: 'Run build steps in a container')
method(name: 'dockerFingerprintFrom', type: 'Object', namedParams: [parameter(name: 'dockerfile', type: 'java.lang.String'), parameter(name: 'image', type: 'java.lang.String'), parameter(name: 'commandLine', type: 'java.lang.String'), parameter(name: 'toolName', type: 'java.lang.String'),], doc: 'Record trace of a Docker image used in FROM')
method(name: 'unarchive', type: 'Object', params: [:], doc: 'Advanced/Deprecated Copy archived artifacts into the workspace')
method(name: 'unarchive', type: 'Object', namedParams: [parameter(name: 'mapping', type: 'Map'),], doc: 'Copy archived artifacts into the workspace')
method(name: 'withDockerContainer', type: 'Object', params: [image: java.lang.String, body: 'Closure'], doc: 'Advanced/Deprecated Run build steps inside a Docker container')
method(name: 'withDockerContainer', type: 'Object', params: [image: String, body: 'Closure'], doc: 'Advanced/Deprecated Run build steps inside a Docker container')
method(name: 'withDockerContainer', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'image', type: 'java.lang.String'), parameter(name: 'args', type: 'java.lang.String'), parameter(name: 'toolName', type: 'java.lang.String'),], doc: 'Run build steps inside a Docker container')
method(name: 'withDockerRegistry', type: 'Object', params: [registry: Map, body: 'Closure'], doc: 'Advanced/Deprecated Sets up Docker registry endpoint')
method(name: 'withDockerRegistry', type: 'Object', params: [body: Closure], namedParams: [parameter(name: 'registry', type: 'Map'), parameter(name: 'toolName', type: 'java.lang.String'),], doc: 'Sets up Docker registry endpoint')

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

@ -7,6 +7,7 @@ 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.VRunner
import ru.pulsar.jenkins.library.utils.VersionParser
@ -14,8 +15,6 @@ import static ru.pulsar.jenkins.library.configuration.Secrets.UNKNOWN_ID
class InitFromStorage implements Serializable {
final static REPO_SLUG_REGEXP = ~/(?m)^(?:[^:\/?#\n]+:)?(?:\/+[^\/?#\n]*)?\/?([^?\n]*)/
private final JobConfiguration config
InitFromStorage(JobConfiguration config) {
@ -37,8 +36,7 @@ class InitFromStorage implements Serializable {
String storageVersion = VersionParser.storage()
String storageVersionParameter = storageVersion == "" ? "" : "--storage-ver $storageVersion"
EnvironmentAction env = steps.env();
String repoSlug = computeRepoSlug(env.GIT_URL)
String repoSlug = RepoUtils.getRepoSlug()
Secrets secrets = config.secrets
@ -61,14 +59,4 @@ class InitFromStorage implements Serializable {
}
}
@NonCPS
private static String computeRepoSlug(String text) {
def matcher = text =~ REPO_SLUG_REGEXP
String repoSlug = matcher != null && matcher.getCount() == 1 ? matcher[0][1] : ""
if (repoSlug.endsWith(".git")) {
repoSlug = repoSlug[0..-5]
}
repoSlug = repoSlug.replace('/', '_')
return repoSlug
}
}

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

@ -6,6 +6,7 @@ import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.utils.FileUtils
import ru.pulsar.jenkins.library.utils.Logger
import ru.pulsar.jenkins.library.utils.StringJoiner
import ru.pulsar.jenkins.library.utils.VRunner
class SmokeTest implements Serializable {
@ -61,14 +62,14 @@ class SmokeTest implements Serializable {
FilePath pathToAllureReport = FileUtils.getFilePath("$env.WORKSPACE/$allureReport")
String allureReportDir = FileUtils.getLocalPath(pathToAllureReport.getParent())
StringBuilder reportsConfigConstructor = new StringBuilder()
StringJoiner reportsConfigConstructor = new StringJoiner(";")
if (options.publishToJUnitReport) {
steps.createDir(junitReportDir)
String junitReportCommand = "ГенераторОтчетаJUnitXML{$junitReport}"
reportsConfigConstructor.append(junitReportCommand)
reportsConfigConstructor.add(junitReportCommand)
}
if (options.publishToAllureReport) {
@ -76,10 +77,7 @@ class SmokeTest implements Serializable {
String allureReportCommand = "ГенераторОтчетаAllureXMLВерсия2{$allureReport}"
if (reportsConfigConstructor.length() > 0) {
reportsConfigConstructor.append(';')
}
reportsConfigConstructor.append(allureReportCommand)
reportsConfigConstructor.add(allureReportCommand)
}
if (reportsConfigConstructor.length() > 0) {

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

@ -0,0 +1,24 @@
package ru.pulsar.jenkins.library.utils
import com.cloudbees.groovy.cps.NonCPS
class RepoUtils implements Serializable {
private final static REPO_SLUG_REGEXP = ~/(?m)^(?:[^:\/?#\n]+:)?(?:\/+[^\/?#\n]*)?\/?([^?\n]*)/
private static String REPO_SLUG;
@NonCPS
static void computeRepoSlug(String text) {
def matcher = text =~ REPO_SLUG_REGEXP
String repoSlug = matcher != null && matcher.getCount() == 1 ? matcher[0][1] : ""
if (repoSlug.endsWith(".git")) {
repoSlug = repoSlug[0..-5]
}
REPO_SLUG = repoSlug.replace('/', '_')
}
static String getRepoSlug() {
return REPO_SLUG
}
}

View File

@ -0,0 +1,37 @@
package ru.pulsar.jenkins.library.utils
class StringJoiner implements Serializable {
private final String delimiter
private StringBuilder value
StringJoiner(CharSequence delimiter) {
this.delimiter = delimiter
}
StringJoiner add(CharSequence newElement) {
prepareBuilder().append(newElement)
return this
}
int length() {
return (value != null ? value.length() : 0)
}
@Override
String toString() {
if (value == null) {
return ""
} else {
return value.toString()
}
}
private StringBuilder prepareBuilder() {
if (value != null) {
value.append(delimiter)
} else {
value = new StringBuilder()
}
return value
}
}

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

@ -2,6 +2,7 @@
import groovy.transform.Field
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.configuration.SourceFormat
import ru.pulsar.jenkins.library.utils.RepoUtils
import java.util.concurrent.TimeUnit
@ -40,6 +41,7 @@ void call() {
config = jobConfiguration() as JobConfiguration
agent1C = config.v8AgentLabel()
agentEdt = config.edtAgentLabel()
RepoUtils.computeRepoSlug(env.GIT_URL)
}
}
}
@ -236,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()
}