Add Protobuf (main, 33.x, 35.x) to downstream pipeline (#2808)

This PR configures Protobuf in the downstream pipeline:
- Adds Protobuf (main), Protobuf 33.x, and Protobuf 35.x to
`DOWNSTREAM_PROJECTS_PRODUCTION` using `.bazelci/presubmit.yml` from
`https://github.com/protocolbuffers/protobuf.git`.
- Removes obsolete `pipelines/protobuf.yml`.
- Supports `git_branch` in `DOWNSTREAM_PROJECTS` by passing
`git_commit="origin/<branch>"` to the downstream `project_pipeline` step
and skipping `get_last_green_commit` when a branch is configured.
- Adds unit tests in `buildkite/bazelci_test.py`.
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 858ba12..01ab9bc 100755
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -242,6 +242,23 @@
         "pipeline_slug": "intellij-plugin-aspect-google",
         "disabled_reason": "https://github.com/bazelbuild/intellij/issues/7232#issuecomment-2617127267",
     },
+    "Protobuf": {
+        "git_repository": "https://github.com/protocolbuffers/protobuf.git",
+        "file_config": ".bazelci/presubmit.yml",
+        "pipeline_slug": "protobuf",
+    },
+    "Protobuf 33.x": {
+        "git_repository": "https://github.com/protocolbuffers/protobuf.git",
+        "git_branch": "33.x",
+        "file_config": ".bazelci/presubmit.yml",
+        "pipeline_slug": "protobuf",
+    },
+    "Protobuf 35.x": {
+        "git_repository": "https://github.com/protocolbuffers/protobuf.git",
+        "git_branch": "35.x",
+        "file_config": ".bazelci/presubmit.yml",
+        "pipeline_slug": "protobuf",
+    },
     "Stardoc": {
         "git_repository": "https://github.com/bazelbuild/stardoc.git",
         "pipeline_slug": "stardoc",
@@ -3268,10 +3285,11 @@
     http_config,
     file_config,
     git_repository,
-    monitor_flaky_tests,
-    use_but,
-    notify,
-    print_shard_summary,
+    git_commit=None,
+    monitor_flaky_tests=False,
+    use_but=False,
+    notify=False,
+    print_shard_summary=False,
 ):
     task_configs = configs.get("tasks", None)
     if not task_configs:
@@ -3299,7 +3317,8 @@
             )
 
     # In Bazel Downstream Project pipelines, we should test the project at the last green commit.
-    git_commit = get_last_green_commit(project_name) if is_downstream_pipeline() else None
+    if is_downstream_pipeline() and not git_commit:
+        git_commit = get_last_green_commit(project_name)
 
     config_hashes = set()
     skipped_downstream_tasks = []
@@ -3729,7 +3748,9 @@
     )
 
 
-def upload_project_pipeline_step(project_name, git_repository, http_config, file_config):
+def upload_project_pipeline_step(
+    project_name, git_repository, http_config, file_config, git_commit=None
+):
     pipeline_command = (
         '{0} bazelci.py project_pipeline --project_name="{1}" ' + "--git_repository={2}"
     ).format(PLATFORMS[DEFAULT_PLATFORM]["python"], project_name, git_repository)
@@ -3738,6 +3759,8 @@
         pipeline_command += " --http_config=" + http_config
     if file_config:
         pipeline_command += " --file_config=" + file_config
