From da8bb767f13e5ca594f4a6fcffaae9c15ae375b0 Mon Sep 17 00:00:00 2001 From: Dries Delanghe Date: Tue, 15 Sep 2026 13:46:14 +0200 Subject: [PATCH 1/6] feat: add 'auto-merge' option to merge methods across git providers Signed-off-by: Dries Delanghe --- gitopscli/cliparser.py | 2 +- gitopscli/commands/deploy.py | 2 +- .../azure_devops_git_repo_api_adapter.py | 23 +++++++++----- .../git_api/bitbucket_git_repo_api_adapter.py | 5 +++- gitopscli/git_api/git_repo_api.py | 2 +- .../git_api/git_repo_api_logging_proxy.py | 2 +- .../git_api/github_git_repo_api_adapter.py | 6 +++- .../git_api/gitlab_git_repo_api_adapter.py | 5 +++- .../test_azure_devops_git_repo_api_adapter.py | 30 +++++++++++++++++++ tests/test_cliparser.py | 4 +-- 10 files changed, 65 insertions(+), 16 deletions(-) diff --git a/gitopscli/cliparser.py b/gitopscli/cliparser.py index 42091be1..1eafd9e2 100644 --- a/gitopscli/cliparser.py +++ b/gitopscli/cliparser.py @@ -134,7 +134,7 @@ def __create_deploy_parser() -> ArgumentParser: ) parser.add_argument( "--merge-method", - help="Merge Method (e.g., 'squash', 'rebase', 'merge') (default: merge)", + help="Merge Method (e.g., 'squash', 'rebase', 'merge', 'auto-merge') (default: merge)", type=str, default="merge", ) diff --git a/gitopscli/commands/deploy.py b/gitopscli/commands/deploy.py index 39d7b33c..fc748d2a 100644 --- a/gitopscli/commands/deploy.py +++ b/gitopscli/commands/deploy.py @@ -37,7 +37,7 @@ class Args(GitApiConfig): pr_labels: list[str] | None merge_parameters: Any | None - merge_method: Literal["squash", "rebase", "merge"] = "merge" + merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge" branch: str | None = None def __init__(self, args: DeployCommand.Args) -> None: diff --git a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py index 25aeb031..791c6c50 100644 --- a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py +++ b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py @@ -110,7 +110,7 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", merge_parameters: dict[str, Any] | None = None, ) -> None: try: @@ -135,18 +135,27 @@ def merge_pull_request( completion_options.merge_strategy = "squash" elif merge_method == "rebase": completion_options.merge_strategy = "rebase" - else: # merge + else: # merge and auto-merge both default to noFastForward completion_options.merge_strategy = "noFastForward" if merge_parameters: for key, value in merge_parameters.items(): setattr(completion_options, key, value) - pr_update = GitPullRequest( - status="completed", - last_merge_source_commit=pr.last_merge_source_commit, - completion_options=completion_options, - ) + if merge_method == "auto-merge": + # Queue the PR to complete automatically once all branch policies are satisfied. + # Setting auto_complete_set_by (without status="completed") tells ADO to merge + # when policies pass, rather than attempting an immediate merge that would fail. + pr_update = GitPullRequest( + auto_complete_set_by=pr.created_by, + completion_options=completion_options, + ) + else: + pr_update = GitPullRequest( + status="completed", + last_merge_source_commit=pr.last_merge_source_commit, + completion_options=completion_options, + ) self.__git_client.update_pull_request( git_pull_request_to_update=pr_update, diff --git a/gitopscli/git_api/bitbucket_git_repo_api_adapter.py b/gitopscli/git_api/bitbucket_git_repo_api_adapter.py index 81af5b84..e87fa711 100644 --- a/gitopscli/git_api/bitbucket_git_repo_api_adapter.py +++ b/gitopscli/git_api/bitbucket_git_repo_api_adapter.py @@ -1,3 +1,4 @@ +import logging from typing import Any, Literal import requests @@ -88,9 +89,11 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge"] = "merge", # noqa: ARG002 + merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", # noqa: ARG002 merge_parameters: dict[str, Any] | None = None, # noqa: ARG002 ) -> None: + if merge_method == "auto-merge": + logging.info("Auto-merge is not natively supported for this git provider; falling back to immediate merge.") pull_request = self.__bitbucket.get_pull_request(self.__organisation, self.__repository_name, pr_id) self.__bitbucket.merge_pull_request( self.__organisation, diff --git a/gitopscli/git_api/git_repo_api.py b/gitopscli/git_api/git_repo_api.py index 76f2ee68..95ecea5d 100644 --- a/gitopscli/git_api/git_repo_api.py +++ b/gitopscli/git_api/git_repo_api.py @@ -37,7 +37,7 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", merge_parameters: dict[str, Any] | None = None, ) -> None: ... diff --git a/gitopscli/git_api/git_repo_api_logging_proxy.py b/gitopscli/git_api/git_repo_api_logging_proxy.py index a455d9f0..8733bcc1 100644 --- a/gitopscli/git_api/git_repo_api_logging_proxy.py +++ b/gitopscli/git_api/git_repo_api_logging_proxy.py @@ -39,7 +39,7 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", merge_parameters: dict[str, Any] | None = None, # noqa: ARG002 ) -> None: logging.info("Merging pull request %s", pr_id) diff --git a/gitopscli/git_api/github_git_repo_api_adapter.py b/gitopscli/git_api/github_git_repo_api_adapter.py index 0b22bd73..2a1a3719 100644 --- a/gitopscli/git_api/github_git_repo_api_adapter.py +++ b/gitopscli/git_api/github_git_repo_api_adapter.py @@ -1,3 +1,4 @@ +import logging from typing import Any, Literal from github import ( @@ -60,9 +61,12 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", merge_parameters: dict[str, Any] | None = None, # noqa: ARG002 ) -> None: + if merge_method == "auto-merge": + logging.info("Auto-merge is not natively supported for this git provider; falling back to immediate merge.") + merge_method = "merge" pull_request = self.__get_pull_request(pr_id) pull_request.merge(merge_method=merge_method) diff --git a/gitopscli/git_api/gitlab_git_repo_api_adapter.py b/gitopscli/git_api/gitlab_git_repo_api_adapter.py index 5a30cccd..f63b0b40 100644 --- a/gitopscli/git_api/gitlab_git_repo_api_adapter.py +++ b/gitopscli/git_api/gitlab_git_repo_api_adapter.py @@ -71,9 +71,12 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", merge_parameters: dict[str, Any] | None = None, ) -> None: + if merge_method == "auto-merge": + logging.info("Auto-merge is not natively supported for this git provider; falling back to immediate merge.") + merge_method = "merge" merge_request = self.__project.mergerequests.get(pr_id) max_retries = MAX_MERGE_RETRIES diff --git a/tests/git_api/test_azure_devops_git_repo_api_adapter.py b/tests/git_api/test_azure_devops_git_repo_api_adapter.py index 4133c98f..fbfc386a 100644 --- a/tests/git_api/test_azure_devops_git_repo_api_adapter.py +++ b/tests/git_api/test_azure_devops_git_repo_api_adapter.py @@ -188,6 +188,36 @@ def test_merge_pull_request_different_strategies(self): pr_update = call_args.kwargs["git_pull_request_to_update"] self.assertEqual(pr_update.completion_options.merge_strategy, "rebase") + def test_merge_pull_request_auto_merge(self): + mock_pr = MagicMock() + mock_pr.last_merge_source_commit = MagicMock() + mock_pr.created_by = MagicMock() + self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.return_value = mock_pr + + self.adapter.merge_pull_request(123, "auto-merge") + + call_args = self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.update_pull_request.call_args + pr_update = call_args.kwargs["git_pull_request_to_update"] + + self.assertEqual(pr_update.auto_complete_set_by, mock_pr.created_by) + self.assertIsNone(pr_update.status) + self.assertIsNotNone(pr_update.completion_options) + + def test_merge_pull_request_auto_merge_preserves_completion_options(self): + mock_pr = MagicMock() + mock_pr.last_merge_source_commit = MagicMock() + mock_pr.created_by = MagicMock() + self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.return_value = mock_pr + + self.adapter.merge_pull_request(123, "auto-merge", merge_parameters={"merge_strategy": "squash"}) + + call_args = self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.update_pull_request.call_args + pr_update = call_args.kwargs["git_pull_request_to_update"] + + self.assertEqual(pr_update.auto_complete_set_by, mock_pr.created_by) + self.assertEqual(pr_update.completion_options.merge_strategy, "squash") + self.assertTrue(pr_update.completion_options.delete_source_branch) + def test_add_pull_request_comment_success(self): self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.create_thread.return_value = None diff --git a/tests/test_cliparser.py b/tests/test_cliparser.py index 202ca116..8ea7c5cc 100644 --- a/tests/test_cliparser.py +++ b/tests/test_cliparser.py @@ -379,8 +379,8 @@ Automatically merge the created PR (only valid with --create-pr) --merge-method MERGE_METHOD - Merge Method (e.g., 'squash', 'rebase', 'merge') - (default: merge) + Merge Method (e.g., 'squash', 'rebase', 'merge', + 'auto-merge') (default: merge) --json [JSON] Print a JSON object containing deployment information --pr-labels PR_LABELS JSON array pr labels (Gitlab, Github supported) From ab41a3696c2202cac8bb56a5c2baec317771c856 Mon Sep 17 00:00:00 2001 From: Dries Delanghe Date: Tue, 15 Sep 2026 14:35:52 +0200 Subject: [PATCH 2/6] feat: fixed identity assignment issue Signed-off-by: Dries Delanghe --- .../azure_devops_git_repo_api_adapter.py | 5 ++- .../test_azure_devops_git_repo_api_adapter.py | 37 ++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py index 791c6c50..16e615bd 100644 --- a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py +++ b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py @@ -8,6 +8,7 @@ GitPullRequest, GitPullRequestCommentThread, GitPullRequestCompletionOptions, + IdentityRef, ) from msrest.exceptions import ClientException @@ -49,6 +50,8 @@ def __init__( # noqa: PLR0917 credentials = BasicAuthentication(self.__username, password) self.__connection = Connection(base_url=self.__base_url, creds=credentials) self.__git_client = self.__connection.clients.get_git_client() + profile = self.__connection.clients.get_profile_client().get_profile("me") + self.__current_user_identity = IdentityRef(id=profile.id) def get_username(self) -> str | None: return self.__username @@ -147,7 +150,7 @@ def merge_pull_request( # Setting auto_complete_set_by (without status="completed") tells ADO to merge # when policies pass, rather than attempting an immediate merge that would fail. pr_update = GitPullRequest( - auto_complete_set_by=pr.created_by, + auto_complete_set_by=self.__current_user_identity, completion_options=completion_options, ) else: diff --git a/tests/git_api/test_azure_devops_git_repo_api_adapter.py b/tests/git_api/test_azure_devops_git_repo_api_adapter.py index fbfc386a..0b2770cd 100644 --- a/tests/git_api/test_azure_devops_git_repo_api_adapter.py +++ b/tests/git_api/test_azure_devops_git_repo_api_adapter.py @@ -14,7 +14,10 @@ def mock_sleep_func(_: int) -> None: class AzureDevOpsGitRepoApiAdapterTest(unittest.TestCase): def setUp(self): - with patch("gitopscli.git_api.azure_devops_git_repo_api_adapter.Connection"): + self.mock_profile = MagicMock() + self.mock_profile.id = "test-user-guid-1234" + with patch("gitopscli.git_api.azure_devops_git_repo_api_adapter.Connection") as mock_conn: + mock_conn.return_value.clients.get_profile_client.return_value.get_profile.return_value = self.mock_profile self.adapter = AzureDevOpsGitRepoApiAdapter( git_provider_url="https://dev.azure.com/testorg", username="testuser", @@ -191,7 +194,6 @@ def test_merge_pull_request_different_strategies(self): def test_merge_pull_request_auto_merge(self): mock_pr = MagicMock() mock_pr.last_merge_source_commit = MagicMock() - mock_pr.created_by = MagicMock() self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.return_value = mock_pr self.adapter.merge_pull_request(123, "auto-merge") @@ -199,14 +201,13 @@ def test_merge_pull_request_auto_merge(self): call_args = self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.update_pull_request.call_args pr_update = call_args.kwargs["git_pull_request_to_update"] - self.assertEqual(pr_update.auto_complete_set_by, mock_pr.created_by) + self.assertEqual(pr_update.auto_complete_set_by.id, "test-user-guid-1234") self.assertIsNone(pr_update.status) self.assertIsNotNone(pr_update.completion_options) def test_merge_pull_request_auto_merge_preserves_completion_options(self): mock_pr = MagicMock() mock_pr.last_merge_source_commit = MagicMock() - mock_pr.created_by = MagicMock() self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.return_value = mock_pr self.adapter.merge_pull_request(123, "auto-merge", merge_parameters={"merge_strategy": "squash"}) @@ -214,10 +215,36 @@ def test_merge_pull_request_auto_merge_preserves_completion_options(self): call_args = self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.update_pull_request.call_args pr_update = call_args.kwargs["git_pull_request_to_update"] - self.assertEqual(pr_update.auto_complete_set_by, mock_pr.created_by) + self.assertEqual(pr_update.auto_complete_set_by.id, "test-user-guid-1234") self.assertEqual(pr_update.completion_options.merge_strategy, "squash") self.assertTrue(pr_update.completion_options.delete_source_branch) + def test_merge_pull_request_unauthorized(self): + self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.side_effect = ClientException("401") + + with pytest.raises(GitOpsException) as context: + self.adapter.merge_pull_request(123) + + self.assertEqual(str(context.value), "Bad credentials") + + def test_merge_pull_request_not_found(self): + self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.side_effect = ClientException("404") + + with pytest.raises(GitOpsException) as context: + self.adapter.merge_pull_request(123) + + self.assertEqual(str(context.value), "Pull request with ID '123' does not exist") + + def test_merge_pull_request_connection_error(self): + self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.side_effect = Exception( + "Connection failed" + ) + + with pytest.raises(GitOpsException) as context: + self.adapter.merge_pull_request(123) + + self.assertIn("Error connecting to 'https://dev.azure.com/testorg'", str(context.value)) + def test_add_pull_request_comment_success(self): self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.create_thread.return_value = None From 66fb9f53c78966b3b9f2908a69fd66f4f08c19f7 Mon Sep 17 00:00:00 2001 From: Dries Delanghe Date: Tue, 15 Sep 2026 14:54:37 +0200 Subject: [PATCH 3/6] rework: added the auto-complete functionality under the auto-merge flag for ADO Signed-off-by: Dries Delanghe --- gitopscli/cliparser.py | 2 +- gitopscli/commands/deploy.py | 2 +- .../azure_devops_git_repo_api_adapter.py | 24 +++++++------------ .../git_api/bitbucket_git_repo_api_adapter.py | 5 +--- gitopscli/git_api/git_repo_api.py | 2 +- .../git_api/git_repo_api_logging_proxy.py | 2 +- .../git_api/github_git_repo_api_adapter.py | 6 +---- .../git_api/gitlab_git_repo_api_adapter.py | 5 +--- .../test_azure_devops_git_repo_api_adapter.py | 23 ++++-------------- tests/test_cliparser.py | 4 ++-- 10 files changed, 22 insertions(+), 53 deletions(-) diff --git a/gitopscli/cliparser.py b/gitopscli/cliparser.py index 1eafd9e2..42091be1 100644 --- a/gitopscli/cliparser.py +++ b/gitopscli/cliparser.py @@ -134,7 +134,7 @@ def __create_deploy_parser() -> ArgumentParser: ) parser.add_argument( "--merge-method", - help="Merge Method (e.g., 'squash', 'rebase', 'merge', 'auto-merge') (default: merge)", + help="Merge Method (e.g., 'squash', 'rebase', 'merge') (default: merge)", type=str, default="merge", ) diff --git a/gitopscli/commands/deploy.py b/gitopscli/commands/deploy.py index fc748d2a..39d7b33c 100644 --- a/gitopscli/commands/deploy.py +++ b/gitopscli/commands/deploy.py @@ -37,7 +37,7 @@ class Args(GitApiConfig): pr_labels: list[str] | None merge_parameters: Any | None - merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge" + merge_method: Literal["squash", "rebase", "merge"] = "merge" branch: str | None = None def __init__(self, args: DeployCommand.Args) -> None: diff --git a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py index 16e615bd..0dfcc1ec 100644 --- a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py +++ b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py @@ -113,7 +113,7 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge"] = "merge", merge_parameters: dict[str, Any] | None = None, ) -> None: try: @@ -138,27 +138,19 @@ def merge_pull_request( completion_options.merge_strategy = "squash" elif merge_method == "rebase": completion_options.merge_strategy = "rebase" - else: # merge and auto-merge both default to noFastForward + else: # merge completion_options.merge_strategy = "noFastForward" if merge_parameters: for key, value in merge_parameters.items(): setattr(completion_options, key, value) - if merge_method == "auto-merge": - # Queue the PR to complete automatically once all branch policies are satisfied. - # Setting auto_complete_set_by (without status="completed") tells ADO to merge - # when policies pass, rather than attempting an immediate merge that would fail. - pr_update = GitPullRequest( - auto_complete_set_by=self.__current_user_identity, - completion_options=completion_options, - ) - else: - pr_update = GitPullRequest( - status="completed", - last_merge_source_commit=pr.last_merge_source_commit, - completion_options=completion_options, - ) + # Queue the PR to complete automatically once all branch policies are satisfied. + # If no policies are configured, ADO merges immediately — same behaviour as before. + pr_update = GitPullRequest( + auto_complete_set_by=self.__current_user_identity, + completion_options=completion_options, + ) self.__git_client.update_pull_request( git_pull_request_to_update=pr_update, diff --git a/gitopscli/git_api/bitbucket_git_repo_api_adapter.py b/gitopscli/git_api/bitbucket_git_repo_api_adapter.py index e87fa711..81af5b84 100644 --- a/gitopscli/git_api/bitbucket_git_repo_api_adapter.py +++ b/gitopscli/git_api/bitbucket_git_repo_api_adapter.py @@ -1,4 +1,3 @@ -import logging from typing import Any, Literal import requests @@ -89,11 +88,9 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", # noqa: ARG002 + merge_method: Literal["squash", "rebase", "merge"] = "merge", # noqa: ARG002 merge_parameters: dict[str, Any] | None = None, # noqa: ARG002 ) -> None: - if merge_method == "auto-merge": - logging.info("Auto-merge is not natively supported for this git provider; falling back to immediate merge.") pull_request = self.__bitbucket.get_pull_request(self.__organisation, self.__repository_name, pr_id) self.__bitbucket.merge_pull_request( self.__organisation, diff --git a/gitopscli/git_api/git_repo_api.py b/gitopscli/git_api/git_repo_api.py index 95ecea5d..76f2ee68 100644 --- a/gitopscli/git_api/git_repo_api.py +++ b/gitopscli/git_api/git_repo_api.py @@ -37,7 +37,7 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge"] = "merge", merge_parameters: dict[str, Any] | None = None, ) -> None: ... diff --git a/gitopscli/git_api/git_repo_api_logging_proxy.py b/gitopscli/git_api/git_repo_api_logging_proxy.py index 8733bcc1..a455d9f0 100644 --- a/gitopscli/git_api/git_repo_api_logging_proxy.py +++ b/gitopscli/git_api/git_repo_api_logging_proxy.py @@ -39,7 +39,7 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge"] = "merge", merge_parameters: dict[str, Any] | None = None, # noqa: ARG002 ) -> None: logging.info("Merging pull request %s", pr_id) diff --git a/gitopscli/git_api/github_git_repo_api_adapter.py b/gitopscli/git_api/github_git_repo_api_adapter.py index 2a1a3719..0b22bd73 100644 --- a/gitopscli/git_api/github_git_repo_api_adapter.py +++ b/gitopscli/git_api/github_git_repo_api_adapter.py @@ -1,4 +1,3 @@ -import logging from typing import Any, Literal from github import ( @@ -61,12 +60,9 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge"] = "merge", merge_parameters: dict[str, Any] | None = None, # noqa: ARG002 ) -> None: - if merge_method == "auto-merge": - logging.info("Auto-merge is not natively supported for this git provider; falling back to immediate merge.") - merge_method = "merge" pull_request = self.__get_pull_request(pr_id) pull_request.merge(merge_method=merge_method) diff --git a/gitopscli/git_api/gitlab_git_repo_api_adapter.py b/gitopscli/git_api/gitlab_git_repo_api_adapter.py index f63b0b40..5a30cccd 100644 --- a/gitopscli/git_api/gitlab_git_repo_api_adapter.py +++ b/gitopscli/git_api/gitlab_git_repo_api_adapter.py @@ -71,12 +71,9 @@ def create_pull_request( def merge_pull_request( self, pr_id: int, - merge_method: Literal["squash", "rebase", "merge", "auto-merge"] = "merge", + merge_method: Literal["squash", "rebase", "merge"] = "merge", merge_parameters: dict[str, Any] | None = None, ) -> None: - if merge_method == "auto-merge": - logging.info("Auto-merge is not natively supported for this git provider; falling back to immediate merge.") - merge_method = "merge" merge_request = self.__project.mergerequests.get(pr_id) max_retries = MAX_MERGE_RETRIES diff --git a/tests/git_api/test_azure_devops_git_repo_api_adapter.py b/tests/git_api/test_azure_devops_git_repo_api_adapter.py index 0b2770cd..82cf5c7d 100644 --- a/tests/git_api/test_azure_devops_git_repo_api_adapter.py +++ b/tests/git_api/test_azure_devops_git_repo_api_adapter.py @@ -170,7 +170,8 @@ def test_merge_pull_request_success(self): self.assertEqual(call_args.kwargs["project"], "testproject") pr_update = call_args.kwargs["git_pull_request_to_update"] - self.assertEqual(pr_update.status, "completed") + self.assertEqual(pr_update.auto_complete_set_by.id, "test-user-guid-1234") + self.assertIsNone(pr_update.status) self.assertEqual(pr_update.completion_options.merge_strategy, "squash") def test_merge_pull_request_different_strategies(self): @@ -191,33 +192,19 @@ def test_merge_pull_request_different_strategies(self): pr_update = call_args.kwargs["git_pull_request_to_update"] self.assertEqual(pr_update.completion_options.merge_strategy, "rebase") - def test_merge_pull_request_auto_merge(self): - mock_pr = MagicMock() - mock_pr.last_merge_source_commit = MagicMock() - self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.return_value = mock_pr - - self.adapter.merge_pull_request(123, "auto-merge") - - call_args = self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.update_pull_request.call_args - pr_update = call_args.kwargs["git_pull_request_to_update"] - - self.assertEqual(pr_update.auto_complete_set_by.id, "test-user-guid-1234") - self.assertIsNone(pr_update.status) - self.assertIsNotNone(pr_update.completion_options) - - def test_merge_pull_request_auto_merge_preserves_completion_options(self): + def test_merge_pull_request_preserves_merge_parameters(self): mock_pr = MagicMock() mock_pr.last_merge_source_commit = MagicMock() self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.return_value = mock_pr - self.adapter.merge_pull_request(123, "auto-merge", merge_parameters={"merge_strategy": "squash"}) + self.adapter.merge_pull_request(123, "squash", merge_parameters={"delete_source_branch": False}) call_args = self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.update_pull_request.call_args pr_update = call_args.kwargs["git_pull_request_to_update"] self.assertEqual(pr_update.auto_complete_set_by.id, "test-user-guid-1234") self.assertEqual(pr_update.completion_options.merge_strategy, "squash") - self.assertTrue(pr_update.completion_options.delete_source_branch) + self.assertFalse(pr_update.completion_options.delete_source_branch) def test_merge_pull_request_unauthorized(self): self.adapter._AzureDevOpsGitRepoApiAdapter__git_client.get_pull_request.side_effect = ClientException("401") diff --git a/tests/test_cliparser.py b/tests/test_cliparser.py index 8ea7c5cc..202ca116 100644 --- a/tests/test_cliparser.py +++ b/tests/test_cliparser.py @@ -379,8 +379,8 @@ Automatically merge the created PR (only valid with --create-pr) --merge-method MERGE_METHOD - Merge Method (e.g., 'squash', 'rebase', 'merge', - 'auto-merge') (default: merge) + Merge Method (e.g., 'squash', 'rebase', 'merge') + (default: merge) --json [JSON] Print a JSON object containing deployment information --pr-labels PR_LABELS JSON array pr labels (Gitlab, Github supported) From 6ad3ab0937cc8d72816141703d923312c73046f5 Mon Sep 17 00:00:00 2001 From: Dries Delanghe Date: Tue, 15 Sep 2026 14:58:41 +0200 Subject: [PATCH 4/6] chore: added a note on the updated behaviour for ADO to the documentation Signed-off-by: Dries Delanghe --- docs/commands/deploy.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md index 47eb7068..4608c4eb 100644 --- a/docs/commands/deploy.md +++ b/docs/commands/deploy.md @@ -99,6 +99,9 @@ This will end up in one single commit with your specified commit-message. In some cases you might want to create a pull request for your updates. You can achieve this by adding `--create-pr` to the command. The pull request can be left open or merged directly with `--auto-merge`. +!!! note "Azure DevOps: auto-merge uses native auto-complete" + On Azure DevOps, `--auto-merge` sets the PR to [auto-complete](https://learn.microsoft.com/en-us/azure/devops/repos/git/complete-pull-requests) rather than attempting an immediate merge. This means the PR will be completed automatically once all branch policies (e.g. build validation, required reviewers) are satisfied. If no policies are configured the PR merges immediately. The merge strategy is controlled as usual via `--merge-method`. + By default GitOps CLI creates a random branch for the pull request (e.g. `gitopscli-deploy-b973b5bb`). Use `--branch` to specify that branch name instead: an existing remote branch is checked out, otherwise a new branch is created. `--branch` also works without `--create-pr`. ```bash From 8d9d3e26373b441775a5e4819992979330e66c47 Mon Sep 17 00:00:00 2001 From: Dries Delanghe Date: Tue, 15 Sep 2026 15:05:09 +0200 Subject: [PATCH 5/6] chore: updated documentation Signed-off-by: Dries Delanghe --- docs/commands/deploy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md index 4608c4eb..1fcf78a6 100644 --- a/docs/commands/deploy.md +++ b/docs/commands/deploy.md @@ -99,8 +99,8 @@ This will end up in one single commit with your specified commit-message. In some cases you might want to create a pull request for your updates. You can achieve this by adding `--create-pr` to the command. The pull request can be left open or merged directly with `--auto-merge`. -!!! note "Azure DevOps: auto-merge uses native auto-complete" - On Azure DevOps, `--auto-merge` sets the PR to [auto-complete](https://learn.microsoft.com/en-us/azure/devops/repos/git/complete-pull-requests) rather than attempting an immediate merge. This means the PR will be completed automatically once all branch policies (e.g. build validation, required reviewers) are satisfied. If no policies are configured the PR merges immediately. The merge strategy is controlled as usual via `--merge-method`. +> [!NOTE] +> On Azure DevOps, `--auto-merge` uses the platform's native [auto-complete](https://learn.microsoft.com/en-us/azure/devops/repos/git/complete-pull-requests) feature. The PR is completed automatically once all branch policies (e.g. build validation, required reviewers) are satisfied — or immediately if no policies are configured. Use `--merge-method` to control the merge strategy. By default GitOps CLI creates a random branch for the pull request (e.g. `gitopscli-deploy-b973b5bb`). Use `--branch` to specify that branch name instead: an existing remote branch is checked out, otherwise a new branch is created. `--branch` also works without `--create-pr`. From 53526406b136e6729fce5c79ca1ceb2ab40f42d7 Mon Sep 17 00:00:00 2001 From: Dries Delanghe Date: Tue, 15 Sep 2026 15:12:04 +0200 Subject: [PATCH 6/6] chore: cleanup Signed-off-by: Dries Delanghe --- gitopscli/git_api/azure_devops_git_repo_api_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py index 0dfcc1ec..a87eaf9d 100644 --- a/gitopscli/git_api/azure_devops_git_repo_api_adapter.py +++ b/gitopscli/git_api/azure_devops_git_repo_api_adapter.py @@ -121,7 +121,7 @@ def merge_pull_request( # and the PullRequest completion can be requested self.__sleep_func(3) - pr = self.__git_client.get_pull_request( + self.__git_client.get_pull_request( repository_id=self.__repository_name, pull_request_id=pr_id, project=self.__project_name,