Fix shard summaries for Windows tasks (#1897)

Buildkite cannot download logs for Windows tasks since it mishandles
backslashes. This change fixes the problem by downloading log files
manually.

Fixes https://github.com/bazelbuild/continuous-integration/issues/1891

Drive-by changes: Improved error handling & formatted bazelci.py
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 24fa625..351479b 100755
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -703,12 +703,16 @@
 
     def _get_buildkite_token(self):
         return decrypt_token(
-            encrypted_token=self._ENCRYPTED_BUILDKITE_API_TESTING_TOKEN
-            if THIS_IS_TESTING
-            else self._ENCRYPTED_BUILDKITE_API_TOKEN,
-            kms_key="buildkite-testing-api-token"
-            if THIS_IS_TESTING
-            else "buildkite-untrusted-api-token",
+            encrypted_token=(
+                self._ENCRYPTED_BUILDKITE_API_TESTING_TOKEN
+                if THIS_IS_TESTING
+                else self._ENCRYPTED_BUILDKITE_API_TOKEN
+            ),
+            kms_key=(
+                "buildkite-testing-api-token"
+                if THIS_IS_TESTING
+                else "buildkite-untrusted-api-token"
+            ),
         )
 
     def _open_url(self, url, params=[]):
@@ -2378,7 +2382,11 @@
         diffbase_archive_url = get_commit_archive_url(resolved_diffbase)
         local_archive_path = download_file(diffbase_archive_url, tmpdir, "repo.tar.gz")
         diffbase_repo_dir = os.path.join(tmpdir, resolved_diffbase)
-        extract_archive(local_archive_path, diffbase_repo_dir, strip_top_level_dir = not is_googlesource_repo(diffbase_archive_url))
+        extract_archive(
+            local_archive_path,
+            diffbase_repo_dir,
+            strip_top_level_dir=not is_googlesource_repo(diffbase_archive_url),
+        )
 
         eprint("Setting up comparison repository...")
         os.chdir(diffbase_repo_dir)
@@ -2458,7 +2466,7 @@
 def resolve_diffbase(diffbase):
     if diffbase in AUTO_DIFFBASE_VALUES:
         fetch_base_branch()
-        return execute_command_and_get_output(["git", "merge-base", "HEAD", 'FETCH_HEAD']).strip()
+        return execute_command_and_get_output(["git", "merge-base", "HEAD", "FETCH_HEAD"]).strip()
     elif COMMIT_RE.fullmatch(diffbase):
         return diffbase
 
@@ -2716,12 +2724,12 @@
         env=os.environ,
         cwd=cwd,
         errors="replace",
-        stdout=subprocess.DEVNULL
-        if suppress_stdout
-        else None,  # suppress_stdout=True when we don't want the output to be printed
-        stderr=subprocess.PIPE
-        if capture_stderr
-        else None,  # capture_stderr=True when we want exceptions to contain stderr
+        stdout=(
+            subprocess.DEVNULL if suppress_stdout else None
+        ),  # suppress_stdout=True when we don't want the output to be printed
+        stderr=(
+            subprocess.PIPE if capture_stderr else None
+        ),  # capture_stderr=True when we want exceptions to contain stderr
     ).returncode
 
 
@@ -3095,9 +3103,18 @@
 
 def get_modified_files(git_commit):
     fetch_base_branch()
-    merge_base_commit = execute_command_and_get_output(["git", "merge-base", git_commit, 'FETCH_HEAD']).strip()
+    merge_base_commit = execute_command_and_get_output(
+        ["git", "merge-base", git_commit, "FETCH_HEAD"]
+    ).strip()
     output = execute_command_and_get_output(
-        ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", "{}..{}".format(merge_base_commit, git_commit)]
+        [
+            "git",
+            "diff-tree",
+            "--no-commit-id",
+            "--name-only",
+            "-r",
+            "{}..{}".format(merge_base_commit, git_commit),
+        ]
     )
     return output.split("\n")
 
@@ -3783,7 +3800,10 @@
             for test_artifact in current_test_artifacts:
                 local_bep_path = test_artifact.download_bep(tmpdir)
                 if not local_bep_path:
-                    # TODO: propagate errors
+                    eprint(
+                        f"Skipping step {test_artifact.job_id} since "
+                        "{test_artifact.relative_bep_path} could not be downloaded..."
+                    )
                     continue
 
                 for test_execution in parse_bep(local_bep_path):
@@ -3804,6 +3824,8 @@
                         f"{base_task}",
                     ]
                 )
+    except Exception as ex:
+        eprint(f"Failed to print shard summary: {ex}")
     finally:
         shutil.rmtree(tmpdir)
 
@@ -3855,33 +3877,28 @@
 
 
 class TestArtifacts:
+
     def __init__(self, job_id, relative_bep_path, relative_log_paths) -> None:
         self.job_id = job_id
         self.relative_bep_path = relative_bep_path
         self.relative_log_paths = relative_log_paths
 
     def download_bep(self, dest_dir: str) -> str:
+        if not LOG_BUCKET:
+            return None
+
         job_dir = os.path.join(dest_dir, self.job_id)
         os.makedirs(job_dir)
 
+        # We cannot use `buildkite agent download *` since it cannot handle backslashes in Windows artifact paths.
+        # Consequently, we just escape all backslashes and download files directly from GCS.
+        url = "/".join([LOG_BUCKET, self.job_id, self.relative_bep_path.replace("\\", "%5C")])
         try:
-            execute_command(
-                [
-                    "buildkite-agent",
-                    "artifact",
-                    "download",
-                    f"*/{_TEST_BEP_FILE}",
-                    job_dir,
-                    "--step",
-                    self.job_id,
-                ]
-            )
+            return download_file(url, job_dir, _TEST_BEP_FILE)
         except:
             # TODO: handle exception
             return None
 
-        return os.path.join(job_dir, self.relative_bep_path)
-
 
 def get_test_file_paths(job_id):
     bep_path = None
@@ -4027,9 +4044,7 @@
 
         def format_shard(s):
             overall, statistics = s.get_details()
-            return (
-                f"{format_test_status(overall)} {statistics}: [log]({get_log_url_for_shard(s)})"
-            )
+            return f"{format_test_status(overall)} {statistics}: [log]({get_log_url_for_shard(s)})"
 
         failing_shards = [s for s in self.shards if s.overall_status != "PASSED"]
         if len(failing_shards) == 1: