New pipeline: make release pipeline works

The old pipeline was relying on some very peculiar layout of the files
and that prevented to provide a build.log artifact. This change
transfer the reponsibility of the layout from script/ci/build.sh (function
bazel_release) to pushRelease.

Change-Id: I59a3c33966bee769313fb8babe1bad3799f5b830
diff --git a/jenkins/jobs/configs/bootstrap.json b/jenkins/jobs/configs/bootstrap.json
index 4528d8b..7636ca8 100644
--- a/jenkins/jobs/configs/bootstrap.json
+++ b/jenkins/jobs/configs/bootstrap.json
@@ -11,9 +11,9 @@
         "parameters": {
             "archive": {
                 "bazel-bin/src/bazel": "bazel",
-                "bazel-bin/scripts/packages/with-jdk/install.sh": "bazel-%{release_name}-installer.sh",
-                "bazel-bin/scripts/packages/without-jdk/install.sh": "bazel-%{release_name}-without-jdk-installer.sh",
-                "bazel-bin/scripts/packages/debian/bazel-debian.deb": "bazel_%{release_name}.deb",
+                "bazel-bin/scripts/packages/with-jdk/install.sh": "bazel-%{release_name}-installer-linux-x86_64.sh",
+                "bazel-bin/scripts/packages/without-jdk/install.sh": "bazel-%{release_name}-without-jdk-installer-linux-x86_64.sh",
+                "bazel-bin/scripts/packages/debian/bazel-debian.deb": "bazel_%{release_name}-linux-x86_64.deb",
                 "bazel-genfiles/bazel-distfile.zip": "bazel-%{release_name}-dist.zip"
             },
             "stash": {
@@ -46,8 +46,8 @@
         "parameters": {
             "archive": {
                 "bazel-bin/src/bazel": "bazel",
-                "bazel-bin/scripts/packages/with-jdk/install.sh": "bazel-%{release_name}-installer.sh",
-                "bazel-bin/scripts/packages/without-jdk/install.sh": "bazel-%{release_name}-without-jdk-installer.sh"
+                "bazel-bin/scripts/packages/with-jdk/install.sh": "bazel-%{release_name}-darwin-x86_64-installer.sh",
+                "bazel-bin/scripts/packages/without-jdk/install.sh": "bazel-%{release_name}-darwin-x86_64-without-jdk-installer.sh"
             },
             "targets": [
                 "//scripts/packages"
@@ -63,7 +63,7 @@
         "node": "windows-x86_64",
         "parameters": {
             "archive": {
-                "bazel-bin/src/bazel": ["bazel.exe", "bazel-%{release_name}.exe"]
+                "bazel-bin/src/bazel": ["bazel.exe", "bazel-%{release_name}-windows-x86_64.exe"]
             },
             "opts": [
                 "--cpu=x64_windows_msys",
@@ -77,7 +77,7 @@
         "node": "windows-msvc-x86_64",
         "parameters": {
             "archive": {
-                "bazel-bin/src/bazel": ["bazel.exe", "bazel-msvc-%{release_name}.exe"]
+                "bazel-bin/src/bazel": ["bazel.exe", "bazel-msvc-%{release_name}-windows-x86_64.exe"]
             },
             "opts": [
                 "--copt=-w",
diff --git a/jenkins/jobs/global.groovy b/jenkins/jobs/global.groovy
index ade08a5..7466e69 100644
--- a/jenkins/jobs/global.groovy
+++ b/jenkins/jobs/global.groovy
@@ -69,12 +69,12 @@
 '''
       } else {
         def r_name = sh(script: "bash -c 'source scripts/release/common.sh; get_full_release_name'",
-                        returnStdout: true)
+                        returnStdout: true).trim()
         if (!r_name.isEmpty()) {
           pushRelease(name: r_name,
                       configuration: json_config,
                       restrict_configuration: restrict_configuration,
-                      excludes: "node=*/variation=*/bazel node=*/variation=*/*.bazel.build.tar*")
+                      excludes: ["**/*.bazel.build.tar*", "**/bazel", "**/bazel.exe"])
           // TODO(dmarting): trigger bazel install everywhere in case of release.
         }
       }
diff --git a/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy b/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy
index 6ef6dc1..9451176 100644
--- a/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy
+++ b/jenkins/lib/src/build/bazel/ci/JenkinsUtils.groovy
@@ -228,4 +228,13 @@
   public static void saveLog(env, RunWrapper run, path) {
     createFilePath(env, path).copyFrom(run.getRawBuild().getLogInputStream())
   }
+
+  /** Returns the recursive list of files of a folder, ignoring some files. */
+  @NonCPS
+  public static def list(env, dir, excludes) {
+    def directory = createFilePath(env, dir)
+    def results = directory.list("**", excludes.join(","))
+    def directoryUri = directory.toURI()
+    return results.collect { it -> directoryUri.relativize(it.toURI()) }
+  }
 }
diff --git a/jenkins/lib/vars/bootstrapBazel.groovy b/jenkins/lib/vars/bootstrapBazel.groovy
index 084436c..e799cce 100644
--- a/jenkins/lib/vars/bootstrapBazel.groovy
+++ b/jenkins/lib/vars/bootstrapBazel.groovy
@@ -47,7 +47,6 @@
       release_name =
         sh(script: "bash -c 'source scripts/release/common.sh; get_full_release_name'",
            returnStdout: true).trim()
-
       def opts = config.get("opts", [])
       opts <<= "--stamp"
       if (!isWindows) {
diff --git a/jenkins/lib/vars/pushRelease.groovy b/jenkins/lib/vars/pushRelease.groovy
index d2e7cf1..8db9a34 100644
--- a/jenkins/lib/vars/pushRelease.groovy
+++ b/jenkins/lib/vars/pushRelease.groovy
@@ -15,60 +15,104 @@
 import build.bazel.ci.JenkinsUtils
 import build.bazel.ci.BazelConfiguration
 
+private def ensureGpgSecretKeyImported() {
+  sh '''#!/bin/bash
+echo "Import GPG Secret key"
+(gpg --list-secret-keys | grep "${APT_GPG_KEY_ID}" > /dev/null) || \\
+  gpg --allow-secret-key-import --import "${APT_GPG_KEY_PATH}"
+# Make sure we use stronger digest algorithm.
+# We use reprepro to generate the debian repository,
+# but there is no way to pass flags to gpg using reprepro, so writting it into
+# ~/.gnupg/gpg.conf
+(grep "digest-algo sha256" ~/.gnupg/gpg.conf > /dev/null) || \\
+  echo "digest-algo sha256" >> ~/.gnupg/gpg.conf
+'''
+}
+
+// Generate the SHA-256 checksum and the GPG signature for a list of artifacts.
+// Returns a new list of artifacts that include the generated checksum and signature files.
+private def signArtifacts(files) {
+  def result = []
+  def script = []
+  for (def file : files) {
+    script <<= "echo 'Signing ${file}'"
+    script <<= "(cd \"\$(dirname '${file}')\" && sha256sum \"\$(basename '${file}')\") > '${file}.sha256'"
+    script <<= "gpg --no-tty --detach-sign -u \"\${APT_GPG_KEY_ID}\" '${file}'"
+    result <<= file
+    result <<= "${file}.sha256"
+    result <<= "${file}.sig"
+  }
+  sh "#!/bin/bash \n${script.join '\n'}"
+  return result
+}
+
+@NonCPS
+private def listArtifacts(ws, dir, excludes) {
+  return JenkinsUtils.list(env, "${ws}/${dir}", excludes).collect { "${dir}/${it}" }
+}
+
+private def listStashes(configuration, restrict_configuration) {
+  def result = []
+  def conf = BazelConfiguration.flattenConfigurations(
+    BazelConfiguration.parse(configuration), restrict_configuration)
+  for (k in conf.keySet()) {
+    if ("stash" in conf[k] || "archive" in conf[k]) {
+      result.add("bazel--node=${k.node}--variation=${k.variation}")
+    }
+  }
+  return result
+}
+
 // A step that push a release for Bazel.
 def call(params = [:]) {
+  // Parameters
   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 release_script = params.get("release_script", "source scripts/ci/build.sh; deploy_release")
+  def email_script = params.get("email_script", "source scripts/ci/build.sh; generate_email")
   def repository = params.get("repository", "https://github.com/bazelbuild/bazel")
   def replyTo = params.get("replyTo", "bazel-ci@googlegroups.com")
+
+  def ws = pwd()
+
+  // Save the build log
+  JenkinsUtils.saveLog(env, currentBuild, "${ws}/build.log")
+
   // 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) {
+  dir("artifacts") {
+    def stashNames = listStashes(params.configuration, params.restrict_configuration)
+    for (def stashName : stashNames) {
       unstash stashName
     }
   }
-  // Delete files we do not need
-  if ("excludes" in params) {
-    sh "rm -f ${params.excludes}"
-  }
+  def artifacts = listArtifacts(ws, "artifacts", params.get("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}")"
+           "GIT_REPOSITORY_URL=${repository}",
+           "GITHUB_TOKEN=${readFile(env.GITHUB_TOKEN_FILE).trim()}",
+           "APT_GPG_KEY_ID=${readFile(env.APT_GPG_KEY_ID_FILE).trim()}",
+           // TODO(dmarting): hack to work with release_to_apt(), we should get rid of it.
+           "tmpdir=${ws}/artifacts/node=linux-x86_64/variation="]) {
+    // Sign artifacts
+    ensureGpgSecretKeyImported()
+    def artifact_list = signArtifacts(artifacts)
 
-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
+    // Release
+    sh "#!/bin/bash\n${release_script} build.log ${artifact_list.join ' '}"
 
-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"),
+    // Send email announcement
+    stage("Announcement mail") {
+      def email = sh(script: "bash -c '${email_script}'", returnStdout: true)
+      echo "Mail to: ${email}"
+      if (r_name.contains("test")) {
+        echo "Test release, skipping announcement mail."
+      } else {
+        def splittedEmail = email.split("\n", 3)
+        mail(subject: splittedEmail[1],
+             to: splittedEmail[0],
              replyTo: replyTo,
-             body: JenkinsUtils.readFile(env, "output/ci/content"))
+             body: splittedEmail[2])
       }
     }
   }