+    if git_commit:
+        pipeline_command += " --git_commit=" + git_commit
     pipeline_command += " | tee /dev/tty | buildkite-agent pipeline upload"
 
     return create_step(
@@ -4075,12 +4098,16 @@
         if (test_disabled_projects and disabled_reason) or (
             not test_disabled_projects and not disabled_reason
         ):
+            git_commit = (
+                "origin/" + config["git_branch"] if "git_branch" in config else None
+            )
             pipeline_steps.append(
                 upload_project_pipeline_step(
                     project_name=project,
                     git_repository=config["git_repository"],
                     http_config=config.get("http_config", None),
                     file_config=config.get("file_config", None),
+                    git_commit=git_commit,
                 )
             )
 
@@ -4201,6 +4228,9 @@
 
 
 def get_last_green_commit(project_name):
+    # If a specific branch is configured for downstream testing, don't use the main branch's last green commit
+    if DOWNSTREAM_PROJECTS.get(project_name, {}).get("git_branch"):
+        return None
     last_green_commit_url = bazelci_last_green_commit_url(
         DOWNSTREAM_PROJECTS[project_name]["git_repository"],
         DOWNSTREAM_PROJECTS[project_name]["pipeline_slug"],
@@ -4979,6 +5009,9 @@
     project_pipeline.add_argument("--file_config", type=str)
     project_pipeline.add_argument("--http_config", type=str)
     project_pipeline.add_argument("--git_repository", type=str)
+    project_pipeline.add_argument(
+        "--git_commit", type=str, help="Reset the git repository to this commit after cloning it"
+    )
     project_pipeline.add_argument("--monitor_flaky_tests", type=bool, nargs="?", const=True)
     project_pipeline.add_argument("--use_but", type=bool, nargs="?", const=True)
     project_pipeline.add_argument("--notify", type=bool, nargs="?", const=True)
@@ -5048,7 +5081,13 @@
             # Fetch the repo in case we need to use file_config.
             if args.git_repository:
                 git_commit = (
-                    get_last_green_commit(args.project_name) if is_downstream_pipeline() else None
+                    args.git_commit
+                    if args.git_commit
+                    else (
+                        get_last_green_commit(args.project_name)
+                        if is_downstream_pipeline()
+                        else None
+                    )
                 )
                 clone_git_repository(args.git_repository, git_commit, suppress_stdout=True)
 
@@ -5059,6 +5098,7 @@
                 http_config=args.http_config,
                 file_config=args.file_config,
                 git_repository=args.git_repository,
+                git_commit=args.git_commit,
                 monitor_flaky_tests=args.monitor_flaky_tests,
                 use_but=args.use_but,
                 notify=args.notify,
diff --git a/buildkite/bazelci_test.py b/buildkite/bazelci_test.py
index 5437884..141d055 100755
--- a/buildkite/bazelci_test.py
+++ b/buildkite/bazelci_test.py
@@ -673,6 +673,83 @@
                 self.assertEqual(bazelci.get_ci_script_ref(), "testing")
 
 
+class GitBranchHandlingTest(unittest.TestCase):
+
+    def test_upload_project_pipeline_step_with_git_commit(self):
+        step = bazelci.upload_project_pipeline_step(
+            project_name="Protobuf 35.x",
+            git_repository="https://github.com/protocolbuffers/protobuf.git",
+            http_config=None,
+            file_config=".bazelci/presubmit.yml",
+            git_commit="origin/35.x",
+        )
+        commands = step["command"]
+        project_pipeline_cmd = [c for c in commands if "project_pipeline" in c][0]
+        self.assertIn("--git_commit=origin/35.x", project_pipeline_cmd)
+        self.assertIn('--project_name="Protobuf 35.x"', project_pipeline_cmd)
+        self.assertIn('--file_config=.bazelci/presubmit.yml', project_pipeline_cmd)
+
+    def test_print_bazel_downstream_pipeline_passes_origin_branch_as_git_commit(self):
+        test_projects = {
+            "Protobuf 35.x": {
+                "git_repository": "https://github.com/protocolbuffers/protobuf.git",
+                "git_branch": "35.x",
+                "file_config": ".bazelci/presubmit.yml",
+                "pipeline_slug": "protobuf",
+            }
+        }
+        with mock.patch.dict(bazelci.DOWNSTREAM_PROJECTS, test_projects, clear=True), \
+             mock.patch.object(bazelci, "upload_project_pipeline_step") as mock_upload, \
+             mock.patch.object(bazelci, "print_pipeline_steps"), \
+             mock.patch.object(bazelci, "bazel_build_step", return_value={"label": "Bazel"}), \
+             mock.patch.object(bazelci, "get_platform_for_task", return_value="ubuntu2004"):
+            bazelci.print_bazel_downstream_pipeline(
+                task_configs={"basic": {}},
+                http_config=None,
+                file_config=None,
+                test_disabled_projects=False,
+                notify=False,
+            )
+            mock_upload.assert_called_once_with(
+                project_name="Protobuf 35.x",
+                git_repository="https://github.com/protocolbuffers/protobuf.git",
+                http_config=None,
+                file_config=".bazelci/presubmit.yml",
+                git_commit="origin/35.x",
+            )
+
+    def test_get_last_green_commit_ignores_projects_with_git_branch(self):
+        with mock.patch.dict(
+            bazelci.DOWNSTREAM_PROJECTS,
+            {
+                "TestProject": {
+                    "git_repository": "https://github.com/foo/bar.git",
+                    "pipeline_slug": "bar",
+                    "git_branch": "35.x",
+                }
+            },
+            clear=True,
+        ):
+            self.assertIsNone(bazelci.get_last_green_commit("TestProject"))
+
+    def test_clone_git_repository_resets_to_git_commit(self):
+        executed_commands = []
+
+        def fake_execute(args, print_output=True, suppress_stdout=False):
+            executed_commands.append(args)
+
+        with mock.patch.object(bazelci, "execute_command", side_effect=fake_execute), \
+             mock.patch("os.path.exists", return_value=True), \
+             mock.patch("os.chdir"):
+            bazelci.clone_git_repository(
+                "https://github.com/protocolbuffers/protobuf.git",
+                git_commit="origin/35.x",
+                suppress_stdout=True,
+            )
+
+        self.assertIn(["git", "reset", "origin/35.x", "--hard"], executed_commands)
+
+
 if __name__ == "__main__":
     unittest.main()
 
diff --git a/pipelines/protobuf.yml b/pipelines/protobuf.yml
deleted file mode 100644
index fa61e8d..0000000
--- a/pipelines/protobuf.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-matrix:
-  platform: ["debian10", "macos", "ubuntu2004"]
-  bazel: [7.x, 8.x]
-
-tasks:
-  verify_targets:
-    name: "Verify build targets"
-    platform: ${{ platform }}
-    bazel: ${{ bazel }}
-    build_flags:
-    - '--host_cxxopt=-std=c++17'
-    - '--cxxopt=-std=c++17'
-    build_targets:
-    - '//:protobuf'
-    - '//:protobuf_lite'
-    - '//:protobuf_python'
-    - '//:protobuf_java'
-    - '//:protoc'
-    - '//:test_messages_proto2_cc_proto'
-    - '//:test_messages_proto3_cc_proto'
-
-  # Separate windows for different c++ build flags.
-  verify_targets_windows:
-    name: "Verify windows build targets"
-    platform: windows
-    bazel: ${{ bazel }}
-    build_flags:
-    - '--cxxopt=/std:c++17'
-    - '--host_cxxopt=/std:c++17'
-    # Run using clang-cl
-    - '--extra_execution_platforms=//build_defs:x64_windows-clang-cl'
-    - '--host_platform=//build_defs:x64_windows-clang-cl'
-    - '--extra_toolchains=@local_config_cc//:cc-toolchain-x64_windows-clang-cl'
-    build_targets:
-    - '//:protobuf'
-    - '//:protobuf_lite'
-    - '//:protobuf_python'
-    - '//:protobuf_java'
-    - '//:protoc'
-    - '//:test_messages_proto2_cc_proto'
-    - '//:test_messages_proto3_cc_proto'
-
-  run_test_module:
-    name: "Run test module"
-    working_directory: "examples"
-    platform: ${{ platform }}
-    bazel: ${{ bazel }}
-    build_flags:
-    - '--host_cxxopt=-std=c++17'
-    - '--cxxopt=-std=c++17'
-    build_targets:
-    - "//..."
-
-  # Separate windows for different c++ build flags.
-  run_test_module_windows:
-    name: "Run windows test module"
-    working_directory: "examples"
-    platform: windows
-    bazel: ${{ bazel }}
-    build_flags:
-    - '--cxxopt=/std:c++17'
-    - '--host_cxxopt=/std:c++17'
-    build_targets:
-    - "//..."