diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md index 47eb706..1fcf78a 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] +> 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`. ```bash 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 25aeb03..a87eaf9 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 @@ -118,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, @@ -142,9 +145,10 @@ def merge_pull_request( for key, value in merge_parameters.items(): setattr(completion_options, key, value) + # 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( - status="completed", - last_merge_source_commit=pr.last_merge_source_commit, + auto_complete_set_by=self.__current_user_identity, completion_options=completion_options, ) 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 4133c98..82cf5c7 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", @@ -167,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): @@ -188,6 +192,46 @@ 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_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, "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.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") + + 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