add support for publishing latest Bazel binaries to GCS
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 3db0992..6e1d33f 100644
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -14,7 +14,9 @@
 
 from __future__ import print_function
 import argparse
+import base64
 import codecs
+import hashlib
 import json
 import os.path
 import multiprocessing
@@ -263,9 +265,8 @@
     cleanup(platform)
     tmpdir = tempfile.mkdtemp()
     if use_but:
-      source_step = create_label(platform_name(platform), "Bazel",
-                                 build_only=True, test_only=False)
-      bazel_binary = download_bazel_binary(tmpdir, source_step)
+      print("\n--- Downloading Bazel under test")
+      bazel_binary = download_bazel_binary(tmpdir, platform)
     execute_shell_commands(config.get("shell_commands", None))
     execute_bazel_run(bazel_binary, config.get("run_targets", None))
     if not test_only:
@@ -292,8 +293,9 @@
                                    "bazel-bin/src/bazel"]))
 
 
-def download_bazel_binary(dest_dir, source_step):
-  print("\n--- Downloading Bazel under test")
+def download_bazel_binary(dest_dir, platform):
+  source_step = create_label(platform_name(platform), "Bazel", build_only=True,
+                             test_only=False)
   fail_if_nonzero(execute_command(["buildkite-agent", "artifact", "download",
                                    "bazel-bin/src/bazel", dest_dir, "--step", source_step]))
   bazel_binary_path = os.path.join(dest_dir, "bazel-bin/src/bazel")
@@ -544,6 +546,16 @@
                              pipeline_command, platform)
 
 
+def publish_bazel_binary_step(platform):
+  command = python_binary() + " bazelci.py publish_binary --platform=" + platform
+  return """
+  - label: \"Publish Bazel Binary ({0})\"
+    command: \"{1}\\n{2}\"
+    agents:
+      - \"pipeline=true\"""".format(platform_name(platform), fetch_bazelcipy_command(),
+                                    command)
+
+
 def print_bazel_postsubmit_pipeline(configs, http_config):
   if not configs:
     eprint("Bazel postsubmit pipeline configuration is empty.")
@@ -556,6 +568,10 @@
     pipeline_steps.append(bazel_build_step(platform, "Bazel",
                                            http_config, build_only=True))
   pipeline_steps.append(wait_step())
+
+  for platform in supported_platforms():
+    pipeline_steps.append(publish_bazel_binary_step(platform))
+
   for platform, config in configs.items():
     pipeline_steps.append(bazel_build_step(platform, "Bazel",
                                            http_config, test_only=True))
@@ -568,6 +584,100 @@
   print_pipeline(pipeline_steps)
 
 
+def bazelci_builds_download_url(platform, build_number):
+  return "https://storage.googleapis.com/bazel-builds/{0}/{1}/bazel".format(build_number, platform)
+
+
+def bazelci_builds_upload_url(platform, build_number):
+  return "gs//bazel-builds/{0}/{1}/bazel".format(build_number, platform)
+
+
+def bazelci_builds_metadata_url(platform):
+  return "gs://bazel-builds/metadata/{0}/latest.json"
+
+
+def latest_generation_and_build_number(platform):
+  output = None
+  attempt = 0
+  while attempt < 5:
+    output = subprocess.check_output(
+        ["gsutil", "stat", bazelci_builds_metadata_url(platform)])
+    match = re.search("Generation:[ ]*([0-9]+)", output.decode("utf-8"))
+    if not match:
+      eprint("Couldn't parse generation. gsutil output format changed?")
+    generation = match.group(1)
+
+    match = re.search("Hash \(md5\):[ ]*([^\s]+)", output.decode("utf-8"))
+    if not match:
+      eprint("Couldn't parse md5 hash. gsutil output format changed?")
+    expected_md5hash = base64.b64decode(match.group(1))
+
+    output = subprocess.check_output(
+        ["gsutil", "cat", bazelci_builds_metadata_url(platform)])
+    hasher = hashlib.md5()
+    hasher.update(output)
+    actual_md5hash = hasher.digest()
+
+    if expected_md5hash == actual_md5hash:
+      break
+    attempt = attempt + 1
+  info = json.loads(output.decode("utf-8"))
+  return (generation, info["build_number"])
+
+
+def try_publish_binary(platform, build_number, expected_generation):
+  tmpdir = None
+  try:
+    tmpdir = tempfile.mkdtemp()
+    bazel_binary_path = download_bazel_binary(tmpdir, platform)
+    fail_if_nonzero(["gsutil", "cp", bazel_binary_path,
+                     bazelci_builds_upload_url(platform, build_number)])
+
+    info = {
+        "build_number": build_number,
+        "binary": bazelci_builds_download_url(platform, build_number),
+        "commit": os.environ["BUILDKITE_COMMIT"],
+        "platform": platform,
+    }
+    info_file = os.path.join(tmpdir, "info.json")
+    fp = tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False)
+    json.dump(info, fp)
+    fp.close()
+    exitcode = subprocess.run(["gsutil", "-h", "x-goog-if-generation-match:" + expected_generation,
+                               "cp", fp.name, bazelci_builds_metadata_url(platform)]).returncode
+    os.unlink(fp.name)
+    return exitcode == 0
+  finally:
+    if tmpdir:
+      shutil.rmdir(tmpdir)
+
+
+def publish_binary(platform):
+  '''
+  Publish tested Bazel binary to GCS.
+  '''
+  attempt = 0
+  while attempt < 5:
+    latest_generation, latest_build_number = latest_generation_and_build_number(
+        platform)
+
+    current_build_number = os.environ.get("BUILDKITE_BUILD_NUMBER", None)
+    if not current_build_number:
+      eprint("Not running inside Buildkite")
+    current_build_number = int(current_build_number)
+    if current_build_number <= latest_build_number:
+      print(("Current build '{0}' is not newer than latest published '{1}'. " +
+             "Skipping publishing of binaries.").format(current_build_number,
+                                                        latest_build_number))
+      break
+
+    if try_publish_binary(platform, current_build_number, latest_generation):
+      print("Successfully published binaries of build '{0}'.".format(
+          current_build_number))
+      break
+    attempt = attempt + 1
+
+
 if __name__ == "__main__":
   parser = argparse.ArgumentParser(
       description='Bazel Continuous Integration Script')
@@ -595,6 +705,10 @@
   runner.add_argument("--build_only", type=bool, nargs="?", const=True)
   runner.add_argument("--test_only", type=bool, nargs="?", const=True)
 
+  runner = subparsers.add_parser("publish_binary")
+  runner.add_argument("--platform", action="store", required=True,
+                      choices=list(supported_platforms()))
+
   args = parser.parse_args()
 
   if args.subparsers_name == "bazel_postsubmit_pipeline":
@@ -610,5 +724,7 @@
     execute_commands(configs.get("platforms", None)[args.platform],
                      args.platform, args.git_repository, args.use_but, args.save_but,
                      args.build_only, args.test_only)
+  elif args.subparsers_name == "publish_binary":
+    publish_binary(args.platform)
   else:
     parser.print_help()