├── .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 |
270 |
271 |
272 |
273 |
274 | <%
275 |
276 | if (build.changeSets != null) {
277 | boolean hadChanges = false
278 | %>
279 |
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 |
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 | ${p.editType.name} |
302 |
303 | ${p.path}
304 | |
305 |
306 | <%
307 | }
308 | %>
309 |
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 | ${toolchain} - OK |
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 | ${toolchain} - ${__result}.
355 | ${buildResultMessages.toArray().join(", ")}
356 | |
357 | <%
358 | }
359 | }
360 | %>
361 |
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 | ${architecture} - OK |
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 | ${architecture} - ${__result}.
399 | ${buildResultMessages.toArray().join(", ")}
400 | |
401 | <%
402 | }
403 | }
404 | %>
405 |
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 |
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 | Class |
511 | Duration |
512 | Age |
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 | ${title} |
547 | ${String.format("%.1f", duration)} sec |
548 | ${age} |
549 |
550 | <%
551 | } else {
552 | ignoredMessages++
553 | }
554 |
555 | }
556 | %>
557 |
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
--------------------------------------------------------------------------------