bazel: Extract ResultStore links from Bazel output

Bug: b/363338443
Change-Id: Ie7a308b7da56d13a477454b9b20c8977a3192665
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/234973
Reviewed-by: Ted Pudlik <tpudlik@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed-service-accounts.iam.gserviceaccount.com>
diff --git a/recipe_modules/bazel/__init__.py b/recipe_modules/bazel/__init__.py
index 0b3cf54..8b84c4a 100644
--- a/recipe_modules/bazel/__init__.py
+++ b/recipe_modules/bazel/__init__.py
@@ -25,6 +25,7 @@
     'recipe_engine/file',
     'recipe_engine/path',
     'recipe_engine/properties',
+    'recipe_engine/raw_io',
     'recipe_engine/step',
     'recipe_engine/time',
 ]
diff --git a/recipe_modules/bazel/api.py b/recipe_modules/bazel/api.py
index 6243fef..97ceb53 100644
--- a/recipe_modules/bazel/api.py
+++ b/recipe_modules/bazel/api.py
@@ -139,13 +139,42 @@
                     assert program in programs, f'{program} not in {programs}'
                     assert programs[program]
                     for args in programs[program]:
-                        cmd = [self.ensure(), *args, *base_args]
-                        defer(
-                            self.api.step,
-                            shlex.join(args),
-                            cmd,
-                            **kwargs,
-                        )
+                        with self.api.step.nest(shlex.join(args)) as pres:
+                            temp = self.api.path.mkdtemp()
+                            stdout = temp / 'stdout-copy'
+                            defer(
+                                self.api.step,
+                                'bazel',
+                                [self.ensure(), *args, *base_args],
+                                stderr=self.api.raw_io.output_text(
+                                    leak_to=stdout,
+                                ),
+                                **kwargs,
+                            )
+
+                            with self.api.step.nest(
+                                'find resultstore link from stdout'
+                            ):
+                                link = None
+                                rx = re.compile(
+                                    r'Streaming build results to:\s*'
+                                    '(https?://.*?)\s*$'
+                                )
+                                test_data = (
+                                    '\nStreaming build results to: '
+                                    'https://foo.bar.baz/results\n'
+                                )
+                                for line in self.api.file.read_text(
+                                    'read',
+                                    stdout,
+                                    test_data=test_data,
+                                ).splitlines():
+                                    if match := rx.search(line):
+                                        link = match.group(1)
+                                        break
+
+                                if link:
+                                    pres.links['resultstore'] = link
 
 
 def nwise(iterable, n):
diff --git a/recipe_modules/bazel/tests/full.py b/recipe_modules/bazel/tests/full.py
index e784b83..8be8186 100644
--- a/recipe_modules/bazel/tests/full.py
+++ b/recipe_modules/bazel/tests/full.py
@@ -71,7 +71,7 @@
         check = post_process.StepCommandContains
         if invert:
             check = post_process.StepCommandDoesNotContain
