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.SimpleTemplateEngine 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 // load default & individual configuration Map config = ConfigurationHelper.newInstance(this) .loadStepDefaults() .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, 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 = SimpleTemplateEngine.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} --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() if(recipientAddresses != null) { for(address in recipientAddresses) { address = address.trim() if(address && address.contains("@") && !address.startsWith("noreply") && !knownAddresses.contains(address)) { knownAddresses.add(address) } } result = knownAddresses.join(" ") } return result } def hasRecovered(buildResult, currentBuild){ return buildResult == 'SUCCESS' && currentBuild.getPreviousBuild()?.result != 'SUCCESS' }