github.com/jaylevin/jenkins-library@v1.230.4/vars/mailSendNotification.groovy (about) 1 import static com.sap.piper.Prerequisites.checkScript 2 3 import com.sap.piper.ConfigurationHelper 4 import com.sap.piper.GenerateDocumentation 5 import com.sap.piper.Utils 6 import groovy.text.GStringTemplateEngine 7 import groovy.transform.Field 8 9 @Field String STEP_NAME = getClass().getName() 10 @Field Set GENERAL_CONFIG_KEYS = [ 11 /** 12 * Only if `notifyCulprits` is set: 13 * Credentials if the repository is protected. 14 * @possibleValues Jenkins credentials id 15 */ 16 'gitSshKeyCredentialsId' 17 ] 18 @Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ 19 /** 20 * Set the build result used to determine the mail template. 21 * default `currentBuild.result` 22 */ 23 'buildResult', 24 /** 25 * Only if `notifyCulprits` is set: 26 * Defines a dedicated git commitId for culprit retrieval. 27 * default `commonPipelineEnvironment.getGitCommitId()` 28 */ 29 'gitCommitId', 30 /** 31 * Only if `notifyCulprits` is set: 32 * Repository url used to retrieve culprit information. 33 * default `commonPipelineEnvironment.getGitSshUrl()` 34 */ 35 'gitUrl', 36 /** 37 * defines if the console log file should be attached to the notification mail. 38 * @possibleValues `true`, `false` 39 */ 40 'notificationAttachment', 41 /** 42 * A space-separated list of recipients that always get the notification. 43 */ 44 'notificationRecipients', 45 /** 46 * Notify all committers since the last successful build. 47 * @possibleValues `true`, `false` 48 */ 49 'notifyCulprits', 50 /** 51 * Number of log line which are included in the email body. 52 */ 53 'numLogLinesInBody', 54 /** 55 * The project name used in the email subject. 56 * default `currentBuild.fullProjectName` 57 */ 58 'projectName', 59 /** 60 * Needs to be set to `true` if step is used outside of a node context, e.g. post actions in a declarative pipeline script. 61 * default `false` 62 * @possibleValues `true`, `false` 63 */ 64 'wrapInNode' 65 ]) 66 @Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS 67 68 /** 69 * Sends notifications to all potential culprits of a current or previous build failure and to fixed list of recipients. 70 * It will attach the current build log to the email. 71 * 72 * Notifications are sent in following cases: 73 * 74 * * current build failed or is unstable 75 * * current build is successful and previous build failed or was unstable 76 */ 77 @GenerateDocumentation 78 void call(Map parameters = [:]) { 79 handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters, allowBuildFailure: true) { 80 def script = checkScript(this, parameters) ?: this 81 String stageName = parameters.stageName ?: env.STAGE_NAME 82 83 // load default & individual configuration 84 Map config = ConfigurationHelper.newInstance(this) 85 .loadStepDefaults([:], stageName) 86 .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) 87 .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) 88 .mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS) 89 .mixin( 90 projectName: script.currentBuild.fullProjectName, 91 displayName: script.currentBuild.displayName, 92 buildResult: script.currentBuild.result, 93 gitUrl: script.commonPipelineEnvironment.getGitSshUrl(), 94 gitCommitId: script.commonPipelineEnvironment.getGitCommitId() 95 ) 96 .mixin(parameters, PARAMETER_KEYS) 97 .use() 98 99 new Utils().pushToSWA([step: STEP_NAME], config) 100 101 //this takes care that terminated builds due to milestone-locking do not cause an error 102 if (script.commonPipelineEnvironment.getBuildResult() == 'ABORTED') return 103 104 def subject = "${config.buildResult}: Build ${config.projectName} ${config.displayName}" 105 def log = '' 106 def mailTemplate 107 if (config.buildResult == 'UNSTABLE' || config.buildResult == 'FAILURE'){ 108 mailTemplate = 'com.sap.piper/templates/mailFailure.html' 109 log = script.currentBuild.rawBuild.getLog(config.numLogLinesInBody).join('\n') 110 }else if(hasRecovered(config.buildResult, script.currentBuild)){ 111 mailTemplate = 'com.sap.piper/templates/mailRecover.html' 112 subject += ' is back to normal' 113 } 114 if(mailTemplate){ 115 def mailContent = GStringTemplateEngine.newInstance().createTemplate(libraryResource(mailTemplate)).make([env: env, log: log]).toString() 116 def recipientList = '' 117 if(config.notifyCulprits){ 118 if (!config.gitUrl) { 119 echo "[${STEP_NAME}] no gitUrl available, -> exiting without sending mails" 120 return 121 } 122 recipientList += getCulpritCommitters(config, script.currentBuild) 123 } 124 if(config.notificationRecipients) 125 recipientList += " ${config.notificationRecipients}" 126 emailext( 127 mimeType: 'text/html', 128 subject: subject.trim(), 129 body: mailContent, 130 to: recipientList.trim(), 131 recipientProviders: [requestor()], 132 attachLog: config.notificationAttachment 133 ) 134 } 135 } 136 } 137 138 def getNumberOfCommits(buildList){ 139 def numCommits = 0 140 if(buildList != null) 141 for(actBuild in buildList) { 142 def changeLogSets = actBuild.getChangeSets() 143 if(changeLogSets != null) 144 for(changeLogSet in changeLogSets) 145 for(change in changeLogSet) 146 numCommits++ 147 } 148 return numCommits 149 } 150 151 def getCulpritCommitters(config, currentBuild) { 152 def recipients 153 def buildList = [] 154 def build = currentBuild 155 156 if (build != null) { 157 // At least add the current build 158 buildList.add(build) 159 160 // Now collect FAILED or ABORTED ones 161 build = build.getPreviousBuild() 162 while (build != null) { 163 if (build.getResult() != 'SUCCESS') { 164 buildList.add(build) 165 } else { 166 break 167 } 168 build = build.getPreviousBuild() 169 } 170 } 171 def numberOfCommits = getNumberOfCommits(buildList) 172 if(config.wrapInNode){ 173 node(){ 174 try{ 175 recipients = getCulprits(config, env.BRANCH_NAME, numberOfCommits) 176 }finally{ 177 deleteDir() 178 } 179 } 180 }else{ 181 try{ 182 recipients = getCulprits(config, env.BRANCH_NAME, numberOfCommits) 183 }finally{ 184 deleteDir() 185 } 186 } 187 echo "[${STEP_NAME}] last ${numberOfCommits} commits revealed following responsibles ${recipients}" 188 return recipients 189 } 190 191 def getCulprits(config, branch, numberOfCommits) { 192 try { 193 if (branch?.startsWith('PR-')) { 194 //special GitHub Pull Request handling 195 deleteDir() 196 sshagent( 197 credentials: [config.gitSshKeyCredentialsId], 198 ignoreMissing: true 199 ) { 200 def pullRequestID = branch.replaceAll('PR-', '') 201 def localBranchName = "pr" + pullRequestID 202 sh """git init 203 git fetch ${config.gitUrl} pull/${pullRequestID}/head:${localBranchName} > /dev/null 2>&1 204 git checkout -f ${localBranchName} > /dev/null 2>&1 205 """ 206 } 207 } else { 208 //standard git/GitHub handling 209 if (config.gitCommitId) { 210 deleteDir() 211 sshagent( 212 credentials: [config.gitSshKeyCredentialsId], 213 ignoreMissing: true 214 ) { 215 sh """git clone ${config.gitUrl} . 216 git checkout ${config.gitCommitId} > /dev/null 2>&1""" 217 } 218 } else { 219 def retCode = sh(returnStatus: true, script: 'git log > /dev/null 2>&1') 220 if (retCode != 0) { 221 echo "[${STEP_NAME}] No git context available to retrieve culprits" 222 return '' 223 } 224 } 225 } 226 227 def recipients = sh(returnStdout: true, script: "git log -${numberOfCommits} --first-parent --pretty=format:'%ae %ce'") 228 return getDistinctRecipients(recipients) 229 } catch(err) { 230 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." 231 return '' 232 } 233 } 234 235 def getDistinctRecipients(recipients){ 236 def result 237 def recipientAddresses = recipients.split() 238 def knownAddresses = new HashSet<String>() 239 if(recipientAddresses != null) { 240 for(address in recipientAddresses) { 241 address = address.trim() 242 if(address 243 && address.contains("@") 244 && !address.contains("noreply") 245 && !knownAddresses.contains(address)) { 246 knownAddresses.add(address) 247 } 248 } 249 result = knownAddresses.join(" ") 250 } 251 return result 252 } 253 254 def hasRecovered(buildResult, currentBuild){ 255 return buildResult == 'SUCCESS' && currentBuild.getPreviousBuild()?.result != 'SUCCESS' 256 }