mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-12 10:55:20 +02:00
50ac1a3ab5
* fix(mailSendNotification): do not send recovery notifications on first build * added unit tests
263 lines
9.2 KiB
Groovy
263 lines
9.2 KiB
Groovy
import static com.sap.piper.Prerequisites.checkScript
|
|
|
|
import com.sap.piper.ConfigurationHelper
|
|
import com.sap.piper.GenerateDocumentation
|
|
import com.sap.piper.Utils
|
|
import groovy.text.GStringTemplateEngine
|
|
import groovy.transform.Field
|
|
|
|
@Field String STEP_NAME = getClass().getName()
|
|
@Field Set GENERAL_CONFIG_KEYS = [
|
|
/**
|
|
* Only if `notifyCulprits` is set:
|
|
* Credentials if the repository is protected.
|
|
* @possibleValues Jenkins credentials id
|
|
*/
|
|
'gitSshKeyCredentialsId'
|
|
]
|
|
@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([
|
|
/**
|
|
* Set the build result used to determine the mail template.
|
|
* default `currentBuild.result`
|
|
*/
|
|
'buildResult',
|
|
/**
|
|
* Only if `notifyCulprits` is set:
|
|
* Defines a dedicated git commitId for culprit retrieval.
|
|
* default `commonPipelineEnvironment.getGitCommitId()`
|
|
*/
|
|
'gitCommitId',
|
|
/**
|
|
* Only if `notifyCulprits` is set:
|
|
* Repository url used to retrieve culprit information.
|
|
* default `commonPipelineEnvironment.getGitSshUrl()`
|
|
*/
|
|
'gitUrl',
|
|
/**
|
|
* defines if the console log file should be attached to the notification mail.
|
|
* @possibleValues `true`, `false`
|
|
*/
|
|
'notificationAttachment',
|
|
/**
|
|
* A space-separated list of recipients that always get the notification.
|
|
*/
|
|
'notificationRecipients',
|
|
/**
|
|
* Notify all committers since the last successful build.
|
|
* @possibleValues `true`, `false`
|
|
*/
|
|
'notifyCulprits',
|
|
/**
|
|
* Number of log line which are included in the email body.
|
|
*/
|
|
'numLogLinesInBody',
|
|
/**
|
|
* The project name used in the email subject.
|
|
* default `currentBuild.fullProjectName`
|
|
*/
|
|
'projectName',
|
|
/**
|
|
* Needs to be set to `true` if step is used outside of a node context, e.g. post actions in a declarative pipeline script.
|
|
* default `false`
|
|
* @possibleValues `true`, `false`
|
|
*/
|
|
'wrapInNode'
|
|
])
|
|
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
|
|
|
|
/**
|
|
* Sends notifications to all potential culprits of a current or previous build failure and to fixed list of recipients.
|
|
* It will attach the current build log to the email.
|
|
*
|
|
* Notifications are sent in following cases:
|
|
*
|
|
* * current build failed or is unstable
|
|
* * current build is successful and previous build failed or was unstable
|
|
*/
|
|
@GenerateDocumentation
|
|
void call(Map parameters = [:]) {
|
|
handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters, allowBuildFailure: true) {
|
|
def script = checkScript(this, parameters) ?: this
|
|
String stageName = parameters.stageName ?: env.STAGE_NAME
|
|
|
|
// load default & individual configuration
|
|
Map config = ConfigurationHelper.newInstance(this)
|
|
.loadStepDefaults([:], stageName)
|
|
.mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS)
|
|
.mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
|
|
.mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS)
|
|
.mixin(
|
|
projectName: script.currentBuild.fullProjectName,
|
|
displayName: script.currentBuild.displayName,
|
|
buildResult: script.currentBuild.result,
|
|
gitUrl: script.commonPipelineEnvironment.getGitSshUrl(),
|
|
gitCommitId: script.commonPipelineEnvironment.getGitCommitId()
|
|
)
|
|
.mixin(parameters, PARAMETER_KEYS)
|
|
.use()
|
|
|
|
new Utils().pushToSWA([step: STEP_NAME], config)
|
|
|
|
//this takes care that terminated builds due to milestone-locking do not cause an error
|
|
if (script.commonPipelineEnvironment.getBuildResult() == 'ABORTED') return
|
|
|
|
def subject = "${config.buildResult}: Build ${config.projectName} ${config.displayName}"
|
|
def log = ''
|
|
def mailTemplate
|
|
if (config.buildResult == 'UNSTABLE' || config.buildResult == 'FAILURE'){
|
|
mailTemplate = 'com.sap.piper/templates/mailFailure.html'
|
|
log = script.currentBuild.rawBuild.getLog(config.numLogLinesInBody).join('\n')
|
|
}else if(hasRecovered(config.buildResult, script.currentBuild)){
|
|
mailTemplate = 'com.sap.piper/templates/mailRecover.html'
|
|
subject += ' is back to normal'
|
|
}
|
|
if(mailTemplate){
|
|
def mailContent = GStringTemplateEngine.newInstance().createTemplate(libraryResource(mailTemplate)).make([env: env, log: log]).toString()
|
|
def recipientList = ''
|
|
if(config.notifyCulprits){
|
|
if (!config.gitUrl) {
|
|
echo "[${STEP_NAME}] no gitUrl available, -> exiting without sending mails"
|
|
return
|
|
}
|
|
recipientList += getCulpritCommitters(config, script.currentBuild)
|
|
}
|
|
if(config.notificationRecipients)
|
|
recipientList += " ${config.notificationRecipients}"
|
|
emailext(
|
|
mimeType: 'text/html',
|
|
subject: subject.trim(),
|
|
body: mailContent,
|
|
to: recipientList.trim(),
|
|
recipientProviders: [requestor()],
|
|
attachLog: config.notificationAttachment
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
def getNumberOfCommits(buildList){
|
|
def numCommits = 0
|
|
if(buildList != null)
|
|
for(actBuild in buildList) {
|
|
def changeLogSets = actBuild.getChangeSets()
|
|
if(changeLogSets != null)
|
|
for(changeLogSet in changeLogSets)
|
|
for(change in changeLogSet)
|
|
numCommits++
|
|
}
|
|
return numCommits
|
|
}
|
|
|
|
def getCulpritCommitters(config, currentBuild) {
|
|
def recipients
|
|
def buildList = []
|
|
def build = currentBuild
|
|
|
|
if (build != null) {
|
|
// At least add the current build
|
|
buildList.add(build)
|
|
|
|
// Now collect FAILED or ABORTED ones
|
|
build = build.getPreviousBuild()
|
|
while (build != null) {
|
|
if (build.getResult() != 'SUCCESS') {
|
|
buildList.add(build)
|
|
} else {
|
|
break
|
|
}
|
|
build = build.getPreviousBuild()
|
|
}
|
|
}
|
|
def numberOfCommits = getNumberOfCommits(buildList)
|
|
if(config.wrapInNode){
|
|
node(){
|
|
try{
|
|
recipients = getCulprits(config, env.BRANCH_NAME, numberOfCommits)
|
|
}finally{
|
|
deleteDir()
|
|
}
|
|
}
|
|
}else{
|
|
try{
|
|
recipients = getCulprits(config, env.BRANCH_NAME, numberOfCommits)
|
|
}finally{
|
|
deleteDir()
|
|
}
|
|
}
|
|
echo "[${STEP_NAME}] last ${numberOfCommits} commits revealed following responsibles ${recipients}"
|
|
return recipients
|
|
}
|
|
|
|
def getCulprits(config, branch, numberOfCommits) {
|
|
try {
|
|
if (branch?.startsWith('PR-')) {
|
|
//special GitHub Pull Request handling
|
|
deleteDir()
|
|
sshagent(
|
|
credentials: [config.gitSshKeyCredentialsId],
|
|
ignoreMissing: true
|
|
) {
|
|
def pullRequestID = branch.replaceAll('PR-', '')
|
|
def localBranchName = "pr" + pullRequestID
|
|
sh """git init
|
|
git fetch ${config.gitUrl} pull/${pullRequestID}/head:${localBranchName} > /dev/null 2>&1
|
|
git checkout -f ${localBranchName} > /dev/null 2>&1
|
|
"""
|
|
}
|
|
} else {
|
|
//standard git/GitHub handling
|
|
if (config.gitCommitId) {
|
|
deleteDir()
|
|
sshagent(
|
|
credentials: [config.gitSshKeyCredentialsId],
|
|
ignoreMissing: true
|
|
) {
|
|
sh """git clone ${config.gitUrl} .
|
|
git checkout ${config.gitCommitId} > /dev/null 2>&1"""
|
|
}
|
|
} else {
|
|
def retCode = sh(returnStatus: true, script: 'git log > /dev/null 2>&1')
|
|
if (retCode != 0) {
|
|
echo "[${STEP_NAME}] No git context available to retrieve culprits"
|
|
return ''
|
|
}
|
|
}
|
|
}
|
|
|
|
def recipients = sh(returnStdout: true, script: "git log -${numberOfCommits} --first-parent --pretty=format:'%ae %ce'")
|
|
return getDistinctRecipients(recipients)
|
|
} catch(err) {
|
|
echo "[${STEP_NAME}] Culprit retrieval from git failed with '${err.getMessage()}'. Please make sure to configure gitSshKeyCredentialsId. So far, only fixed list of recipients is used."
|
|
return ''
|
|
}
|
|
}
|
|
|
|
def getDistinctRecipients(recipients){
|
|
def result
|
|
def recipientAddresses = recipients.split()
|
|
def knownAddresses = new HashSet<String>()
|
|
if(recipientAddresses != null) {
|
|
for(address in recipientAddresses) {
|
|
address = address.trim()
|
|
if(address
|
|
&& address.contains("@")
|
|
&& !address.contains("noreply")
|
|
&& !knownAddresses.contains(address)) {
|
|
knownAddresses.add(address)
|
|
}
|
|
}
|
|
result = knownAddresses.join(" ")
|
|
}
|
|
return result
|
|
}
|
|
|
|
def hasRecovered(buildResult, currentBuild){
|
|
def previousBuild = currentBuild.getPreviousBuild()
|
|
|
|
if (previousBuild) {
|
|
return buildResult == 'SUCCESS' && previousBuild.result != 'SUCCESS'
|
|
}
|
|
|
|
return false
|
|
}
|