├── .gitignore ├── DockerBuildUbuntuBase.groovy ├── DockerPushUbuntu.groovy ├── GenericBuild.groovy ├── Incrementals.groovy ├── Pullrequests.groovy ├── PullrequestsRoottest.groovy ├── resources └── jenkins-pipeline-email-html.template ├── src └── cern │ └── root │ └── pipeline │ ├── BotParser.groovy │ ├── BuildConfiguration.groovy │ ├── GenericBuild.groovy │ ├── GitHub.groovy │ ├── GraphiteReporter.groovy │ └── Mattermost.groovy └── test ├── BotTests.groovy └── cern └── root └── pipeline ├── BotParser.groovy └── BuildConfiguration.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | -------------------------------------------------------------------------------- /DockerBuildUbuntuBase.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | node('docker-host') { 4 | timestamps { 5 | git 'http://root.cern/git/rootspi.git' 6 | 7 | dir('docker/ubuntu16-base') { 8 | stage('Build') { 9 | sh "docker build -t rootproject/root-ubuntu16-base ." 10 | } 11 | 12 | stage('Push') { 13 | withCredentials([usernamePassword(credentialsId: 'root_dockerhub_deploy_user', passwordVariable: 'password', usernameVariable: 'username')]) { 14 | sh "HOME=\$(pwd) && docker login -u '$username' -p '$password'" 15 | } 16 | 17 | sh "HOME=\$(pwd) && docker push rootproject/root-ubuntu16-base" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DockerPushUbuntu.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | def repoName = 'rootproject/root-ubuntu16' 4 | 5 | node('docker-host') { 6 | timestamps { 7 | def stagingName = "rootbuild-${java.util.UUID.randomUUID()}" 8 | git 'http://root.cern/git/rootspi.git' 9 | 10 | dir('docker/ubuntu16') { 11 | try { 12 | def ccacheVolumeName = "root-ccache-ubuntu16-native-Release-$branch" 13 | stage('Build') { 14 | dir('root-build') { 15 | dir('roottest') { 16 | git url: 'http://root.cern/git/roottest.git', branch: branch 17 | } 18 | 19 | dir('root') { 20 | git url: 'http://root.cern/git/root.git', branch: branch 21 | } 22 | 23 | dir('rootspi') { 24 | git url: 'http://root.cern/git/rootspi.git' 25 | } 26 | } 27 | 28 | sh "docker volume create $ccacheVolumeName" 29 | sh "docker pull rootproject/root-ubuntu16-base" 30 | sh "docker build -t $stagingName --build-arg uid=\$(id -u \$USER) ." 31 | sh "HOME=\$(pwd) && docker run -t --name='$stagingName' -v $ccacheVolumeName:/ccache -v \$(pwd)/root-build:/root-build $stagingName /build.sh ubuntu16 native Release" 32 | 33 | def testThreshold = [[$class: 'FailedThreshold', 34 | failureNewThreshold: '0', failureThreshold: '0', unstableNewThreshold: '0', 35 | unstableThreshold: '0'], [$class: 'SkippedThreshold', failureNewThreshold: '', 36 | failureThreshold: '', unstableNewThreshold: '', unstableThreshold: '']] 37 | 38 | step([$class: 'XUnitPublisher', 39 | testTimeMargin: '3000', thresholdMode: 1, thresholds: testThreshold, 40 | tools: [[$class: 'CTestType', 41 | deleteOutputFiles: true, failIfNotNew: false, pattern: 'root-build/Testing/*/Test.xml', 42 | skipNoTestFiles: false, stopProcessingIfError: true]]]) 43 | 44 | if (currentBuild.result == 'FAILURE') { 45 | throw new Exception("Test results failed the build") 46 | } 47 | } 48 | 49 | stage('Push') { 50 | sh "HOME=\$(pwd) && docker commit --change='CMD [\"root.exe\"]' $stagingName '$repoName:$tag'" 51 | withCredentials([usernamePassword(credentialsId: 'root_dockerhub_deploy_user', passwordVariable: 'password', usernameVariable: 'username')]) { 52 | sh "HOME=\$(pwd) && docker login -u '$username' -p '$password'" 53 | } 54 | 55 | sh "HOME=\$(pwd) && docker push $repoName:$tag" 56 | 57 | if (params['latestTag']) { 58 | sh "HOME=\$(pwd) && docker tag $repoName:$tag $repoName:latest" 59 | sh "HOME=\$(pwd) && docker push $repoName:latest" 60 | } 61 | } 62 | } catch (e) { 63 | println 'Build failed because:' 64 | println e 65 | currentBuild.result = 'FAILURE' 66 | } finally { 67 | // Build back to green 68 | if (currentBuild.result == 'SUCCESS' && currentBuild.previousBuild?.result != 'SUCCESS') { 69 | mattermostSend color: 'good', message: 'Docker build is back to green!' 70 | } 71 | 72 | // Build just failed 73 | if (currentBuild.result != 'SUCCESS' && currentBuild.previousBuild?.result == 'SUCCESS') { 74 | mattermostSend color: 'danger', message: "Docker build [just failed](${currentBuild.absoluteUrl})" 75 | } 76 | 77 | // Remove containers/cleanup 78 | sh "HOME=\$(pwd) && docker rm -f \$(docker ps -a -f name=$stagingName -q)" 79 | sh "HOME=\$(pwd) && docker rmi -f $stagingName" 80 | sh "HOME=\$(pwd) && docker rmi -f $repoName:$tag" 81 | } 82 | } 83 | } 84 | cleanWs() 85 | } 86 | -------------------------------------------------------------------------------- /GenericBuild.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | properties([ 4 | parameters([ 5 | string(name: 'ROOT_REFSPEC', defaultValue: '', description: 'Refspec for ROOT repository'), 6 | string(name: 'ROOTTEST_REFSPEC', defaultValue: '', description: 'Refspec for ROOTtest repository'), 7 | string(name: 'ROOTTEST_BRANCH', defaultValue: 'master', description: 'Name of the ROOT branch to work with'), 8 | string(name: 'ROOT_BRANCH', defaultValue: 'master', description: 'Name of the roottest branch to work with'), 9 | string(name: 'BUILD_NOTE', defaultValue: '', description: 'Note to add after label/compiler in job name'), 10 | string(name: 'BUILD_DESCRIPTION', defaultValue: '', description: 'Build description') 11 | ]) 12 | ]) 13 | 14 | 15 | // Treat parameters as environment variables 16 | for (ParameterValue p in params) { 17 | env[p.key] = p.value 18 | } 19 | 20 | env.GIT_URL = 'http://root.cern/git/root.git' 21 | 22 | currentBuild.setDisplayName("#$BUILD_NUMBER $LABEL/$SPEC $BUILD_NOTE") 23 | currentBuild.setDescription("$BUILD_DESCRIPTION") 24 | 25 | node(LABEL) { 26 | timestamps { 27 | stage('Checkout') { 28 | dir('root') { 29 | retry(3) { 30 | // TODO: Use the git step when it has implemented specifying refspecs 31 | // See https://jenkins.io/doc/pipeline/steps/workflow-scm-step/ for CloneOption 32 | checkout([$class: 'GitSCM', branches: [[name: ROOT_BRANCH]], doGenerateSubmoduleConfigurations: false, 33 | extensions: [[$class: 'CloneOption', timeout: 10, noTags: true, shallow: false]] 34 | +[[$class: 'LocalBranch', localBranch: '']] 35 | +[[$class: 'CleanBeforeCheckout', deleteUntrackedNestedRepositories: true]], 36 | submoduleCfg: [], userRemoteConfigs: [[refspec: ROOT_REFSPEC, url: env.GIT_URL]]]) 37 | } 38 | } 39 | 40 | dir('roottest') { 41 | retry(3) { 42 | def rootTestUrl = 'http://root.cern/git/roottest.git'; 43 | // TODO: Use the git step when it has implemented specifying refspecs 44 | checkout([$class: 'GitSCM', branches: [[name: ROOTTEST_BRANCH]], doGenerateSubmoduleConfigurations: false, 45 | extensions: [[$class: 'CloneOption', timeout: 10, noTags: true, shallow: false]] 46 | +[[$class: 'LocalBranch', localBranch: '']] 47 | +[[$class: 'CleanBeforeCheckout', deleteUntrackedNestedRepositories: true]], 48 | submoduleCfg: [], userRemoteConfigs: [[refspec: ROOTTEST_REFSPEC, url: rootTestUrl]]]) 49 | } 50 | } 51 | 52 | dir('rootspi') { 53 | retry(3) { 54 | git url: 'http://root.cern/git/rootspi.git' 55 | } 56 | } 57 | } 58 | 59 | try { 60 | stage('Build') { 61 | timeout(time: 240, unit: 'MINUTES') { 62 | if (LABEL == 'windows10') { 63 | bat 'rootspi/jenkins/jk-all.bat build' 64 | } else { 65 | sh 'rootspi/jenkins/jk-all build' 66 | } 67 | } 68 | } 69 | 70 | stage('Test') { 71 | timeout(time: 240, unit: 'MINUTES') { 72 | if (LABEL == 'windows10') { 73 | bat 'rootspi/jenkins/jk-all.bat test' 74 | } else { 75 | sh 'rootspi/jenkins/jk-all test' 76 | } 77 | 78 | def testThreshold = [[$class: 'FailedThreshold', 79 | failureNewThreshold: '0', failureThreshold: '0', unstableNewThreshold: '0', 80 | unstableThreshold: '0'], [$class: 'SkippedThreshold', failureNewThreshold: '', 81 | failureThreshold: '', unstableNewThreshold: '', unstableThreshold: '']] 82 | 83 | if (LABEL == 'windows10') { 84 | step([$class: 'XUnitPublisher', 85 | testTimeMargin: '3000', thresholdMode: 1, thresholds: testThreshold, 86 | tools: [[$class: 'CTestType', 87 | deleteOutputFiles: true, failIfNotNew: false, pattern: 'build/Testing/*/Test.xml', 88 | skipNoTestFiles: true, stopProcessingIfError: true]]]) 89 | } else { 90 | step([$class: 'XUnitPublisher', 91 | testTimeMargin: '3000', thresholdMode: 1, thresholds: testThreshold, 92 | tools: [[$class: 'CTestType', 93 | deleteOutputFiles: true, failIfNotNew: false, pattern: 'build/Testing/*/Test.xml', 94 | skipNoTestFiles: false, stopProcessingIfError: true]]]) 95 | } 96 | 97 | if (currentBuild.result == 'FAILURE') { 98 | throw new Exception("Test result caused build to fail") 99 | } 100 | } 101 | } 102 | } catch (err) { 103 | println 'Build failed because:' 104 | println err 105 | currentBuild.result = 'FAILURE' 106 | } 107 | 108 | 109 | //stage('Archive environment') { 110 | // TODO: Bundle and store build env in here 111 | //archiveArtifacts artifacts: 'build/' 112 | //} 113 | stash includes: 'rootspi/jenkins/logparser-rules/*', name: 'logparser-rules' 114 | } 115 | } 116 | 117 | // Log-parser-plugin will look for rules on master node. Unstash the rules and parse the rules. (JENKINS-38840) 118 | node('master') { 119 | stage('Generate reports') { 120 | unstash 'logparser-rules' 121 | step([$class: 'LogParserPublisher', 122 | parsingRulesPath: "${pwd()}/rootspi/jenkins/logparser-rules/ROOT-incremental-LogParserRules.txt", 123 | useProjectRule: false, unstableOnWarning: true, failBuildOnError: true]) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Incrementals.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | @Library('root-pipelines') 4 | import cern.root.pipeline.* 5 | 6 | properties([ 7 | pipelineTriggers([githubPush(), pollSCM('H/10 * * * *')]), 8 | parameters([ 9 | string(name: 'VERSION', defaultValue: 'master', description: 'Branch to be built'), 10 | string(name: 'EXTERNALS', defaultValue: 'ROOT-latest', description: ''), 11 | string(name: 'EMPTY_BINARY', defaultValue: 'false', description: 'Boolean to empty the binary directory (i.e. to force a full re-build)'), 12 | string(name: 'ExtraCMakeOptions', defaultValue: '', description: 'Additional CMake configuration options of the form "-Doption1=value1 -Doption2=value2"'), 13 | string(name: 'MODE', defaultValue: 'experimental', description: 'The build mode') 14 | ]) 15 | ]) 16 | 17 | GenericBuild build = new GenericBuild(this, 'root-incrementals-build', params.MODE) 18 | 19 | stage('Configuring') { 20 | node('master') { 21 | git url: 'http://root.cern/git/root.git', branch: 'master' 22 | } 23 | 24 | build.addConfigurations(BuildConfiguration.incrementalConfiguration) 25 | 26 | def mattermost = new Mattermost() 27 | build.afterBuild({ finishedBuild -> 28 | // mattermost.postMattermostReport(finishedBuild) 29 | }) 30 | } 31 | 32 | stage('Building') { 33 | build.build() 34 | } 35 | 36 | stage('Publish reports') { 37 | build.sendEmails() 38 | } 39 | -------------------------------------------------------------------------------- /Pullrequests.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | @Library('root-pipelines') 4 | import cern.root.pipeline.* 5 | 6 | properties([ 7 | parameters([ 8 | string(name: 'ghprbPullId', defaultValue: '516'), 9 | string(name: 'ghprbGhRepository', defaultValue: 'root-project/root'), 10 | string(name: 'ghprbCommentBody', defaultValue: '@phsft-bot build'), 11 | string(name: 'ghprbTargetBranch', defaultValue: 'master'), 12 | string(name: 'ghprbActualCommit', defaultValue: ''), 13 | string(name: 'ghprbPullAuthorLogin', defaultValue: ''), 14 | string(name: 'ghprbSourceBranch', defaultValue: ''), 15 | string(name: 'ghprbAuthorRepoGitUrl', defaultValue: ''), 16 | string(name: 'sha1', defaultValue: ''), 17 | string(name: 'VERSION', defaultValue: 'master', description: 'Branch to be built'), 18 | string(name: 'EXTERNALS', defaultValue: 'ROOT-latest', description: ''), 19 | string(name: 'EMPTY_BINARY', defaultValue: 'true', description: 'Boolean to empty the binary directory (i.e. to force a full re-build)'), 20 | string(name: 'ExtraCMakeOptions', defaultValue: '', description: 'Additional CMake configuration options of the form "-Doption1=value1 -Doption2=value2"'), 21 | string(name: 'MODE', defaultValue: 'pullrequests', description: 'The build mode'), 22 | string(name: 'PARENT', defaultValue: 'root-pullrequests-trigger', description: 'Trigger job name') 23 | ]) 24 | ]) 25 | 26 | timestamps { 27 | GitHub gitHub = new GitHub(this, PARENT, ghprbGhRepository, ghprbPullId, params.ghprbActualCommit) 28 | BotParser parser = new BotParser(this, params.ExtraCMakeOptions) 29 | GenericBuild build = new GenericBuild(this, 'root-pullrequests-build', params.MODE) 30 | 31 | build.addBuildParameter('ROOT_REFSPEC', "+refs/pull/${ghprbPullId}/head:refs/remotes/origin/pr/${ghprbPullId}/head +refs/heads/${params.ghprbTargetBranch}:refs/remotes/origin/${params.ghprbTargetBranch}") 32 | build.addBuildParameter('ROOT_BRANCH', "${params.ghprbTargetBranch}") 33 | build.addBuildParameter('ROOTTEST_BRANCH', "${params.ghprbTargetBranch}") 34 | build.addBuildParameter('GIT_COMMIT', "${params.sha1}") 35 | build.addBuildParameter('BUILD_NOTE', "$ghprbPullAuthorLogin PR #$ghprbPullId") 36 | 37 | currentBuild.setDisplayName("#$BUILD_NUMBER $ghprbPullAuthorLogin PR #$ghprbPullId") 38 | 39 | build.cancelBuilds('.*PR #' + ghprbPullId + '$') 40 | 41 | build.afterBuild({buildWrapper -> 42 | if (buildWrapper.result.result != 'SUCCESS' && currentBuild.result != 'ABORTED') { 43 | gitHub.postResultComment(buildWrapper) 44 | } 45 | }) 46 | 47 | if (parser.isParsableComment(ghprbCommentBody.trim())) { 48 | parser.parse() 49 | } 50 | 51 | parser.postStatusComment(gitHub) 52 | parser.configure(build) 53 | 54 | gitHub.setPendingCommitStatus('Building') 55 | 56 | build.build() 57 | 58 | stage('Publish reports') { 59 | if (currentBuild.result == 'SUCCESS') { 60 | gitHub.setSucceedCommitStatus('Build passed') 61 | } else if (currentBuild.result != 'ABORTED') { 62 | gitHub.setFailedCommitStatus('Build failed') 63 | } 64 | 65 | if (currentBuild.result != null) { 66 | build.sendEmails() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /PullrequestsRoottest.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | @Library('root-pipelines') 4 | import cern.root.pipeline.* 5 | 6 | properties([ 7 | parameters([ 8 | string(name: 'ghprbPullId', defaultValue: '516'), 9 | string(name: 'ghprbGhRepository', defaultValue: 'root-project/roottest'), 10 | string(name: 'ghprbCommentBody', defaultValue: '@phsft-bot build'), 11 | string(name: 'ghprbTargetBranch', defaultValue: 'master'), 12 | string(name: 'ghprbActualCommit', defaultValue: ''), 13 | string(name: 'ghprbPullAuthorLogin', defaultValue: ''), 14 | string(name: 'ghprbSourceBranch', defaultValue: ''), 15 | string(name: 'ghprbAuthorRepoGitUrl', defaultValue: ''), 16 | string(name: 'sha1', defaultValue: ''), 17 | string(name: 'VERSION', defaultValue: 'master', description: 'Branch to be built'), 18 | string(name: 'EXTERNALS', defaultValue: 'ROOT-latest', description: ''), 19 | string(name: 'EMPTY_BINARY', defaultValue: 'true', description: 'Boolean to empty the binary directory (i.e. to force a full re-build)'), 20 | string(name: 'ExtraCMakeOptions', defaultValue: '', description: 'Additional CMake configuration options of the form "-Doption1=value1 -Doption2=value2"'), 21 | string(name: 'MODE', defaultValue: 'pullrequests', description: 'The build mode'), 22 | string(name: 'PARENT', defaultValue: 'roottest-pullrequests-trigger', description: 'Trigger job name') 23 | ]) 24 | ]) 25 | 26 | timestamps { 27 | GitHub gitHub = new GitHub(this, PARENT, ghprbGhRepository, ghprbPullId, params.ghprbActualCommit) 28 | BotParser parser = new BotParser(this, params.ExtraCMakeOptions) 29 | GenericBuild build = new GenericBuild(this, 'roottest-pullrequests-build', params.MODE) 30 | 31 | build.addBuildParameter('ROOTTEST_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*') 32 | build.addBuildParameter('ROOTTEST_BRANCH', "origin/pr/${ghprbPullId}/head") 33 | build.addBuildParameter('ROOT_BRANCH', "${params.ghprbTargetBranch}") 34 | build.addBuildParameter('GIT_COMMIT', "${params.sha1}") 35 | build.addBuildParameter('BUILD_NOTE', "$ghprbPullAuthorLogin PR #$ghprbPullId") 36 | 37 | currentBuild.setDisplayName("#$BUILD_NUMBER $ghprbPullAuthorLogin PR #$ghprbPullId") 38 | 39 | build.cancelBuilds('.*PR #' + ghprbPullId + '$') 40 | 41 | build.afterBuild({buildWrapper -> 42 | if (buildWrapper.result.result != 'SUCCESS' && currentBuild.result != 'ABORTED') { 43 | gitHub.postResultComment(buildWrapper) 44 | } 45 | }) 46 | 47 | if (parser.isParsableComment(ghprbCommentBody.trim())) { 48 | parser.parse() 49 | } 50 | 51 | parser.postStatusComment(gitHub) 52 | parser.configure(build) 53 | 54 | gitHub.setPendingCommitStatus('Building') 55 | 56 | build.build() 57 | 58 | stage('Publish reports') { 59 | if (currentBuild.result == 'SUCCESS') { 60 | gitHub.setSucceedCommitStatus('Build passed') 61 | } else if (currentBuild.result != 'ABORTED') { 62 | gitHub.setFailedCommitStatus('Build failed') 63 | } 64 | 65 | if (currentBuild.result != null) { 66 | build.sendEmails() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /resources/jenkins-pipeline-email-html.template: -------------------------------------------------------------------------------- 1 | ${project.name} 2 | 3 | 76 | 77 | <% 78 | 79 | import hudson.Util 80 | import hudson.Functions 81 | import hudson.model.Action 82 | import hudson.model.Result 83 | import jenkins.model.Jenkins 84 | import hudson.plugins.emailext.plugins.content.ScriptContentBuildWrapper 85 | 86 | def healthIconSize = "16x16" 87 | def healthReports = project.buildHealthReports 88 | 89 | Integer total_builds = buildResults.size() 90 | Integer worst_error = 0 91 | 92 | final BUILD_TYPES = 5 93 | 94 | final BUILD_SUCCESS = 0 95 | final BUILD_UNSTABLE = 1 96 | final BUILD_FAILURE = 2 97 | final BUILD_NOT_BUILT = 3 98 | final BUILD_ABORTED = 4 99 | 100 | final MAX_MESSAGES = 10 101 | 102 | // Total amount of occurences per status.CC9933 103 | // See ordinal value for index (hudson.model.Result) 104 | Integer[] total_status = new Integer[BUILD_TYPES] 105 | 106 | Map toolchain_status = new HashMap() 107 | Map toolchain_total_builds = new HashMap() 108 | 109 | Map[] toolchain_builds = new HashMap[BUILD_TYPES] 110 | 111 | Map architecture_status = new HashMap() 112 | Map architecture_total_builds = new HashMap() 113 | Map[] architecture_builds = new HashMap[BUILD_TYPES] 114 | 115 | for (i = 0; i < BUILD_TYPES; i++) { 116 | toolchain_builds[i] = new HashMap() 117 | architecture_builds[i] = new HashMap() 118 | total_status[i] = 0 119 | } 120 | 121 | // We want to initial all status maps to SUCCESS 122 | // We use runs to get actual matrix values because matrix_axis keeps a record of all 123 | // matrix/values. So, if you remove things they can still show up there. 124 | 125 | buildResults.each { buildResult -> 126 | toolchain_total_builds.put(buildResult.compiler, 0) 127 | architecture_total_builds.put(buildResult.label, 0) 128 | 129 | for (i = 0; i < BUILD_TYPES; i++) { 130 | toolchain_builds[i].put(buildResult.compiler, 0) 131 | architecture_builds[i].put(buildResult.label, 0) 132 | total_status[i] = 0 133 | } 134 | } 135 | 136 | // OK, now run thru runs and set anything that did not succeed. 137 | buildResults.each { buildResult -> 138 | def run = buildResult.result.getRawBuild() 139 | def toolchain = buildResult.compiler 140 | def architecture = buildResult.label 141 | 142 | def toolchainBuildCount = toolchain_total_builds.get(toolchain) + 1 143 | toolchain_total_builds.put(toolchain, toolchainBuildCount) 144 | 145 | def architectureBuildCount = architecture_total_builds.get(architecture) + 1 146 | architecture_total_builds.put(architecture, architectureBuildCount) 147 | 148 | total_status[run.getResult().ordinal]++ 149 | 150 | if (!toolchain_status.containsKey(toolchain) 151 | || toolchain_status.get(toolchain).ordinal < run.getResult().ordinal) { 152 | toolchain_status.put(toolchain, run.getResult()) 153 | } 154 | 155 | if (!architecture_status.containsKey(architecture) 156 | || architecture_status.get(architecture).ordinal < run.getResult().ordinal) { 157 | architecture_status.put(architecture, run.getResult()) 158 | } 159 | 160 | def toolchainCount = toolchain_builds[run.getResult().ordinal].get(toolchain) + 1 161 | toolchain_builds[run.getResult().ordinal].put(toolchain, toolchainCount) 162 | 163 | def architectureCount = architecture_builds[run.getResult().ordinal].get(architecture) + 1 164 | architecture_builds[run.getResult().ordinal].put(architecture, architectureCount) 165 | 166 | if (run.getResult().ordinal > worst_error) { 167 | worst_error = run.getResult().ordinal 168 | } 169 | } 170 | 171 | %> 172 |
173 | 174 | 175 | 176 | 195 | 198 | 199 | 200 | 201 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 239 | 240 | <% 241 | if (build.description != null) { 242 | %> 243 | 244 | 245 | 246 | 247 | <% 248 | } 249 | %> 250 | 251 | 252 | 255 | 256 | <% 257 | if (total_status[2] != 0) { 258 | %> 259 | 260 | 261 | 264 | 265 | <% 266 | } 267 | %> 268 |
177 | <% 178 | if (build.result == Result.SUCCESS) { 179 | %> 180 | 181 | <% 182 | } else if (build.result == Result.FAILURE) { 183 | %> 184 | 185 | 186 | <% 187 | } else { 188 | %> 189 | 190 | <% 191 | } 192 | %> 193 | 194 | 196 | BUILD ${build.result} 197 |
Build URL 202 | ${rooturl}${build.url} 203 |
Project:${project.name}
Date of build:${it.timestampString}
Build duration:${build.durationString}
Build cause: 220 | <% 221 | 222 | hudson.model.Cause firstCause = build.causes.get(0); 223 | allCausesSame = false 224 | for (hudson.model.Cause cause : build.causes) { 225 | if (!cause.equals(firstCause.shortDescription)) { 226 | allCausesSame = true 227 | } 228 | } 229 | 230 | if (allCausesSame) { 231 | %> ${firstCause.shortDescription} <% 232 | } else { 233 | for (hudson.model.Cause cause : build.causes) { 234 | %> ${cause.shortDescription} <% 235 | } 236 | } 237 | %> 238 |
Build description:${build.description}
Total Builds: 253 | ${total_builds} 254 |
FAILURES: 262 | ${total_status[2]} out of ${total_builds} builds FAILED! 263 |
269 |
270 | 271 | 272 | 273 |
274 | <% 275 | 276 | if (build.changeSets != null) { 277 | boolean hadChanges = false 278 | %> 279 |

Changes

280 | <% 281 | for (hudson.scm.ChangeLogSet cs : build.getChangeSets()) { 282 | for (hudson.scm.ChangeLogSet.Entry entry : cs) { 283 | hadChanges = true 284 | commitUrlPrefix = 'https://github.com/root-project/root/commit/' 285 | if (commitUrlPrefix != null) { 286 | %> 287 |

${entry.msgAnnotated} (commit ${entry.commitId})

288 | <% 289 | } else { 290 | %> 291 |

${entry.msgAnnotated} (commit ${entry.commitId})

292 | <% 293 | } 294 | %> 295 |

by ${entry.author}

296 | 297 | <% 298 | for (hudson.scm.ChangeLogSet.AffectedFile p : entry.affectedFiles) { 299 | %> 300 | 301 | 302 | 305 | 306 | <% 307 | } 308 | %> 309 |
${p.editType.name} 303 | ${p.path} 304 |

310 | <% 311 | } 312 | } 313 | if (hadChanges == false) { 314 | %> 315 |

No Changes

316 | <% 317 | } 318 | } 319 | %> 320 |
321 | 322 |
323 |

Toolchain Summary

324 | 325 | <% 326 | for (String toolchain : toolchain_status.keySet()) { 327 | hudson.model.Result __result = toolchain_status.get(toolchain) 328 | 329 | Integer _total_cnt = toolchain_total_builds.get(toolchain) 330 | Integer _unstable_cnt = toolchain_builds[BUILD_UNSTABLE].get(toolchain) 331 | Integer _failed_cnt = toolchain_builds[BUILD_FAILURE].get(toolchain) 332 | Integer _not_built_cnt = toolchain_builds[BUILD_NOT_BUILT].get(toolchain) 333 | Integer _aborted_cnt = toolchain_builds[BUILD_ABORTED].get(toolchain) 334 | 335 | resultStyle = "test_passed" 336 | if (__result == Result.FAILURE) { 337 | resultStyle = "test_failed" 338 | } else if (__result == Result.UNSTABLE) { 339 | resultStyle = "test_unstable" 340 | } 341 | 342 | if (__result == Result.SUCCESS) { 343 | %> 344 | 345 | <% 346 | } else { 347 | List buildResultMessages = new LinkedList() 348 | if (_unstable_cnt > 0) { buildResultMessages.add("${_unstable_cnt}/${_total_cnt} unstable") } 349 | if (_failed_cnt > 0) { buildResultMessages.add("${_failed_cnt}/${_total_cnt} failed") } 350 | if (_not_built_cnt > 0) { buildResultMessages.add("${_not_built_cnt}/${_total_cnt} not built") } 351 | if (_aborted_cnt > 0) { buildResultMessages.add("${_aborted_cnt}/${_total_cnt} aborted") } 352 | 353 | %> 354 | 357 | <% 358 | } 359 | } 360 | %> 361 |
${toolchain} - OK
${toolchain} - ${__result}. 355 | ${buildResultMessages.toArray().join(", ")} 356 |
362 |
363 | 364 | 365 |
366 |

Architecture Summary

367 | 368 | <% 369 | for (String architecture : architecture_status.keySet()) { 370 | hudson.model.Result __result = architecture_status.get(architecture) 371 | 372 | Integer _total_cnt = architecture_total_builds.get(architecture) 373 | Integer _unstable_cnt = architecture_builds[BUILD_UNSTABLE].get(architecture) 374 | Integer _failed_cnt = architecture_builds[BUILD_FAILURE].get(architecture) 375 | Integer _not_built_cnt = architecture_builds[BUILD_NOT_BUILT].get(architecture) 376 | Integer _aborted_cnt = architecture_builds[BUILD_ABORTED].get(architecture) 377 | 378 | resultStyle = "test_passed" 379 | if (__result == Result.FAILURE) { 380 | resultStyle = "test_failed" 381 | } else if (__result == Result.UNSTABLE) { 382 | resultStyle = "test_unstable" 383 | } 384 | 385 | 386 | if (__result == Result.SUCCESS) { 387 | %> 388 | 389 | <% 390 | } else { 391 | List buildResultMessages = new LinkedList() 392 | if (_unstable_cnt > 0) { buildResultMessages.add("${_unstable_cnt}/${_total_cnt} unstable") } 393 | if (_failed_cnt > 0) { buildResultMessages.add("${_failed_cnt}/${_total_cnt} failed") } 394 | if (_not_built_cnt > 0) { buildResultMessages.add("${_not_built_cnt}/${_total_cnt} not built") } 395 | if (_aborted_cnt > 0) { buildResultMessages.add("${_aborted_cnt}/${_total_cnt} aborted") } 396 | 397 | %> 398 | 401 | <% 402 | } 403 | } 404 | %> 405 |
${architecture} - OK
${architecture} - ${__result}. 399 | ${buildResultMessages.toArray().join(", ")} 400 |
406 | <% 407 | buildResults.each { buildResult -> 408 | def run = buildResult.result.getRawBuild() 409 | if (run.getResult() != Result.SUCCESS) { 410 | url = run.getUrl() 411 | toolchain = buildResult.compiler 412 | architecture = buildResult.label 413 | 414 | def toolchain_val = toolchain 415 | %> 416 |
417 |

${toolchain_val} - ${architecture} Build Details

418 | <% 419 | 420 | log_parser_class = Jenkins.getInstance().getPluginManager().uberClassLoader 421 | .loadClass("hudson.plugins.logparser.LogParserAction") 422 | log_parser = run.getAction(log_parser_class.asSubclass(Action.class)) 423 | 424 | if (log_parser?.getResult() != null) { 425 | log_parser_result = log_parser.getResult() 426 | 427 | total_warnings = log_parser_result.getTotalWarnings() 428 | total_errors = log_parser_result.getTotalErrors() 429 | 430 | if (total_errors > 0) { 431 | 432 | %> 433 |

Errors (${total_errors})

434 |
    435 | <% 436 | 437 | error_links_file = new File(log_parser_result.getErrorLinksFile()) 438 | 439 | patternFont = ~// 440 | patternLink = ~/target="content" href="/ 441 | replaceLink = "href=\"${rooturl}${url}parsed_console/" 442 | 443 | errors = error_links_file.readLines().grep { it.contains "href" }.collect { line -> 444 | line.replaceAll(patternFont, "").replaceAll(patternLink, replaceLink).minus("").minus("/mnt") // TODO: fix this 445 | } 446 | def ignoredMessages = 0 447 | def totalMessages = 0 448 | for (String error : errors) { 449 | if (++totalMessages < MAX_MESSAGES) { 450 | %> ${error} <% 451 | } else { 452 | ignoredMessages++ 453 | } 454 | } 455 | 456 | if (ignoredMessages > 0) { 457 | %> And ${ignoredMessages} more <% 458 | } 459 | 460 | %>
<% 461 | } 462 | 463 | if (total_warnings > 0) { 464 | 465 | %> 466 |

Warnings (${total_warnings})

467 |
    468 | <% 469 | 470 | warning_links_file = new File(log_parser_result.getWarningLinksFile()) 471 | 472 | patternFont = ~// 473 | patternLink = ~/target="content" href="/ 474 | replaceLink = "href=\"${rooturl}${url}parsed_console/" 475 | 476 | warnings = warning_links_file.readLines().grep { it.contains "href" }.collect { line -> 477 | line.replaceAll(patternFont, "").replaceAll(patternLink, replaceLink).minus("").minus("/mnt") 478 | } 479 | 480 | def ignoredMessages = 0 481 | def totalMessages = 0 482 | for (String warning : warnings) { 483 | if (++totalMessages < MAX_MESSAGES) { 484 | %> ${warning} <% 485 | } else { 486 | ignoredMessages++ 487 | } 488 | } 489 | 490 | if (ignoredMessages > 0) { 491 | %> And ${ignoredMessages} more <% 492 | } 493 | %> 494 |
495 | <% 496 | } 497 | 498 | } %> 499 | <% 500 | 501 | def buildWrapper = new ScriptContentBuildWrapper(run) 502 | def test_results = buildWrapper.getAction("hudson.tasks.junit.TestResultAction") 503 | 504 | if (test_results != null && test_results.getFailCount() > 0) { 505 | failed_tests = test_results.getFailCount() 506 | %> 507 |

Failed tests (${failed_tests})

508 | 509 | 510 | 511 | 512 | 513 | 514 | <% 515 | 516 | def ignoredMessages = 0 517 | def totalMessages = 0 518 | test_results.getFailedTests().each { result -> 519 | String title = result.getFullName() 520 | int age = 0 521 | if (result.getFailedSinceRun() != null) { 522 | age = run.number - result.getFailedSinceRun().number 523 | } 524 | 525 | float duration = result.getDuration() 526 | 527 | String locationBuilder = rooturl + url + "testReport/" 528 | String[] titleParts = title.split("\\.") 529 | 530 | if (titleParts.length > 3) { 531 | for (i = 0; i < titleParts.length - 2; i++) { 532 | locationBuilder += titleParts[i] 533 | 534 | if (i < titleParts.length - 3) { 535 | locationBuilder += "." 536 | } 537 | } 538 | 539 | locationBuilder += "/" + titleParts[titleParts.length - 2] 540 | locationBuilder += "/" + titleParts[titleParts.length - 1] 541 | } 542 | 543 | if (++totalMessages < MAX_MESSAGES) { 544 | %> 545 | 546 | 547 | 548 | 549 | 550 | <% 551 | } else { 552 | ignoredMessages++ 553 | } 554 | 555 | } 556 | %> 557 |
ClassDurationAge
${title}${String.format("%.1f", duration)} sec${age}
558 | <% 559 | 560 | if (ignoredMessages > 0) { 561 | %> And ${ignoredMessages} more <% 562 | } 563 | } 564 | %> 565 |
566 | <% 567 | } 568 | } 569 | 570 | %> 571 | 572 | -------------------------------------------------------------------------------- /src/cern/root/pipeline/BotParser.groovy: -------------------------------------------------------------------------------- 1 | package cern.root.pipeline 2 | 3 | import java.util.regex.Pattern 4 | import cern.root.pipeline.BuildConfiguration 5 | 6 | /** 7 | * Handles parsing of build configurations from a user-specified command. 8 | * This command could for example be a comment that is posted on GitHub. 9 | */ 10 | class BotParser implements Serializable { 11 | private boolean parsableComment 12 | private String matrix 13 | private String flags 14 | private static final String COMMENT_REGEX = 'build (((?just|also)\\s)?on (?([A-Za-z0-9_\\.-]*\\/[A-Za-z0-9_\\.-]*,?\\s?)*))?(with flags (?.*))?' 15 | private def script 16 | 17 | /** 18 | * Whether the default configuration for the job should be discarded. 19 | */ 20 | boolean overrideDefaultConfiguration = false 21 | 22 | /** 23 | * List of the build configurations that was not recognized. 24 | */ 25 | def invalidBuildConfigurations = [] 26 | 27 | /** 28 | * List of the recognized build configurations. 29 | */ 30 | def validBuildConfigurations = [] 31 | 32 | /** 33 | * The original/default ExtraCMakeOptions value from the original build. 34 | */ 35 | String defaultExtraCMakeOptions 36 | 37 | /** 38 | * CMake options to use for this build. 39 | */ 40 | String extraCMakeOptions 41 | 42 | /** 43 | * Initiates a new BotParser 44 | * @param script Pipeline script context. 45 | * @param defaultExtraCMakeOptions The default CMake options to use if user didn't specify anything. 46 | */ 47 | BotParser(script, defaultExtraCMakeOptions) { 48 | this.script = script 49 | this.defaultExtraCMakeOptions = defaultExtraCMakeOptions 50 | this.extraCMakeOptions = defaultExtraCMakeOptions 51 | } 52 | 53 | @NonCPS 54 | private def appendFlagsToMap(flags, map) { 55 | def pattern = Pattern.compile('([a-zA-Z0-9_-]+)=([a-zA-Z0-9_-]+|(("|\')[^"]*("|\')))') 56 | def matcher = pattern.matcher(flags) 57 | 58 | while (matcher.find()) { 59 | def key = matcher.group(1) 60 | def value = matcher.group(2) 61 | 62 | if (map.containsKey(key)) { 63 | map[key] = value 64 | } else { 65 | map.put(key, value) 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Checks if a comment is recognized as a comment or not. If it was, it will also pull out the recognized bits from 72 | * the comment. It will however, not parse the job configuration. 73 | * @param comment Comment to check. 74 | * @return True if is parsable, otherwise false. 75 | */ 76 | @NonCPS 77 | boolean isParsableComment(comment) { 78 | comment = comment.replace('\\\"', '\"') 79 | script.println "AXEL: Comment is \"$comment\"" 80 | 81 | def matcher = Pattern.compile(COMMENT_REGEX).matcher(comment) 82 | 83 | parsableComment = matcher.find() 84 | 85 | if (parsableComment) { 86 | overrideDefaultConfiguration = matcher.group('overrideDefaultConfiguration').equals('just') 87 | matrix = matcher.group('matrix') 88 | script.println "AXEL: matrix is \"$matrix\"" 89 | flags = matcher.group('flags') 90 | script.println "AXEL: flags is \"$flags\"" 91 | 92 | // If we specify a build configuration without "also/just on ...", only build on those platforms 93 | if (matrix != null && matcher.group('overrideDefaultConfiguration') == null) { 94 | overrideDefaultConfiguration = true 95 | } 96 | } 97 | 98 | return parsableComment 99 | } 100 | 101 | /** 102 | * Parses and sets the build configuration based on the comment set in isParsableComment. 103 | */ 104 | @NonCPS 105 | void parse() { 106 | script.println "Comment recognized as a parseable command: $matrix" 107 | 108 | if (matrix != null) { 109 | // Parse and set the config 110 | def patterns = matrix.trim().replace(',' ,'').split(' ') 111 | 112 | for (unparsedPattern in patterns) { 113 | script.println "Working on unparsedPattern \"$unparsedPattern\"" 114 | def patternArgs = unparsedPattern.split('/') 115 | def platform = patternArgs[0] 116 | def spec = patternArgs[1] 117 | 118 | 119 | script.println "Received label $platform with specialization $spec" 120 | 121 | if (!BuildConfiguration.recognizedPlatform(spec, platform)) { 122 | invalidBuildConfigurations << [spec: spec, platform: platform] 123 | } else { 124 | validBuildConfigurations << [spec: spec, platform: platform] 125 | } 126 | } 127 | } 128 | 129 | if (flags != null) { 130 | def cmakeFlagsMap = [:] 131 | appendFlagsToMap(defaultExtraCMakeOptions, cmakeFlagsMap) 132 | appendFlagsToMap(flags, cmakeFlagsMap) 133 | 134 | extraCMakeOptions = cmakeFlagsMap.collect { /$it.key=$it.value/ } join ' ' 135 | } 136 | 137 | script.println "ExtraCMakeOptions set to $extraCMakeOptions" 138 | script.println "Add default matrix config: $overrideDefaultConfiguration" 139 | script.println "CMake flags: $flags" 140 | } 141 | 142 | /** 143 | * Posts a comment to GitHub about what configuration will be used. 144 | * @param gitHub GitHub. 145 | */ 146 | void postStatusComment(gitHub) { 147 | // If someone posted a platform/spec that isn't recognized, abort the build. 148 | if (invalidBuildConfigurations.size() > 0) { 149 | def unrecognizedPlatforms = new StringBuilder() 150 | 151 | for (config in invalidBuildConfigurations) { 152 | unrecognizedPlatforms.append('`' + config.platform + '`/`' + config.spec + '`, ') 153 | } 154 | 155 | unrecognizedPlatforms.replace(unrecognizedPlatforms.length() - 2, unrecognizedPlatforms.length(), ' ') 156 | gitHub.postComment("Didn't recognize ${unrecognizedPlatforms.toString().trim()} aborting build.") 157 | 158 | throw new Exception("Unrecognized specialization(s)/platform(s): ${unrecognizedPlatforms.toString()}") 159 | } else { 160 | def commentResponse = new StringBuilder() 161 | commentResponse.append('Starting build on ') 162 | 163 | for (config in validBuildConfigurations) { 164 | commentResponse.append('`' + config.platform + '`/`' + config.spec + '`, ') 165 | } 166 | 167 | if (!overrideDefaultConfiguration) { 168 | for (config in BuildConfiguration.getPullrequestConfiguration(extraCMakeOptions)) { 169 | commentResponse.append('`' + config.label + '`/`' + config.spec + '`, ') 170 | } 171 | } 172 | 173 | // Remove last ',' after platforms listing 174 | commentResponse.replace(commentResponse.length() - 2, commentResponse.length(), ' ') 175 | 176 | if (extraCMakeOptions != null && extraCMakeOptions.size() > 0) { 177 | commentResponse.append("with flags `$extraCMakeOptions`") 178 | } 179 | 180 | commentResponse.append("\n[How to customize builds](https://github.com/phsft-bot/build-configuration/blob/master/README.md)") 181 | 182 | gitHub.postComment(commentResponse.toString()) 183 | } 184 | } 185 | 186 | /** 187 | * Configures a build to use the configuration that has been parsed. 188 | * @param script Script context. 189 | * @param build Build to configure. 190 | */ 191 | void configure(build) { 192 | for (config in validBuildConfigurations) { 193 | def opts = '' 194 | if (config.opts != null) { 195 | opts = config.opts + ' ' 196 | } 197 | script.println "Creating build : with flags $opts" 198 | build.buildOn(config.platform, config.spec, 'Release', opts + extraCMakeOptions) 199 | } 200 | 201 | // If no override of the platforms, add the default ones 202 | if (!overrideDefaultConfiguration) { 203 | script.println 'Adding default config' 204 | build.addConfigurations(BuildConfiguration.getPullrequestConfiguration(extraCMakeOptions)) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/cern/root/pipeline/BuildConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package cern.root.pipeline 2 | 3 | /** 4 | * Contains available and default build configurations. 5 | */ 6 | class BuildConfiguration { 7 | /** 8 | * @return The available platforms/labels that can be used. 9 | */ 10 | @NonCPS 11 | static def getAvailablePlatforms() { 12 | return [ 13 | 'arm64', 14 | 'ROOT-centos8', 15 | 'ROOT-debian10-i386', 16 | 'ROOT-performance-centos8-multicore', 17 | 'mac12', 18 | 'mac12arm', 19 | 'mac13', 20 | 'mac13arm', 21 | 'macbeta', 22 | 'ROOT-ubuntu2004', 23 | 'ROOT-ubuntu2004-clang', 24 | 'ROOT-ubuntu2204', 25 | 'windows10' 26 | ] 27 | } 28 | 29 | /** 30 | * @return The available specializations that can be used. 31 | */ 32 | @NonCPS 33 | static def getAvailableSpecializations() { 34 | return [ 35 | 'default', 36 | 'cxx17', 37 | 'cxx20', 38 | 'python3', 39 | 'noimt', 40 | 'soversion', 41 | 'rtcxxmod', 42 | 'nortcxxmod', 43 | 'cxxmod', 44 | ] 45 | } 46 | 47 | /** 48 | * @return Build configuration for pull requests. 49 | */ 50 | static def getPullrequestConfiguration(extraCMakeOptions) { 51 | return [ 52 | [ label: 'ROOT-performance-centos8-multicore', opts: extraCMakeOptions + ' -DCTEST_TEST_EXCLUDE_NONE=On', spec: 'soversion' ], 53 | [ label: 'ROOT-ubuntu2204', opts: extraCMakeOptions, spec: 'nortcxxmod' ], 54 | [ label: 'ROOT-ubuntu2004', opts: extraCMakeOptions, spec: 'python3' ], 55 | [ label: 'mac12arm', opts: extraCMakeOptions + ' -DCTEST_TEST_EXCLUDE_NONE=On', spec: 'cxx20' ], 56 | [ label: 'windows10', opts: extraCMakeOptions, spec: 'default'] 57 | ] 58 | } 59 | 60 | /** 61 | * Checks if a specified configuration is valid or not. 62 | * @param compiler Compiler to check. 63 | * @param platform Platform to check. 64 | * @return True if recognized, otherwise false. 65 | */ 66 | @NonCPS 67 | static boolean recognizedPlatform(String spec, String platform) { 68 | return getAvailableSpecializations().contains(spec) && getAvailablePlatforms().contains(platform) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/cern/root/pipeline/GenericBuild.groovy: -------------------------------------------------------------------------------- 1 | package cern.root.pipeline 2 | 3 | import hudson.model.Result 4 | import hudson.plugins.emailext.plugins.content.ScriptContentBuildWrapper 5 | import java.io.DataOutputStream 6 | import java.net.Socket 7 | import jenkins.metrics.impl.TimeInQueueAction 8 | import jenkins.model.Jenkins 9 | 10 | /** 11 | * Class for setting up a generic build of ROOT across a number of platforms. 12 | */ 13 | class GenericBuild implements Serializable { 14 | private def configuration = [:] 15 | private def buildResults = [] 16 | private def postBuildSteps = [] 17 | private def script 18 | private def mode 19 | private def graphiteReporter 20 | private def buildParameters = [] 21 | private def jobName 22 | 23 | /** 24 | * Creates a new generic build. 25 | * @param script Script context. 26 | * @param jobName Name of generic job that will execute across all platforms. 27 | */ 28 | GenericBuild(script, jobName, mode) { 29 | this.script = script 30 | this.mode = mode 31 | this.graphiteReporter = new GraphiteReporter(script, mode) 32 | this.jobName = jobName 33 | 34 | for (ParameterValue p in script.currentBuild.rawBuild.getAction(ParametersAction.class)) { 35 | addBuildParameter(p.name, String.valueOf(p.value)) 36 | } 37 | 38 | // Always build the same branch on root and roottest 39 | addBuildParameter('ROOT_BRANCH', script.params.VERSION) 40 | addBuildParameter('ROOTTEST_BRANCH', script.params.VERSION) 41 | } 42 | 43 | private def performBuild(label, spec, buildType, opts) { 44 | def jobParameters = [] 45 | 46 | // Copy parameters from build parameters 47 | for (parameter in buildParameters) { 48 | jobParameters << parameter 49 | } 50 | 51 | jobParameters << script.string(name: 'LABEL', value: label) 52 | jobParameters << script.string(name: 'SPEC', value: spec) 53 | jobParameters << script.string(name: 'BUILDTYPE', value: buildType) 54 | jobParameters << script.string(name: 'ExtraCMakeOptions', value: opts) 55 | 56 | def result = script.build job: jobName, parameters: jobParameters, propagate: false 57 | def resultWrapper = [result: result, label: label, spec: spec, buildType: buildType] 58 | buildResults << resultWrapper 59 | 60 | for (postStep in postBuildSteps) { 61 | postStep(resultWrapper) 62 | } 63 | 64 | graphiteReporter.reportBuild(result.rawBuild) 65 | 66 | // Propagate build result 67 | if (result.result != 'SUCCESS') { 68 | script.currentBuild.result = result.result 69 | throw new Exception("Build completed with result: ${result.result}") 70 | } 71 | } 72 | 73 | /** 74 | * Adds a configuration that ROOT should be built on. 75 | * @param label Label to build on, e.g. slc6. 76 | * @param spec Specialization to build on, e.g. noimt. 77 | * @param buildType Build type, e.g. Debug. 78 | */ 79 | void buildOn(label, spec, buildType, opts) { 80 | script.println "Preparing '$buildType' build on $label with options $opts" 81 | def configurationLabel = "$label-$spec-$buildType" 82 | configuration[configurationLabel] = { 83 | script.stage("Build - $configurationLabel") { 84 | performBuild(label, spec, buildType, opts) 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Adds a set of pre-defined configurations. 91 | * @param configs Configurations to add. 92 | */ 93 | void addConfigurations(configs) { 94 | for (config in configs) { 95 | buildOn(config.label, config.spec, 'Release', config.opts) 96 | } 97 | } 98 | 99 | /** 100 | * Starts the build. 101 | */ 102 | void build() { 103 | try { 104 | script.parallel(configuration) 105 | 106 | if (script.currentBuild.result == null) { 107 | script.currentBuild.result = Result.SUCCESS 108 | } 109 | } catch (e) { 110 | script.println "Build failed because: ${e.message}" 111 | } 112 | } 113 | 114 | /** 115 | * Adds a post-build step that will be executed after each build. 116 | * @param postStep Closure that will execute after the build. 117 | */ 118 | void afterBuild(postStep) { 119 | postBuildSteps << postStep 120 | } 121 | 122 | /** 123 | * Adds a build parameter to the build. 124 | * @param key Name of the build parameter. 125 | * @param value Value of the build parameter. 126 | */ 127 | @NonCPS 128 | void addBuildParameter(key, value) { 129 | buildParameters << script.string(name: key, value: String.valueOf(value)) 130 | } 131 | 132 | /** 133 | * Sends an email report about the current build to a set of participants. 134 | * The email is generated from the template in resources/jenkins-pipeline-email-html.template. 135 | */ 136 | /*@NonCPS -- AXEL: cannot use as this calls `emailext`, see 137 | "Use of Pipeline steps from @NonCPS" at Use of Pipeline steps from @NonCPS */ 138 | void sendEmails() { 139 | def binding = ['build': script.currentBuild.rawBuild, 140 | 'rooturl': Jenkins.getActiveInstance().getRootUrl(), 141 | 'buildResults': buildResults, 142 | 'it': new ScriptContentBuildWrapper(script.currentBuild.rawBuild), 143 | 'project': script.currentBuild.rawBuild.getParent()] 144 | 145 | def classLoader = Jenkins.getActiveInstance().getPluginManager().uberClassLoader 146 | def shell = new GroovyShell(classLoader) 147 | def engine = new groovy.text.SimpleTemplateEngine(shell) 148 | def template = engine.createTemplate(script.libraryResource('jenkins-pipeline-email-html.template')) 149 | 150 | def result = template.make(binding).toString() 151 | 152 | def recipients = 'pcanal@fnal.gov, danilo.piparo@cern.ch' 153 | 154 | script.emailext( 155 | body: result, mimeType: 'text/html', 156 | recipientProviders: 157 | [[$class: 'CulpritsRecipientProvider'], [$class: 'DevelopersRecipientProvider']], 158 | replyTo: '$DEFAULT_REPLYTO', subject: '$DEFAULT_SUBJECT', 159 | to: recipients) 160 | } 161 | 162 | /** 163 | * Cancels all running builds where title matches a certain pattern. 164 | * @param pattern The pattern to match the build titles to cancel. 165 | */ 166 | @NonCPS 167 | void cancelBuilds(String pattern) { 168 | script.currentBuild.rawBuild.parent.builds.each { run -> 169 | if (run.number != script.currentBuild.number && run.displayName.matches(pattern) && run.isBuilding()) { 170 | script.println "Aborting build #${run.number}" 171 | run.executor.interrupt(Result.ABORTED); 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/cern/root/pipeline/GitHub.groovy: -------------------------------------------------------------------------------- 1 | package cern.root.pipeline 2 | 3 | import hudson.plugins.logparser.LogParserAction 4 | import hudson.tasks.junit.TestResultAction 5 | import jenkins.model.Jenkins 6 | import org.jenkinsci.plugins.ghprb.GhprbTrigger 7 | import org.jenkinsci.plugins.workflow.actions.WorkspaceAction 8 | import org.jenkinsci.plugins.workflow.job.WorkflowRun 9 | import org.jenkinsci.plugins.workflow.flow.FlowExecution; 10 | import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker; 11 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 12 | import org.jenkinsci.plugins.workflow.graph.StepStartNode; 13 | import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode; 14 | import org.jenkinsci.plugins.workflow.actions.WorkspaceAction 15 | import org.kohsuke.github.GHCommitState 16 | 17 | /** 18 | * Facade towards GitHub. 19 | */ 20 | class GitHub implements Serializable { 21 | private def script 22 | private def parentJob 23 | private def repo 24 | private def prId 25 | private def sha1 26 | 27 | /** 28 | * Initialized a new GitHub facade. 29 | * @param script Script context. 30 | * @param parentJob The job to read the GitHub auth from. 31 | * @param repo Repository used for this build. 32 | * @param prId The pull request ID for this build. 33 | * @param sha1 Sha1 for the commit that triggered this build. 34 | */ 35 | GitHub(script, parentJob, repo, prId, sha1) { 36 | this.script = script 37 | this.parentJob = parentJob 38 | this.repo = repo 39 | this.prId = prId 40 | this.sha1 = sha1 41 | } 42 | 43 | /** 44 | * Sets the commit status of this current build to failure. 45 | * @param statusText Status text to add on GitHub. 46 | */ 47 | void setFailedCommitStatus(statusText) { 48 | setCommitStatus(GHCommitState.FAILURE, statusText) 49 | } 50 | 51 | /** 52 | * Sets the commit status of this current build to success. 53 | * @param statusText Status text to add on GitHub. 54 | */ 55 | void setSucceedCommitStatus(statusText) { 56 | setCommitStatus(GHCommitState.SUCCESS, statusText) 57 | } 58 | 59 | /** 60 | * Sets the commit status of this build to pending. 61 | * @param statusText Status text to add on GitHub. 62 | */ 63 | void setPendingCommitStatus(statusText) { 64 | setCommitStatus(GHCommitState.PENDING, statusText) 65 | } 66 | 67 | @NonCPS 68 | private void setCommitStatus(status, statusText) { 69 | def triggerJob = script.manager.hudson.getJob(parentJob) 70 | def prbTrigger = triggerJob.getTrigger(GhprbTrigger.class) 71 | def repo = prbTrigger.getGitHub().getRepository(repo) 72 | 73 | repo.createCommitStatus(sha1, status, script.currentBuild.absoluteUrl + 'console', statusText, 'Jenkins CI build') 74 | script.println "Updating commit status to $status" 75 | } 76 | 77 | /** 78 | * Posts a comment on GitHub on the current pull request. 79 | * @param comment Comment to post. 80 | */ 81 | @NonCPS 82 | void postComment(String comment) { 83 | script.println "Posting comment $comment for pr $prId" 84 | def triggerJob = script.manager.hudson.getJob(parentJob) 85 | def prbTrigger = triggerJob.getTrigger(GhprbTrigger.class) 86 | prbTrigger.getRepository().addComment(Integer.valueOf(prId), comment) 87 | } 88 | 89 | /** 90 | * Posts a build summary comment on GitHub on the current pull request. 91 | * @param buildWrapper Build result wrapper. 92 | */ 93 | @NonCPS 94 | void postResultComment(buildWrapper) { 95 | def commentBuilder = new StringBuilder() 96 | def buildUrl = Jenkins.activeInstance.rootUrl + buildWrapper.result.rawBuild.url 97 | def today = new Date().format("yyyy-MM-dd"); 98 | def label = buildWrapper.label; 99 | def spec = buildWrapper.spec; 100 | 101 | commentBuilder.append("Build failed on ${label}/${spec}.\n") 102 | 103 | def exec = buildWrapper.result.rawBuild.getExecution() 104 | if (exec != null) { 105 | FlowGraphWalker w = new FlowGraphWalker(exec) 106 | def skippedWorspaces = 0; 107 | for (FlowNode n : w) { 108 | if (n instanceof StepStartNode) { 109 | def wsAction = n.getAction(WorkspaceAction) 110 | if (wsAction) { 111 | def workspace = wsAction.getWorkspace() 112 | if (workspace != null) { 113 | if (skippedWorspaces == 0) { 114 | skippedWorspaces = 1 115 | continue 116 | } 117 | def computer = workspace.toComputer().getHostName() 118 | def wspath = workspace.getRemote() 119 | commentBuilder.append("Running on ${computer}:${wspath}\n") 120 | break 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | /* 128 | commentBuilder.append("[See cdash](http://cdash.cern.ch/index.php?project=ROOT&filtercount=1&field1=buildname/string&compare1=65&value1=PR-${prId}-${label}-${spec}&date=${today}).\n") 129 | */ 130 | commentBuilder.append("[See console output](${buildUrl}console).\n") 131 | 132 | def logParserAction = buildWrapper.result.rawBuild.getAction(LogParserAction.class) 133 | def testResultAction = buildWrapper.result.rawBuild.getAction(TestResultAction.class) 134 | 135 | def maxMessages = 10 136 | 137 | if (logParserAction?.result?.totalErrors > 0) { 138 | commentBuilder.append("### Errors:\n") 139 | def ignoredMessages = 0 140 | def totalMessages = 0 141 | 142 | logParserAction.result.getErrorLinksReader().withReader { 143 | def line = null 144 | while ((line = it.readLine()) != null) { 145 | def start = '' 146 | def startPos = line.indexOf(start) + start.length() 147 | def endPos = line.indexOf('') 148 | 149 | if (endPos > startPos) { 150 | def msg = line.substring(startPos, endPos) 151 | 152 | if (totalMessages++ < maxMessages) { 153 | commentBuilder.append("- $msg \n") 154 | } else { 155 | ignoredMessages++ 156 | } 157 | } 158 | } 159 | } 160 | 161 | if (ignoredMessages > 0) { 162 | commentBuilder.append("\nAnd $ignoredMessages more\n") 163 | } 164 | commentBuilder.append("\n") 165 | } 166 | 167 | if (logParserAction?.result?.totalWarnings > 0) { 168 | commentBuilder.append("### Warnings:\n") 169 | def ignoredMessages = 0 170 | def totalMessages = 0 171 | 172 | logParserAction.result.getWarningLinksReader().withReader { 173 | def line = null 174 | while ((line = it.readLine()) != null) { 175 | def start = '' 176 | def startPos = line.indexOf(start) + start.length() 177 | def endPos = line.indexOf('') 178 | 179 | if (endPos > startPos) { 180 | def msg = line.substring(startPos, endPos) 181 | 182 | if (totalMessages++ < maxMessages) { 183 | commentBuilder.append("- $msg \n") 184 | } else { 185 | ignoredMessages++ 186 | } 187 | } 188 | } 189 | } 190 | 191 | if (ignoredMessages > 0) { 192 | commentBuilder.append("\nAnd $ignoredMessages more\n") 193 | } 194 | 195 | commentBuilder.append("\n") 196 | } 197 | 198 | if (testResultAction?.failCount > 0) { 199 | commentBuilder.append("### Failing tests:\n") 200 | def ignoredMessages = 0 201 | def totalMessages = 0 202 | 203 | testResultAction.failedTests.each { test -> 204 | if (totalMessages++ < maxMessages) { 205 | def testLocation = test.getRelativePathFrom(null).minus('junit/') 206 | def testUrl = "${buildUrl}testReport/${testLocation}" 207 | 208 | commentBuilder.append("- [${test.fullName}](${testUrl})\n") 209 | } else { 210 | ignoredMessages++ 211 | } 212 | } 213 | 214 | if (ignoredMessages > 0) { 215 | commentBuilder.append("\nAnd $ignoredMessages more\n") 216 | } 217 | } 218 | 219 | postComment(commentBuilder.toString()) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/cern/root/pipeline/GraphiteReporter.groovy: -------------------------------------------------------------------------------- 1 | package cern.root.pipeline 2 | 3 | import org.jenkinsci.plugins.plaincredentials.StringCredentials 4 | import com.cloudbees.plugins.credentials.CredentialsMatchers 5 | import com.cloudbees.plugins.credentials.CredentialsProvider 6 | import com.cloudbees.plugins.credentials.domains.DomainRequirement 7 | 8 | import java.io.DataOutputStream 9 | import java.net.Socket 10 | import java.util.Collections 11 | import java.util.regex.Pattern 12 | 13 | import hudson.security.ACL 14 | import hudson.tasks.junit.TestResultAction 15 | import jenkins.metrics.impl.TimeInQueueAction 16 | import jenkins.model.Jenkins 17 | 18 | /** 19 | * Reports build statistics to Graphite. 20 | */ 21 | class GraphiteReporter implements Serializable { 22 | private String graphiteServer 23 | private int graphiteServerPort 24 | private String graphiteMetricPath 25 | private def script 26 | private def mode 27 | 28 | /** 29 | * Creates a new GraphiteReporter. 30 | * The server configuration is read as secrets from Jenkins (secret text): 31 | * graphite-server: Server hostname of the Graphite server. 32 | * graphite-server-port: Server port of the Graphite server. 33 | * graphite-metric-path: Metric path to send metrics to. 34 | * @param script Script context. 35 | * @param mode The build mode, e.g. continuous or experimental. 36 | */ 37 | GraphiteReporter(script, mode) { 38 | this.script = script 39 | this.mode = mode 40 | this.graphiteServer = getSecret('graphite-server') 41 | this.graphiteServerPort = Integer.valueOf(getSecret('graphite-server-port')) 42 | this.graphiteMetricPath = getSecret('graphite-metric-path') 43 | } 44 | 45 | @NonCPS 46 | private def getSecret(secretId) { 47 | def creds = CredentialsMatchers.filter( 48 | CredentialsProvider.lookupCredentials(StringCredentials.class, 49 | Jenkins.getInstance(), ACL.SYSTEM, 50 | Collections.emptyList()), 51 | CredentialsMatchers.withId(secretId) 52 | ) 53 | 54 | if (creds.size() == 0) { 55 | throw new Exception("No key for secret $secretId found, did you forget registering such a secret?") 56 | } 57 | 58 | return creds.get(0).getSecret().getPlainText() 59 | } 60 | 61 | @NonCPS 62 | private def reportMetrics(metricName, metrics) { 63 | def connection = new Socket(graphiteServer, graphiteServerPort) 64 | def stream = new DataOutputStream(connection.getOutputStream()) 65 | def metricPath = "${graphiteMetricPath}.${metricName}" 66 | def payload = "${metricPath} ${metrics.join(' ')}\n" 67 | 68 | stream.writeBytes(payload) 69 | connection.close() 70 | } 71 | 72 | /** 73 | * Reports a build and its statistics to Graphite. 74 | * @param build Build to report. 75 | */ 76 | def reportBuild(build) { 77 | def now = (long)(System.currentTimeMillis() / 1000) 78 | def totalRunTime = System.currentTimeMillis() - build.getTimeInMillis() 79 | 80 | reportMetrics("build.${mode}.total_run_time", [(long)(totalRunTime / 1000), now]) 81 | 82 | def action = build.getAction(TimeInQueueAction) 83 | if (action != null) { 84 | // Time it takes to actually build 85 | def buildTime = totalRunTime - action.getQueuingDurationMillis() 86 | 87 | reportMetrics("build.${mode}.run_time", [(long)(buildTime / 1000), now]) 88 | reportMetrics("build.${mode}.queue_time", [(long)(action.getQueuingDurationMillis() / 1000), now]) 89 | } 90 | 91 | def testResults = build.getAction(TestResultAction) 92 | 93 | if (testResults != null) { 94 | def failedTests = testResults.result.failedTests 95 | def skippedTests = testResults.result.skippedTests 96 | def passedTests = testResults.result.passedTests 97 | 98 | def totalTestCount = failedTests.size() + skippedTests.size() + passedTests.size() 99 | 100 | def platform = getPlatform(build) 101 | if (platform != null) { 102 | def buildName = "${script.VERSION}-$platform" 103 | 104 | reportMetrics("${buildName}.testresult.total", [totalTestCount, now]) 105 | reportMetrics("${buildName}.testresult.passed", [passedTests.size(), now]) 106 | reportMetrics("${buildName}.testresult.skipped", [skippedTests.size(), now]) 107 | reportMetrics("${buildName}.testresult.failed", [failedTests.size(), now]) 108 | 109 | for (test in passedTests) { 110 | def title = test.name.replace('.', '-') 111 | reportMetrics("${buildName}.tests.${title}", [0, now]) 112 | } 113 | 114 | for (test in skippedTests) { 115 | def title = test.name.replace('.', '-') 116 | reportMetrics("${buildName}.tests.${title}", [1, now]) 117 | } 118 | 119 | for (test in failedTests) { 120 | def title = test.name.replace('.', '-') 121 | reportMetrics("${buildName}.tests.${title}", [2, now]) 122 | } 123 | } 124 | } 125 | } 126 | 127 | 128 | @NonCPS 129 | private def getPlatform(build) { 130 | def platform = null 131 | def pattern = Pattern.compile('\\+*\\sPLATFORM=(?.*)') 132 | 133 | build.logReader.withReader { 134 | def line = null 135 | while ((line = it.readLine()) != null) { 136 | def matcher = pattern.matcher(line) 137 | if (matcher.find()) { 138 | platform = matcher.group('platform') 139 | } 140 | } 141 | } 142 | 143 | if (platform == null) { 144 | script.println 'WARNING: No platform was found for this build. Did jenkins/getPlatform.py get executed?' 145 | } 146 | 147 | return platform 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/cern/root/pipeline/Mattermost.groovy: -------------------------------------------------------------------------------- 1 | package cern.root.pipeline 2 | 3 | import hudson.tasks.test.AbstractTestResultAction 4 | import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper 5 | 6 | /** 7 | * Sends a status message to Mattermost generated from the build. 8 | * @param buildWrapper Build wrapper. 9 | */ 10 | void postMattermostReport(buildWrapper) { 11 | def result = buildWrapper.result 12 | def lastBuildFailed = !hudson.model.Result.SUCCESS.equals(currentBuild.rawBuild.getPreviousBuild()?.getResult()) 13 | 14 | if (lastBuildFailed || result.getResult() != 'SUCCESS') { 15 | def messageBuilder = new StringBuilder() 16 | messageBuilder.append("${currentBuild.fullProjectName} - ${currentBuild.displayName} ") 17 | messageBuilder.append("completed with status ${result.result} ") 18 | messageBuilder.append("[Open](${currentBuild.absoluteUrl})\n") 19 | 20 | def summary = buildTestSummary(result) 21 | messageBuilder.append(summary) 22 | 23 | mattermostSend message: messageBuilder.toString(), color: getBuildColor(result) 24 | } 25 | } 26 | 27 | private def buildTestSummary(result) { 28 | def summary = new StringBuilder() 29 | 30 | def action = result.getRawBuild().getAction(AbstractTestResultAction.class) 31 | if (action != null) { 32 | def total = action.getTotalCount() 33 | def failed = action.getFailCount() 34 | def skipped = action.getSkipCount() 35 | summary.append('\nTest Status:\n') 36 | summary.append("\tPassed: ${(total - failed - skipped)}") 37 | summary.append(", Failed: $failed") 38 | summary.append(", Skipped: $skipped") 39 | } else { 40 | summary.append('\nNo Tests found.') 41 | } 42 | return summary.toString() 43 | } 44 | 45 | private def getBuildColor(RunWrapper r) { 46 | def result = r.getResult() 47 | if (result == 'SUCCESS') { 48 | return 'good' 49 | } else if (result == 'FAILURE') { 50 | return 'danger' 51 | } else { 52 | return 'warning' 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/BotTests.groovy: -------------------------------------------------------------------------------- 1 | import cern.root.pipeline.BotParser 2 | import cern.root.pipeline.BuildConfiguration 3 | 4 | @interface NonCPS { } 5 | 6 | // Mock class for GitHub 7 | class GitHub { 8 | String postedComment 9 | def postComment(String comment) { 10 | this.postedComment = comment 11 | } 12 | } 13 | def assertConfiguration(actual, expected) { 14 | assert (actual.size() == expected.size()) 15 | 16 | actual.each { actualConfiguration -> 17 | assert (expected.contains(actualConfiguration)) 18 | } 19 | } 20 | 21 | // Assert default build config is discarded 22 | parser = new BotParser(this, "") 23 | assert(parser.isParsableComment("@phsft-bot build just on mac1011/gcc49")) 24 | parser.parse() 25 | assert(parser.overrideDefaultConfiguration) 26 | assertConfiguration(parser.validBuildConfigurations, [[compiler: 'gcc49', platform: 'mac1011']]) 27 | 28 | 29 | // Assert default build config is discarded 30 | parser = new BotParser(this, "") 31 | assert(parser.isParsableComment("@phsft-bot build on mac1011/gcc49")) 32 | parser.parse() 33 | assert(parser.overrideDefaultConfiguration) 34 | assertConfiguration(parser.validBuildConfigurations, [[compiler: 'gcc49', platform: 'mac1011']]) 35 | 36 | // Bot should run default build with no recognizable command 37 | parser = new BotParser(this, "") 38 | assert(!parser.isParsableComment("@phsft-bot build!")) 39 | parser.parse() 40 | assert(!parser.overrideDefaultConfiguration) 41 | 42 | 43 | // Default build config is not discarded 44 | parser = new BotParser(this, "") 45 | assert(parser.isParsableComment("@phsft-bot build also on mac1011/gcc49")) 46 | parser.parse() 47 | assert(!parser.overrideDefaultConfiguration) 48 | assertConfiguration(parser.validBuildConfigurations, [[compiler: 'gcc49', platform: 'mac1011']]) 49 | 50 | 51 | // Just cmake options are read 52 | parser = new BotParser(this, "") 53 | assert(parser.isParsableComment("@phsft-bot build with flags -Dfoo=bar")) 54 | parser.parse() 55 | assert(parser.extraCMakeOptions == "-Dfoo=bar") 56 | 57 | 58 | // Cmake flags are overwritten 59 | parser = new BotParser(this, "-Dfoo=don") 60 | assert(parser.isParsableComment("@phsft-bot build with flags -Dfoo=bar")) 61 | parser.parse() 62 | assert(parser.extraCMakeOptions == "-Dfoo=bar") 63 | 64 | 65 | // Multiple platforms are added 66 | parser = new BotParser(this, "") 67 | assert(parser.isParsableComment("@phsft-bot build just on mac1011/gcc49 ubuntu14/native")) 68 | parser.parse() 69 | assert(parser.overrideDefaultConfiguration) 70 | assertConfiguration(parser.validBuildConfigurations, [[compiler: "gcc49", platform: "mac1011"], 71 | [compiler: "native", platform: "ubuntu14"]]) 72 | 73 | 74 | // Multiple platforms are added separated by comma 75 | parser = new BotParser(this, "") 76 | assert(parser.isParsableComment("@phsft-bot build just on mac1011/gcc49, ubuntu14/native")) 77 | parser.parse() 78 | assert(parser.overrideDefaultConfiguration) 79 | assertConfiguration(parser.validBuildConfigurations, [[compiler: "gcc49", platform: "mac1011"], 80 | [compiler: "native", platform: "ubuntu14"]]) 81 | 82 | 83 | // Ignore unsupported platforms 84 | parser = new BotParser(this, "") 85 | assert(parser.isParsableComment("@phsft-bot build just on mac1011/blaah, blaah/native")) 86 | parser.parse() 87 | assert(parser.overrideDefaultConfiguration) 88 | assertConfiguration(parser.invalidBuildConfigurations, [[compiler: "blaah", platform: "mac1011"], 89 | [compiler: "native", platform: "blaah"]]) 90 | assertConfiguration(parser.validBuildConfigurations, []) 91 | 92 | 93 | // Newlines are not part of the command with flags 94 | parser = new BotParser(this, "") 95 | assert(parser.isParsableComment("@phsft-bot build with flags -Dfoo=bar\nhello this is do")) 96 | parser.parse() 97 | assert(!parser.overrideDefaultConfiguration) 98 | assert(parser.extraCMakeOptions == "-Dfoo=bar") 99 | 100 | 101 | // Period are not part of the command with platforms 102 | parser = new BotParser(this, "") 103 | assert(parser.isParsableComment("@phsft-bot build just on mac1011/gcc49.")) 104 | parser.parse() 105 | assert(parser.overrideDefaultConfiguration) 106 | assertConfiguration(parser.validBuildConfigurations, [[compiler: "gcc49", platform: "mac1011"]]) 107 | 108 | 109 | // Underscores are recognized 110 | parser = new BotParser(this, "") 111 | assert(parser.isParsableComment("@phsft-bot build just on slc6/clang_gcc52")) 112 | parser.parse() 113 | assert(parser.overrideDefaultConfiguration) 114 | assertConfiguration(parser.validBuildConfigurations, [[compiler: "clang_gcc52", platform: "slc6"]]) 115 | 116 | 117 | // Right comment is posted 118 | gitHub = new GitHub() 119 | parser = new BotParser(this, "") 120 | assert(parser.isParsableComment("@phsft-bot build just on slc6/clang_gcc52")) 121 | parser.postStatusComment(gitHub) 122 | assert(gitHub.postedComment.size() > 0) 123 | 124 | 125 | // Assert cmake flags are posted in comments 126 | gitHub = new GitHub() 127 | parser = new BotParser(this, "") 128 | assert(parser.isParsableComment("@phsft-bot build with flags -Dfoo=bar")) 129 | parser.parse() 130 | parser.postStatusComment(gitHub) 131 | println parser.extraCMakeOptions 132 | assert(gitHub.postedComment.size() > 0) 133 | assert(gitHub.postedComment.contains("-Dfoo=bar")) 134 | 135 | 136 | // ensure " is not double escaped 137 | parser = new BotParser(this, "") 138 | assert(parser.isParsableComment("@phsft-bot build with flags -DCMAKE_CXX_FLAGS=\\\"-DR__COMPLETE_MEM_TERMINATION\\\" -Dccache=OFF")) 139 | parser.parse() 140 | assert(parser.extraCMakeOptions == "-DCMAKE_CXX_FLAGS=\"-DR__COMPLETE_MEM_TERMINATION\" -Dccache=OFF") 141 | 142 | println 'All tests passing' 143 | -------------------------------------------------------------------------------- /test/cern/root/pipeline/BotParser.groovy: -------------------------------------------------------------------------------- 1 | ../../../../src/cern/root/pipeline/BotParser.groovy -------------------------------------------------------------------------------- /test/cern/root/pipeline/BuildConfiguration.groovy: -------------------------------------------------------------------------------- 1 | ../../../../src/cern/root/pipeline/BuildConfiguration.groovy --------------------------------------------------------------------------------