-        return api.post_process(check, step, pattern)
+        return api.post_process(check, f'{step}.bazel', pattern)
 
     def lacks_override(step: str, name: str, bzlmod=False):
         return contains_override(
diff --git a/recipes/bazel.expected/simple.json b/recipes/bazel.expected/simple.json
index cedbc89..e235b0d 100644
--- a/recipes/bazel.expected/simple.json
+++ b/recipes/bazel.expected/simple.json
@@ -1201,6 +1201,14 @@
     "name": "default"
   },
   {
+    "cmd": [],
+    "name": "default.build //...",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LINK@resultstore@https://foo.bar.baz/results@@@"
+    ]
+  },
+  {
     "cmd": [
       "[CLEANUP]/tmp_tmp_2/bazelisk",
       "build",
@@ -1229,9 +1237,65 @@
       "TEST_TMPDIR": "[CACHE]/bazel",
       "TRIGGERING_CHANGES_JSON": "[CLEANUP]/tmp_tmp_1"
     },
-    "name": "default.build //...",
+    "name": "default.build //....bazel",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "default.build //....find resultstore link from stdout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/tmp_tmp_3/stdout-copy",
+      "/path/to/tmp/"
+    ],
+    "cwd": "[START_DIR]/co",
+    "env": {
+      "BUILDBUCKET_ID": "0",
+      "BUILDBUCKET_NAME": "project:bucket:builder",
+      "BUILD_NUMBER": "0",
+      "CCACHE_DIR": "[CACHE]/ccache",
+      "CLICOLOR": "0",
+      "CLICOLOR_FORCE": "0",
+      "CTCACHE_DIR": "[CACHE]/clang_tidy",
+      "GCC_COLORS": "",
+      "GOCACHE": "[CACHE]/go",
+      "NO_COLOR": "1",
+      "PIP_CACHE_DIR": "[CACHE]/pip",
+      "PW_ENVIRONMENT_NO_ERROR_ON_UNRECOGNIZED": "1",
+      "PW_ENVSETUP_DISABLE_SPINNER": "1",
+      "PW_PRESUBMIT_DISABLE_SUBPROCESS_CAPTURE": "1",
+      "PW_TEST_VAR": "test_value",
+      "PW_USE_COLOR": "",
+      "TEST_TMPDIR": "[CACHE]/bazel",
+      "TRIGGERING_CHANGES_JSON": "[CLEANUP]/tmp_tmp_1"
+    },
+    "infra_step": true,
+    "name": "default.build //....find resultstore link from stdout.read",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@stdout-copy@@@@",
+      "@@@STEP_LOG_LINE@stdout-copy@Streaming build results to: https://foo.bar.baz/results@@@",
+      "@@@STEP_LOG_END@stdout-copy@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "default.test //...",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LINK@resultstore@https://foo.bar.baz/results@@@"
     ]
   },
   {
@@ -1263,9 +1327,57 @@
       "TEST_TMPDIR": "[CACHE]/bazel",
       "TRIGGERING_CHANGES_JSON": "[CLEANUP]/tmp_tmp_1"
     },
-    "name": "default.test //...",
+    "name": "default.test //....bazel",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "default.test //....find resultstore link from stdout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/tmp_tmp_4/stdout-copy",
+      "/path/to/tmp/"
+    ],
+    "cwd": "[START_DIR]/co",
+    "env": {
+      "BUILDBUCKET_ID": "0",
+      "BUILDBUCKET_NAME": "project:bucket:builder",
+      "BUILD_NUMBER": "0",
+      "CCACHE_DIR": "[CACHE]/ccache",
+      "CLICOLOR": "0",
+      "CLICOLOR_FORCE": "0",
+      "CTCACHE_DIR": "[CACHE]/clang_tidy",
+      "GCC_COLORS": "",
+      "GOCACHE": "[CACHE]/go",
+      "NO_COLOR": "1",
+      "PIP_CACHE_DIR": "[CACHE]/pip",
+      "PW_ENVIRONMENT_NO_ERROR_ON_UNRECOGNIZED": "1",
+      "PW_ENVSETUP_DISABLE_SPINNER": "1",
+      "PW_PRESUBMIT_DISABLE_SUBPROCESS_CAPTURE": "1",
+      "PW_TEST_VAR": "test_value",
+      "PW_USE_COLOR": "",
+      "TEST_TMPDIR": "[CACHE]/bazel",
+      "TRIGGERING_CHANGES_JSON": "[CLEANUP]/tmp_tmp_1"
+    },
+    "infra_step": true,
+    "name": "default.test //....find resultstore link from stdout.read",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@stdout-copy@@@@",
+      "@@@STEP_LOG_LINE@stdout-copy@Streaming build results to: https://foo.bar.baz/results@@@",
+      "@@@STEP_LOG_END@stdout-copy@@@"
     ]
   },
   {