Generate a build log to push to GCS with the release

To do so, save the log from the Jenkins master, stop saving
the README.md file. Moved the pushing release to a specific steps
so it can actually access those special JenkinsUtils functions.

Change-Id: I6ef891d302270c59efbf038dfac0ab364f134833
diff --git a/jenkins/jobs/configs/bootstrap.json b/jenkins/jobs/configs/bootstrap.json
index 0f3d81b..4528d8b 100644
--- a/jenkins/jobs/configs/bootstrap.json
+++ b/jenkins/jobs/configs/bootstrap.json
@@ -17,7 +17,6 @@
                 "bazel-genfiles/bazel-distfile.zip": "bazel-%{release_name}-dist.zip"
             },
             "stash": {
-                "bazel-genfiles/scripts/packages/README.md": "README.md",
                 "bazel-genfiles/scripts/packages/debian/bazel.dsc": "bazel.dsc",
                 "bazel-genfiles/scripts/packages/debian/bazel.tar.gz": "bazel.tar.gz",
                 "bazel-genfiles/site/jekyll-tree.tar": "docs.bazel.build.tar",
diff --git a/jenkins/jobs/global.groovy b/jenkins/jobs/global.groovy
index 2514531..ade08a5 100644
--- a/jenkins/jobs/global.groovy
+++ b/jenkins/jobs/global.groovy
@@ -25,7 +25,7 @@
   // First we bootstrap bazel on all platform
   stage("Bootstrap on all platforms") {
     bootstrapBazelAll(repository: params.REPOSITORY,
-		      branch: params.BRANCH,
+                      branch: params.BRANCH,
                       refspec: params.REFSPEC,
                       email: mail_recipient,
                       configuration: json_config,
@@ -47,14 +47,18 @@
 
 
 // Deployment steps
-if(params.BRANCH.matches('^(.*/)master$')) {
-  stage("Push website") {
+def is_master = params.BRANCH.matches('^(.*/)?master$')
+def is_rc = params.BRANCH.matches('^(refs/heads/)?release-.*$')
+def is_release = params.BRANCH.matches('^refs/tags/.*$')
+if(is_master || is_rc | is_release) {
+  stage(is_master ? "Push website" : "Push release") {
     machine("deploy") {
       recursiveGit(repository: params.REPOSITORY,
                    refspec: params.REFSPEC,
                    branch: params.BRANCH)
-      unstash "bazel--node=linux-x86_64--variation="
-      sh script: '''#!/bin/bash
+      if (is_master) {
+        unstash "bazel--node=linux-x86_64--variation="
+        sh script: '''#!/bin/bash
 . scripts/ci/build.sh
 for i in $(find input -name \'*.bazel.build.tar\'); do
   build_and_publish_site "$i" "$(basename $i .tar)" "build"
@@ -63,62 +67,22 @@
   build_and_publish_site "$i" "$(basename $i .tar.nobuild)" "nobuild"
 done
 '''
-    }
-  }
-} else if(params.BRANCH.matches('^refs/((heads/release-)|(tags/)).*$')) {
-  def r_name = ""
-  machine("deploy") {
-    r_name = sh(script: "bash -c 'source scripts/release/common.sh; get_full_release_name'",
-                returnStdout: true)
-    if (!r_name.isEmpty()) {
-      stage("Push release") {
-        // unstash all the things
-        def conf = BazelConfiguration.flattenConfigurations(
-          BazelConfiguration.parse(json_config), restrict_configuration).keySet().toArray()
-        for (int k = 0; k < entrySet.size; k++) {
-          unstash "bazel--node=${conf[k].node}--variation=${conf[k].variation}"
-        }
-        // Delete files we do not need
-        sh "rm -f node=*/variation=*/bazel node=*/variation=*/*.bazel.build.tar*"
-        // Now the actual release
-        withEnv(["GCS_BUCKET=bazel",
-                 "GIT_REPOSITORY_URL=https://github.com/bazelbuild/bazel"]) {
-          dir("output/ci") { -> }
-          sh '''#!/bin/bash
-# Credentials should not be displayed on the command line
-export GITHUB_TOKEN="$(cat "$GITHUB_TOKEN_FILE")"
-export APT_GPG_KEY_ID="$(cat "${APT_GPG_KEY_ID_FILE}")"
-source scripts/ci/build.sh
-
-args=()
-for i in node=*; do
-  for j in $i/variation=*/*; do
-    args+=("$(echo $i | cut -d = -f 2)" "$j")
-done
-
-bazel_release "${args[@]}"
-echo "${RELEASE_EMAIL_RECIPIENT}" | tee output/ci/recipient
-echo "${RELEASE_EMAIL_SUBJECT}" | tee output/ci/subject
-echo "${RELEASE_EMAIL_CONTENT}" | tee output/ci/content
-'''
-          if (r_name.contains("test")) {
-            echo "Test release, skipping announcement mail"
-          } else {
-            stage("Announcement mail") {
-              mail(subject: readFile("output/ci/subject"),
-                   to: readFile("output/ci/recipient"),
-                   replyTo: "bazel-ci@googlegroups.com",
-                   body: readFile("output/ci/content"))
-            }
-          }
+      } else {
+        def r_name = sh(script: "bash -c 'source scripts/release/common.sh; get_full_release_name'",
+                        returnStdout: true)
+        if (!r_name.isEmpty()) {
+          pushRelease(name: r_name,
+                      configuration: json_config,
+                      restrict_configuration: restrict_configuration,
+                      excludes: "node=*/variation=*/bazel node=*/variation=*/*.bazel.build.tar*")
+          // TODO(dmarting): trigger bazel install everywhere in case of release.
         }
       }
     }
   }
-  // TODO(dmarting): trigger bazel install everywhere in case of release.
 }
 
-// Then we run all jobs in the Global folder except the global pipeline job (the current job). 
+// Then we run all jobs in the Global folder except the global pipeline job (the current job).
 report = null
 stage("Test downstream jobs") {
   report = runAll(folder: "Global",
diff --git a/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy b/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy
index 2a2f820..6ef6dc1 100644
--- a/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy
+++ b/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy
@@ -187,16 +187,25 @@
     return false
   }
 
+  @NonCPS
+  private static def createFilePath(env, path) {
+    if (env['NODE_NAME'].equals("master")) {
+        return new FilePath(path);
+    } else {
+        return new FilePath(Jenkins.getInstance().getComputer(env['NODE_NAME']).getChannel(), path);
+    }
+  }
+
   /** Prune file that are older than timestamp on the current node. */
   @NonCPS
-  public static def pruneIfOlderThan(path, timestamp) {
-    return _pruneIfOlderThan(new FilePath(Channel.current(), path), timestamp)
+  public static def pruneIfOlderThan(env, path, timestamp) {
+    return _pruneIfOlderThan(createFilePath(env, path), timestamp)
   }
 
   /** Touch a file anywhere on the FS on the current node. */
   @NonCPS
-  public static def touchFileIfExists(path) {
-    FilePath f = new FilePath(Channel.current(), path)
+  public static def touchFileIfExists(env, path) {
+    FilePath f = createFilePath(env, path)
     def r = f.exists()
     if (r) {
       try {
@@ -210,7 +219,13 @@
 
   /** Read a file from the node, but without reporting anything in the Jenkins UI. */
   @NonCPS
-  public static def readFile(path) {
-    return new FilePath(Channel.current(), path).readToString()
+  public static def readFile(env, path) {
+    return createFilePath(env, path).readToString()
+  }
+
+  /** Save the current log to a file on the current node. */
+  @NonCPS
+  public static void saveLog(env, RunWrapper run, path) {
+    createFilePath(env, path).copyFrom(run.getRawBuild().getLogInputStream())
   }
 }
diff --git a/jenkins/lib/vars/bazelPath.groovy b/jenkins/lib/vars/bazelPath.groovy
index 4a62e8f..647d168 100644
--- a/jenkins/lib/vars/bazelPath.groovy
+++ b/jenkins/lib/vars/bazelPath.groovy
@@ -20,7 +20,8 @@
     // but using FilePath.act needs a class that can be shiped to the client, so
     // needs to be in the client classpath. If the number of RPC became a problem,
     // maybe we can use a Jenkins plugins.
-    JenkinsUtils.pruneIfOlderThan(getBazelInstallBase(node_label) +  "custom",
+    JenkinsUtils.pruneIfOlderThan(env,
+                                  getBazelInstallBase(node_label) + "custom",
                                   System.currentTimeMillis() - 172800000 /* 2 days */)
   } catch(IOException ex) {
     // Several error can occurs, we ignore them all as this step
@@ -68,7 +69,7 @@
       cause.upstreamProject.toString().replaceAll("/", "_"),
       cause.upstreamBuild.toString(),
       "variation_${variation}")
-    if (!JenkinsUtils.touchFileIfExists(bazel)) {
+    if (!JenkinsUtils.touchFileIfExists(env, bazel)) {
       dir(".bazel") { deleteDir() }
       step([$class: 'CopyArtifact',
             filter: cause.artifactPath,
diff --git a/jenkins/lib/vars/bootstrapBazel.groovy b/jenkins/lib/vars/bootstrapBazel.groovy
index fe5950a..084436c 100644
--- a/jenkins/lib/vars/bootstrapBazel.groovy
+++ b/jenkins/lib/vars/bootstrapBazel.groovy
@@ -62,7 +62,6 @@
 
     stage("[${config.node},${variation}] bootstrap") {
       def envs = ["BUILD_BY=Jenkins",
-                  "BUILD_LOG=${currentBuild.absoluteUrl}",
                   "GIT_REPOSITORY_URL=${env.GIT_URL}"]
       utils.build(["//src:bazel"] + targets)
     }
diff --git a/jenkins/lib/vars/pushRelease.groovy b/jenkins/lib/vars/pushRelease.groovy
new file mode 100644
index 0000000..d2e7cf1
--- /dev/null
+++ b/jenkins/lib/vars/pushRelease.groovy
@@ -0,0 +1,75 @@
+// Copyright (C) 2017 The Bazel Authors
+//
+// 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 build.bazel.ci.JenkinsUtils
+import build.bazel.ci.BazelConfiguration
+
+// A step that push a release for Bazel.
+def call(params = [:]) {
+  def r_name = params.name
+  def stashes = params.get("stashes",
+                           { conf -> "bazel--node=${conf.node}--variation=${conf.variation}" })
+  def bucket = params.get("bucket", "bazel")
+  def release_script = params.get("script", "source scripts/ci/build.sh; bazel_release")
+  def repository = params.get("repository", "https://github.com/bazelbuild/bazel")
+  def replyTo = params.get("replyTo", "bazel-ci@googlegroups.com")
+  // unstash all the things
+  def conf = BazelConfiguration.flattenConfigurations(
+    BazelConfiguration.parse(params.configuration),
+    params.restrict_configuration).keySet().toArray()
+  for (int k = 0; k < conf.length; k++) {
+    def stashName = stashes(conf[k])
+    if (stashName) {
+      unstash stashName
+    }
+  }
+  // Delete files we do not need
+  if ("excludes" in params) {
+    sh "rm -f ${params.excludes}"
+  }
+  // Now the actual release
+  withEnv(["GCS_BUCKET=${bucket}",
+           "GIT_REPOSITORY_URL=${repository}"]) {
+    JenkinsUtils.saveLog(env, currentBuild, "${pwd()}/build.log")
+    sh '''#!/bin/bash
+# Credentials should not be displayed on the command line
+export GITHUB_TOKEN="$(cat "$GITHUB_TOKEN_FILE")"
+export APT_GPG_KEY_ID="$(cat "${APT_GPG_KEY_ID_FILE}")"
+
+args=()
+# TODO(dmarting): Add build.log to the list of artifacts to deploy
+for i in node=*; do
+  for j in $i/variation=*; do
+    args+=("$(echo $i | cut -d = -f 2)" "$j")
+  done
+done
+
+set -x
+''' + release_script + ''' "${args[@]}"
+echo "${RELEASE_EMAIL_RECIPIENT}" | tee output/ci/recipient
+echo "${RELEASE_EMAIL_SUBJECT}" | tee output/ci/subject
+echo "${RELEASE_EMAIL_CONTENT}" | tee output/ci/content
+'''
+    if (r_name.contains("test")) {
+      echo "Test release, skipping announcement mail"
+    } else {
+      stage("Announcement mail") {
+        mail(subject: JenkinsUtils.readFile(env, "output/ci/subject"),
+             to: JenkinsUtils.readFile(env, "output/ci/recipient"),
+             replyTo: replyTo,
+             body: JenkinsUtils.readFile(env, "output/ci/content"))
+      }
+    }
+  }
+}