add --flaky_test_attempts=3, fail the pipeline when a test is flaky:
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index dede4dc..1f59ea1 100644
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -273,7 +273,10 @@
       bep_file = os.path.join(tmpdir, "build_event_json_file.json")
       exit_code = execute_bazel_test(bazel_binary, config.get("test_flags", []),
                                      config.get("test_targets", None), bep_file)
-      upload_failed_test_logs(bep_file, tmpdir)
+      # Fail the pipeline if there were any flaky tests.
+      if has_flaky_tests() and exit_code == 0:
+        exit_code = 1
+      upload_test_logs(bep_file, tmpdir)
   finally:
     if tmpdir:
       shutil.rmtree(tmpdir)
@@ -282,6 +285,10 @@
       exit(exit_code)
 
 
+def has_flaky_tests(bep_file):
+  return len(test_logs_for_status(bep_file, status="FLAKY")) > 0
+
+
 def print_bazel_version_info(bazel_binary):
   print_collapsed_group("Bazel Info")
   fail_if_nonzero(execute_command([bazel_binary, "version"]))
@@ -380,8 +387,9 @@
   print_expanded_group("Test")
   num_jobs = str(multiprocessing.cpu_count())
   common_flags = ["--color=yes", "--keep_going", "--verbose_failures",
-                  "--build_tests_only",  "--jobs=" + num_jobs,
-                  "--local_test_jobs=" + num_jobs, "--build_event_json_file=" + bep_file]
+                  "--flaky_test_attempts=3", "--build_tests_only",
+                  "--jobs=" + num_jobs, "--local_test_jobs=" + num_jobs,
+                  "--build_event_json_file=" + bep_file]
   return execute_command([bazel_binary, "test"] + common_flags + flags + targets)
 
 
@@ -390,19 +398,16 @@
     exit(exitcode)
 
 
-def upload_failed_test_logs(bep_file, tmpdir):
+def upload_test_logs(bep_file, tmpdir):
   if not os.path.exists(bep_file):
     return
-  failed_tests = failed_test_logs(bep_file, tmpdir)
-  if failed_tests:
+  test_logs = test_logs_to_upload(bep_file, tmpdir)
+  if test_logs:
     cwd = os.getcwd()
     try:
       os.chdir(tmpdir)
-      print_expanded_group("Failed Tests")
-      for label, _ in failed_tests:
-        print(label)
-      print_collapsed_group("Uploading logs of failed tests")
-      for _, logfile in failed_tests:
+      print_collapsed_group("Uploading test logs")
+      for logfile in test_logs:
         relative_path = os.path.relpath(logfile, tmpdir)
         fail_if_nonzero(execute_command(["buildkite-agent", "artifact", "upload",
                                          relative_path]))
@@ -410,27 +415,38 @@
       os.chdir(cwd)
 
 
-def failed_test_logs(bep_file, tmpdir):
-  failed_tests = test_targets_with_status(bep_file, status="FAILED")
-  timeout_tests = test_targets_with_status(bep_file, status="TIMEOUT")
-  test_logs = []
-  for label, test_log in (failed_tests + timeout_tests):
-    new_path = test_label_to_path(tmpdir, label)
-    os.makedirs(os.path.dirname(new_path), exist_ok=True)
-    copyfile(test_log, new_path)
-    test_logs.append((label, new_path))
-  return test_logs
+def test_logs_to_upload(bep_file, tmpdir):
+  failed = test_logs_for_status(bep_file, status="FAILED")
+  timed_out = test_logs_for_status(bep_file, status="TIMEOUT")
+  flaky = test_logs_for_status(bep_file, status="FLAKY")
+  # Rename the test.log files to the target that created them
+  # so that it's easy to associate test.log and target.
+  new_paths = []
+  for label, test_logs in (failed + timed_out + flaky):
+    attempt = 0
+    if len(test_logs) > 1:
+      attempt = 1
+    for test_log in test_logs:
+      new_path = test_label_to_path(tmpdir, label, attempt)
+      os.makedirs(os.path.dirname(new_path), exist_ok=True)
+      copyfile(test_logs, new_path)
+      new_paths.append(new_path)
+      attempt = attempt + 1
+  return new_paths
 
 
-def test_label_to_path(tmpdir, label):
+def test_label_to_path(tmpdir, label, attempt):
   # remove leading //
   path = label[2:]
   path = path.replace(":", "/")
-  path = os.path.join(path, "test.log")
+  if attempt == 0:
+    path = os.path.join(path, "test.log")
+  else:
+    path = os.path.join(path, "attempt_" + str(attempt) + ".log")
   return os.path.join(tmpdir, path)
 
 
-def test_targets_with_status(bep_file, status):
+def test_logs_for_status(bep_file, status):
   targets = []
   raw_data = ""
   with open(bep_file) as f:
@@ -441,13 +457,16 @@
   while pos < len(raw_data):
     bep_obj, size = decoder.raw_decode(raw_data[pos:])
     if "testResult" in bep_obj:
+      test_target = bep_obj["id"]["testResult"]["label"]
       test_result = bep_obj["testResult"]
       if test_result["status"] == status:
         outputs = test_result["testActionOutput"]
+        test_logs = []
         for output in outputs:
           if output["name"] == "test.log":
-            targets.append((bep_obj["id"]["testResult"]
-                            ["label"], urlparse(output["uri"]).path))
+            test_logs.append(urlparse(output["uri"]).path)
+        if test_logs:
+          targets.append((test_target, test_logs))
     pos += size + 1
   return targets