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
+}