Test downstream projects at their last green commit (#392)

Change-Id: I5a2081b067529d2eba8242408156e78d6aab7a57
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 4c02a9b..04c3206 100644
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -425,8 +425,9 @@
     eprint("\n\n+++ {0}\n\n".format(name))
 
 
-def execute_commands(config, platform, git_repository, git_repo_location, use_bazel_at_commit,
-                     use_but, save_but, build_only, test_only, monitor_flaky_tests):
+def execute_commands(config, platform, git_repository, git_commit, git_repo_location,
+                     use_bazel_at_commit, use_but, save_but, build_only, test_only,
+                     monitor_flaky_tests):
     fail_pipeline = False
     tmpdir = None
     bazel_binary = "bazel"
@@ -442,11 +443,10 @@
     tmpdir = tempfile.mkdtemp()
     sc_process = None
     try:
-        if git_repository:
-            if git_repo_location:
-                os.chdir(git_repo_location)
-            else:
-                clone_git_repository(git_repository, platform)
+        if git_repo_location:
+            os.chdir(git_repo_location)
+        elif git_repository:
+            clone_git_repository(git_repository, platform, git_commit)
         else:
             git_repository = os.getenv("BUILDKITE_REPO")
 
@@ -738,11 +738,14 @@
     os.chmod(bazel_binary_path, st.st_mode | stat.S_IEXEC)
     return bazel_binary_path
 
-def clone_git_repository(git_repository, platform):
+def clone_git_repository(git_repository, platform, git_commit=None):
     root = downstream_projects_root(platform)
     project_name = re.search(r"/([^/]+)\.git$", git_repository).group(1)
     clone_path = os.path.join(root, project_name)
-    print_collapsed_group("Fetching " + project_name + " sources")
+    if git_commit:
+        print_collapsed_group("Fetching " + project_name + " sources at " + git_commit)
+    else:
+        print_collapsed_group("Fetching " + project_name + " sources")
 
     if not os.path.exists(clone_path):
         if platform in ["ubuntu1404", "ubuntu1604", "ubuntu1804", "rbe_ubuntu1604"]:
@@ -758,12 +761,14 @@
     execute_command(["git", "remote", "set-url", "origin", git_repository])
     execute_command(["git", "clean", "-fdqx"])
     execute_command(["git", "submodule", "foreach", "--recursive", "git", "clean", "-fdqx"])
-    # sync to the latest commit of HEAD. Unlikely git pull this also works after a force push.
     execute_command(["git", "fetch", "origin"])
-    remote_head = subprocess.check_output(["git", "symbolic-ref", "refs/remotes/origin/HEAD"])
-    remote_head = remote_head.decode("utf-8")
-    remote_head = remote_head.rstrip()
-    execute_command(["git", "reset", remote_head, "--hard"])
+    if git_commit:
+        # sync to a specific commit of this repository
+        execute_command(["git", "reset", git_commit, "--hard"])
+    else:
+        # sync to the latest commit of HEAD. Unlikely git pull this also works after a force push.
+        remote_head = subprocess.check_output(["git", "symbolic-ref", "refs/remotes/origin/HEAD"]).decode("utf-8").rstrip()
+        execute_command(["git", "reset", remote_head, "--hard"])
     execute_command(["git", "submodule", "sync", "--recursive"])
     execute_command(["git", "submodule", "update", "--init", "--recursive", "--force"])
     execute_command(["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"])
@@ -1104,9 +1109,14 @@
                 "prompt": "Did you review this pull request for malicious code?"
             })
 
+    # In Bazel Downstream Project pipeline, git_repository and project_name must be specified,
+    # and we should test the project at the last green commit.
+    git_commit = None
+    if use_but and git_repository and project_name:
+        git_commit = get_last_green_commit(git_repository, DOWNSTREAM_PROJECTS[project_name]["pipeline_slug"])
     for platform in platform_configs:
-        step = runner_step(platform, project_name,
-                           http_config, file_config, git_repository, monitor_flaky_tests, use_but)
+        step = runner_step(platform, project_name, http_config, file_config,
+                           git_repository, git_commit, monitor_flaky_tests, use_but)
         pipeline_steps.append(step)
 
     pipeline_slug = os.getenv("BUILDKITE_PIPELINE_SLUG")
@@ -1145,8 +1155,8 @@
 
     print(yaml.dump({"steps": pipeline_steps}))
 
-def runner_step(platform, project_name=None, http_config=None,
-                file_config=None, git_repository=None, monitor_flaky_tests=False, use_but=False):
+def runner_step(platform, project_name=None, http_config=None, file_config=None,
+                git_repository=None, git_commit=None, monitor_flaky_tests=False, use_but=False):
     host_platform = PLATFORMS[platform].get("host-platform", platform)
     command = python_binary(host_platform) + " bazelci.py runner --platform=" + platform
     if http_config:
@@ -1155,6 +1165,8 @@
         command += " --file_config=" + file_config
     if git_repository:
         command += " --git_repository=" + git_repository
+    if git_commit:
+        command += " --git_commit=" + git_commit
     if monitor_flaky_tests:
         command += " --monitor_flaky_tests"
     if use_but:
@@ -1498,6 +1510,7 @@
     runner.add_argument("--file_config", type=str)
     runner.add_argument("--http_config", type=str)
     runner.add_argument("--git_repository", type=str)
+    runner.add_argument("--git_commit", type=str, help="Reset the git repository to this commit after cloning it")
     runner.add_argument("--git_repo_location", type=str, help="Use an existing repository instead of cloning from github")
     runner.add_argument("--use_bazel_at_commit", type=str, help="Use Bazel binariy built at a specifc commit")
     runner.add_argument("--use_but", type=bool, nargs="?", const=True)
@@ -1535,6 +1548,7 @@
             execute_commands(config=configs.get("platforms", None)[args.platform],
                              platform=args.platform,
                              git_repository=args.git_repository,
+                             git_commit=args.git_commit,
                              git_repo_location=args.git_repo_location,
                              use_bazel_at_commit=args.use_bazel_at_commit,
                              use_but=args.use_but,