Implement Bazel Auto Sheriff (#953) (Second try)

This reverts commit 689368dcb43aa76f844e227c2c680a82d16ea0d3.
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 1eade27..7d27414 100755
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -25,6 +25,7 @@
 import os.path
 import random
 import re
+import requests
 from shutil import copyfile
 import shutil
 import stat
@@ -565,11 +566,20 @@
         "https://api.buildkite.com/v2/organizations/{}/pipelines/{}/builds/{}"
     )
 
+    _NEW_BUILD_URL_TEMPLATE = (
+        "https://api.buildkite.com/v2/organizations/{}/pipelines/{}/builds"
+    )
+
+    _RETRY_JOB_URL_TEMPLATE = (
+        "https://api.buildkite.com/v2/organizations/{}/pipelines/{}/builds/{}/jobs/{}/retry"
+    )
+
     def __init__(self, org, pipeline):
         self._org = org
         self._pipeline = pipeline
         self._token = self._get_buildkite_token()
 
+
     def _get_buildkite_token(self):
         return decrypt_token(
             encrypted_token=self._ENCRYPTED_BUILDKITE_API_TESTING_TOKEN
@@ -580,25 +590,174 @@
             else "buildkite-untrusted-api-token",
         )
 
-    def _open_url(self, url):
+
+    def _open_url(self, url, params = []):
         try:
+            params_str = "".join("&{}={}".format(k, v) for k, v in params)
             return (
-                urllib.request.urlopen("{}?access_token={}".format(url, self._token))
+                urllib.request.urlopen("{}?access_token={}{}".format(url, self._token, params_str))
                 .read()
                 .decode("utf-8")
             )
         except urllib.error.HTTPError as ex:
             raise BuildkiteException("Failed to open {}: {} - {}".format(url, ex.code, ex.reason))
 
+
     def get_build_info(self, build_number):
+        """Get build info for a pipeline with a given build number
+        See https://buildkite.com/docs/apis/rest-api/builds#get-a-build
+
+        Parameters
+        ----------
+        build_number : the build number
+
+        Returns
+        -------
+        dict
+            the metadata for the build
+        """
         url = self._BUILD_STATUS_URL_TEMPLATE.format(self._org, self._pipeline, build_number)
         output = self._open_url(url)
         return json.loads(output)
 
+
+    def get_build_info_list(self, params):
+        """Get a list of build infos for this pipeline
+        See https://buildkite.com/docs/apis/rest-api/builds#list-builds-for-a-pipeline
+
+        Parameters
+        ----------
+        params : the parameters to filter the result
+
+        Returns
+        -------
+        list of dict
+            the metadata for a list of builds
+        """
+        url = self._BUILD_STATUS_URL_TEMPLATE.format(self._org, self._pipeline, "")
+        output = self._open_url(url, params)
+        return json.loads(output)
+
+
     def get_build_log(self, job):
         return self._open_url(job["raw_log_url"])
 
 
+    @staticmethod
+    def _check_response(response, expected_status_code):
+        if response.status_code != expected_status_code:
+            eprint("Exit code:", response.status_code)
+            eprint("Response:\n", response.text)
+            response.raise_for_status()
+
+
+    def trigger_new_build(self, commit, message = None, env = {}):
+        """Trigger a new build at a given commit and return the build metadata.
+        See https://buildkite.com/docs/apis/rest-api/builds#create-a-build
+
+        Parameters
+        ----------
+        commit : the commit we want to build at
+        message : the message we should as the build titile
+        env : (optional) the environment variables to set
+
+        Returns
+        -------
+        dict
+            the metadata for the build
+        """
+        url = self._NEW_BUILD_URL_TEMPLATE.format(self._org, self._pipeline)
+        data = {
+            "commit": commit,
+            "branch": "master",
+            "message": message if message else f"Trigger build at {commit}",
+            "env": env,
+        }
+        response = requests.post(url + "?access_token=" + self._token, json = data)
+        BuildkiteClient._check_response(response, requests.codes.created)
+        return json.loads(response.text)
+
+
+    def trigger_job_retry(self, build_number, job_id):
+        """Trigger a job retry and return the job metadata.
+        See https://buildkite.com/docs/apis/rest-api/jobs#retry-a-job
+
+        Parameters
+        ----------
+        build_number : the number of the build we want to retry
+        job_id : the id of the job we want to retry
+
+        Returns
+        -------
+        dict
+            the metadata for the job
+        """
+        url = self._RETRY_JOB_URL_TEMPLATE.format(self._org, self._pipeline, build_number, job_id)
+        response = requests.put(url + "?access_token=" + self._token)
+        BuildkiteClient._check_response(response, requests.codes.ok)
+        return json.loads(response.text)
+
+
+    def wait_job_to_finish(self, build_number, job_id, interval_time=30, logger=None):
+        """Wait a job to finish and return the job metadata
+
+        Parameters
+        ----------
+        build_number : the number of the build we want to wait
+        job_id : the id of the job we want to wait
+        interval_time : (optional) the interval time to check the build status, default to 30s
+        logger : (optional) a logger to report progress
+
+        Returns
+        -------
+        dict
+            the latest metadata for the job
+        """
+        t = 0
+        build_info = self.get_build_info(build_number)
+        while True:
+            for job in build_info["jobs"]:
+                if job["id"] == job_id:
+                    state = job["state"]
+                    if state != "scheduled" and state != "running" and state != "assigned":
+                        return job
+                    break
+            else:
+                raise BuildkiteException(f"job id {job_id} doesn't exist in build " + build_info["web_url"])
+            url = build_info["web_url"]
+            if logger:
+                logger.log(f"Waiting for {url}, waited {t} seconds...")
+            time.sleep(interval_time)
+            t += interval_time
+            build_info = self.get_build_info(build_number)
+
+
+    def wait_build_to_finish(self, build_number, interval_time=30, logger=None):
+        """Wait a build to finish and return the build metadata
+
+        Parameters
+        ----------
+        build_number : the number of the build we want to wait
+        interval_time : (optional) the interval time to check the build status, default to 30s
+        logger : (optional) a logger to report progress
+
+        Returns
+        -------
+        dict
+            the latest metadata for the build
+        """
+        t = 0
+        build_info = self.get_build_info(build_number)
+        while build_info["state"] == "scheduled" or build_info["state"] == "running":
+            url = build_info["web_url"]
+            if logger:
+                logger.log(f"Waiting for {url}, waited {t} seconds...")
+            time.sleep(interval_time)
+            t += interval_time
+            build_info = self.get_build_info(build_number)
+        return build_info
+
+
 def decrypt_token(encrypted_token, kms_key):
     return (
         subprocess.check_output(
@@ -1719,8 +1878,9 @@
     return process.stdout
 
 
-def execute_command(args, shell=False, fail_if_nonzero=True, cwd=None):
-    eprint(" ".join(args))
+def execute_command(args, shell=False, fail_if_nonzero=True, cwd=None, print_output=True):
+    if print_output:
+        eprint(" ".join(args))
     return subprocess.run(
         args, shell=shell, check=fail_if_nonzero, env=os.environ, cwd=cwd
     ).returncode