From d8fea6afc402664ab9e0f08e026bb26a5929a351 Mon Sep 17 00:00:00 2001 From: abishlal Date: Sat, 21 Jun 2025 17:20:31 +0530 Subject: [PATCH 1/7] feat: enhance Azure DevOps integration by adding work item as a ticket retrieval methods - Supporting ticket context for Azure DevOps Signed-off-by: abishlal --- .../git_providers/azuredevops_provider.py | 53 +++++++++++++++++-- pr_agent/settings/pr_reviewer_prompts.toml | 7 +++ pr_agent/tools/ticket_pr_compliance_check.py | 27 ++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index d71a029f..a8adabb0 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -39,7 +39,7 @@ class AzureDevopsProvider(GitProvider): "Azure DevOps provider is not available. Please install the required dependencies." ) - self.azure_devops_client = self._get_azure_devops_client() + self.azure_devops_client, self.azure_devops_board_client = self._get_azure_devops_client() self.diff_files = None self.workspace_slug = None self.repo_slug = None @@ -593,8 +593,9 @@ class AzureDevopsProvider(GitProvider): credentials = BasicAuthentication("", auth_token) azure_devops_connection = Connection(base_url=org, creds=credentials) azure_devops_client = azure_devops_connection.clients.get_git_client() + azure_devops_board_client = azure_devops_connection.clients.get_work_item_tracking_client() - return azure_devops_client + return azure_devops_client, azure_devops_board_client def _get_repo(self): if self.repo is None: @@ -635,4 +636,50 @@ class AzureDevopsProvider(GitProvider): last = commits[0] url = self.azure_devops_client.normalized_url + "/" + self.workspace_slug + "/_git/" + self.repo_slug + "/commit/" + last.commit_id return url - \ No newline at end of file + + def get_linked_work_items(self) -> list: + """ + Get linked work items from the PR. + """ + try: + work_items = self.azure_devops_client.get_pull_request_work_item_refs( + project=self.workspace_slug, + repository_id=self.repo_slug, + pull_request_id=self.pr_num, + ) + ids = [work_item.id for work_item in work_items] + if not work_items: + return [] + items = self.get_work_items(ids) + return items + except Exception as e: + get_logger().exception(f"Failed to get linked work items, error: {e}") + return [] + + def get_work_items(self, work_item_ids: int) -> list: + """ + Get work items by their IDs. + """ + try: + raw_work_items = self.azure_devops_board_client.get_work_items( + project=self.workspace_slug, + ids=work_item_ids, + ) + work_items = [] + for item in raw_work_items: + work_items.append( + { + "id": item.id, + "title": item.fields.get("System.Title", ""), + "url": item.url, + "body": item.fields.get("System.Description", ""), + "state": item.fields.get("System.State", ""), + "acceptance_criteria": item.fields.get( + "Microsoft.VSTS.Common.AcceptanceCriteria", "" + ), + } + ) + return work_items + except Exception as e: + get_logger().exception(f"Failed to get work items, error: {e}") + return [] diff --git a/pr_agent/settings/pr_reviewer_prompts.toml b/pr_agent/settings/pr_reviewer_prompts.toml index 8c69e612..789bcc19 100644 --- a/pr_agent/settings/pr_reviewer_prompts.toml +++ b/pr_agent/settings/pr_reviewer_prompts.toml @@ -199,6 +199,13 @@ Ticket Description: {{ ticket.body }} ##### {%- endif %} + +{%- if ticket.requirements %} +Ticket Requirements: +##### +{{ ticket.requirements }} +##### +{%- endif %} ===== {% endfor %} {%- endif %} diff --git a/pr_agent/tools/ticket_pr_compliance_check.py b/pr_agent/tools/ticket_pr_compliance_check.py index 45baa0d2..02abcabc 100644 --- a/pr_agent/tools/ticket_pr_compliance_check.py +++ b/pr_agent/tools/ticket_pr_compliance_check.py @@ -3,6 +3,7 @@ import traceback from pr_agent.config_loader import get_settings from pr_agent.git_providers import GithubProvider +from pr_agent.git_providers import AzureDevopsProvider from pr_agent.log import get_logger # Compile the regex pattern once, outside the function @@ -131,6 +132,32 @@ async def extract_tickets(git_provider): return tickets_content + elif isinstance(git_provider, AzureDevopsProvider): + tickets_info = git_provider.get_linked_work_items() + tickets_content = [] + for ticket in tickets_info: + try: + ticket_body_str = ticket.get("body", "") + if len(ticket_body_str) > MAX_TICKET_CHARACTERS: + ticket_body_str = ticket_body_str[:MAX_TICKET_CHARACTERS] + "..." + + tickets_content.append( + { + "ticket_id": ticket.get("id"), + "ticket_url": ticket.get("url"), + "title": ticket.get("title"), + "body": ticket_body_str, + "labels": ", ".join(ticket.get("labels", [])), + "requirements": ticket.get("acceptance_criteria", ""), + } + ) + except Exception as e: + get_logger().error( + f"Error processing Azure DevOps ticket: {e}", + artifact={"traceback": traceback.format_exc()}, + ) + return tickets_content + except Exception as e: get_logger().error(f"Error extracting tickets error= {e}", artifact={"traceback": traceback.format_exc()}) From ea63c8e63a9c63137e155fafc302255212787012 Mon Sep 17 00:00:00 2001 From: abishlal Date: Sat, 21 Jun 2025 17:22:04 +0530 Subject: [PATCH 2/7] fix: remove redundant line for BasicAuthentication in Azure DevOps provider Signed-off-by: abishlal --- pr_agent/git_providers/azuredevops_provider.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index a8adabb0..09b8c39b 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -588,8 +588,6 @@ class AzureDevopsProvider(GitProvider): get_logger().error(f"No PAT found in settings, and Azure Default Authentication failed, error: {e}") raise - credentials = BasicAuthentication("", auth_token) - credentials = BasicAuthentication("", auth_token) azure_devops_connection = Connection(base_url=org, creds=credentials) azure_devops_client = azure_devops_connection.clients.get_git_client() From fbce8cd2f54814fad62bbe30da7b8b19574db473 Mon Sep 17 00:00:00 2001 From: abishlal Date: Sat, 21 Jun 2025 17:22:24 +0530 Subject: [PATCH 3/7] fix: update comment thread status to active when publishing comments Signed-off-by: abishlal --- pr_agent/git_providers/azuredevops_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 09b8c39b..1f08f4b8 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -350,7 +350,7 @@ class AzureDevopsProvider(GitProvider): get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}") return None comment = Comment(content=pr_comment) - thread = CommentThread(comments=[comment], thread_context=thread_context, status="closed") + thread = CommentThread(comments=[comment], thread_context=thread_context, status="active") thread_response = self.azure_devops_client.create_thread( comment_thread=thread, project=self.workspace_slug, From 738f9856a49490902421030f385afbd4efc42852 Mon Sep 17 00:00:00 2001 From: abishlal Date: Sat, 21 Jun 2025 17:28:59 +0530 Subject: [PATCH 4/7] feat: add WorkItemTrackingClient to Azure DevOps provider and update client return type Signed-off-by: abishlal --- pr_agent/git_providers/azuredevops_provider.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 1f08f4b8..3ce70c94 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -22,6 +22,7 @@ try: from azure.devops.connection import Connection # noinspection PyUnresolvedReferences from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext, CommentPosition) + from azure.devops.released.work_item_tracking import WorkItemTrackingClient # noinspection PyUnresolvedReferences from azure.identity import DefaultAzureCredential from msrest.authentication import BasicAuthentication @@ -566,7 +567,7 @@ class AzureDevopsProvider(GitProvider): return workspace_slug, repo_slug, pr_number @staticmethod - def _get_azure_devops_client() -> GitClient: + def _get_azure_devops_client() -> Tuple[GitClient, WorkItemTrackingClient]: org = get_settings().azure_devops.get("org", None) pat = get_settings().azure_devops.get("pat", None) @@ -654,7 +655,7 @@ class AzureDevopsProvider(GitProvider): get_logger().exception(f"Failed to get linked work items, error: {e}") return [] - def get_work_items(self, work_item_ids: int) -> list: + def get_work_items(self, work_item_ids: list) -> list: """ Get work items by their IDs. """ From 7759d1d3fc9042e8bc6d716c8df3309d0ce32a5e Mon Sep 17 00:00:00 2001 From: abishlal Date: Sat, 21 Jun 2025 17:31:24 +0530 Subject: [PATCH 5/7] fix: remove redundant state and labels fields from ticket data extraction Signed-off-by: abishlal --- pr_agent/git_providers/azuredevops_provider.py | 1 - pr_agent/tools/ticket_pr_compliance_check.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 3ce70c94..d9406c03 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -672,7 +672,6 @@ class AzureDevopsProvider(GitProvider): "title": item.fields.get("System.Title", ""), "url": item.url, "body": item.fields.get("System.Description", ""), - "state": item.fields.get("System.State", ""), "acceptance_criteria": item.fields.get( "Microsoft.VSTS.Common.AcceptanceCriteria", "" ), diff --git a/pr_agent/tools/ticket_pr_compliance_check.py b/pr_agent/tools/ticket_pr_compliance_check.py index 02abcabc..05ebe2c7 100644 --- a/pr_agent/tools/ticket_pr_compliance_check.py +++ b/pr_agent/tools/ticket_pr_compliance_check.py @@ -147,7 +147,6 @@ async def extract_tickets(git_provider): "ticket_url": ticket.get("url"), "title": ticket.get("title"), "body": ticket_body_str, - "labels": ", ".join(ticket.get("labels", [])), "requirements": ticket.get("acceptance_criteria", ""), } ) From 299a2c89d1f332394e77df9ba73da8fe9ab3f4c6 Mon Sep 17 00:00:00 2001 From: abishlal Date: Wed, 25 Jun 2025 20:57:56 +0530 Subject: [PATCH 6/7] feat: add tags extraction from work item fields in Azure DevOps provider Signed-off-by: abishlal --- pr_agent/git_providers/azuredevops_provider.py | 1 + pr_agent/tools/ticket_pr_compliance_check.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index d9406c03..76f9612e 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -675,6 +675,7 @@ class AzureDevopsProvider(GitProvider): "acceptance_criteria": item.fields.get( "Microsoft.VSTS.Common.AcceptanceCriteria", "" ), + "tags": item.fields.get("System.Tags", "").split("; ") if item.fields.get("System.Tags") else [], } ) return work_items diff --git a/pr_agent/tools/ticket_pr_compliance_check.py b/pr_agent/tools/ticket_pr_compliance_check.py index 05ebe2c7..523e21f9 100644 --- a/pr_agent/tools/ticket_pr_compliance_check.py +++ b/pr_agent/tools/ticket_pr_compliance_check.py @@ -148,6 +148,7 @@ async def extract_tickets(git_provider): "title": ticket.get("title"), "body": ticket_body_str, "requirements": ticket.get("acceptance_criteria", ""), + "labels": ", ".join(ticket.get("labels", [])), } ) except Exception as e: From 3251f19a1936754fd7f3a48bb32123ec94c78fe3 Mon Sep 17 00:00:00 2001 From: abishlal Date: Wed, 25 Jun 2025 20:59:34 +0530 Subject: [PATCH 7/7] fix: change comment thread status to closed when publishing comments Signed-off-by: abishlal --- pr_agent/git_providers/azuredevops_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 76f9612e..283f28c7 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -351,7 +351,7 @@ class AzureDevopsProvider(GitProvider): get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}") return None comment = Comment(content=pr_comment) - thread = CommentThread(comments=[comment], thread_context=thread_context, status="active") + thread = CommentThread(comments=[comment], thread_context=thread_context, status="closed") thread_response = self.azure_devops_client.create_thread( comment_thread=thread, project=self.workspace_slug,