feat: add Azure DevOps integration support (#11243)

Co-authored-by: Graham Neubig <neubig@gmail.com>
This commit is contained in:
Wan Arif
2025-11-23 03:00:24 +08:00
committed by GitHub
parent 1e513ad63f
commit 3504ca7752
58 changed files with 3108 additions and 96 deletions

View File

@@ -147,9 +147,11 @@ def runtime(temp_dir):
return runtime
def mock_repo_and_patch(monkeypatch, provider=ProviderType.GITHUB, is_public=True):
def mock_repo_and_patch(
monkeypatch, provider=ProviderType.GITHUB, is_public=True, full_name='owner/repo'
):
repo = Repository(
id='123', full_name='owner/repo', git_provider=provider, is_public=is_public
id='123', full_name=full_name, git_provider=provider, is_public=is_public
)
async def mock_verify_repo_provider(*_args, **_kwargs):
@@ -216,11 +218,14 @@ async def test_export_latest_git_provider_tokens_success(runtime):
async def test_export_latest_git_provider_tokens_multiple_refs(temp_dir):
"""Test token export with multiple token references"""
config = OpenHandsConfig()
# Initialize with both GitHub and GitLab tokens
# Initialize with GitHub, GitLab, and Azure DevOps tokens
git_provider_tokens = MappingProxyType(
{
ProviderType.GITHUB: ProviderToken(token=SecretStr('github_token')),
ProviderType.GITLAB: ProviderToken(token=SecretStr('gitlab_token')),
ProviderType.AZURE_DEVOPS: ProviderToken(
token=SecretStr('azure_devops_token')
),
}
)
file_store = get_file_store('local', temp_dir)
@@ -234,15 +239,18 @@ async def test_export_latest_git_provider_tokens_multiple_refs(temp_dir):
)
# Create a command that references multiple tokens
cmd = CmdRunAction(command='echo $GITHUB_TOKEN && echo $GITLAB_TOKEN')
cmd = CmdRunAction(
command='echo $GITHUB_TOKEN && echo $GITLAB_TOKEN && echo $AZURE_DEVOPS_TOKEN'
)
# Export the tokens
await runtime._export_latest_git_provider_tokens(cmd)
# Verify that both tokens were exported
# Verify that all tokens were exported
assert event_stream.secrets == {
'github_token': 'github_token',
'gitlab_token': 'gitlab_token',
'azure_devops_token': 'azure_devops_token',
}
@@ -478,6 +486,57 @@ async def test_clone_or_init_repo_gitlab_with_token(temp_dir, monkeypatch):
assert result == 'repo'
@pytest.mark.asyncio
async def test_clone_or_init_repo_azure_devops_with_token(temp_dir, monkeypatch):
"""Test cloning Azure DevOps repository with token"""
config = OpenHandsConfig()
# Set up Azure DevOps token
azure_devops_token = 'azure_devops_test_token'
git_provider_tokens = MappingProxyType(
{ProviderType.AZURE_DEVOPS: ProviderToken(token=SecretStr(azure_devops_token))}
)
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('abc', file_store)
runtime = MockRuntime(
config=config,
event_stream=event_stream,
user_id='test_user',
git_provider_tokens=git_provider_tokens,
)
# Mock the repository to be Azure DevOps with 3-part format: org/project/repo
azure_repo_name = 'testorg/testproject/testrepo'
mock_repo_and_patch(
monkeypatch, provider=ProviderType.AZURE_DEVOPS, full_name=azure_repo_name
)
# Call the method with Azure DevOps 3-part format: org/project/repo
result = await runtime.clone_or_init_repo(
git_provider_tokens=git_provider_tokens,
selected_repository=azure_repo_name,
selected_branch=None,
)
# Check that the first command is the git clone with the correct URL format with token
# Azure DevOps uses Basic auth format: https://org:token@dev.azure.com/org/project/_git/repo
clone_cmd = runtime.run_action_calls[0].command
expected_repo_path = str(runtime.workspace_root / 'testrepo')
assert (
f'https://testorg:{azure_devops_token}@dev.azure.com/testorg/testproject/_git/testrepo'
in clone_cmd
)
assert expected_repo_path in clone_cmd
# Check that the second command is the checkout
checkout_cmd = runtime.run_action_calls[1].command
assert f'cd {expected_repo_path}' in checkout_cmd
assert 'git checkout -b openhands-workspace-' in checkout_cmd
assert result == 'testrepo'
@pytest.mark.asyncio
async def test_clone_or_init_repo_with_branch(temp_dir, monkeypatch):
"""Test cloning a repository with a specified branch"""