From e516d66c1c4a6eb93c31d149b7a9004a5d708de8 Mon Sep 17 00:00:00 2001 From: Thomas De Keulenaer <11250711+twdkeule@users.noreply.github.com> Date: Fri, 9 May 2025 11:58:24 +0200 Subject: [PATCH 1/7] Azure: return Comment object when creating comment --- .../git_providers/azuredevops_provider.py | 33 ++++++++----------- pr_agent/git_providers/git_provider.py | 9 +++-- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 80bf68c5..7524896c 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -18,14 +18,10 @@ ADO_APP_CLIENT_DEFAULT_ID = "499b84ac-1321-427f-aa17-267ca6975798/.default" MAX_PR_DESCRIPTION_AZURE_LENGTH = 4000-1 try: - # noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences from azure.devops.connection import Connection # noinspection PyUnresolvedReferences - from azure.devops.v7_1.git.models import (Comment, CommentThread, - GitPullRequest, - GitPullRequestIterationChanges, - GitVersionDescriptor) + from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient) # noinspection PyUnresolvedReferences from azure.identity import DefaultAzureCredential from msrest.authentication import BasicAuthentication @@ -121,31 +117,29 @@ class AzureDevopsProvider(GitProvider): get_logger().warning(f"Azure failed to publish code suggestion, error: {e}") return True - - def get_pr_description_full(self) -> str: return self.pr.description - def edit_comment(self, comment, body: str): + def edit_comment(self, comment: Comment, body: str): try: self.azure_devops_client.update_comment( repository_id=self.repo_slug, pull_request_id=self.pr_num, - thread_id=comment["thread_id"], - comment_id=comment["comment_id"], + thread_id=comment.thread_id, + comment_id=comment.id, comment=Comment(content=body), project=self.workspace_slug, ) except Exception as e: get_logger().exception(f"Failed to edit comment, error: {e}") - def remove_comment(self, comment): + def remove_comment(self, comment: Comment): try: self.azure_devops_client.delete_comment( repository_id=self.repo_slug, pull_request_id=self.pr_num, - thread_id=comment["thread_id"], - comment_id=comment["comment_id"], + thread_id=comment.thread_id, + comment_id=comment.id, project=self.workspace_slug, ) except Exception as e: @@ -378,7 +372,7 @@ class AzureDevopsProvider(GitProvider): get_logger().exception(f"Failed to get diff files, error: {e}") return [] - def publish_comment(self, pr_comment: str, is_temporary: bool = False, thread_context=None): + def publish_comment(self, pr_comment: str, is_temporary: bool = False, thread_context=None) -> Comment: if is_temporary and not get_settings().config.publish_output_progress: get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}") return None @@ -390,10 +384,11 @@ class AzureDevopsProvider(GitProvider): repository_id=self.repo_slug, pull_request_id=self.pr_num, ) - response = {"thread_id": thread_response.id, "comment_id": thread_response.comments[0].id} + created_comment = thread_response.comments[0] + created_comment.thread_id = thread_response.id if is_temporary: - self.temp_comments.append(response) - return response + self.temp_comments.append(created_comment) + return created_comment def publish_description(self, pr_title: str, pr_body: str): if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH: @@ -522,7 +517,7 @@ class AzureDevopsProvider(GitProvider): def get_user_id(self): return 0 - def get_issue_comments(self): + def get_issue_comments(self) -> list[Comment]: threads = self.azure_devops_client.get_threads(repository_id=self.repo_slug, pull_request_id=self.pr_num, project=self.workspace_slug) threads.reverse() comment_list = [] @@ -562,7 +557,7 @@ class AzureDevopsProvider(GitProvider): return workspace_slug, repo_slug, pr_number @staticmethod - def _get_azure_devops_client(): + def _get_azure_devops_client() -> GitClient: org = get_settings().azure_devops.get("org", None) pat = get_settings().azure_devops.get("pat", None) diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 2895bd55..dfb5b224 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -228,7 +228,7 @@ class GitProvider(ABC): update_header: bool = True, name='review', final_update_message=True): - self.publish_comment(pr_comment) + return self.publish_comment(pr_comment) def publish_persistent_comment_full(self, pr_comment: str, initial_header: str, @@ -250,14 +250,13 @@ class GitProvider(ABC): # response = self.mr.notes.update(comment.id, {'body': pr_comment_updated}) self.edit_comment(comment, pr_comment_updated) if final_update_message: - self.publish_comment( + return self.publish_comment( f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}") - return + return comment except Exception as e: get_logger().exception(f"Failed to update persistent review, error: {e}") pass - self.publish_comment(pr_comment) - + return self.publish_comment(pr_comment) @abstractmethod def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, original_suggestion=None): From c924affebc3e85e15d64dcd47c8c1591ff7f9ec1 Mon Sep 17 00:00:00 2001 From: Thomas De Keulenaer <11250711+twdkeule@users.noreply.github.com> Date: Wed, 7 May 2025 14:46:08 +0200 Subject: [PATCH 2/7] Azure devops provider: add persistent comment --- .../git_providers/azuredevops_provider.py | 22 ++++++++++++++----- pr_agent/tools/pr_code_suggestions.py | 8 ------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 7524896c..4dd61e5b 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -170,10 +170,6 @@ class AzureDevopsProvider(GitProvider): return [] def is_supported(self, capability: str) -> bool: - if capability in [ - "get_issue_comments", - ]: - return False return True def set_pr(self, pr_url: str): @@ -390,6 +386,13 @@ class AzureDevopsProvider(GitProvider): self.temp_comments.append(created_comment) return created_comment + def publish_persistent_comment(self, pr_comment: str, + initial_header: str, + update_header: bool = True, + name='review', + final_update_message=True): + return self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message) + def publish_description(self, pr_title: str, pr_body: str): if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH: @@ -433,7 +436,6 @@ class AzureDevopsProvider(GitProvider): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, original_suggestion=None): self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)]) - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, absolute_position: int = None): position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), @@ -617,3 +619,13 @@ class AzureDevopsProvider(GitProvider): def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str: return self.pr_url+f"?_a=files&path={relevant_file}" + + def get_comment_url(self, comment) -> str: + return self.pr_url + "?discussionId=" + str(comment.thread_id) + + def get_latest_commit_url(self) -> str: + commits = self.azure_devops_client.get_pull_request_commits(self.repo_slug, self.pr_num, self.workspace_slug) + 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 diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index c742aa06..0dea2e70 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -267,14 +267,6 @@ class PRCodeSuggestions: up_to_commit_txt = f" up to commit {match.group(0)[4:-3].strip()}" return up_to_commit_txt - if isinstance(git_provider, AzureDevopsProvider): # get_latest_commit_url is not supported yet - if progress_response: - git_provider.edit_comment(progress_response, pr_comment) - new_comment = progress_response - else: - new_comment = git_provider.publish_comment(pr_comment) - return new_comment - history_header = f"#### Previous suggestions\n" last_commit_num = git_provider.get_latest_commit_url().split('/')[-1][:7] if only_fold: # A user clicked on the 'self-review' checkbox From 3a07b55d0c0dcb3ae2299caee04139f2493620a8 Mon Sep 17 00:00:00 2001 From: Thomas De Keulenaer <11250711+twdkeule@users.noreply.github.com> Date: Wed, 7 May 2025 16:38:11 +0200 Subject: [PATCH 3/7] Azure: dont start threads as active because they block the pull request --- 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 4dd61e5b..39e971ec 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -373,7 +373,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=1) + 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, From 67272700a6bf0b6ad932edc04bde687a0a96f6cc Mon Sep 17 00:00:00 2001 From: Thomas De Keulenaer <11250711+twdkeule@users.noreply.github.com> Date: Fri, 9 May 2025 09:07:16 +0200 Subject: [PATCH 4/7] Azure: handle line comments --- .../git_providers/azuredevops_provider.py | 27 +++++++++++++ .../servers/azuredevops_server_webhook.py | 40 +++++++++++-------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 39e971ec..426953ae 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -117,6 +117,10 @@ class AzureDevopsProvider(GitProvider): get_logger().warning(f"Azure failed to publish code suggestion, error: {e}") return True + def reply_to_comment_from_comment_id(self, comment_id: int, body: str, is_temporary: bool = False) -> Comment: + # comment_id is actually thread_id + return self.reply_to_thread(comment_id, body, is_temporary) + def get_pr_description_full(self) -> str: return self.pr.description @@ -537,6 +541,29 @@ class AzureDevopsProvider(GitProvider): def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: return True + def set_like(self, thread_id: int, comment_id: int, create: bool = True): + if create: + self.azure_devops_client.create_like(self.repo_slug, self.pr_num, thread_id, comment_id, project=self.workspace_slug) + else: + self.azure_devops_client.delete_like(self.repo_slug, self.pr_num, thread_id, comment_id, project=self.workspace_slug) + + def set_thread_status(self, thread_id: int, status: str): + try: + self.azure_devops_client.update_thread(CommentThread(status=status), self.repo_slug, self.pr_num, thread_id, self.workspace_slug) + except Exception as e: + get_logger().exception(f"Failed to set thread status, error: {e}") + + def reply_to_thread(self, thread_id: int, body: str, is_temporary: bool = False) -> Comment: + try: + comment = Comment(content=body) + response = self.azure_devops_client.create_comment(comment, self.repo_slug, self.pr_num, thread_id, self.workspace_slug) + response.thread_id = thread_id + if is_temporary: + self.temp_comments.append(response) + return response + except Exception as e: + get_logger().exception(f"Failed to reply to thread, error: {e}") + @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]: parsed_url = urlparse(pr_url) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index bb97b839..3a03250b 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -22,6 +22,7 @@ from starlette_context.middleware import RawContextMiddleware from pr_agent.agent.pr_agent import PRAgent, command2class from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings +from pr_agent.git_providers import get_git_provider_with_context from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.log import LoggingFormat, get_logger, setup_logger @@ -33,14 +34,18 @@ azure_devops_server = get_settings().get("azure_devops_server") WEBHOOK_USERNAME = azure_devops_server.get("webhook_username") WEBHOOK_PASSWORD = azure_devops_server.get("webhook_password") -async def handle_request_comment( url: str, body: str, log_context: dict -): +async def handle_request_comment(url: str, body: str, thread_id: int, comment_id: int, log_context: dict): log_context["action"] = body log_context["api_url"] = url - try: with get_logger().contextualize(**log_context): - await PRAgent().handle_request(url, body) + agent = PRAgent() + provider = get_git_provider_with_context(pr_url=url) + handled = await agent.handle_request(url, body, notify=lambda: provider.reply_to_thread(thread_id, "On it! ⏳", True)) + # mark command comment as closed + if handled: + provider.set_thread_status(thread_id, "closed") + provider.remove_initial_comment() except Exception as e: get_logger().exception(f"Failed to handle webhook", artifact={"url": url, "body": body}, error=str(e)) @@ -83,7 +88,6 @@ async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: s async def handle_request_azure(data, log_context): - actions = [] if data["eventType"] == "git.pullrequest.created": # API V1 (latest) pr_url = unquote(data["resource"]["_links"]["web"]["href"].replace("_apis/git/repositories", "_git")) @@ -95,11 +99,16 @@ async def handle_request_azure(data, log_context): content=jsonable_encoder({"message": "webhook triggered successfully"}) ) elif data["eventType"] == "ms.vss-code.git-pullrequest-comment-event" and "content" in data["resource"]["comment"]: - if available_commands_rgx.match(data["resource"]["comment"]["content"]): + comment = data["resource"]["comment"] + if available_commands_rgx.match(comment["content"]): if(data["resourceVersion"] == "2.0"): repo = data["resource"]["pullRequest"]["repository"]["webUrl"] pr_url = unquote(f'{repo}/pullrequest/{data["resource"]["pullRequest"]["pullRequestId"]}') - actions = [data["resource"]["comment"]["content"]] + action = comment["content"] + thread_url = comment["_links"]["threads"]["href"] + thread_id = int(thread_url.split("/")[-1]) + comment_id = int(comment["id"]) + pass else: # API V1 not supported as it does not contain the PR URL return JSONResponse( @@ -119,15 +128,14 @@ async def handle_request_azure(data, log_context): log_context["event"] = data["eventType"] log_context["api_url"] = pr_url - for action in actions: - try: - await handle_request_comment(pr_url, action, log_context) - except Exception as e: - get_logger().error("Azure DevOps Trigger failed. Error:" + str(e)) - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=json.dumps({"message": "Internal server error"}), - ) + try: + await handle_request_comment(pr_url, action, thread_id, comment_id, log_context) + except Exception as e: + get_logger().error("Azure DevOps Trigger failed. Error:" + str(e)) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=json.dumps({"message": "Internal server error"}), + ) return JSONResponse( status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder({"message": "webhook triggered successfully"}) ) From 24a90cab8e91475c3dce65aade65c73892b2e611 Mon Sep 17 00:00:00 2001 From: Thomas De Keulenaer <11250711+twdkeule@users.noreply.github.com> Date: Fri, 9 May 2025 12:13:42 +0200 Subject: [PATCH 5/7] Azure: handle inline /ask --- .../git_providers/azuredevops_provider.py | 9 ++++++- .../servers/azuredevops_server_webhook.py | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 426953ae..d77efc27 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -21,7 +21,7 @@ try: # noinspection PyUnresolvedReferences from azure.devops.connection import Connection # noinspection PyUnresolvedReferences - from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient) + from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext) # noinspection PyUnresolvedReferences from azure.identity import DefaultAzureCredential from msrest.authentication import BasicAuthentication @@ -564,6 +564,13 @@ class AzureDevopsProvider(GitProvider): except Exception as e: get_logger().exception(f"Failed to reply to thread, error: {e}") + def get_thread_context(self, thread_id: int) -> CommentThreadContext: + try: + thread = self.azure_devops_client.get_pull_request_thread(self.repo_slug, self.pr_num, thread_id, self.workspace_slug) + return thread.thread_context + except Exception as e: + get_logger().exception(f"Failed to set thread status, error: {e}") + @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]: parsed_url = urlparse(pr_url) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 3a03250b..45533385 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -23,6 +23,7 @@ from pr_agent.agent.pr_agent import PRAgent, command2class from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider_with_context +from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.log import LoggingFormat, get_logger, setup_logger @@ -41,6 +42,7 @@ async def handle_request_comment(url: str, body: str, thread_id: int, comment_id with get_logger().contextualize(**log_context): agent = PRAgent() provider = get_git_provider_with_context(pr_url=url) + body = handle_line_comment(body, thread_id, provider) handled = await agent.handle_request(url, body, notify=lambda: provider.reply_to_thread(thread_id, "On it! ⏳", True)) # mark command comment as closed if handled: @@ -49,6 +51,29 @@ async def handle_request_comment(url: str, body: str, thread_id: int, comment_id except Exception as e: get_logger().exception(f"Failed to handle webhook", artifact={"url": url, "body": body}, error=str(e)) +def handle_line_comment(body: str, thread_id: int, provider: AzureDevopsProvider): + body = body.strip() + if not body.startswith('/ask '): + return body + thread_context = provider.get_thread_context(thread_id) + if not thread_context: + return body + + path = thread_context.file_path + if thread_context.left_file_end or thread_context.left_file_start: + start_line = thread_context.left_file_start.line + end_line = thread_context.left_file_end.line + side = "left" + elif thread_context.right_file_end or thread_context.right_file_start: + start_line = thread_context.right_file_start.line + end_line = thread_context.right_file_end.line + side = "right" + else: + get_logger().info("No line range found in thread context", artifact={"thread_context": thread_context}) + return body + + question = body[5:].lstrip() # remove 4 chars: '/ask ' + return f"/ask_line --line_start={start_line} --line_end={end_line} --side={side} --file_name={path} --comment_id={thread_id} {question}" # currently only basic auth is supported with azure webhooks # for this reason, https must be enabled to ensure the credentials are not sent in clear text From 954d61e5dc232d9151a9820901b4eebff68ed949 Mon Sep 17 00:00:00 2001 From: Thomas De Keulenaer <11250711+twdkeule@users.noreply.github.com> Date: Fri, 9 May 2025 13:12:47 +0200 Subject: [PATCH 6/7] Azure: refactor publish_code_suggestions() to use azure classes --- .../git_providers/azuredevops_provider.py | 43 ++++--------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index d77efc27..35165bdd 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -21,7 +21,7 @@ try: # noinspection PyUnresolvedReferences from azure.devops.connection import Connection # noinspection PyUnresolvedReferences - from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext) + from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext, CommentPosition) # noinspection PyUnresolvedReferences from azure.identity import DefaultAzureCredential from msrest.authentication import BasicAuthentication @@ -73,40 +73,13 @@ class AzureDevopsProvider(GitProvider): f"relevant_lines_start is {relevant_lines_start}") continue - if relevant_lines_end > relevant_lines_start: - post_parameters = { - "body": body, - "path": relevant_file, - "line": relevant_lines_end, - "start_line": relevant_lines_start, - "start_side": "RIGHT", - } - else: # API is different for single line comments - post_parameters = { - "body": body, - "path": relevant_file, - "line": relevant_lines_start, - "side": "RIGHT", - } - post_parameters_list.append(post_parameters) - if not post_parameters_list: - return False - - for post_parameters in post_parameters_list: + thread_context = CommentThreadContext( + file_path=relevant_file, + right_file_start=CommentPosition(offset=1, line=relevant_lines_start), + right_file_end=CommentPosition(offset=1, line=relevant_lines_end)) + comment = Comment(content=body, comment_type=1) + thread = CommentThread(comments=[comment], thread_context=thread_context) try: - comment = Comment(content=post_parameters["body"], comment_type=1) - thread = CommentThread(comments=[comment], - thread_context={ - "filePath": post_parameters["path"], - "rightFileStart": { - "line": post_parameters["start_line"], - "offset": 1, - }, - "rightFileEnd": { - "line": post_parameters["line"], - "offset": 1, - }, - }) self.azure_devops_client.create_thread( comment_thread=thread, project=self.workspace_slug, @@ -114,7 +87,7 @@ class AzureDevopsProvider(GitProvider): pull_request_id=self.pr_num ) except Exception as e: - get_logger().warning(f"Azure failed to publish code suggestion, error: {e}") + get_logger().error(f"Azure failed to publish code suggestion, error: {e}", suggestion=suggestion) return True def reply_to_comment_from_comment_id(self, comment_id: int, body: str, is_temporary: bool = False) -> Comment: From db0c213d72347e7dec8476b313985225f8d78e98 Mon Sep 17 00:00:00 2001 From: Thomas De Keulenaer <11250711+twdkeule@users.noreply.github.com> Date: Fri, 9 May 2025 13:13:29 +0200 Subject: [PATCH 7/7] AzureDevops webhook: allow disabling BasicAuth Azure webhooks do not allow BasicAuth without HTTPS --- .../servers/azuredevops_server_webhook.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 45533385..8eacbf66 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -28,12 +28,12 @@ from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.log import LoggingFormat, get_logger, setup_logger setup_logger(fmt=LoggingFormat.JSON, level=get_settings().get("CONFIG.LOG_LEVEL", "DEBUG")) -security = HTTPBasic() +security = HTTPBasic(auto_error=False) router = APIRouter() available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*") azure_devops_server = get_settings().get("azure_devops_server") -WEBHOOK_USERNAME = azure_devops_server.get("webhook_username") -WEBHOOK_PASSWORD = azure_devops_server.get("webhook_password") +WEBHOOK_USERNAME = azure_devops_server.get("webhook_username", None) +WEBHOOK_PASSWORD = azure_devops_server.get("webhook_password", None) async def handle_request_comment(url: str, body: str, thread_id: int, comment_id: int, log_context: dict): log_context["action"] = body @@ -78,14 +78,17 @@ def handle_line_comment(body: str, thread_id: int, provider: AzureDevopsProvider # currently only basic auth is supported with azure webhooks # for this reason, https must be enabled to ensure the credentials are not sent in clear text def authorize(credentials: HTTPBasicCredentials = Depends(security)): - is_user_ok = secrets.compare_digest(credentials.username, WEBHOOK_USERNAME) - is_pass_ok = secrets.compare_digest(credentials.password, WEBHOOK_PASSWORD) - if not (is_user_ok and is_pass_ok): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Incorrect username or password.', - headers={'WWW-Authenticate': 'Basic'}, - ) + if WEBHOOK_USERNAME is None or WEBHOOK_PASSWORD is None: + return + + is_user_ok = secrets.compare_digest(credentials.username, WEBHOOK_USERNAME) + is_pass_ok = secrets.compare_digest(credentials.password, WEBHOOK_PASSWORD) + if not (is_user_ok and is_pass_ok): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Incorrect username or password.', + headers={'WWW-Authenticate': 'Basic'}, + ) async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):