| /** |
| * |
| * @param projectUrl the github project url |
| */ |
| def getProject(String projectUrl) { |
| return (projectUrl =~ /((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?(github.com\/))([\w\.@\:\/\-~]+)(\.git)(\/)?/)[0][8] |
| } |
| |
| /** |
| * |
| * @param projectUrl the github project url |
| */ |
| def getGroup(String projectUrl) { |
| return getProjectGroupName(getProject(projectUrl))[0] |
| } |
| |
| /** |
| * Returns an array containing group and name |
| * |
| * @param project the project |
| * @param defaultGroup the default project group. Optional. |
| */ |
| def getProjectGroupName(String project, String defaultGroup = "apache") { |
| def projectNameGroup = project.split("\\/") |
| def group = projectNameGroup.size() > 1 ? projectNameGroup[0] : defaultGroup |
| def name = projectNameGroup.size() > 1 ? projectNameGroup[1] : project |
| return [group, name] |
| } |
| |
| /** |
| * Returns the path to the project dir |
| * @param projectGroupName |
| * @return |
| */ |
| def getProjectDirPath(String project, String defaultGroup = "apache") { |
| def projectGroupName = getProjectGroupName(project, defaultGroup) |
| return "${env.WORKSPACE}/${projectGroupName[0]}_${projectGroupName[1]}" |
| } |
| |
| /** |
| * |
| * Stores git information into an env variable to be retrievable at any point of the pipeline |
| * |
| * @param projectName to store commit |
| */ |
| def storeGitInformation(String projectName) { |
| def gitInformationReport = env.GIT_INFORMATION_REPORT ? "${env.GIT_INFORMATION_REPORT}; " : "" |
| gitInformationReport += "${projectName}=${githubscm.getCommit().replace(';', '').replace('=', '')} Branch [${githubscm.getBranch().replace(';', '').replace('=', '')}] Remote [${githubscm.getRemoteInfo('origin', 'url').replace(';', '').replace('=', '')}]" |
| env.GIT_INFORMATION_REPORT = gitInformationReport |
| |
| def gitHashes = env.GIT_INFORMATION_HASHES ? "${env.GIT_INFORMATION_HASHES};" : "" |
| gitHashes += "${projectName}=${githubscm.getCommitHash()}" |
| env.GIT_INFORMATION_HASHES = gitHashes |
| } |
| |
| /** |
| * |
| * prints GIT_INFORMATION_REPORT variable |
| */ |
| def printGitInformationReport() { |
| if (env.GIT_INFORMATION_REPORT?.trim()) { |
| def result = env.GIT_INFORMATION_REPORT.split(';').inject([:]) { map, token -> |
| token.split('=').with { key, value -> |
| map[key.trim()] = value.trim() |
| } |
| map |
| } |
| def report = ''' |
| ------------------------------------------ |
| GIT INFORMATION REPORT |
| ------------------------------------------ |
| ''' |
| result.each { key, value -> |
| report += "${key}: ${value}\n" |
| } |
| println report |
| } else { |
| println '[WARNING] The variable GIT_INFORMATION_REPORT does not exist' |
| } |
| } |
| |
| /* |
| * Get the next major/minor/micro version, with a specific suffix if needed. |
| * The version string needs to be in the form X.Y.Z |
| */ |
| |
| def getNextVersion(String version, String type, String suffix = 'SNAPSHOT', boolean resetSubVersions = true) { |
| assert ['major', 'minor', 'micro'].contains(type) |
| Integer[] versionSplit = parseVersion(version) |
| if (versionSplit != null) { |
| int majorVersion = versionSplit[0] + (type == 'major' ? 1 : 0) |
| int minorVersion = resetSubVersions && type == 'major' ? 0 : (versionSplit[1] + (type == 'minor' ? 1 : 0)) |
| int microVersion = resetSubVersions && (type == 'major' || type == 'minor') ? 0 : (versionSplit[2] + (type == 'micro' ? 1 : 0)) |
| return "${majorVersion}.${minorVersion}.${microVersion}${suffix ? '-' + suffix : ''}" |
| } else { |
| return null |
| } |
| } |
| |
| String getMajorMinorVersion(String version) { |
| try { |
| String[] versionSplit = version.split("\\.") |
| return "${versionSplit[0]}.${versionSplit[1]}" |
| } catch (err) { |
| println "[ERROR] ${version} cannot be reduced to Major.minor" |
| throw err |
| } |
| } |
| |
| /* |
| * It parses a version string, which needs to be in the format X.Y.Z or X.Y.Z.suffix and returns the 3 numbers |
| * in an array. The optional suffix must not be numeric. |
| * <p> |
| * Valid version examples: |
| * 1.0.0 |
| * 1.0.0.Final |
| */ |
| |
| Integer[] parseVersion(String version) { |
| String[] versionSplit = version.split("\\.") |
| boolean hasNonNumericSuffix = versionSplit.length == 4 && !(versionSplit[3].isNumber()) |
| if (versionSplit.length == 3 || hasNonNumericSuffix) { |
| if (versionSplit[0].isNumber() && versionSplit[1].isNumber() && versionSplit[2].isNumber()) { |
| Integer[] vs = new Integer[3] |
| vs[0] = Integer.parseInt(versionSplit[0]) |
| vs[1] = Integer.parseInt(versionSplit[1]) |
| vs[2] = Integer.parseInt(versionSplit[2]) |
| return vs |
| } else { |
| error "Version ${version} is not in the required format. The major, minor, and micro parts should contain only numeric characters." |
| } |
| } else { |
| error "Version ${version} is not in the required format X.Y.Z or X.Y.Z.suffix." |
| } |
| } |
| |
| String getReleaseBranchFromVersion(String version) { |
| Integer[] versionSplit = parseVersion(version) |
| return "${versionSplit[0]}.${versionSplit[1]}.x" |
| } |
| |
| String calculateTargetReleaseBranch(String currentReleaseBranch, int addToMajor = 0, int addToMinor = 0) { |
| String targetBranch = currentReleaseBranch |
| String [] versionSplit = targetBranch.split("\\.") |
| if (versionSplit.length == 3 |
| && versionSplit[0].isNumber() |
| && versionSplit[1].isNumber() |
| && (versionSplit[2] == 'x' || versionSplit[2] == 'x-prod')) { |
| Integer newMajor = Integer.parseInt(versionSplit[0]) + addToMajor |
| Integer newMinor = Integer.parseInt(versionSplit[1]) + addToMinor |
| targetBranch = "${newMajor}.${newMinor}.${versionSplit[2]}" |
| } else { |
| println "Cannot parse targetBranch as release branch so going further with current value: ${targetBranch}" |
| } |
| return targetBranch |
| } |
| |
| /** |
| * It prepares the environment to avoid problems with plugins. For example files from SCM pipeline are deleted during checkout |
| */ |
| def prepareEnvironment() { |
| println """ |
| [INFO] Preparing Environment |
| [INFO] Copying WORKSPACE content env folder |
| """ |
| def envFolderName = '.ci-env' |
| if (fileExists("${env.WORKSPACE}/${envFolderName}")) { |
| println "[WARNING] folder ${env.WORKSPACE}/${envFolderName} already exist, won't create env folder again." |
| } else { |
| dir(env.WORKSPACE) { |
| sh "mkdir ${envFolderName}" |
| sh "cp -r `ls -A | grep -v '${envFolderName}'` ${envFolderName}/" |
| } |
| } |
| } |
| |
| /* |
| * Generate a hash composed of alphanumeric characters (lowercase) of a given size |
| */ |
| |
| String generateHash(int size) { |
| String alphabet = (('a'..'z') + ('0'..'9')).join("") |
| def random = new Random() |
| return (1..size).collect { alphabet[random.nextInt(alphabet.length())] }.join("") |
| } |
| |
| String generateTempFile() { |
| return sh(returnStdout: true, script: 'mktemp').trim() |
| } |
| |
| String generateTempFolder() { |
| return sh(returnStdout: true, script: 'mktemp -d').trim() |
| } |
| |
| void executeWithCredentialsMap(Map credentials, Closure closure) { |
| if (credentials.token) { |
| withCredentials([string(credentialsId: credentials.token, variable: 'QUAY_TOKEN')]) { |
| closure() |
| } |
| } else if (credentials.usernamePassword) { |
| withCredentials([usernamePassword(credentialsId: credentials.usernamePassword, usernameVariable: 'QUAY_USER', passwordVariable: 'QUAY_TOKEN')]) { |
| closure() |
| } |
| } else { |
| error 'No credentials given to execute the given closure' |
| } |
| } |
| |
| void cleanNode(String containerEngine = '') { |
| println '[INFO] Clean workspace' |
| cleanWs(disableDeferredWipeout: true) |
| println '[INFO] Workspace cleaned' |
| println '[INFO] Cleanup Maven artifacts' |
| maven.cleanRepository() |
| println '[INFO] .m2/repository cleaned' |
| if (containerEngine) { |
| println "[INFO] Cleanup ${containerEngine} containers/images" |
| cloud.cleanContainersAndImages(containerEngine) |
| } |
| } |
| |
| def spaceLeft() { |
| dir(env.WORKSPACE) { |
| println '[INFO] space left on the machine' |
| sh 'df -h' |
| println '[INFO] space of /home/jenkins' |
| sh "du -h -d1 /home/jenkins" |
| println '[INFO] space of workspace' |
| sh "du -h -d3 /home/jenkins/workspace" |
| } |
| } |
| |
| def replaceInAllFilesRecursive(String findPattern, String oldValueSedPattern, String newSedValue) { |
| sh "find . -name '${findPattern}' -type f -exec sed -i 's/${oldValueSedPattern}/${newSedValue}/g' {} \\;" |
| } |
| |
| /* |
| * Removes any partial downloaded dependencies from .m2 if the previous run was interrupted and no post actions were |
| * executed (cleanRepository()) and a new build is executed on the same machine |
| */ |
| def rmPartialDeps(){ |
| dir("${env.WORKSPACE}/.m2") { |
| sh "find . -regex \".*\\.part\\(\\.lock\\)?\" -exec rm -rf {} \\;" |
| } |
| } |
| |
| String retrieveConsoleLog(int numberOfLines = 100, String buildUrl = "${BUILD_URL}") { |
| return sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${buildUrl}consoleText | tail -n ${numberOfLines}") |
| } |
| |
| String archiveConsoleLog(String id = '', int numberOfLines = 100, String buildUrl = "${BUILD_URL}") { |
| String filename = "${id ? "${id}_" : ''}console.log" |
| sh "rm -rf ${filename}" |
| writeFile(text: retrieveConsoleLog(numberOfLines, buildUrl), file: filename) |
| archiveArtifacts(artifacts: filename) |
| } |
| |
| def retrieveTestResults(String buildUrl = "${BUILD_URL}") { |
| return readJSON(text: sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${buildUrl}testReport/api/json?depth=1")) |
| } |
| |
| def retrieveFailedTests(String buildUrl = "${BUILD_URL}") { |
| def testResults = retrieveTestResults(buildUrl) |
| |
| def allCases = [] |
| testResults.suites?.each { testSuite -> |
| allCases.addAll(testSuite.cases) |
| } |
| |
| def failedTests = [] |
| testResults.suites?.each { testSuite -> |
| testSuite.cases?.each { testCase -> |
| if (!['PASSED', 'SKIPPED', 'FIXED'].contains(testCase.status)) { |
| def failedTest = [:] |
| |
| boolean hasSameNameCases = allCases.findAll { it.name == testCase.name && it.className == testCase.className }.size() > 1 |
| |
| failedTest.status = testCase.status |
| |
| // Retrieve class name |
| fullClassName = testCase.className |
| int lastIndexOf = fullClassName.lastIndexOf('.') |
| packageName = fullClassName.substring(0, lastIndexOf) |
| className = fullClassName.substring(lastIndexOf + 1) |
| |
| failedTest.name = testCase.name |
| failedTest.packageName = packageName |
| failedTest.className = className |
| failedTest.enclosingBlockNames = testSuite.enclosingBlockNames?.reverse()?.join(' / ') |
| |
| failedTest.fullName = "${packageName}.${className}.${failedTest.name}" |
| // If other cases have the same className / name, Jenkins uses the enclosingBlockNames for the URL distinction |
| if (hasSameNameCases && testSuite.enclosingBlockNames) { |
| failedTest.fullName = "${testSuite.enclosingBlockNames.reverse().join(' / ')} / ${failedTest.fullName}" |
| } |
| |
| // Construct test url |
| String urlLeaf = '' |
| // If other cases have the same className / name, Jenkins uses the enclosingBlockNames for the URL distinction |
| if (hasSameNameCases && testSuite.enclosingBlockNames) { |
| urlLeaf += testSuite.enclosingBlockNames.reverse().join('___') |
| } |
| urlLeaf += urlLeaf ? '___' : urlLeaf |
| urlLeaf += "${failedTest.name == "(?)" ? "___" : failedTest.name}/" |
| urlLeaf = urlLeaf.replaceAll(' ', '_') |
| .replaceAll('&', '_') |
| .replaceAll('-', '_') |
| failedTest.url = "${buildUrl}testReport/${packageName}/${className}/${urlLeaf}" |
| |
| failedTest.details = [null, 'null'].contains(testCase.errorDetails) ? '' : testCase.errorDetails |
| failedTest.stacktrace = [null, 'null'].contains(testCase.errorStackTrace) ? '' : testCase.errorStackTrace |
| failedTests.add(failedTest) |
| } |
| } |
| } |
| |
| return failedTests |
| } |
| |
| String retrieveArtifact(String artifactPath, String buildUrl = "${BUILD_URL}") { |
| String finalUrl = "${buildUrl}artifact/${artifactPath}" |
| String httpCode = sh(returnStdout: true, script: "curl -o /dev/null --silent -Iw '%{http_code}' ${finalUrl}") |
| return httpCode == "200" ? sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${finalUrl}") : '' |
| } |
| |
| def retrieveJobInformation(String buildUrl = "${BUILD_URL}") { |
| return readJSON(text: sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${buildUrl}api/json?depth=0")) |
| } |
| |
| boolean isJobResultSuccess(String jobResult) { |
| return jobResult == 'SUCCESS' |
| } |
| |
| boolean isJobResultFailure(String jobResult) { |
| return jobResult == 'FAILURE' |
| } |
| |
| boolean isJobResultAborted(String jobResult) { |
| return jobResult == 'ABORTED' |
| } |
| |
| boolean isJobResultUnstable(String jobResult) { |
| return jobResult == 'UNSTABLE' |
| } |
| |
| /* |
| * Return the build/test summary of a job |
| * |
| * outputStyle possibilities: 'ZULIP' (default), 'GITHUB' |
| */ |
| String getMarkdownTestSummary(String jobId = '', String additionalInfo = '', String buildUrl = "${BUILD_URL}", String outputStyle = 'ZULIP') { |
| def jobInfo = retrieveJobInformation(buildUrl) |
| |
| // Check if any *_console.log is available as artifact first |
| String defaultConsoleLogId = 'Console Logs' |
| Map consoleLogs = jobInfo.artifacts?.collect { it.fileName } |
| .findAll { it.endsWith('console.log') } |
| .collectEntries { filename -> |
| int index = filename.lastIndexOf('_') |
| String logId = index > 0 ? filename.substring(0, index) : defaultConsoleLogId |
| return [ (logId) : retrieveArtifact(filename, buildUrl) ] |
| } ?: [ (defaultConsoleLogId) : retrieveConsoleLog(50, buildUrl)] |
| |
| String jobResult = jobInfo.result |
| String summary = """ |
| ${jobId ? "**${jobId} job**" : 'Job'} ${formatBuildNumber(outputStyle, BUILD_NUMBER)} was: **${jobResult}** |
| """ |
| |
| if (!isJobResultSuccess(jobResult)) { |
| summary += "Possible explanation: ${getResultExplanationMessage(jobResult)}\n" |
| } |
| |
| if (additionalInfo) { |
| summary += """ |
| ${additionalInfo} |
| """ |
| } |
| |
| if (!isJobResultSuccess(jobResult)) { |
| boolean testResultsFound = false |
| summary += "\nPlease look here: ${buildUrl}display/redirect" |
| |
| try { |
| def testResults = retrieveTestResults(buildUrl) |
| def failedTests = retrieveFailedTests(buildUrl) |
| testResultsFound=true |
| |
| summary += """ |
| \n**Test results:** |
| - PASSED: ${testResults.passCount} |
| - FAILED: ${testResults.failCount} |
| """ |
| |
| summary += 'GITHUB'.equalsIgnoreCase(outputStyle) ? """ |
| Those are the test failures: ${failedTests.size() <= 0 ? 'none' : '\n'}${failedTests.collect { failedTest -> |
| return """<details> |
| <summary><a href="${failedTest.url}">${failedTest.fullName}</a></summary> |
| ${formatTextForHtmlDisplay(failedTest.details ?: failedTest.stacktrace)} |
| </details>""" |
| }.join('\n')} |
| """ |
| : """ |
| Those are the test failures: ${failedTests.size() <= 0 ? 'none' : '\n'}${failedTests.collect { failedTest -> |
| return """```spoiler [${failedTest.fullName}](${failedTest.url}) |
| ${failedTest.details ?: failedTest.stacktrace} |
| ```""" |
| }.join('\n')} |
| """ |
| } catch (err) { |
| echo 'No test results found' |
| } |
| |
| // Display console logs if no test results found |
| if (!(jobResult == 'UNSTABLE' && testResultsFound)) { |
| summary += 'GITHUB'.equalsIgnoreCase(outputStyle) ? """ |
| See console log: |
| ${consoleLogs.collect { key, value -> |
| return """<details> |
| <summary><b>${key}</b></summary> |
| ${formatTextForHtmlDisplay(value)} |
| </details> |
| """ |
| }.join('')}""" |
| : """ |
| See console log: |
| ${consoleLogs.collect { key, value -> |
| return """```spoiler ${key} |
| ${value} |
| ``` |
| """ |
| }.join('')}""" |
| } |
| } |
| |
| return summary |
| } |
| |
| String getResultExplanationMessage(String jobResult) { |
| switch (jobResult) { |
| case 'SUCCESS': |
| return 'Do I need to explain ?' |
| case 'UNSTABLE': |
| return 'This should be test failures' |
| case 'FAILURE': |
| return 'Pipeline failure or project build failure' |
| case 'ABORTED': |
| return 'Most probably a timeout, please review' |
| default: |
| return 'Woops ... I don\'t know about this result value ... Please ask maintainer.' |
| } |
| } |
| |
| String formatTextForHtmlDisplay(String text) { |
| return text.replaceAll('\n', '<br/>') |
| } |
| |
| String formatBuildNumber(String outputStyle, String buildNumber) { |
| return 'GITHUB'.equalsIgnoreCase(outputStyle) ? "`#${buildNumber}`" : "#${buildNumber}" |
| } |
| |
| /** |
| * Encode the provided string value in the provided encoding |
| * @param value string to encode |
| * @param encoding [default UTF-8] |
| * @return the encoded string |
| */ |
| String encode(String value, String encoding='UTF-8') { |
| return URLEncoder.encode(value, encoding) |
| } |
| |
| /** |
| * Serialize the parameters converting a Map into an URL query string, like: |
| * {A: 1, B: 2} --> 'A=1&B=2' |
| * @param params key-value map representation of the parameters |
| * @return URL query string |
| */ |
| String serializeQueryParams(Map params) { |
| return params.collect { "${it.getKey()}=${encode(it.getValue() as String)}" }.join('&') |
| } |
| |
| /** |
| * Execute the provided closure within Kerberos authentication context |
| * @param keytabId id of the keytab to be used |
| * @param closure code to run in the kerberos auth context |
| * @param domain kerberos domain to look for into the keytab |
| * @param retry number of max retries to perform if kinit fails |
| */ |
| def withKerberos(String keytabId, Closure closure, String domain = 'REDHAT.COM', int nRetries = 5) { |
| withCredentials([file(credentialsId: keytabId, variable: 'KEYTAB_FILE')]) { |
| env.KERBEROS_PRINCIPAL = sh(returnStdout: true, script: "klist -kt $KEYTAB_FILE |grep $domain | awk -F' ' 'NR==1{print \$4}' ").trim() |
| |
| if (!env.KERBEROS_PRINCIPAL?.trim()) { |
| throw new Exception("[ERROR] found blank KERBEROS_PRINCIPAL, kerberos authetication failed.") |
| } |
| |
| // check if kerberos authentication already exists with provided principal |
| def currentPrincipal = sh(returnStdout: true, script: "klist | grep -i 'Default principal' | awk -F':' 'NR==1{print \$2}' ").trim() |
| |
| if (currentPrincipal != env.KERBEROS_PRINCIPAL) { |
| def kerberosStatus = 0 |
| for (int i = 0; i < nRetries; i++) { |
| kerberosStatus = sh(returnStatus: true, script: "kinit ${env.KERBEROS_PRINCIPAL} -kt $KEYTAB_FILE") |
| if (kerberosStatus == 0) { |
| // exit at first success |
| break |
| } |
| } |
| |
| // if the kerberos status is still != 0 after nRetries throw exception |
| if (kerberosStatus != 0) { |
| throw new Exception("[ERROR] kinit failed with non-zero status.") |
| } |
| } else { |
| println "[INFO] ${env.KERBEROS_PRINCIPAL} already authenticated, skipping kinit." |
| } |
| |
| closure() |
| } |
| } |
| |
| def runWithPythonVirtualEnv(String cmd, String virtualEnvName, boolean returnStdout = false) { |
| return sh(returnStdout: returnStdout, script: """ |
| source ~/virtenvs/${virtualEnvName}/bin/activate |
| ${cmd} |
| """) |
| } |
| |
| int getJobDurationInSeconds() { |
| long startTimestamp = retrieveJobInformation().timestamp |
| long currentTimestamp = new Date().getTime() |
| return (int) ((currentTimestamp - startTimestamp) / 1000) |
| } |
| |
| String displayDurationFromSeconds(int durationInSec) { |
| String result = '' |
| int seconds = durationInSec |
| int minutes = durationInSec / 60 |
| if (minutes > 0) { |
| seconds = seconds - minutes * 60 |
| int hours = minutes / 60 |
| if (hours > 0) { |
| minutes = minutes - hours*60 |
| result += "${hours}h" |
| } |
| result += "${minutes}m" |
| } |
| result += "${seconds}s" |
| return result |
| } |
| |
| /** |
| * Method to wait for dockerd to be started. Put in initial pipeline stages to make sure all starts in time. |
| */ |
| void waitForDocker() { |
| sleep(10) // give it some ahead time not to invoke docker exec immediately after container start |
| sh 'wait-for-docker.sh' // script in kogito-ci-build image itself put in /usr/local/bin |
| } |
| |
| /** |
| * Method to wrap original label and exclude nodes that were marked as faulty in some of the parent folders. |
| * Example usage in agent block: `label util.avoidFaultyNodes('ubuntu')` |
| * `FAULTY_NODES` environment variable is inherited down the folder tree and available in jobs. |
| * @param label Node label to be used. If empty, 'ubuntu' will be used as default |
| * @return Node label extended with an expression ensuring to exclude nodes marked as faulty. |
| */ |
| String avoidFaultyNodes(String label = 'ubuntu') { |
| if (label.isEmpty()) { |
| label = 'ubuntu' |
| } |
| String faultyNodesString = env.FAULTY_NODES |
| if((faultyNodesString == null) || faultyNodesString.isEmpty()) { |
| return label |
| } |
| String[] faultyNodes = faultyNodesString.split(',') |
| String result = "(${label}) && !(${String.join(' || ', faultyNodes)})" |
| return result.toString() |
| } |