BCR bazel compatibility test: Add generate_report.py (#2091)

This will be run in the last step for a
https://buildkite.com/bazel/bcr-bazel-compatibility-test build without
`USE_BAZELISK_MIGRATE` and generate a report in markdown that can be
used for filing a GitHub issue report for broken modules.

e.g.:
<img width="685" alt="image"
src="https://github.com/user-attachments/assets/3e888dd0-601c-4b38-bce0-047eb80be9bb">
diff --git a/buildkite/bazel-central-registry/bcr_compatibility.py b/buildkite/bazel-central-registry/bcr_compatibility.py
index 2c8253d..3f8c559 100644
--- a/buildkite/bazel-central-registry/bcr_compatibility.py
+++ b/buildkite/bazel-central-registry/bcr_compatibility.py
@@ -23,6 +23,7 @@
 import os
 import sys
 import subprocess
+import time
 
 import bazelci
 import bcr_presubmit
@@ -47,6 +48,14 @@
 # Default to use only 30% of CI resources for each type of machines.
 CI_RESOURCE_PERCENTAGE = int(os.environ.get('CI_RESOURCE_PERCENTAGE', 30))
 
+SCRIPT_URL = "https://raw.githubusercontent.com/bazelbuild/continuous-integration/{}/buildkite/bazel-central-registry/generate_report.py?{}".format(
+    bazelci.GITHUB_BRANCH, int(time.time())
+)
+
+
+def fetch_generate_report_py_command():
+    return "curl -s {0} -o generate_report.py".format(SCRIPT_URL)
+
 
 def select_modules_from_env_vars():
     """
@@ -108,6 +117,26 @@
         ),
     ]
 
+def create_step_for_generate_report():
+    parts = [
+        bazelci.PLATFORMS[bazelci.DEFAULT_PLATFORM]["python"],
+        "generate_report.py",
+        "--build_number=%s" % os.getenv("BUILDKITE_BUILD_NUMBER"),
+    ]
+    return [
+        {"wait": "~", "continue_on_failure": "true"},
+        bazelci.create_step(
+            label="Generate report in markdown",
+            commands=[
+                bazelci.fetch_bazelcipy_command(),
+                bcr_presubmit.fetch_bcr_presubmit_py_command(),
+                fetch_generate_report_py_command(),
+                " ".join(parts),
+            ],
+            platform=bazelci.DEFAULT_PLATFORM,
+        ),
+    ]
+
 def main():
     modules = get_target_modules()
     pipeline_steps = []
@@ -127,6 +156,8 @@
             pipeline_steps.insert(0, {"block": "Please review generated jobs before proceeding", "blocked_state": "running"})
         if bazelci.use_bazelisk_migrate():
             pipeline_steps += create_step_for_report_flags_results()
+        else:
+            pipeline_steps += create_step_for_generate_report()
 
     bcr_presubmit.upload_jobs_to_pipeline(pipeline_steps)
 
diff --git a/buildkite/bazel-central-registry/generate_report.py b/buildkite/bazel-central-registry/generate_report.py
new file mode 100755
index 0000000..f9176ff
--- /dev/null
+++ b/buildkite/bazel-central-registry/generate_report.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# 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.
+# pylint: disable=line-too-long
+# pylint: disable=missing-function-docstring
+# pylint: disable=unspecified-encoding
+# pylint: disable=invalid-name
+"""The CI script for generate report for BCR Bazel Compatibility Test pipeline."""
+
+
+import argparse
+import collections
+import os
+import json
+import re
+import sys
+
+import bazelci
+import bcr_presubmit
+
+BUILDKITE_ORG = os.environ["BUILDKITE_ORGANIZATION_SLUG"]
+
+PIPELINE = os.environ["BUILDKITE_PIPELINE_SLUG"]
+
+MODULE_VERSION_PATTERN = re.compile(r'(?P<module_version>[a-z](?:[a-z0-9._-]*[a-z0-9])?@[^\s]+)')
+
+def extract_module_version(line):
+    match = MODULE_VERSION_PATTERN.search(line)
+    if match:
+        return match.group("module_version")
+
+
+def get_github_maintainer(module_name):
+    metadata = json.load(open(bcr_presubmit.get_metadata_json(module_name), "r"))
+    github_maintainers = []
+    for maintainer in metadata["maintainers"]:
+        if "github" in maintainer:
+            github_maintainers.append(maintainer["github"])
+
+    if not github_maintainers:
+        github_maintainers.append("bazelbuild/bcr-maintainers")
+    return github_maintainers
+
+
+def print_report_in_markdown(failed_jobs_per_module, pipeline_url):
+    bazel_version = os.environ.get("USE_BAZEL_VERSION")
+    print("\n")
+    print("## The following modules are broken%s:" % (f" with Bazel@{bazel_version}" if bazel_version else ""))
+    print("BCR Bazel Compatibility Test: ", pipeline_url)
+    for module, jobs in failed_jobs_per_module.items():
+        module_name = module.strip().split("@")[0]
+        github_maintainers = get_github_maintainer(module_name)
+        print(f"### {module}")
+        print("Maintainers: ", ", ".join(f"@{maintainer}" for maintainer in github_maintainers))
+        for job in jobs:
+            print(f"- [{job['name']}]({job['web_url']})")
+    print("\n")
+
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv[1:]
+
+    parser = argparse.ArgumentParser(description="Script to report BCR Bazel Compatibility Test result.")
+    parser.add_argument("--build_number", type=str)
+
+    args = parser.parse_args(argv)
+    if not args.build_number:
+        parser.print_help()
+        return 2
+
+    client = bazelci.BuildkiteClient(org=BUILDKITE_ORG, pipeline=PIPELINE)
+    build_info = client.get_build_info(args.build_number)
+    failed_jobs_per_module = collections.defaultdict(list)
+    for job in build_info["jobs"]:
+        if job.get("state") == "failed" and "name" in job:
+            module = extract_module_version(job["name"])
+            if not module:
+                continue
+            failed_jobs_per_module[module].append(job)
+
+    print_report_in_markdown(failed_jobs_per_module, build_info["web_url"])
+
+if __name__ == "__main__":
+    sys.exit(main())