Wan Arif 3504ca7752
feat: add Azure DevOps integration support (#11243)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-11-22 14:00:24 -05:00

130 lines
4.7 KiB
Python

"""Work item operations for Azure DevOps integration."""
from datetime import datetime
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.azure_devops.service.base import AzureDevOpsMixinBase
from openhands.integrations.service_types import Comment, RequestMethod
class AzureDevOpsWorkItemsMixin(AzureDevOpsMixinBase):
"""Mixin for Azure DevOps work item operations.
Work Items are unique to Azure DevOps and represent tasks, bugs, user stories, etc.
in Azure Boards. This mixin provides methods to interact with work item comments.
"""
def _truncate_comment(self, comment: str, max_length: int = 1000) -> str:
"""Truncate comment to max length."""
if len(comment) <= max_length:
return comment
return comment[:max_length] + '...'
async def add_work_item_comment(
self, repository: str, work_item_id: int, comment_text: str
) -> dict:
"""Add a comment to an Azure DevOps work item.
API Reference: https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/comments/add-comment
Args:
repository: Repository name in format "organization/project/repo" (project extracted)
work_item_id: The work item ID
comment_text: The comment text to post
Returns:
API response with created comment information
Raises:
HTTPException: If the API request fails
"""
org, project, _ = self._parse_repository(repository)
# URL-encode components to handle spaces and special characters
org_enc = self._encode_url_component(org)
project_enc = self._encode_url_component(project)
url = f'{self.base_url}/{org_enc}/{project_enc}/_apis/wit/workItems/{work_item_id}/comments?api-version=7.1-preview.4'
payload = {
'text': comment_text,
}
response, _ = await self._make_request(
url=url, params=payload, method=RequestMethod.POST
)
logger.info(f'Added comment to work item {work_item_id} in project {project}')
return response
async def get_work_item_comments(
self, repository: str, work_item_id: int, max_comments: int = 100
) -> list[Comment]:
"""Get all comments from a work item.
API Reference: https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/comments/get-comments
Args:
repository: Repository name in format "organization/project/repo" (project extracted)
work_item_id: The work item ID
max_comments: Maximum number of comments to return
Returns:
List of Comment objects sorted by creation date
"""
org, project, _ = self._parse_repository(repository)
# URL-encode components to handle spaces and special characters
org_enc = self._encode_url_component(org)
project_enc = self._encode_url_component(project)
url = f'{self.base_url}/{org_enc}/{project_enc}/_apis/wit/workItems/{work_item_id}/comments?api-version=7.1-preview.4'
response, _ = await self._make_request(url)
comments_data = response.get('comments', [])
all_comments: list[Comment] = []
for comment_data in comments_data:
# Extract author information
author_info = comment_data.get('createdBy', {})
author = author_info.get('displayName', 'unknown')
# Parse dates
created_at = (
datetime.fromisoformat(
comment_data.get('createdDate', '').replace('Z', '+00:00')
)
if comment_data.get('createdDate')
else datetime.fromtimestamp(0)
)
modified_at = (
datetime.fromisoformat(
comment_data.get('modifiedDate', '').replace('Z', '+00:00')
)
if comment_data.get('modifiedDate')
else created_at
)
comment = Comment(
id=str(comment_data.get('id', 0)),
body=self._truncate_comment(comment_data.get('text', '')),
author=author,
created_at=created_at,
updated_at=modified_at,
system=False,
)
all_comments.append(comment)
# Sort by creation date and limit
all_comments.sort(key=lambda c: c.created_at)
return all_comments[:max_comments]
async def add_work_item_reaction(
self, repository: str, work_item_id: int, reaction_type: str = ':thumbsup:'
) -> dict:
comment_text = f'{reaction_type} OpenHands is processing this work item...'
return await self.add_work_item_comment(repository, work_item_id, comment_text)