Files
rippled/Jenkinsfile
Mike Ellery b9fc9f6334 Add jenkins multiconfig pipeline support:
Fixes: RIPD-1514

Create a Jenkinsfile to build on rippled slaves. Modify
build_and_test.sh to support a few additional ENV settings.
2017-12-01 13:49:58 -05:00

297 lines
11 KiB
Groovy

#!/usr/bin/env groovy
import groovy.json.JsonOutput
import java.text.*
def all_status = [:]
def commit_id = ''
// this is not the actual token, but an ID/key into the jenkins
// credential store which httpRequest can access.
def github_cred = '6bd3f3b9-9a35-493e-8aef-975403c82d3e'
try {
stage ('Startup Checks') {
// here we check the commit author against collaborators
// we need a node to do this because readJSON requires
// a filesystem (although we just pass it text)
node {
echo "CHANGE AUTHOR ---> $CHANGE_AUTHOR"
echo "CHANGE TARGET ---> $CHANGE_TARGET"
echo "CHANGE ID ---> $CHANGE_ID"
checkout scm
commit_id = sh(script: 'git rev-parse HEAD', returnStdout: true)
commit_id = commit_id.trim()
echo "commit ID is ${commit_id}"
def response = httpRequest(
timeout: 10,
authentication: github_cred,
url: 'https://api.github.com/repos/ripple/rippled/collaborators')
def collab_data = readJSON(text: response.content)
collab_found = false;
for (collaborator in collab_data) {
if (collaborator['login'] == "$CHANGE_AUTHOR") {
echo "$CHANGE_AUTHOR is a collaborator!"
collab_found = true;
break;
}
}
if (! collab_found) {
manager.addShortText(
"Author of this change is not a collaborator!",
"Crimson",
"white",
"0px",
"white")
all_status['startup'] =
[false, 'Author Check', "$CHANGE_AUTHOR is not a collaborator!"]
error "$CHANGE_AUTHOR does not appear to be a collaborator...bailing on this build"
}
}
}
stage ('Parallel Build') {
def variants = [
'coverage',
'clang.debug.unity',
'clang.debug.nounity',
'gcc.debug.unity',
'gcc.debug.nounity',
'clang.release.unity',
'gcc.release.unity'] as String[]
// create a map of all builds
// that we want to run. The map
// is string keys and node{} object values
def builds = [:]
for (int index = 0; index < variants.size(); index++) {
def bldtype = variants[index]
builds[bldtype] = {
node('rippled-dev') {
checkout scm
dir ('build') {
deleteDir()
}
def cdir = upDir(pwd())
echo "BASEDIR: ${cdir}"
def compiler = getCompiler(bldtype)
def target = getTarget(bldtype)
if (compiler == "coverage") {
compiler = 'gcc'
}
echo "COMPILER: ${compiler}"
echo "TARGET: ${target}"
def clang_cc =
(compiler == "clang") ? "${LLVM_ROOT}/bin/clang" : ''
def clang_cxx =
(compiler == "clang") ? "${LLVM_ROOT}/bin/clang++" : ''
def ucc = isNoUnity(target) ? 'true' : 'false'
echo "USE_CC: ${ucc}"
withEnv(["CCACHE_BASEDIR=${cdir}",
"CCACHE_NOHASHDIR=true",
'LCOV_ROOT=""',
"TARGET=${target}",
"CC=${compiler}",
'BUILD=cmake',
'VERBOSE_BUILD=true',
"CLANG_CC=${clang_cc}",
"CLANG_CXX=${clang_cxx}",
"USE_CCACHE=${ucc}"])
{
myStage(bldtype)
try {
sh "ccache -s > ${bldtype}.txt"
// the devtoolset from SCL gives us a recent gcc. It's
// not strictly needed when we are building with clang,
// but it doesn't seem to interfere either
sh "source /opt/rh/devtoolset-6/enable && " +
"(/usr/bin/time -p ./bin/ci/ubuntu/build-and-test.sh 2>&1) 2>&1 " +
">> ${bldtype}.txt"
sh "ccache -s >> ${bldtype}.txt"
}
finally {
def outstr = readFile("${bldtype}.txt")
def st = getResults(outstr)
def time = getTime(outstr)
def fail_count = getFailures(outstr)
outstr = null
def txtcolor =
fail_count == 0 ? "DarkGreen" : "Crimson"
def shortbld = bldtype
shortbld = shortbld.replace('debug', 'dbg')
shortbld = shortbld.replace('release', 'rel')
shortbld = shortbld.replace('unity', 'un')
manager.addShortText(
"${shortbld}: ${st}, t: ${time}",
txtcolor,
"white",
"0px",
"white")
archive("${bldtype}.txt")
lock('rippled_dev_status') {
all_status[bldtype] =
[fail_count == 0, bldtype, "${st}, t: ${time}"]
}
}
}
}
}
}
parallel builds
}
}
finally {
// anything here should run always...
stage ('Final Status') {
node {
def start_time = new Date()
def sdf = new SimpleDateFormat("yyyyMMdd - HH:mm:ss")
def datestamp = sdf.format(start_time)
def results = """
## Jenkins Build Summary
Built from [this commit](https://github.com/ripple/rippled/commit/${commit_id})
Built at __${datestamp}__
### Test Results
Build Type | Result | Status
---------- | ------ | ------
"""
for ( e in all_status) {
results += e.value[1] + " | " + e.value[2] + " | " +
(e.value[0] ? "PASS :white_check_mark: " : "FAIL :red_circle: ") + "\n"
}
results += "\n"
echo "FINAL BUILD RESULTS"
echo results
try {
def url_comment = ""
if ("$CHANGE_ID" ==~ /\d+/) {
// CHANGE_ID indicates we are building a PR
// find PR comments
def resp = httpRequest(
timeout: 10,
authentication: github_cred,
url: "https://api.github.com/repos/ripple/rippled/pulls/$CHANGE_ID")
def result = readJSON(text: resp.content)
// follow issue comments link
url_comment = result['_links']['issue']['href'] + '/comments'
}
else {
// if not a PR, just search comments for our commit ID
url_comment =
'https://api.github.com/repos/ripple/rippled/commits/' +
"${commit_id}/comments"
}
def response = httpRequest(
timeout: 10,
authentication: github_cred,
url: url_comment)
def data = readJSON(text: response.content)
def comment_id = 0
def mode = 'POST'
// see if we can find and existing comment here with
// a heading that matches ours...
for (comment in data) {
if (comment['body'] =~ /(?m)^##\s+Jenkins Build/) {
comment_id = comment['id']
echo "existing status comment ${comment_id} found"
url_comment = comment['url']
mode = 'PATCH'
break;
}
}
if (comment_id == 0) {
echo "no existing status comment found"
}
def body = JsonOutput.toJson([
body: results
])
response = httpRequest(
timeout: 10,
authentication: github_cred,
url: url_comment,
contentType: 'APPLICATION_JSON',
httpMode: mode,
requestBody: body)
}
catch (any) {
echo "had a problem interacting with github...status is probably not updated"
}
}
}
}
// ---------------
// util functions
// ---------------
def myStage(name) {
echo """
+++++++++++++++++++++++++++++++++++++++++
>> building ${name}
+++++++++++++++++++++++++++++++++++++++++
"""
}
@NonCPS
def getResults(text) {
// example:
/// 194.5s, 154 suites, 948 cases, 360485 tests total, 0 failures
def matcher = text =~ /(\d+) cases, (\d+) tests total, (\d+) (failure(s?))/
matcher ? matcher[0][1] + " cases, " + matcher[0][3] + " failed" : "no test results"
}
def getFailures(text) {
// example:
/// 194.5s, 154 suites, 948 cases, 360485 tests total, 0 failures
def matcher = text =~ /(\d+) tests total, (\d+) (failure(s?))/
// if we didn't match, then return 1 since something is
// probably wrong, e.g. maybe the build failed...
matcher ? matcher[0][2] as Integer : 1i
}
@NonCPS
def getCompiler(bld) {
def matcher = bld =~ /^(.+?)\.(.+)$/
matcher ? matcher[0][1] : bld
}
@NonCPS
def isNoUnity(bld) {
def matcher = bld =~ /\.nounity\s*$/
matcher ? true : false
}
@NonCPS
def getTarget(bld) {
def matcher = bld =~ /^(.+?)\.(.+)$/
matcher ? matcher[0][2] : bld
}
// because I can't seem to find path manipulation
// functions in groovy....
@NonCPS
def upDir(path) {
def matcher = path =~ /^(.+)\/(.+?)/
matcher ? matcher[0][1] : path
}
@NonCPS
def getTime(text) {
// look for text following a label 'real' for
// wallclock time. Some `time`s report fractional
// seconds and we can omit those in what we report
def matcher = text =~ /(?m)^real\s+(.+)\.(\d+?)[s]?/
matcher ? matcher[0][1] + "s" : "n/a"
}