Trigger multiple builds from a single run.

The purpose of this recipe is to trigger multiple builds at the same
revision. Specify build request arguments in a property and trigger the
builds in a batch, this may be in separate projects and/or buckets as
long as the service account this recipe runs under has the corresponding
permissions in buildbucket.

R=nodir,liaoyuke,sajjadm,iannucci,phosek

Bug: 944752
Change-Id: Icd2543cc8823ec2dc5b6e4bc80322df0c359683e
Reviewed-on: https://chromium-review.googlesource.com/c/infra/luci/recipes-py/+/1537739
Commit-Queue: Roberto Carrillo <robertocn@chromium.org>
Reviewed-by: Nodir Turakulov <nodir@chromium.org>
diff --git a/README.recipes.md b/README.recipes.md
index 1fa2ad8..ee6f2c3 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -41,6 +41,7 @@
   * [assertions:tests/long_message](#recipes-assertions_tests_long_message)
   * [assertions:tests/max_diff](#recipes-assertions_tests_max_diff)
   * [buildbucket:examples/full](#recipes-buildbucket_examples_full) &mdash; This file is a recipe demonstrating the buildbucket recipe module.
+  * [buildbucket:run/multi](#recipes-buildbucket_run_multi) &mdash; Launches multiple builds at the same revision.
   * [buildbucket:tests/build](#recipes-buildbucket_tests_build)
   * [buildbucket:tests/collect](#recipes-buildbucket_tests_collect)
   * [buildbucket:tests/get](#recipes-buildbucket_tests_get)
@@ -238,7 +239,7 @@
 
 A module for interacting with buildbucket.
 
-&emsp; **@property**<br>&mdash; **def [bucket\_v1](/recipe_modules/buildbucket/api.py#636)(self):**
+&emsp; **@property**<br>&mdash; **def [bucket\_v1](/recipe_modules/buildbucket/api.py#652)(self):**
 
 Returns bucket name in v1 format.
 
@@ -261,11 +262,11 @@
 the rules described in the .proto files.
 If the current build is not a buildbucket build, returned `build.id` is 0.
 
-&emsp; **@property**<br>&mdash; **def [build\_id](/recipe_modules/buildbucket/api.py#652)(self):**
+&emsp; **@property**<br>&mdash; **def [build\_id](/recipe_modules/buildbucket/api.py#668)(self):**
 
 DEPRECATED, use build.id instead.
 
-&emsp; **@property**<br>&mdash; **def [build\_input](/recipe_modules/buildbucket/api.py#657)(self):**
+&emsp; **@property**<br>&mdash; **def [build\_input](/recipe_modules/buildbucket/api.py#673)(self):**
 
 DEPRECATED, use build.input instead.
 
@@ -273,7 +274,7 @@
 
 Returns url to a build. Defaults to current build.
 
-&emsp; **@property**<br>&mdash; **def [builder\_id](/recipe_modules/buildbucket/api.py#662)(self):**
+&emsp; **@property**<br>&mdash; **def [builder\_id](/recipe_modules/buildbucket/api.py#678)(self):**
 
 Deprecated. Use build.builder instead.
 
@@ -281,9 +282,9 @@
 
 Returns builder name. Shortcut for `.build.builder.builder`.
 
-&mdash; **def [cancel\_build](/recipe_modules/buildbucket/api.py#523)(self, build_id, \*\*kwargs):**
+&mdash; **def [cancel\_build](/recipe_modules/buildbucket/api.py#525)(self, build_id, \*\*kwargs):**
 
-&mdash; **def [collect\_build](/recipe_modules/buildbucket/api.py#529)(self, build_id, mirror_status=False, \*\*kwargs):**
+&mdash; **def [collect\_build](/recipe_modules/buildbucket/api.py#531)(self, build_id, mirror_status=False, \*\*kwargs):**
 
 Shorthand for `collect_builds` below, but for a single build only.
 
@@ -295,7 +296,7 @@
   [Build](https://chromium.googlesource.com/infra/luci/luci-go/+/master/buildbucket/proto/build.proto).
   for the ended build.
 
-&mdash; **def [collect\_builds](/recipe_modules/buildbucket/api.py#549)(self, build_ids, interval=None, timeout=None, step_name=None):**
+&mdash; **def [collect\_builds](/recipe_modules/buildbucket/api.py#551)(self, build_ids, interval=None, timeout=None, step_name=None, raise_if_unsuccessful=False):**
 
 Waits for a set of builds to end and returns their details.
 
@@ -305,13 +306,15 @@
   Defaults to 1m.
 * timeout: Maximum time to wait for builds to end. Defaults to 1h.
 * step_name: Custom name for the generated step.
+* raise_if_unsuccessful: if any build being collected did not succeed, raise
+  an exception.
 
 Returns:
   A map from integer build IDs to the corresponding
   [Build](https://chromium.googlesource.com/infra/luci/luci-go/+/master/buildbucket/proto/build.proto)
   for all specified builds.
 
-&mdash; **def [get\_build](/recipe_modules/buildbucket/api.py#526)(self, build_id, \*\*kwargs):**
+&mdash; **def [get\_build](/recipe_modules/buildbucket/api.py#528)(self, build_id, \*\*kwargs):**
 
 &emsp; **@property**<br>&mdash; **def [gitiles\_commit](/recipe_modules/buildbucket/api.py#135)(self):**
 
@@ -329,11 +332,11 @@
 Returns True if the build is critical. Build defaults to the current one.
     
 
-&emsp; **@property**<br>&mdash; **def [properties](/recipe_modules/buildbucket/api.py#647)(self):**
+&emsp; **@property**<br>&mdash; **def [properties](/recipe_modules/buildbucket/api.py#663)(self):**
 
 DEPRECATED, use build attribute instead.
 
-&mdash; **def [put](/recipe_modules/buildbucket/api.py#488)(self, builds, \*\*kwargs):**
+&mdash; **def [put](/recipe_modules/buildbucket/api.py#490)(self, builds, \*\*kwargs):**
 
 Puts a batch of builds.
 
@@ -357,7 +360,7 @@
   A step that as its `.stdout` property contains the response object as
   returned by buildbucket.
 
-&mdash; **def [run](/recipe_modules/buildbucket/api.py#238)(self, schedule_build_requests, collect_interval=None, timeout=None, url_title_fn=None, step_name=None):**
+&mdash; **def [run](/recipe_modules/buildbucket/api.py#238)(self, schedule_build_requests, collect_interval=None, timeout=None, url_title_fn=None, step_name=None, raise_if_unsuccessful=False):**
 
 Runs builds and returns results.
 
@@ -369,7 +372,7 @@
   [Builds](https://chromium.googlesource.com/infra/luci/luci-go/+/master/buildbucket/proto/build.proto)
   in the same order as schedule_build_requests.
 
-&mdash; **def [schedule](/recipe_modules/buildbucket/api.py#393)(self, schedule_build_requests, url_title_fn=None, step_name=None):**
+&mdash; **def [schedule](/recipe_modules/buildbucket/api.py#395)(self, schedule_build_requests, url_title_fn=None, step_name=None):**
 
 Schedules a batch of builds.
 
@@ -391,7 +394,7 @@
 *   url_title_fn: a function (build_pb2.Build) -> (str) that returns a title
     of build link for the step. Defaults to build id.
     If returns None, the link is not emitted.
-*   step_name: step name/
+*   step_name: name for this step.
 
 Returns:
   A list of
@@ -401,7 +404,7 @@
 Raises:
   `InfraFailure` if any of the requests fail.
 
-&mdash; **def [schedule\_request](/recipe_modules/buildbucket/api.py#262)(self, builder, project=None, bucket=None, properties=None, experimental=None, gitiles_commit=None, gerrit_changes=None, tags=None, inherit_buildsets=True, dimensions=None, priority=None, critical=None):**
+&mdash; **def [schedule\_request](/recipe_modules/buildbucket/api.py#264)(self, builder, project=None, bucket=None, properties=None, experimental=None, gitiles_commit=None, gerrit_changes=None, tags=None, inherit_buildsets=True, dimensions=None, priority=None, critical=None):**
 
 Creates a new `ScheduleBuildRequest` message with reasonable defaults.
 
@@ -2358,6 +2361,13 @@
 This file is a recipe demonstrating the buildbucket recipe module.
 
 &mdash; **def [RunSteps](/recipe_modules/buildbucket/examples/full.py#18)(api):**
+### *recipes* / [buildbucket:run/multi](/recipe_modules/buildbucket/run/multi.py)
+
+[DEPS](/recipe_modules/buildbucket/run/multi.py#12): [buildbucket](#recipe_modules-buildbucket), [properties](#recipe_modules-properties)
+
+Launches multiple builds at the same revision.
+
+&mdash; **def [RunSteps](/recipe_modules/buildbucket/run/multi.py#29)(api, build_requests, collect_builds):**
 ### *recipes* / [buildbucket:tests/build](/recipe_modules/buildbucket/tests/build.py)
 
 [DEPS](/recipe_modules/buildbucket/tests/build.py#9): [buildbucket](#recipe_modules-buildbucket), [properties](#recipe_modules-properties), [step](#recipe_modules-step)
@@ -2365,9 +2375,9 @@
 &mdash; **def [RunSteps](/recipe_modules/buildbucket/tests/build.py#16)(api):**
 ### *recipes* / [buildbucket:tests/collect](/recipe_modules/buildbucket/tests/collect.py)
 
-[DEPS](/recipe_modules/buildbucket/tests/collect.py#5): [buildbucket](#recipe_modules-buildbucket)
+[DEPS](/recipe_modules/buildbucket/tests/collect.py#5): [buildbucket](#recipe_modules-buildbucket), [properties](#recipe_modules-properties)
 
-&mdash; **def [RunSteps](/recipe_modules/buildbucket/tests/collect.py#10)(api):**
+&mdash; **def [RunSteps](/recipe_modules/buildbucket/tests/collect.py#11)(api):**
 ### *recipes* / [buildbucket:tests/get](/recipe_modules/buildbucket/tests/get.py)
 
 [DEPS](/recipe_modules/buildbucket/tests/get.py#5): [buildbucket](#recipe_modules-buildbucket)
diff --git a/recipe_modules/buildbucket/api.py b/recipe_modules/buildbucket/api.py
index f11140b..17a624a 100644
--- a/recipe_modules/buildbucket/api.py
+++ b/recipe_modules/buildbucket/api.py
@@ -237,7 +237,7 @@
 
   def run(
       self, schedule_build_requests, collect_interval=None, timeout=None,
-      url_title_fn=None, step_name=None):
+      url_title_fn=None, step_name=None, raise_if_unsuccessful=False):
     """Runs builds and returns results.
 
     A shortcut for schedule() and collect_builds().
@@ -256,7 +256,9 @@
           [b.id for b in builds],
           interval=collect_interval,
           timeout=timeout,
-          step_name='collect')
+          step_name='collect',
+          raise_if_unsuccessful=raise_if_unsuccessful,
+      )
       return [build_dict[b.id] for b in builds]
 
   def schedule_request(
@@ -547,7 +549,9 @@
     return build
 
   def collect_builds(
-      self, build_ids, interval=None, timeout=None, step_name=None):
+      self, build_ids, interval=None, timeout=None, step_name=None,
+      raise_if_unsuccessful=False
+  ):
     """Waits for a set of builds to end and returns their details.
 
     Args:
@@ -556,6 +560,8 @@
       Defaults to 1m.
     * timeout: Maximum time to wait for builds to end. Defaults to 1h.
     * step_name: Custom name for the generated step.
+    * raise_if_unsuccessful: if any build being collected did not succeed, raise
+      an exception.
 
     Returns:
       A map from integer build IDs to the corresponding
@@ -580,6 +586,16 @@
     )
     builds = [json_format.ParseDict(build_json, build_pb2.Build())
               for build_json in result.json.output]
+    if raise_if_unsuccessful:
+      unsuccessful_builds = sorted(b.id for b in builds
+                                   if b.status != common_pb2.SUCCESS)
+      if unsuccessful_builds:
+        self.m.step.active_result.presentation.status = self.m.step.FAILURE
+        self.m.step.active_result.presentation.logs[
+            'unsuccessful_builds'] = map(str, unsuccessful_builds)
+        raise self.m.step.InfraFailure(
+            'Triggered build(s) did not succeed, unexpectedly')
+
     return {build.id: build for build in builds}
 
   # Internal.
diff --git a/recipe_modules/buildbucket/run/multi.expected/basic.json b/recipe_modules/buildbucket/run/multi.expected/basic.json
new file mode 100644
index 0000000..71977fd
--- /dev/null
+++ b/recipe_modules/buildbucket/run/multi.expected/basic.json
@@ -0,0 +1,111 @@
+[
+  {
+    "cmd": [
+      "bb",
+      "batch",
+      "-host",
+      "cr-buildbucket.appspot.com"
+    ],
+    "infra_step": true,
+    "name": "buildbucket.schedule",
+    "stdin": "{\"requests\": [{\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"linux\", \"project\": \"chromium\"}, \"requestId\": \"0-00000000-0000-0000-0000-000000001337\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}, {\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"win\", \"project\": \"chromium\"}, \"requestId\": \"0-00000000-0000-0000-0000-00000000133a\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}, {\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"mac\", \"project\": \"chromium\"}, \"requestId\": \"0-00000000-0000-0000-0000-00000000133d\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}]}",
+    "stdout": "/path/to/tmp/json",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"responses\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514000\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"win\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514001\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"mac\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514002\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  ]@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@request@{@@@",
+      "@@@STEP_LOG_LINE@request@  \"requests\": [@@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"ci\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"0-00000000-0000-0000-0000-000000001337\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ]@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }, @@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"win\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"ci\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"0-00000000-0000-0000-0000-00000000133a\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ]@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }, @@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"mac\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"ci\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"0-00000000-0000-0000-0000-00000000133d\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ]@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }@@@",
+      "@@@STEP_LOG_LINE@request@  ]@@@",
+      "@@@STEP_LOG_LINE@request@}@@@",
+      "@@@STEP_LOG_END@request@@@",
+      "@@@STEP_LINK@8922054662172514000@https://cr-buildbucket.appspot.com/build/8922054662172514000@@@",
+      "@@@STEP_LINK@8922054662172514001@https://cr-buildbucket.appspot.com/build/8922054662172514001@@@",
+      "@@@STEP_LINK@8922054662172514002@https://cr-buildbucket.appspot.com/build/8922054662172514002@@@"
+    ]
+  },
+  {
+    "jsonResult": null,
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/buildbucket/run/multi.expected/collect.json b/recipe_modules/buildbucket/run/multi.expected/collect.json
new file mode 100644
index 0000000..331d07e
--- /dev/null
+++ b/recipe_modules/buildbucket/run/multi.expected/collect.json
@@ -0,0 +1,152 @@
+[
+  {
+    "cmd": [],
+    "name": "buildbucket.run"
+  },
+  {
+    "cmd": [
+      "bb",
+      "batch",
+      "-host",
+      "cr-buildbucket.appspot.com"
+    ],
+    "infra_step": true,
+    "name": "buildbucket.run.schedule",
+    "stdin": "{\"requests\": [{\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"linux\", \"project\": \"chromium\"}, \"requestId\": \"0-00000000-0000-0000-0000-000000001337\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}, {\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"win\", \"project\": \"chromium\"}, \"requestId\": \"0-00000000-0000-0000-0000-00000000133a\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}, {\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"mac\", \"project\": \"chromium\"}, \"requestId\": \"0-00000000-0000-0000-0000-00000000133d\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}]}",
+    "stdout": "/path/to/tmp/json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"responses\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514000\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"win\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514001\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"mac\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514002\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  ]@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@request@{@@@",
+      "@@@STEP_LOG_LINE@request@  \"requests\": [@@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"ci\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"0-00000000-0000-0000-0000-000000001337\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ]@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }, @@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"win\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"ci\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"0-00000000-0000-0000-0000-00000000133a\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ]@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }, @@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"mac\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"ci\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"0-00000000-0000-0000-0000-00000000133d\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ]@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }@@@",
+      "@@@STEP_LOG_LINE@request@  ]@@@",
+      "@@@STEP_LOG_LINE@request@}@@@",
+      "@@@STEP_LOG_END@request@@@",
+      "@@@STEP_LINK@8922054662172514000@https://cr-buildbucket.appspot.com/build/8922054662172514000@@@",
+      "@@@STEP_LINK@8922054662172514001@https://cr-buildbucket.appspot.com/build/8922054662172514001@@@",
+      "@@@STEP_LINK@8922054662172514002@https://cr-buildbucket.appspot.com/build/8922054662172514002@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "buildbucket",
+      "collect",
+      "-host",
+      "cr-buildbucket.appspot.com",
+      "-json-output",
+      "/path/to/tmp/json",
+      "-interval",
+      "60s",
+      "8922054662172514000",
+      "8922054662172514001",
+      "8922054662172514002"
+    ],
+    "infra_step": true,
+    "name": "buildbucket.run.collect",
+    "timeout": 3600,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"id\": \"8922054662172514000\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"status\": \"SUCCESS\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"id\": \"8922054662172514001\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"status\": \"SUCCESS\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"id\": \"8922054662172514002\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"status\": \"SUCCESS\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "jsonResult": null,
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/buildbucket/run/multi.py b/recipe_modules/buildbucket/run/multi.py
new file mode 100644
index 0000000..d5cb8c2
--- /dev/null
+++ b/recipe_modules/buildbucket/run/multi.py
@@ -0,0 +1,83 @@
+# Copyright 2019 The LUCI Authors. All rights reserved.
+# Use of this source code is governed under the Apache License, Version 2.0
+# that can be found in the LICENSE file.
+
+"""Launches multiple builds at the same revision."""
+
+from recipe_engine.config import List
+from recipe_engine.config import Single
+from recipe_engine.recipe_api import Property
+
+
+DEPS = [
+    'buildbucket',
+    'properties',
+]
+
+PROPERTIES = {
+    'build_requests': Property(
+        kind=List(dict),
+        help='List of params to buildbucket.schedule_request for builds'
+        ' to trigger.'),
+    'collect_builds': Property(
+        kind=Single(bool),
+        default=False,
+        help='Whether to wait for child builds and surface failures.'),
+}
+
+
+def RunSteps(api, build_requests, collect_builds):
+  builds_to_schedule = []
+  for params in build_requests:
+    builds_to_schedule.append(api.buildbucket.schedule_request(**params))
+  if collect_builds:
+      api.buildbucket.run(builds_to_schedule, raise_if_unsuccessful=True)
+  else:
+      api.buildbucket.schedule(builds_to_schedule)
+
+
+def GenTests(api):
+  yield (
+      api.test('basic') +
+      api.properties(
+          build_requests=[
+              {
+                  'builder': 'linux',
+                  'project': 'chromium',
+                  'bucket': 'ci',
+              },
+              {
+                  'builder': 'win',
+                  'project': 'chromium',
+                  'bucket': 'ci',
+              },
+              {
+                  'builder': 'mac',
+                  'project': 'chromium',
+                  'bucket': 'ci',
+              },
+          ])
+  )
+  yield (
+      api.test('collect') +
+      api.properties(
+          build_requests=[
+              {
+                  'builder': 'linux',
+                  'project': 'chromium',
+                  'bucket': 'ci',
+              },
+              {
+                  'builder': 'win',
+                  'project': 'chromium',
+                  'bucket': 'ci',
+              },
+              {
+                  'builder': 'mac',
+                  'project': 'chromium',
+                  'bucket': 'ci',
+              },
+          ],
+          collect_builds=True,
+      )
+  )
diff --git a/recipe_modules/buildbucket/tests/collect.expected/with mocking and failure raising.json b/recipe_modules/buildbucket/tests/collect.expected/with mocking and failure raising.json
new file mode 100644
index 0000000..9f84db2
--- /dev/null
+++ b/recipe_modules/buildbucket/tests/collect.expected/with mocking and failure raising.json
@@ -0,0 +1,119 @@
+[
+  {
+    "cmd": [
+      "buildbucket",
+      "collect",
+      "-host",
+      "cr-buildbucket.appspot.com",
+      "-json-output",
+      "/path/to/tmp/json",
+      "-interval",
+      "30s",
+      "9016911228971028736"
+    ],
+    "infra_step": true,
+    "name": "collect1",
+    "timeout": 3600,
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"builder\": \"builder\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"project\": \"project\"@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createTime\": \"2018-05-25T23:50:17Z\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createdBy\": \"user:luci-scheduler@appspot.gserviceaccount.com\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"id\": \"9016911228971028736\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"input\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"gitilesCommit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"host\": \"chromium.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"2d72510e447ab60a9728aeea2362d8be2cbd7789\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"project\": \"project\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"ref\": \"refs/heads/master\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"status\": \"INFRA_FAILURE\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_EXCEPTION@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "buildbucket",
+      "collect",
+      "-host",
+      "cr-buildbucket.appspot.com",
+      "-json-output",
+      "/path/to/tmp/json",
+      "-interval",
+      "60s",
+      "9016911228971028737",
+      "123456789012345678"
+    ],
+    "infra_step": true,
+    "name": "buildbucket.collect",
+    "timeout": 600,
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"bucket\": \"try\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"builder\": \"builder\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"project\": \"project\"@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createTime\": \"2018-05-25T23:50:17Z\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createdBy\": \"user:commit-bot@chromium.org\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"id\": \"9016911228971028737\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"input\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"gerritChanges\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@        {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"change\": \"123456\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"host\": \"chromium-review.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"patchset\": \"7\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"project\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }@@@",
+      "@@@STEP_LOG_LINE@json.output@      ]@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"status\": \"SUCCESS\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"builder\": \"builder\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"project\": \"project\"@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createTime\": \"2018-05-25T23:50:17Z\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createdBy\": \"user:luci-scheduler@appspot.gserviceaccount.com\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"id\": \"123456789012345678\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"input\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"gitilesCommit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"host\": \"chromium.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"2d72510e447ab60a9728aeea2362d8be2cbd7789\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"project\": \"project\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"ref\": \"refs/heads/master\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"status\": \"FAILURE\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@unsuccessful_builds@123456789012345678@@@",
+      "@@@STEP_LOG_END@unsuccessful_builds@@@",
+      "@@@STEP_FAILURE@@@"
+    ]
+  },
+  {
+    "failure": {
+      "exception": {
+        "traceback": [
+          "<omitted by recipe engine>"
+        ]
+      },
+      "humanReason": "Triggered build(s) did not succeed, unexpectedly"
+    },
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/buildbucket/tests/collect.py b/recipe_modules/buildbucket/tests/collect.py
index 175182f..e55c2a5 100644
--- a/recipe_modules/buildbucket/tests/collect.py
+++ b/recipe_modules/buildbucket/tests/collect.py
@@ -4,6 +4,7 @@
 
 DEPS = [
   'buildbucket',
+  'properties',
 ]
 
 
@@ -12,7 +13,8 @@
       9016911228971028736, interval=30, step_name='collect1',
       mirror_status=True)
   api.buildbucket.collect_builds(
-      [9016911228971028737, 123456789012345678], timeout=600)
+      [9016911228971028737, 123456789012345678], timeout=600,
+      raise_if_unsuccessful=api.properties.get('raise_if_unsuccessful', False))
 
 
 def GenTests(api):
@@ -33,3 +35,20 @@
             build_id=123456789012345678, status='FAILURE'),
       ])
   )
+
+  yield (
+      api.test('with mocking and failure raising') +
+      api.properties(raise_if_unsuccessful=True) +
+      api.buildbucket.simulated_collect_output(
+        [
+          api.buildbucket.ci_build_message(
+              build_id=9016911228971028736, status='INFRA_FAILURE'),
+        ],
+        step_name='collect1') +
+      api.buildbucket.simulated_collect_output([
+        api.buildbucket.try_build_message(
+            build_id=9016911228971028737, status='SUCCESS'),
+        api.buildbucket.ci_build_message(
+            build_id=123456789012345678, status='FAILURE'),
+      ])
+  )
diff --git a/recipe_modules/buildbucket/tests/schedule.expected/mirror_failure.json b/recipe_modules/buildbucket/tests/schedule.expected/mirror_failure.json
new file mode 100644
index 0000000..2758ed5
--- /dev/null
+++ b/recipe_modules/buildbucket/tests/schedule.expected/mirror_failure.json
@@ -0,0 +1,198 @@
+[
+  {
+    "cmd": [
+      "bb",
+      "batch",
+      "-host",
+      "cr-buildbucket.appspot.com"
+    ],
+    "infra_step": true,
+    "name": "buildbucket.schedule",
+    "stdin": "{\"requests\": [{\"scheduleBuild\": {\"builder\": {\"bucket\": \"try\", \"builder\": \"linux\", \"project\": \"chromium\"}, \"gerritChanges\": [{\"change\": \"123456\", \"host\": \"chromium-review.googlesource.com\", \"patchset\": \"7\", \"project\": \"chromium/src\"}], \"gitilesCommit\": {\"host\": \"chromium.googlesource.com\", \"id\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", \"project\": \"chromium/src\"}, \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", \"tags\": [{\"key\": \"buildset\", \"value\": \"bs\"}, {\"key\": \"user_agent\", \"value\": \"recipe\"}]}}]}",
+    "stdout": "/path/to/tmp/json",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"responses\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"try\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514000\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  ]@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@request@{@@@",
+      "@@@STEP_LOG_LINE@request@  \"requests\": [@@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"try\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"gerritChanges\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"project\": \"chromium/src\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"host\": \"chromium-review.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"patchset\": \"7\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"change\": \"123456\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ], @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"bs\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"buildset\"@@@",
+      "@@@STEP_LOG_LINE@request@          }, @@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ], @@@",
+      "@@@STEP_LOG_LINE@request@        \"gitilesCommit\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium/src\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"host\": \"chromium.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"id\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"@@@",
+      "@@@STEP_LOG_LINE@request@        }@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }@@@",
+      "@@@STEP_LOG_LINE@request@  ]@@@",
+      "@@@STEP_LOG_LINE@request@}@@@",
+      "@@@STEP_LOG_END@request@@@",
+      "@@@STEP_LINK@8922054662172514000@https://cr-buildbucket.appspot.com/build/8922054662172514000@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "buildbucket.run"
+  },
+  {
+    "cmd": [
+      "bb",
+      "batch",
+      "-host",
+      "cr-buildbucket.appspot.com"
+    ],
+    "infra_step": true,
+    "name": "buildbucket.run.schedule",
+    "stdin": "{\"requests\": [{\"scheduleBuild\": {\"builder\": {\"bucket\": \"try\", \"builder\": \"linux\", \"project\": \"chromium\"}, \"gerritChanges\": [{\"change\": \"123456\", \"host\": \"chromium-review.googlesource.com\", \"patchset\": \"7\", \"project\": \"chromium/src\"}], \"gitilesCommit\": {\"host\": \"chromium.googlesource.com\", \"id\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", \"project\": \"chromium/src\"}, \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", \"tags\": [{\"key\": \"buildset\", \"value\": \"bs\"}, {\"key\": \"user_agent\", \"value\": \"recipe\"}]}}]}",
+    "stdout": "/path/to/tmp/json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"responses\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@    {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"bucket\": \"try\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"project\": \"chromium\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }, @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8922054662172514001\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  ]@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@request@{@@@",
+      "@@@STEP_LOG_LINE@request@  \"requests\": [@@@",
+      "@@@STEP_LOG_LINE@request@    {@@@",
+      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
+      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"builder\": \"linux\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bucket\": \"try\"@@@",
+      "@@@STEP_LOG_LINE@request@        }, @@@",
+      "@@@STEP_LOG_LINE@request@        \"gerritChanges\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"project\": \"chromium/src\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"host\": \"chromium-review.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"patchset\": \"7\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"change\": \"123456\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ], @@@",
+      "@@@STEP_LOG_LINE@request@        \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", @@@",
+      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"bs\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"buildset\"@@@",
+      "@@@STEP_LOG_LINE@request@          }, @@@",
+      "@@@STEP_LOG_LINE@request@          {@@@",
+      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\", @@@",
+      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\"@@@",
+      "@@@STEP_LOG_LINE@request@          }@@@",
+      "@@@STEP_LOG_LINE@request@        ], @@@",
+      "@@@STEP_LOG_LINE@request@        \"gitilesCommit\": {@@@",
+      "@@@STEP_LOG_LINE@request@          \"project\": \"chromium/src\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"host\": \"chromium.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"id\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"@@@",
+      "@@@STEP_LOG_LINE@request@        }@@@",
+      "@@@STEP_LOG_LINE@request@      }@@@",
+      "@@@STEP_LOG_LINE@request@    }@@@",
+      "@@@STEP_LOG_LINE@request@  ]@@@",
+      "@@@STEP_LOG_LINE@request@}@@@",
+      "@@@STEP_LOG_END@request@@@",
+      "@@@STEP_LINK@8922054662172514001@https://cr-buildbucket.appspot.com/build/8922054662172514001@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "buildbucket",
+      "collect",
+      "-host",
+      "cr-buildbucket.appspot.com",
+      "-json-output",
+      "/path/to/tmp/json",
+      "-interval",
+      "60s",
+      "8922054662172514001"
+    ],
+    "infra_step": true,
+    "name": "buildbucket.run.collect",
+    "timeout": 3600,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"builder\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"bucket\": \"ci\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"builder\": \"builder\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"project\": \"project\"@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createTime\": \"2018-05-25T23:50:17Z\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"createdBy\": \"user:luci-scheduler@appspot.gserviceaccount.com\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"id\": \"8922054662172514001\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"input\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"gitilesCommit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"host\": \"chromium.googlesource.com\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"id\": \"2d72510e447ab60a9728aeea2362d8be2cbd7789\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"project\": \"project\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"ref\": \"refs/heads/master\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"status\": \"FAILURE\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@unsuccessful_builds@8922054662172514001@@@",
+      "@@@STEP_LOG_END@unsuccessful_builds@@@",
+      "@@@STEP_FAILURE@@@"
+    ]
+  },
+  {
+    "failure": {
+      "exception": {
+        "traceback": [
+          "<omitted by recipe engine>"
+        ]
+      },
+      "humanReason": "Triggered build(s) did not succeed, unexpectedly"
+    },
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/buildbucket/tests/schedule.py b/recipe_modules/buildbucket/tests/schedule.py
index a3b3565..02f424c 100644
--- a/recipe_modules/buildbucket/tests/schedule.py
+++ b/recipe_modules/buildbucket/tests/schedule.py
@@ -23,7 +23,8 @@
   req = api.buildbucket.schedule_request(tags=tags, **req_body)
   api.buildbucket.schedule([req])
 
-  api.buildbucket.run([req])
+  api.buildbucket.run([req], raise_if_unsuccessful=api.properties.get(
+      'raise_failed_status'))
 
 
 def GenTests(api):
@@ -94,3 +95,11 @@
       test(test_name='error') +
       api.buildbucket.simulated_schedule_output(err_batch_res)
   )
+  yield (
+      test(test_name='mirror_failure') +
+      api.properties(raise_failed_status=True) +
+      api.buildbucket.simulated_collect_output([
+        api.buildbucket.ci_build_message(
+            build_id=8922054662172514001, status='FAILURE'),
+      ], step_name='buildbucket.run.collect')
+  )