[run_script] Merge functionality from run_and_commit.py

This has no behavioral change until builders are migrated to this recipe
and enable the `attempt_roll` flag.

As a bonus, this can be used to upload packages to CIPD immediately
after a successful roll.

Bug: 75278
Change-Id: Ie94ed739549123f023adc65b9378b574bf10a705
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/recipes/+/519845
Commit-Queue: Anthony Fandrianto <atyfto@google.com>
Reviewed-by: Oliver Newman <olivernewman@google.com>
diff --git a/recipes/run_script.expected/attempt_roll.json b/recipes/run_script.expected/attempt_roll.json
new file mode 100644
index 0000000..1beab55
--- /dev/null
+++ b/recipes/run_script.expected/attempt_roll.json
@@ -0,0 +1,578 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout"
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/checkout"
+    ],
+    "infra_step": true,
+    "name": "checkout.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "remote",
+      "add",
+      "origin",
+      "https://fuchsia.googlesource.com/foo"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.git remote",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "fetch.uriprotocols",
+      "https"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.set fetch.uriprotocols",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout.cache",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "",
+      "[CACHE]/git/.GUARD_FILE"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.cache.write guard file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@.GUARD_FILE@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/git/fuchsia.googlesource.com-foo"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.cache.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init",
+      "--bare"
+    ],
+    "cwd": "[CACHE]/git/fuchsia.googlesource.com-foo",
+    "infra_step": true,
+    "name": "checkout.cache.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "remote.origin.url",
+      "https://fuchsia.googlesource.com/foo"
+    ],
+    "cwd": "[CACHE]/git/fuchsia.googlesource.com-foo",
+    "infra_step": true,
+    "name": "checkout.cache.remote set-url",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "fetch.uriprotocols",
+      "https"
+    ],
+    "cwd": "[CACHE]/git/fuchsia.googlesource.com-foo",
+    "infra_step": true,
+    "name": "checkout.cache.set fetch.uriprotocols",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "--replace-all",
+      "remote.origin.fetch",
+      "+refs/heads/*:refs/heads/*",
+      "\\+refs/heads/\\*:.*"
+    ],
+    "cwd": "[CACHE]/git/fuchsia.googlesource.com-foo",
+    "infra_step": true,
+    "name": "checkout.cache.replace fetch configs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--prune",
+      "--tags",
+      "origin"
+    ],
+    "cwd": "[CACHE]/git/fuchsia.googlesource.com-foo",
+    "infra_step": true,
+    "name": "checkout.cache.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/checkout/.git/objects/info"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.cache.makedirs object/info",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CACHE]/git/fuchsia.googlesource.com-foo/objects\n",
+      "[START_DIR]/checkout/.git/objects/info/alternates"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.cache.alternates",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@alternates@[CACHE]/git/fuchsia.googlesource.com-foo/objects@@@",
+      "@@@STEP_LOG_END@alternates@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "remove",
+      "[CACHE]/git/.GUARD_FILE"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.cache.remove guard file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--tags",
+      "origin",
+      "master"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.git rev-parse",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout.submodule",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.submodule.git submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout.submodule.git submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "update.sh"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "name": "run update.sh"
+  },
+  {
+    "cmd": [
+      "git",
+      "ls-files",
+      "--modified",
+      "--deleted",
+      "--exclude-standard"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "name": "check for no-op commit",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@stdout@hello@@@",
+      "@@@STEP_LOG_END@stdout@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "add",
+      "--update"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "git add"
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "git rev-parse"
+  },
+  {
+    "cmd": [],
+    "name": "calculate Change-Id",
+    "~followup_annotations": [
+      "@@@STEP_TEXT@Ib92a62842ab6b0cc6777dcd33fb1c8532c7396ad@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "diff",
+      "--unified=0",
+      "--cached"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "calculate Change-Id.git diff",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@diff --git a/foo.txt b/foo.txt@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@--- a/foo.txt@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@+++ b/foo.txt@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@@@ -16 +16 @@@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@-        foo = 5@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@+        foo = 6@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@diff --git a/bar.txt b/bar.txt@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@--- a/bar.txt@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@+++ b/bar.txt@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@@@ -5 +5 @@@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@-        bar = 0@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@+        bar = 1@@@",
+      "@@@STEP_LOG_LINE@diff (without hashes)@@@@",
+      "@@@STEP_LOG_END@diff (without hashes)@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "hash-object",
+      "diff --git a/foo.txt b/foo.txt\n--- a/foo.txt\n+++ b/foo.txt\n@@ -16 +16 @@\n-        foo = 5\n+        foo = 6\ndiff --git a/bar.txt b/bar.txt\n--- a/bar.txt\n+++ b/bar.txt\n@@ -5 +5 @@\n-        bar = 0\n+        bar = 1\n############"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "calculate Change-Id.git hash-object",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "install infra/tools/luci/gerrit"
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/cipd/infra/tools/luci/gerrit/pinned-gerrit-version"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "install infra/tools/luci/gerrit.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CACHE]/cipd/infra/tools/luci/gerrit/pinned-gerrit-version",
+      "-ensure-file",
+      "infra/tools/luci/gerrit/${platform} pinned-gerrit-version",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "install infra/tools/luci/gerrit.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-pinned-gerrit-ve\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/gerrit/resolved-platform\"@@@",
+      "@@@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@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CACHE]/cipd/infra/tools/luci/gerrit/pinned-gerrit-version/gerrit",
+      "change-query",
+      "-host",
+      "https://fuchsia-review.googlesource.com",
+      "-input",
+      "{\"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\", \"MESSAGES\"], \"q\": \"change:Ib92a62842ab6b0cc6777dcd33fb1c8532c7396ad repo:foo branch:master\"}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "check for identical roll",
+    "timeout": 600,
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@null@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\", @@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\", @@@",
+      "@@@STEP_LOG_LINE@json.input@      \"MESSAGES\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ], @@@",
+      "@@@STEP_LOG_LINE@json.input@    \"q\": \"change:Ib92a62842ab6b0cc6777dcd33fb1c8532c7396ad repo:foo branch:master\"@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "commit",
+      "-m",
+      "Run \"update.sh\"\n\nRoller-URL: https://ci.chromium.org/b/0\nCq-Cl-Tag: roller-builder:\nCq-Cl-Tag: roller-bid:0\nCQ-Do-Not-Cancel-Tryjobs: true\nChange-Id: Ib92a62842ab6b0cc6777dcd33fb1c8532c7396ad",
+      "-a"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "git commit"
+  },
+  {
+    "cmd": [
+      "git",
+      "push",
+      "--push-option",
+      "nokeycheck",
+      "origin",
+      "HEAD:refs/for/master%l=Commit-Queue+2,l=Code-Review+2"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "git push",
+    "timeout": 180,
+    "~followup_annotations": [
+      "@@@STEP_LINK@gerrit link@https://fuchsia-review.googlesource.com/q/Ib92a62842ab6b0cc6777dcd33fb1c8532c7396ad@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "check for completion"
+  },
+  {
+    "cmd": [
+      "[CACHE]/cipd/infra/tools/luci/gerrit/pinned-gerrit-version/gerrit",
+      "change-detail",
+      "-host",
+      "https://fuchsia-review.googlesource.com",
+      "-input",
+      "{\"change_id\": \"foo~master~Ib92a62842ab6b0cc6777dcd33fb1c8532c7396ad\", \"params\": {\"o\": [\"CURRENT_REVISION\"]}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "check for completion.check if done (0)",
+    "timeout": 30,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"abc123\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"labels\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"Commit-Queue\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"approved\": {}@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"status\": \"MERGED\"@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"foo~master~Ib92a62842ab6b0cc6777dcd33fb1c8532c7396ad\", @@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ]@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipes/run_script.proto b/recipes/run_script.proto
index 9f2157d..014f286 100644
--- a/recipes/run_script.proto
+++ b/recipes/run_script.proto
@@ -28,4 +28,18 @@
 
   // Set tags on CIPD package(s) based on repo snapshot.
   bool set_repo_tags = 7;
+
+  // Whether to attempt a roll after the script updates the checkout.
+  bool attempt_roll = 8;
+
+  // Properties for roll attempt.
+  RollProperties roll_props = 9;
+
+  message RollProperties {
+    // Whether to commit untracked files.
+    bool commit_untracked_files = 1;
+
+    // Whether to dryrun the roll.
+    bool dry_run = 2;
+  }
 }
diff --git a/recipes/run_script.py b/recipes/run_script.py
index 09075ed..7b978a2 100644
--- a/recipes/run_script.py
+++ b/recipes/run_script.py
@@ -3,9 +3,13 @@
 # found in the LICENSE file.
 """Recipe for running a specified script from a given repo."""
 
+from urlparse import urlparse
+
 from PB.recipes.fuchsia.run_script import InputProperties
 
 DEPS = [
+    "fuchsia/auto_roller",
+    "fuchsia/gerrit",
     "fuchsia/git",
     "fuchsia/repo",
     "fuchsia/sso",
@@ -62,6 +66,18 @@
             api.python(step_name, props.script, script_args, venv=True)
         else:
             api.step(step_name, [props.script] + script_args)
+
+        if props.attempt_roll:
+            change = api.auto_roller.attempt_roll(
+                api.gerrit.host_from_remote_url(props.remote),
+                gerrit_project=urlparse(props.remote).path.lstrip("/"),
+                repo_dir=checkout_dir,
+                commit_message='Run "%s"' % props.script,
+                commit_untracked=props.roll_props.commit_untracked_files,
+                dry_run=props.roll_props.dry_run,
+            )
+            return api.auto_roller.raw_result(change)
+
         if props.upload_to_cipd:
             upload_to_cipd(
                 api,
@@ -157,3 +173,9 @@
         api.status_check.test("no_buildbucket_input")
         + api.properties(script="run-tests.sh", remote=remote)
     )
+
+    yield (
+        api.status_check.test("attempt_roll")
+        + api.properties(script="update.sh", remote=remote, attempt_roll=True)
+        + api.auto_roller.success()
+    )