Add Jenkinsfile pipeline for the Gerrit project

Avoid spreading the noise of the Jenkinsfile build pipeline
logic across the Gerrit branches by having a common library
for all the validations.

Change-Id: I52d520ee75b984b72777e4e4f1a1ad9b02fc530f
diff --git a/vars/gerritPipeline.groovy b/vars/gerritPipeline.groovy
new file mode 100644
index 0000000..c3c166e
--- /dev/null
+++ b/vars/gerritPipeline.groovy
@@ -0,0 +1,272 @@
+#!/usr/bin/env groovy
+
+// Copyright (C) 2019 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.
+
+// Scripted pipeline for building and testing Gerrit.
+//
+// Jenkinsfile example:
+// gerritPipeline()
+
+import groovy.json.JsonSlurper
+import groovy.json.JsonOutput
+
+def call(Map parm = [:]) {
+    node ('master') {
+
+        if (hasChangeNumber()) {
+            stage('Preparing'){
+                gerritReview labels: ['Verified': 0, 'Code-Style': 0]
+                collectBuildModes()
+            }
+        }
+
+        parallel(collectBuilds())
+
+        if (hasChangeNumber()) {
+            stage('Retry Flaky Builds'){
+                def flakyBuildsModes = findFlakyBuilds()
+                if (flakyBuildsModes.size() > 0){
+                    parallel flakyBuildsModes.collectEntries {
+                        ["Gerrit-verification(${it})" :
+                            prepareBuildsForMode("Gerrit-verifier-bazel", it, 3)]
+                    }
+                }
+            }
+
+            stage('Report to Gerrit'){
+                resCodeStyle = getLabelValue(1, Builds.codeStyle.result)
+                gerritReview labels: ['Code-Style': resCodeStyle]
+
+                def verificationResults = Builds.verification.collect { k, v -> v }
+                def resVerify = verificationResults.inject(1) {
+                    acc, build -> getLabelValue(acc, build.result)
+                }
+                gerritReview labels: ['Verified': resVerify]
+
+                setResult(resVerify, resCodeStyle)
+            }
+        }
+    }
+}
+
+class Globals {
+    static final String gerritUrl = "https://gerrit-review.googlesource.com/"
+    static final String gerritCredentialsId = "gerrit-review.googlesource.com"
+    static final long curlTimeout = 10000
+    static final int waitForResultTimeout = 10000
+    static final String gerritRepositoryNameSha1Suffix = "-a6a0e4682515f3521897c5f950d1394f4619d928"
+}
+
+class Build {
+    String url
+    String result
+
+    Build(url, result) {
+        this.url = url
+        this.result = result
+    }
+}
+
+class Builds {
+    static Set<String> modes = []
+    static Build codeStyle = null
+    static Map verification = [:]
+}
+
+class GerritCheck {
+    String uuid
+    Build build
+    String consoleUrl
+
+    GerritCheck(name, build) {
+        this.uuid = "gerritforge:" + name.replaceAll("(bazel/)", "") +
+            Globals.gerritRepositoryNameSha1Suffix
+        this.build = build
+        this.consoleUrl = "${build.url}console"
+    }
+
+    def getCheckResultFromBuild() {
+        if (!build.getResult()) {
+            return "RUNNING"
+        }
+        def resultString = build.getResult().toString()
+        if (resultString == 'SUCCESS') {
+            return "SUCCESSFUL"
+        } else if (resultString == 'NOT_BUILT' || resultString == 'ABORTED') {
+            return "NOT_STARTED"
+        }
+
+        // Remaining options: 'FAILURE' or 'UNSTABLE':
+        return "FAILED"
+    }
+}
+
+def hasChangeNumber() {
+    env.GERRIT_CHANGE_NUMBER?.trim()
+}
+
+def postCheck(check) {
+    gerritCheck(checks: [ "${check.uuid}" : "${check.getCheckResultFromBuild()}" ], url: "${check.consoleUrl}")
+}
+
+def queryChangedFiles(url) {
+    def queryUrl = "${url}changes/${env.GERRIT_CHANGE_NUMBER}/revisions/${env.GERRIT_PATCHSET_REVISION}/files/"
+    def response = httpRequest queryUrl
+    def files = response.getContent().substring(5)
+    def filesJson = new JsonSlurper().parseText(files)
+    return filesJson.keySet().findAll { it != "/COMMIT_MSG" }
+}
+
+def collectBuildModes() {
+    Builds.modes = ["notedb"]
+    if (env.GERRIT_BRANCH == "stable-2.16") {
+        Builds.modes += "reviewdb"
+    }
+
+    def changedFiles = queryChangedFiles(Globals.gerritUrl)
+    def isMerge = changedFiles.contains("/MERGE_LIST")
+    def polygerritFiles = changedFiles.findAll { it.startsWith("polygerrit-ui") ||
+        it.startsWith("lib/js") }
+    def bazelFiles = changedFiles.findAll { it == "WORKSPACE" || it.endsWith("BUILD") ||
+        it.endsWith(".bzl") }
+    if(isMerge) {
+        println "Merge commit detected, adding 'polygerrit' validation..."
+        Builds.modes += "polygerrit"
+    } else if(polygerritFiles.size() > 0) {
+        if(changedFiles.size() == polygerritFiles.size() && bazelFiles.isEmpty()) {
+            println "Only PolyGerrit UI changes detected, skipping other test modes..."
+            Builds.modes = ["polygerrit"]
+        } else {
+            println "PolyGerrit UI changes detected, adding 'polygerrit' validation..."
+            Builds.modes += "polygerrit"
+        }
+    } else if(!bazelFiles.isEmpty()) {
+        println "WORKSPACE file changes detected, adding 'polygerrit' validation..."
+        Builds.modes += "polygerrit"
+    }
+}
+
+def prepareBuildsForMode(buildName, mode="reviewdb", retryTimes = 1) {
+    return {
+        stage("${buildName}/${mode}") {
+            def slaveBuild = null
+            for (int i = 1; i <= retryTimes; i++) {
+                postCheck(new GerritCheck(
+                    (buildName == "Gerrit-codestyle") ? "codestyle" : mode,
+                    new Build(currentBuild.getAbsoluteUrl(), null)))
+                try {
+                    slaveBuild = build job: "${buildName}", parameters: [
+                        string(name: 'REFSPEC', value: "refs/changes/${env.BRANCH_NAME}"),
+                        string(name: 'BRANCH', value: env.GERRIT_PATCHSET_REVISION),
+                        string(name: 'CHANGE_URL', value: "${Globals.gerritUrl}c/${env.GERRIT_PROJECT}/+/${env.GERRIT_CHANGE_NUMBER}"),
+                        string(name: 'MODE', value: mode),
+                        string(name: 'TARGET_BRANCH', value: env.GERRIT_BRANCH)
+                    ], propagate: false
+                } finally {
+                    if (buildName == "Gerrit-codestyle"){
+                        Builds.codeStyle = new Build(
+                            slaveBuild.getAbsoluteUrl(), slaveBuild.getResult())
+                        postCheck(new GerritCheck("codestyle", Builds.codeStyle))
+                    } else {
+                        Builds.verification[mode] = new Build(
+                            slaveBuild.getAbsoluteUrl(), slaveBuild.getResult())
+                        postCheck(new GerritCheck(mode, Builds.verification[mode]))
+                    }
+                    if (slaveBuild.getResult() == "SUCCESS") {
+                        break
+                    }
+                }
+            }
+        }
+    }
+}
+
+def collectBuilds() {
+    def builds = [:]
+    if (hasChangeNumber()) {
+       builds["Gerrit-codestyle"] = prepareBuildsForMode("Gerrit-codestyle")
+       Builds.modes.each {
+          builds["Gerrit-verification(${it})"] = prepareBuildsForMode("Gerrit-verifier-bazel", it)
+       }
+    } else {
+       builds["java8"] = { -> build "Gerrit-bazel-${env.BRANCH_NAME}" }
+
+       if (env.BRANCH_NAME == "master") {
+          builds["java11"] = { -> build "Gerrit-bazel-java11-${env.BRANCH_NAME}" }
+       }
+    }
+    return builds
+}
+
+def findFlakyBuilds() {
+    def flaky = Builds.verification.findAll { it.value.result == null ||
+        it.value.result != 'SUCCESS' }
+
+    if(flaky.size() == Builds.verification.size()) {
+        return []
+    }
+
+    def retryBuilds = []
+    flaky.each {
+        def mode = it.key
+        Builds.verification = Builds.verification.findAll { it.key != mode }
+        retryBuilds += mode
+    }
+
+    return retryBuilds
+}
+
+def getLabelValue(acc, res) {
+    if(res == null || res == 'ABORTED') {
+        return 0
+    }
+    switch(acc) {
+        case 0: return 0
+        case 1:
+            if(res == null) {
+                return 0
+            }
+            switch(res) {
+                case 'SUCCESS': return +1
+                case 'FAILURE': return -1
+                default: return 0
+            }
+        case -1: return -1
+    }
+}
+
+def setResult(resultVerify, resultCodeStyle) {
+    if (resultVerify == 0 || resultCodeStyle == 0) {
+        currentBuild.result = 'ABORTED'
+    } else if (resultVerify == -1 || resultCodeStyle == -1) {
+        currentBuild.result = 'FAILURE'
+    } else {
+        currentBuild.result = 'SUCCESS'
+    }
+}
+
+def findCodestyleFilesInLog(build) {
+    def codeStyleFiles = []
+    def needsFormatting = false
+    def response = httpRequest "${build.url}consoleText"
+    response.content.eachLine {
+        needsFormatting = needsFormatting || (it ==~ /.*Need Formatting.*/)
+        if(needsFormatting && it ==~ /\[.*\]/) {
+            codeStyleFiles += it.substring(1,it.length()-1)
+        }
+    }
+
+    return codeStyleFiles
+}