blob: d3f44f9ec32d6eea4d206f7cbf29385c5b889cde [file] [log] [blame]
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import hudson.model.*
import hudson.AbortException
import hudson.console.HyperlinkNote
import java.util.concurrent.CancellationException
import groovy.json.*
import java.text.*
verbose = true
String.metaClass.encodeURL = {
java.net.URLEncoder.encode(delegate)
}
class Globals {
static String gerrit = "https://gerrit-review.googlesource.com/"
static String gerritReviewer = "GerritForge CI <gerritforge@gmail.com>"
static long curlTimeout = 10000
static SimpleDateFormat tsFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss.S Z")
static int maxChanges = 100
static int myAccountId = 1022687
static int waitForResultTimeout = 10000
static Map buildsList = [:]
static def ciTag(String operation) {
" \"tag\" : \"autogenerated:gerrit-ci:$operation\" "
}
static String addReviewerTag = ciTag("addReviewer")
static String addVerifiedTag = ciTag("addVerified")
static String addCommentTag = ciTag("comment")
static Set<String> codeStyleBranches = ["master", "stable-2.14"]
static resTicks = [ 'ABORTED':'\u26aa', 'SUCCESS':'\u2705', 'FAILURE':'\u274c' ]
}
def gerritPost(url, jsonPayload) {
def error = ""
def gerritPostUrl = Globals.gerrit + url
def curl = ['curl',
'-n', '-s', '-S',
'-X', 'POST', '-H', 'Content-Type: application/json',
'--data-binary', jsonPayload,
gerritPostUrl ]
if(verbose) { println "CURL/EXEC> $curl" }
def proc = curl.execute()
def sout = new StringBuffer(), serr = new StringBuffer()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(Globals.curlTimeout)
def curlExit = proc.exitValue()
if(curlExit != 0) {
error = "$curl **FAILED** with exit code = $curlExit"
println error
throw new IOException(error)
}
if(!sout.toString().trim().isEmpty() && verbose) {
println "CURL/OUTPUT> $sout"
}
if(!serr.toString().trim().isEmpty() && verbose) {
println "CURL/ERROR> $serr"
}
return 0
}
def gerritLabelVerify(change, sha1, verified, builds) {
if(verified == 0) {
return;
}
def changeNum = change._number
def msgList = builds.collect { type,build ->
[ 'type': type, 'res': build.getResult().toString(), 'url': build.getBuildUrl() + "console" ]
} sort { a,b -> a['res'].compareTo(b['res']) }
def msgBody = msgList.collect {
"${Globals.resTicks[it.res]} ${it.type} : ${it.res}\n (${it.url})"
} .join('\n')
def addVerifiedExit = gerritLabel(changeNum, sha1, 'Verified', verified, msgBody)
if(addVerifiedExit == 0) {
println "----------------------------------------------------------------------------"
println "Gerrit Review: Verified=" + verified + " to change " + changeNum + "/" + sha1
println "----------------------------------------------------------------------------"
}
return addVerifiedExit
}
def findCodestyleFilesInLog(build) {
def codestyleFiles = []
def needsFormatting = false
def codestyleLogReader = build.getLogReader()
codestyleLogReader.eachLine {
needsFormatting = needsFormatting || (it ==~ /.*Need Formatting.*/)
if(needsFormatting && it ==~ /\[.*\]/) {
codestyleFiles += it.substring(1,it.length()-1)
}
}
return codestyleFiles
}
def gerritLabelCodestyle(change, sha1, cs, files, build) {
if(cs == 0) {
return
}
def changeNum = change._number
def formattingMsg = cs < 0 ? ('The following files need formatting:\n ' + files.join('\n ')) : 'All files are correctly formatted'
def res = build.getResult().toString()
def url = build.getBuildUrl() + "console"
def msgBody = "${Globals.resTicks[res]} $formattingMsg\n (${url})"
def addCodeStyleExit = gerritLabel(changeNum, sha1, 'Code-Style', cs, msgBody)
if(addCodeStyleExit == 0) {
println "----------------------------------------------------------------------------"
println "Gerrit Review: Code-Style=" + cs + " to change " + changeNum + "/" + sha1
println "----------------------------------------------------------------------------"
}
return addCodeStyleExit
}
def gerritLabel(changeNum, sha1, label, score, msgBody = "") {
def notify = score < 0 ? ', "notify" : "OWNER"' : ''
def jsonPayload = '{"labels":{"' + label + '":' + score + '},' +
' "message": "' + msgBody + '"' +
notify + ", ${Globals.addVerifiedTag} }"
return gerritPost("a/changes/" + changeNum + "/revisions/" + sha1 + "/review",
jsonPayload)
}
def gerritComment(buildUrl,changeNum, sha1, msgPrefix) {
return gerritPost("a/changes/$changeNum/revisions/$sha1/review",
"{ \"message\": \"$msgPrefix Gerrit-CI Flow: $buildUrl\", \"notify\" : \"NONE\", ${Globals.addCommentTag} }")
}
def waitForResult(b) {
def res = null
def startWait = System.currentTimeMillis()
while(res == null && (System.currentTimeMillis() - startWait) < Globals.waitForResultTimeout) {
res = b.getResult()
if(res == null) {
Thread.sleep(100) {
}
}
}
return res == null ? Result.FAILURE : res
}
def getVerified(acc, res) {
if(res == null || res == Result.ABORTED) {
return 0
}
switch(acc) {
case 0: return 0
case 1:
if(res == null) {
return 0;
}
switch(res) {
case Result.SUCCESS: return +1;
case Result.FAILURE: return -1;
default: return 0;
}
case -1: return -1
}
}
def getChangedFiles(changeNum, sha1) {
URL filesUrl = new URL(String.format("%schanges/%s/revisions/%s/files/",
Globals.gerrit, changeNum, sha1))
def files = filesUrl.getText().substring(5)
def filesJson = new JsonSlurper().parseText(files)
filesJson.keySet().findAll { it != "/COMMIT_MSG" }
}
def buildsForMode(refspec,sha1,changeUrl,mode,tools,targetBranch,retryTimes,codestyle) {
def builds = []
if(codestyle) {
builds += {
Globals.buildsList.put("codestyle", build("Gerrit-codestyle",
REFSPEC: refspec, BRANCH: sha1, CHANGE_URL: changeUrl, MODE: mode,
TARGET_BRANCH: targetBranch))
println "Builds status:"
Globals.buildsList.each {
n, v -> println " $n : ${v.getResult()}\n (${v.getBuildUrl() + "console"})"
}
}
}
for (tool in tools) {
def buildName = "Gerrit-verifier-$tool"
def key = "$tool/$mode"
builds += {
retry (retryTimes) {
Globals.buildsList.put(key,
build(buildName, REFSPEC: refspec, BRANCH: sha1,
CHANGE_URL: changeUrl, MODE: mode, TARGET_BRANCH: targetBranch))
println "Builds status:"
Globals.buildsList.each {
n, v -> println " $n : ${v.getResult()}\n (${v.getBuildUrl() + "console"})"
}
}
}
}
return builds
}
def sh(cwd, command) {
def sout = new StringBuilder(), serr = new StringBuilder()
println "SH: $command"
def shell = command.execute([],cwd)
shell.consumeProcessOutput(sout, serr)
shell.waitForOrKill(30000)
println "OUT: $sout"
println "ERR: $serr"
}
def buildChange(change) {
def sha1 = change.current_revision
def changeNum = change._number
def revision = change.revisions.get(sha1)
def ref = revision.ref
def patchNum = revision._number
def branch = change.branch
def changeUrl = Globals.gerrit + "#/c/" + changeNum + "/" + patchNum
def refspec = "+" + ref + ":" + ref.replaceAll('ref/', 'ref/remotes/origin/')
def tools = []
def modes = ["reviewdb"]
def workspace = build.environment.get("WORKSPACE")
println "workspace: $workspace"
def cwd = new File("$workspace")
println "cwd: $cwd"
println "ref: $ref"
sh(cwd, "git fetch origin $ref")
sh(cwd, "git checkout FETCH_HEAD")
sh(cwd, "git fetch origin $branch")
sh(cwd, 'git config user.name "Jenkins Build"')
sh(cwd, 'git config user.email "jenkins@gerritforge.com"')
sh(cwd, 'git merge --no-commit --no-edit --no-ff FETCH_HEAD')
if(new java.io.File("$cwd/BUCK").exists()) {
tools += ["buck"]
} else if(new java.io.File("$cwd/BUILD").exists()) {
tools += ["bazel"]
}
println "Building Change " + changeUrl
build.setDescription("""<a href='$changeUrl' target='_blank'>Change #$changeNum</a>""")
if(branch == "master") {
modes += "disableChangeReviewDb"
modes += "fused"
}
if(branch == "master" || branch == "stable-2.14") {
def changedFiles = getChangedFiles(changeNum, sha1)
def polygerritFiles = changedFiles.findAll { it.startsWith("polygerrit-ui") }
if(polygerritFiles.size() > 0) {
if(changedFiles.size() == polygerritFiles.size()) {
println "Only PolyGerrit UI changes detected, skipping other test modes..."
modes = ["polygerrit"]
} else {
println "PolyGerrit UI changes detected, adding 'polygerrit' validation..."
modes += "polygerrit"
}
}
}
def builds = []
println "Running validation jobs using $tools builds for $modes ..."
modes.collect {
buildsForMode(refspec,sha1,changeUrl,it,tools,branch,1,Globals.codeStyleBranches.contains(branch))
}.each { builds += it }
def buildsWithResults = parallelBuilds(builds)
def codestyleResult = buildsWithResults.find{ it[0] == "codestyle" }
if(codestyleResult) {
def resCodeStyle = getVerified(1, codestyleResult[1])
def codestyleBuild = Globals.buildsList["codestyle"]
gerritLabelCodestyle(change, sha1, resCodeStyle, findCodestyleFilesInLog(codestyleBuild), codestyleBuild)
}
flaky = flakyBuilds(buildsWithResults.findAll { it[0] != "codestyle" })
if(flaky.size > 0) {
println "** FLAKY Builds detected: ${flaky}"
def retryBuilds = []
def toolsAndModes = flaky.collect { it.split("/") }
toolsAndModes.each {
def tool = it[0]
def mode = it[1]
Globals.buildsList.remove(it)
retryBuilds += buildsForMode(refspec,sha1,changeUrl,mode,[tool],branch,3,false)
}
buildsWithResults = parallelBuilds(retryBuilds)
}
def resVerify = buildsWithResults.findAll{ it != codestyleResult }.inject(1) { acc, buildResult -> getVerified(acc, buildResult[1]) }
def resAll = getVerified(resVerify, codestyleResult[1])
gerritLabelVerify(change, sha1, resVerify, Globals.buildsList.findAll { key,build -> key != "codestyle" })
switch(resAll) {
case 0: build.state.result = ABORTED
break
case 1: build.state.result = SUCCESS
break
case -1: build.state.result = FAILURE
break
}
}
def parallelBuilds(builds) {
ignore(FAILURE) {
parallel (builds)
}
def results = Globals.buildsList.values().collect { waitForResult(it) }
def buildsWithResults = []
Globals.buildsList.keySet().eachWithIndex {
key,index -> buildsWithResults.add(new Tuple(key, results[index]))
}
return buildsWithResults
}
def flakyBuilds(buildsWithResults) {
def flaky = buildsWithResults.findAll { it[1] == null || it[1] != SUCCESS }
if(flaky.size == buildsWithResults.size) {
return []
}
return flaky.collect { it[0] }
}
def lastBuild = build.getPreviousSuccessfulBuild()
def logOut = new ByteArrayOutputStream()
if(lastBuild != null) {
lastBuild.getLogText().writeLogTo(0,logOut)
}
def lastLog = new String(logOut.toByteArray())
def lastBuildStartTimeMillis = lastBuild == null ?
(System.currentTimeMillis() - 1800000) : lastBuild.getStartTimeInMillis()
def sinceMillis = lastBuildStartTimeMillis - 30000
def since = Globals.tsFormat.format(new Date(sinceMillis))
def requestedChangeId = params.get("CHANGE_ID")
def processAll = false
queryUrl =
new URL(Globals.gerrit + "changes/?pp=0&O=3&q=" + requestedChangeId)
def changes = queryUrl.getText().substring(5)
def jsonSlurper = new JsonSlurper()
def changesJson = jsonSlurper.parseText(changes)
def acceptedChanges = changesJson.findAll {
change ->
sha1 = change.current_revision
if(sha1 == null) {
println "[WARNING] Skipping change " + change.change_id + " because it does not have any current revision or patch-set"
return false
}
def verified = change.labels.Verified
def approved = verified.approved
def rejected = verified.rejected
gerritComment(build.startJob.getBuildUrl() + "console",change._number,change.current_revision,"Verification queued on")
return true
}
for (change in acceptedChanges) {
buildChange(change)
}