[lkgr] Add a 'last known good revision' recipe
This recipe forwards a ref to the last known good revision (LKGR). The
LKGR is the last revision that had successful builds on a configured
set of builders.
https://github.com/dart-lang/sdk/issues/42917
Change-Id: Ic7cccfe3c46e341bba293b1a667bdaaa92107bc6
Reviewed-on: https://dart-review.googlesource.com/c/recipes/+/157501
Commit-Queue: Alexander Thomas <athom@google.com>
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/README.recipes.md b/README.recipes.md
index 8d235eb..90ee9d5 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -21,6 +21,7 @@
* [dart:examples/example](#recipes-dart_examples_example)
* [dart:examples/example-get_secret](#recipes-dart_examples_example-get_secret)
* [presubmit/presubmit](#recipes-presubmit_presubmit)
+ * [roller/lkgr](#recipes-roller_lkgr)
## Recipe Modules
### *recipe_modules* / [bisect\_build](/recipe_modules/bisect_build)
@@ -204,6 +205,11 @@
[DEPS](/recipes/presubmit/presubmit.py#10): [depot\_tools/gclient][depot_tools/recipe_modules/gclient], [depot\_tools/presubmit][depot_tools/recipe_modules/presubmit], [depot\_tools/tryserver][depot_tools/recipe_modules/tryserver], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path]
— **def [RunSteps](/recipes/presubmit/presubmit.py#21)(api):**
+### *recipes* / [roller/lkgr](/recipes/roller/lkgr.py)
+
+[DEPS](/recipes/roller/lkgr.py#17): [depot\_tools/bot\_update][depot_tools/recipe_modules/bot_update], [depot\_tools/gclient][depot_tools/recipe_modules/gclient], [depot\_tools/git][depot_tools/recipe_modules/git], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/runtime][recipe_engine/recipe_modules/runtime], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+— **def [RunSteps](/recipes/roller/lkgr.py#33)(api, properties):**
[build/recipe_modules/goma]: https://chromium.googlesource.com/chromium/tools/build.git/+/ef467ff44548c79ab45d4cc910427d2a176abae6/scripts/slave/README.recipes.md#recipe_modules-goma
[depot_tools/recipe_modules/bot_update]: https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/8351dc1fb7c4eb1858096c51ffe3f45754877cb0/recipes/README.recipes.md#recipe_modules-bot_update
diff --git a/recipes/roller/lkgr.expected/error.json b/recipes/roller/lkgr.expected/error.json
new file mode 100644
index 0000000..fcf1b24
--- /dev/null
+++ b/recipes/roller/lkgr.expected/error.json
@@ -0,0 +1,95 @@
+[
+ {
+ "cmd": [
+ "bb",
+ "batch"
+ ],
+ "infra_step": true,
+ "name": "search for good builds",
+ "stdin": "{\"requests\": [{\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"a\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"b\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"c\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}]}",
+ "timeout": 300,
+ "~followup_annotations": [
+ "@@@STEP_TEXT@Request #0<br>Status code: 1<br>Message: search failed<br>@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@{@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"responses\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"error\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"code\": 1, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"message\": \"search failed\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@}@@@",
+ "@@@STEP_LOG_END@json.output[response]@@@",
+ "@@@STEP_LOG_LINE@request@{@@@",
+ "@@@STEP_LOG_LINE@request@ \"requests\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@}@@@",
+ "@@@STEP_LOG_END@request@@@",
+ "@@@STEP_EXCEPTION@@@"
+ ]
+ },
+ {
+ "failure": {
+ "humanReason": "Infra Failure: Step('search for good builds') (retcode: 1)"
+ },
+ "name": "$result"
+ }
+]
\ No newline at end of file
diff --git a/recipes/roller/lkgr.expected/no-builds.json b/recipes/roller/lkgr.expected/no-builds.json
new file mode 100644
index 0000000..8d1cae2
--- /dev/null
+++ b/recipes/roller/lkgr.expected/no-builds.json
@@ -0,0 +1,95 @@
+[
+ {
+ "cmd": [
+ "bb",
+ "batch"
+ ],
+ "infra_step": true,
+ "name": "search for good builds",
+ "stdin": "{\"requests\": [{\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"a\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"b\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"c\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}]}",
+ "timeout": 300,
+ "~followup_annotations": [
+ "@@@STEP_LOG_LINE@json.output[response]@{@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"responses\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"searchBuilds\": {}@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@}@@@",
+ "@@@STEP_LOG_END@json.output[response]@@@",
+ "@@@STEP_LOG_LINE@request@{@@@",
+ "@@@STEP_LOG_LINE@request@ \"requests\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@}@@@",
+ "@@@STEP_LOG_END@request@@@"
+ ]
+ },
+ {
+ "cmd": [],
+ "name": "no good hashes found",
+ "~followup_annotations": [
+ "@@@STEP_LOG_LINE@builds@{}@@@",
+ "@@@STEP_LOG_END@builds@@@"
+ ]
+ },
+ {
+ "name": "$result"
+ }
+]
\ No newline at end of file
diff --git a/recipes/roller/lkgr.expected/no-intersection.json b/recipes/roller/lkgr.expected/no-intersection.json
new file mode 100644
index 0000000..a468205
--- /dev/null
+++ b/recipes/roller/lkgr.expected/no-intersection.json
@@ -0,0 +1,221 @@
+[
+ {
+ "cmd": [
+ "bb",
+ "batch"
+ ],
+ "infra_step": true,
+ "name": "search for good builds",
+ "stdin": "{\"requests\": [{\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"a\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"b\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"c\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}]}",
+ "timeout": 300,
+ "~followup_annotations": [
+ "@@@STEP_LOG_LINE@json.output[response]@{@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"responses\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builds\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"0\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"1\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"4\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builds\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"1\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"2\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"3\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builds\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"2\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"3\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@}@@@",
+ "@@@STEP_LOG_END@json.output[response]@@@",
+ "@@@STEP_LOG_LINE@request@{@@@",
+ "@@@STEP_LOG_LINE@request@ \"requests\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@}@@@",
+ "@@@STEP_LOG_END@request@@@"
+ ]
+ },
+ {
+ "cmd": [],
+ "name": "no good hashes found",
+ "~followup_annotations": [
+ "@@@STEP_LOG_LINE@builds@{@@@",
+ "@@@STEP_LOG_LINE@builds@ \"a\": [@@@",
+ "@@@STEP_LOG_LINE@builds@ \"0\", @@@",
+ "@@@STEP_LOG_LINE@builds@ \"1\", @@@",
+ "@@@STEP_LOG_LINE@builds@ \"4\"@@@",
+ "@@@STEP_LOG_LINE@builds@ ], @@@",
+ "@@@STEP_LOG_LINE@builds@ \"b\": [@@@",
+ "@@@STEP_LOG_LINE@builds@ \"1\", @@@",
+ "@@@STEP_LOG_LINE@builds@ \"2\", @@@",
+ "@@@STEP_LOG_LINE@builds@ \"3\"@@@",
+ "@@@STEP_LOG_LINE@builds@ ], @@@",
+ "@@@STEP_LOG_LINE@builds@ \"c\": [@@@",
+ "@@@STEP_LOG_LINE@builds@ \"2\", @@@",
+ "@@@STEP_LOG_LINE@builds@ \"3\"@@@",
+ "@@@STEP_LOG_LINE@builds@ ]@@@",
+ "@@@STEP_LOG_LINE@builds@}@@@",
+ "@@@STEP_LOG_END@builds@@@"
+ ]
+ },
+ {
+ "name": "$result"
+ }
+]
\ No newline at end of file
diff --git a/recipes/roller/lkgr.expected/push.json b/recipes/roller/lkgr.expected/push.json
new file mode 100644
index 0000000..436e7d6
--- /dev/null
+++ b/recipes/roller/lkgr.expected/push.json
@@ -0,0 +1,253 @@
+[
+ {
+ "cmd": [
+ "bb",
+ "batch"
+ ],
+ "infra_step": true,
+ "name": "search for good builds",
+ "stdin": "{\"requests\": [{\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"a\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"b\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}, {\"searchBuilds\": {\"fields\": \"builds.*.output.properties,builds.*.builder\", \"predicate\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"c\", \"project\": \"dart\"}, \"status\": \"SUCCESS\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"luci-scheduler\"}]}}}]}",
+ "timeout": 300,
+ "~followup_annotations": [
+ "@@@STEP_LOG_LINE@json.output[response]@{@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"responses\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builds\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"2\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builds\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"1\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"2\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"3\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builds\": [@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"2\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"output\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"properties\": {@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ \"got_revision\": \"3\"@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ }@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@ ]@@@",
+ "@@@STEP_LOG_LINE@json.output[response]@}@@@",
+ "@@@STEP_LOG_END@json.output[response]@@@",
+ "@@@STEP_LOG_LINE@request@{@@@",
+ "@@@STEP_LOG_LINE@request@ \"requests\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"a\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"b\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@ \"searchBuilds\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"fields\": \"builds.*.output.properties,builds.*.builder\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"predicate\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": {@@@",
+ "@@@STEP_LOG_LINE@request@ \"bucket\": \"ci\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"builder\": \"c\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"project\": \"dart\"@@@",
+ "@@@STEP_LOG_LINE@request@ }, @@@",
+ "@@@STEP_LOG_LINE@request@ \"status\": \"SUCCESS\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"tags\": [@@@",
+ "@@@STEP_LOG_LINE@request@ {@@@",
+ "@@@STEP_LOG_LINE@request@ \"key\": \"user_agent\", @@@",
+ "@@@STEP_LOG_LINE@request@ \"value\": \"luci-scheduler\"@@@",
+ "@@@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_LINE@request@}@@@",
+ "@@@STEP_LOG_END@request@@@"
+ ]
+ },
+ {
+ "cmd": [
+ "python",
+ "-u",
+ "RECIPE_MODULE[depot_tools::bot_update]/resources/bot_update.py",
+ "--spec-path",
+ "cache_dir = '[CACHE]/git'\nsolutions = [{'deps_file': 'DEPS', 'managed': False, 'name': 'sdk', 'url': 'https://dart.googlesource.com/sdk.git'}]",
+ "--revision_mapping_file",
+ "{}",
+ "--git-cache-dir",
+ "[CACHE]/git",
+ "--cleanup-dir",
+ "[CLEANUP]/bot_update",
+ "--output_json",
+ "/path/to/tmp/json",
+ "--revision",
+ "sdk@HEAD",
+ "--no_fetch_tags"
+ ],
+ "cwd": "[CACHE]/builder",
+ "env": {
+ "GIT_HTTP_LOW_SPEED_LIMIT": "102400",
+ "GIT_HTTP_LOW_SPEED_TIME": "300"
+ },
+ "env_suffixes": {
+ "DEPOT_TOOLS_UPDATE": [
+ "0"
+ ],
+ "PATH": [
+ "RECIPE_REPO[depot_tools]"
+ ]
+ },
+ "infra_step": true,
+ "name": "bot_update",
+ "timeout": 1080,
+ "~followup_annotations": [
+ "@@@STEP_TEXT@Some step text@@@",
+ "@@@STEP_LOG_LINE@json.output@{@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"did_run\": true, @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"fixed_revisions\": {@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"sdk\": \"HEAD\"@@@",
+ "@@@STEP_LOG_LINE@json.output@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"manifest\": {@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"sdk\": {@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"repository\": \"https://fake.org/sdk.git\", @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"revision\": \"5a374dcd2e5eb762b527af3a5bab6072a4d24493\"@@@",
+ "@@@STEP_LOG_LINE@json.output@ }@@@",
+ "@@@STEP_LOG_LINE@json.output@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"patch_failure\": false, @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"patch_root\": \"sdk\", @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"properties\": {}, @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"root\": \"sdk\", @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"source_manifest\": {@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"directories\": {@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"sdk\": {@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"git_checkout\": {@@@",
+ "@@@STEP_LOG_LINE@json.output@ \"repo_url\": \"https://fake.org/sdk.git\", @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"revision\": \"5a374dcd2e5eb762b527af3a5bab6072a4d24493\"@@@",
+ "@@@STEP_LOG_LINE@json.output@ }@@@",
+ "@@@STEP_LOG_LINE@json.output@ }@@@",
+ "@@@STEP_LOG_LINE@json.output@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"version\": 0@@@",
+ "@@@STEP_LOG_LINE@json.output@ }, @@@",
+ "@@@STEP_LOG_LINE@json.output@ \"step_text\": \"Some step text\"@@@",
+ "@@@STEP_LOG_LINE@json.output@}@@@",
+ "@@@STEP_LOG_END@json.output@@@"
+ ]
+ },
+ {
+ "cmd": [
+ "git",
+ "push",
+ "https://dart.googlesource.com/sdk.git",
+ "2:refs/heads/lkgr"
+ ],
+ "cwd": "[CACHE]/builder/sdk",
+ "infra_step": true,
+ "name": "push 2 to refs/heads/lkgr"
+ },
+ {
+ "name": "$result"
+ }
+]
\ No newline at end of file
diff --git a/recipes/roller/lkgr.proto b/recipes/roller/lkgr.proto
new file mode 100644
index 0000000..1856269
--- /dev/null
+++ b/recipes/roller/lkgr.proto
@@ -0,0 +1,16 @@
+// Copyright (c) 2020, the Dart project authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in the
+// LICENSE file.
+syntax = "proto3";
+
+package recipes.dart.roller.lkgr;
+
+import "go.chromium.org/luci/buildbucket/proto/builder.proto";
+
+message LastKnownGoodRevision {
+ // The builders that are required to be green to advance the LKGR.
+ repeated .buildbucket.v2.BuilderID builders = 1;
+
+ // The ref to advance if the builders are green.
+ string ref = 2;
+}
\ No newline at end of file
diff --git a/recipes/roller/lkgr.py b/recipes/roller/lkgr.py
new file mode 100644
index 0000000..c510f0d
--- /dev/null
+++ b/recipes/roller/lkgr.py
@@ -0,0 +1,245 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from recipe_engine import post_process
+from google.protobuf import field_mask_pb2
+from google.protobuf import json_format
+from google.protobuf.struct_pb2 import Struct
+
+from PB.go.chromium.org.luci.buildbucket.proto import build as build_pb2
+from PB.go.chromium.org.luci.buildbucket.proto import builder as builder_pb2
+from PB.go.chromium.org.luci.buildbucket.proto import builds_service as builds_service_pb2
+from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
+
+from PB.recipes.dart.roller.lkgr import LastKnownGoodRevision
+
+DEPS = [
+ 'depot_tools/bot_update',
+ 'depot_tools/gclient',
+ 'depot_tools/git',
+ 'recipe_engine/context',
+ 'recipe_engine/json',
+ 'recipe_engine/path',
+ 'recipe_engine/properties',
+ 'recipe_engine/raw_io',
+ 'recipe_engine/runtime',
+ 'recipe_engine/step',
+]
+
+PROPERTIES = LastKnownGoodRevision
+
+
+def RunSteps(api, properties):
+ builders = properties.builders
+ assert builders, 'the recipe requires builders to be specified'
+ ref = properties.ref
+ assert ref, 'the recipe requires a ref to be specified'
+ builds = _find_good_builds(api, builders)
+ commit_hash = _find_good_hash(api, builders, builds)
+
+ # Guard against the empty string to avoid deleting the remote branch
+ if not commit_hash:
+ step = api.step('no good hashes found', None)
+ step.presentation.logs['builds'] = _pretty_print(api, builds)
+ return
+
+ api.gclient.set_config('dart')
+ with api.context(cwd=api.path['cache'].join('builder')):
+ api.bot_update.ensure_checkout(timeout=1080, no_fetch_tags=True)
+
+ git_args = ['push']
+ if api.runtime.is_experimental:
+ git_args.append('--dry-run')
+ git_args += [
+ 'https://dart.googlesource.com/sdk.git',
+ '%s:%s' % (commit_hash, ref),
+ ]
+ api.git(*git_args, name='push %s to %s' % (commit_hash, ref))
+
+
+def _find_good_builds(api, builders):
+ batch_request = builds_service_pb2.BatchRequest(
+ requests=[dict(search_builds=_search_req(api, b)) for b in builders])
+ request_dict = json_format.MessageToDict(batch_request)
+ try:
+ api.step(
+ 'search for good builds',
+ ['bb', 'batch'],
+ infra_step=True,
+ stdin=api.json.input(request_dict),
+ stdout=api.json.output(name='response'),
+ timeout=5 * 60, # 5 minutes
+ )
+ finally:
+ step_result = api.step.active_result
+ step_result.presentation.logs['request'] = _pretty_print(api, request_dict)
+
+ batch_result = builds_service_pb2.BatchResponse()
+ json_format.ParseDict(
+ step_result.stdout or {},
+ batch_result,
+ # Do not fail the build because recipe's proto copy is stale.
+ ignore_unknown_fields=True)
+
+ builds = {}
+ step_text = []
+ for i, result in enumerate(batch_result.responses):
+ # Print response errors in step text.
+ if result.HasField('error'):
+ step_text.extend([
+ 'Request #%d' % i,
+ 'Status code: %s' % result.error.code,
+ 'Message: %s' % result.error.message,
+ '', # Blank line.
+ ])
+ elif result.search_builds.builds:
+ commit = lambda build: build.output.properties['got_revision']
+ commit_hashes = map(commit, result.search_builds.builds)
+ builder = result.search_builds.builds[0].builder.builder
+ builds[builder] = commit_hashes
+ step_result.presentation.step_text = '<br>'.join(step_text)
+
+ return builds
+
+
+def _search_req(api, builder):
+ return builds_service_pb2.SearchBuildsRequest(
+ predicate=builds_service_pb2.BuildPredicate(
+ builder=builder,
+ status=common_pb2.SUCCESS,
+ tags=[
+ # Ignore non-standard builds (e.g. bisections)
+ common_pb2.StringPair(key='user_agent', value='luci-scheduler')
+ ],
+ ),
+ fields=field_mask_pb2.FieldMask(
+ paths=['builds.*.output.properties', 'builds.*.builder']))
+
+
+def _find_good_hash(api, builders, builds):
+ first = builders[0]
+ rest = builders[1:]
+
+ # The builds are sorted newest-to-oldest.
+ candidates = builds.get(first.builder, [])
+ if not candidates:
+ return None
+ intersection = set(candidates)
+ for builder in rest:
+ intersection.intersection_update(builds.get(builder.builder, []))
+ if not intersection:
+ return None
+ for commit_hash in candidates:
+ if commit_hash in intersection:
+ return commit_hash
+
+ assert False, 'A commit hash must be in the intersection.' # pragma: no cover
+
+
+def _pretty_print(api, the_dict):
+ return api.json.dumps(the_dict, indent=2, sort_keys=True).splitlines()
+
+
+def GenTests(api):
+ yield api.test(
+ 'no-builders-no-steps',
+ api.post_process(post_process.DoesNotRunRE, '(search|push).*'),
+ api.expect_exception('AssertionError'),
+ api.post_process(post_process.StatusException),
+ api.post_process(post_process.DropExpectation),
+ )
+
+ builders = [
+ builder_pb2.BuilderID(project='dart', bucket='ci', builder=builder)
+ for builder in ['a', 'b', 'c']
+ ]
+ input_properties = api.properties(
+ LastKnownGoodRevision(builders=builders, ref='refs/heads/lkgr'))
+
+ def _search_response(builds):
+ return dict(
+ search_builds=builds_service_pb2.SearchBuildsResponse(builds=builds))
+
+ no_builds_response = json_format.MessageToDict(
+ builds_service_pb2.BatchResponse(responses=[_search_response([])]))
+ yield api.test(
+ 'no-builds',
+ input_properties,
+ api.step_data(
+ 'search for good builds', stdout=api.json.output(no_builds_response)),
+ api.post_process(post_process.MustRun, 'no good hashes found'),
+ api.post_process(post_process.DoesNotRunRE, 'push.*'),
+ api.post_process(post_process.StatusSuccess),
+ )
+
+ yield api.test(
+ 'error',
+ input_properties,
+ api.step_data(
+ 'search for good builds',
+ retcode=1,
+ stdout=api.json.output(
+ json_format.MessageToDict(
+ builds_service_pb2.BatchResponse(responses=[
+ dict(error=dict(code=1, message='search failed'))
+ ])))),
+ api.post_process(post_process.DoesNotRunRE, '(no|push).*'),
+ api.post_process(post_process.StatusException),
+ )
+
+ def build(builder, got_revision):
+ properties = Struct()
+ properties.update({'got_revision': got_revision})
+ return build_pb2.Build(
+ builder=builder,
+ output=build_pb2.Build.Output(properties=properties),
+ )
+
+ responses = []
+ responses.append(_search_response([build(builders[0], '2')]))
+ responses.append(
+ _search_response(
+ [build(builders[1], commit) for commit in ['1', '2', '3']]))
+ responses.append(
+ _search_response([build(builders[2], commit) for commit in ['2', '3']]))
+ test_response = builds_service_pb2.BatchResponse(responses=responses)
+ search_data = api.step_data(
+ 'search for good builds',
+ stdout=api.json.output(json_format.MessageToDict(test_response)))
+ yield api.test(
+ 'push',
+ input_properties,
+ search_data,
+ api.post_process(post_process.MustRun, 'push 2 to refs/heads/lkgr'),
+ api.post_process(post_process.DoesNotRunRE, 'no good hashes found'),
+ api.post_process(post_process.StatusSuccess),
+ )
+ yield api.test(
+ 'dry-run',
+ input_properties,
+ search_data,
+ api.runtime(is_luci=True, is_experimental=True),
+ api.post_process(post_process.MustRun, 'push 2 to refs/heads/lkgr'),
+ api.post_process(post_process.DoesNotRunRE, 'no good hashes found'),
+ api.post_process(post_process.StepCommandContains,
+ 'push 2 to refs/heads/lkgr', ['--dry-run']),
+ api.post_process(post_process.StatusSuccess),
+ api.post_process(post_process.DropExpectation),
+ )
+
+ no_intersection = _search_response(
+ [build(builders[0], commit) for commit in ['0', '1', '4']])
+ no_intersection_response = json_format.MessageToDict(
+ builds_service_pb2.BatchResponse(
+ responses=[no_intersection, responses[1], responses[2]]))
+ yield api.test(
+ 'no-intersection',
+ input_properties,
+ api.step_data(
+ 'search for good builds',
+ stdout=api.json.output(no_intersection_response)),
+ api.post_process(post_process.MustRun, 'no good hashes found'),
+ api.post_process(post_process.DoesNotRunRE, 'push.*'),
+ api.post_process(post_process.StatusSuccess),
+ )