Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/commands/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions gitopscli/git_api/azure_devops_git_repo_api_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
GitPullRequest,
GitPullRequestCommentThread,
GitPullRequestCompletionOptions,
IdentityRef,
)
from msrest.exceptions import ClientException

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "same behaviour as before" makes no sense as a code comment. The before is not visible anymore.

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,
)

Expand Down
48 changes: 46 additions & 2 deletions tests/git_api/test_azure_devops_git_repo_api_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand Down