From 9a54be54144ff821fe10f531fcf0e2a6ae94e0cb Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 16:52:49 -0500 Subject: [PATCH 01/20] add webhook support --- pr_agent/algo/utils.py | 2 +- .../git_providers/azuredevops_provider.py | 6 + .../servers/azuredevops_server_webhook.py | 110 ++++++++++++++++++ pr_agent/settings/.secrets_template.toml | 6 + 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 pr_agent/servers/azuredevops_server_webhook.py diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index e30e2844..b0d4f121 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -320,7 +320,7 @@ def _fix_key_value(key: str, value: str): def load_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict: - response_text = response_text.removeprefix('```yaml').rstrip('`') + response_text = response_text.removeprefix('```yaml').rstrip('`').rstrip(':\n') try: data = yaml.safe_load(response_text) except Exception as e: diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 75fa6001..3698ca6e 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -10,6 +10,7 @@ from .git_provider import GitProvider from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo AZURE_DEVOPS_AVAILABLE = True +MEX_PR_DESCRIPTION_LENGTH = 4000-1 try: # noinspection PyUnresolvedReferences @@ -324,6 +325,11 @@ class AzureDevopsProvider(GitProvider): ) def publish_description(self, pr_title: str, pr_body: str): + if len(pr_body) > MEX_PR_DESCRIPTION_LENGTH: + pr_body = pr_body[:MEX_PR_DESCRIPTION_LENGTH] + get_logger().warning( + "PR description exceeds the maximum character limit of 4000. Truncating the description." + ) try: updated_pr = GitPullRequest() updated_pr.title = pr_title diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py new file mode 100644 index 00000000..58683e28 --- /dev/null +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -0,0 +1,110 @@ +# This file contains the code for the Azure DevOps Server webhook server. +# The server listens for incoming webhooks from Azure DevOps Server and forwards them to the PR Agent. +# ADO webhook documentation: https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops + +import json +import os +import re + +import uvicorn +from fastapi import APIRouter, FastAPI +from fastapi.encoders import jsonable_encoder +from starlette import status +from starlette.background import BackgroundTasks +from starlette.middleware import Middleware +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette_context.middleware import RawContextMiddleware + +from pr_agent.agent.pr_agent import PRAgent, command2class +from pr_agent.config_loader import get_settings +from pr_agent.log import get_logger +import base64 + +router = APIRouter() +available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*") +azuredevops_server = get_settings().get("azure_devops") +WEBHOOK_USERNAME = azuredevops_server.get("webhook_username") +WEBHOOK_PASSWORD = azuredevops_server.get("webhook_password") + +def handle_request( + background_tasks: BackgroundTasks, url: str, body: str, log_context: dict +): + log_context["action"] = body + log_context["api_url"] = url + with get_logger().contextualize(**log_context): + background_tasks.add_task(PRAgent().handle_request, url, body) + + +@router.post("/") +async def handle_webhook(background_tasks: BackgroundTasks, request: Request): + log_context = {"server_type": "azuredevops_server"} + data = await request.json() + get_logger().info(json.dumps(data)) + + if not validate_basic_auth(request): + get_logger().error("Unauthorized webhook request") + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content=json.dumps({"message": "unauthorized"}), + ) + + if data["eventType"] == "git.pullrequest.created": + body = "review" + # API V1 (latest) + pr_url = data["resource"]["_links"]["web"]["href"].replace("_apis/git/repositories", "_git") + elif data["eventType"] == "ms.vss-code.git-pullrequest-comment-event": + if available_commands_rgx.match(data["resource"]["comment"]["content"]): + if(data["resourceVersion"] == "2.0"): + repo = data["resource"]["pullRequest"]["repository"]["webUrl"] + pr_url = f'{repo}/pullrequest/{data["resource"]["pullRequest"]["pullRequestId"]}' + body = data["resource"]["comment"]["content"] + else: + # API V1 not supported as it does not contain the PR URL + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=json.dumps({"message": "version 1.0 webhook for Azure Devops PR comment is not supported. please upgrade to version 2.0"})), + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=json.dumps({"message": "Unsupported command"}), + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=json.dumps({"message": "Unsupported event"}), + ) + + log_context["event"] = data["eventType"] + log_context["api_url"] = pr_url + + try: + handle_request(background_tasks, pr_url, body, log_context) + return JSONResponse( + status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}) + ) + 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"}), + ) + +# currently only basic auth is supported with azure webhooks +def validate_basic_auth(request: Request): + auth = request.headers.get("Authorization") + if not auth: + return False + if not auth.startswith("Basic "): + return False + decoded_auth = base64.b64decode(auth.split(" ")[1]).decode() + username, password = decoded_auth.split(":") + return username == WEBHOOK_USERNAME and password == WEBHOOK_PASSWORD + +def start(): + app = FastAPI(middleware=[Middleware(RawContextMiddleware)]) + app.include_router(router) + uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000"))) + +if __name__ == "__main__": + start() \ No newline at end of file diff --git a/pr_agent/settings/.secrets_template.toml b/pr_agent/settings/.secrets_template.toml index 9ed806be..e398db4a 100644 --- a/pr_agent/settings/.secrets_template.toml +++ b/pr_agent/settings/.secrets_template.toml @@ -76,3 +76,9 @@ base_url = "" [litellm] LITELLM_TOKEN = "" # see https://docs.litellm.ai/docs/debugging/hosted_debugging for details and instructions on how to get a token + +[azuredevops_server] +# For Azure devops Server basic auth - configured in the webhook creation +# Optional, uncomment if you want to use Azure devops webhooks. Value assinged when you create the webhook +# webhook_username = "" +# webhook_password = "" \ No newline at end of file From 86d4a31eef49bd4a163b23197bd4d6f96b221ec9 Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 17:02:14 -0500 Subject: [PATCH 02/20] add docs --- Usage.md | 13 +++++++++++++ pr_agent/servers/azuredevops_server_webhook.py | 2 +- pr_agent/settings/.secrets_template.toml | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Usage.md b/Usage.md index 75504b13..4ec84567 100644 --- a/Usage.md +++ b/Usage.md @@ -441,3 +441,16 @@ And use the following settings (you have to replace the values) in .secrets.toml org = "https://dev.azure.com/YOUR_ORGANIZATION/" pat = "YOUR_PAT_TOKEN" ``` + +##### Azure DevOps webhook +To allow triggering from azure webhook, you need to manually [add webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops) +of type "Pull request created" to trigger a review, or "Pull request commented on" to trigger any supported comment with / comment on the relevant PR. +note the for "Pull request commented on" trigger, only API v2.0 is supported. + +To use webhook security, you need to configure webhook user name and password, both on the server and azure devops webhook. +These will be sent as basic Auth data by thewebhook with each request: +``` +[azuredevops_server] +webhook_username = "" +webhook_password = "" +``` \ No newline at end of file diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 58683e28..9bcc35e4 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -23,7 +23,7 @@ import base64 router = APIRouter() available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*") -azuredevops_server = get_settings().get("azure_devops") +azuredevops_server = get_settings().get("azure_devops_server") WEBHOOK_USERNAME = azuredevops_server.get("webhook_username") WEBHOOK_PASSWORD = azuredevops_server.get("webhook_password") diff --git a/pr_agent/settings/.secrets_template.toml b/pr_agent/settings/.secrets_template.toml index e398db4a..b5ce13b6 100644 --- a/pr_agent/settings/.secrets_template.toml +++ b/pr_agent/settings/.secrets_template.toml @@ -77,7 +77,7 @@ base_url = "" [litellm] LITELLM_TOKEN = "" # see https://docs.litellm.ai/docs/debugging/hosted_debugging for details and instructions on how to get a token -[azuredevops_server] +[azure_devops_server] # For Azure devops Server basic auth - configured in the webhook creation # Optional, uncomment if you want to use Azure devops webhooks. Value assinged when you create the webhook # webhook_username = "" From b7a522ed695c82238287847c0cd0622081bc8dfb Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 17:05:44 -0500 Subject: [PATCH 03/20] add docker file --- docker/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0f669e89..eca88f4e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,6 +26,10 @@ FROM base as gitlab_webhook ADD pr_agent pr_agent CMD ["python", "pr_agent/servers/gitlab_webhook.py"] +FROM base as azure_devops_webhook +ADD pr_agent pr_agent +CMD ["python", "pr_agent/servers/azuredevops_server_webhook.py"] + FROM base as test ADD requirements-dev.txt . RUN pip install -r requirements-dev.txt && rm requirements-dev.txt From a168defd28cd491f235c9fc8327dfa62488596c2 Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 17:09:09 -0500 Subject: [PATCH 04/20] clean readme --- Usage.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Usage.md b/Usage.md index 4ec84567..59004d20 100644 --- a/Usage.md +++ b/Usage.md @@ -442,13 +442,10 @@ org = "https://dev.azure.com/YOUR_ORGANIZATION/" pat = "YOUR_PAT_TOKEN" ``` -##### Azure DevOps webhook -To allow triggering from azure webhook, you need to manually [add webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops) -of type "Pull request created" to trigger a review, or "Pull request commented on" to trigger any supported comment with / comment on the relevant PR. -note the for "Pull request commented on" trigger, only API v2.0 is supported. +##### Azure DevOps Webhook +To trigger from an Azure webhook, you need to manually [add a webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops). Use the "Pull request created" type to trigger a review, or "Pull request commented on" to trigger any supported comment with / comment on the relevant PR. Note that for the "Pull request commented on" trigger, only API v2.0 is supported. -To use webhook security, you need to configure webhook user name and password, both on the server and azure devops webhook. -These will be sent as basic Auth data by thewebhook with each request: +For webhook security, configure the webhook username and password on both the server and Azure DevOps webhook. These will be sent as basic Auth data by the webhook with each request: ``` [azuredevops_server] webhook_username = "" From 22d0c275d773ff7d4993ecd70e591f1902e82810 Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 17:13:59 -0500 Subject: [PATCH 05/20] fix PR comments --- Usage.md | 4 +++- pr_agent/git_providers/azuredevops_provider.py | 3 ++- pr_agent/servers/azuredevops_server_webhook.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Usage.md b/Usage.md index 59004d20..1969911c 100644 --- a/Usage.md +++ b/Usage.md @@ -450,4 +450,6 @@ For webhook security, configure the webhook username and password on both the se [azuredevops_server] webhook_username = "" webhook_password = "" -``` \ No newline at end of file +``` +> :warning: **Ensure that the webhook endpoint is only accessible over HTTPS** to mitigate the risk of credential interception when using basic authentication. + diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 3698ca6e..d9c09d3b 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -326,7 +326,8 @@ class AzureDevopsProvider(GitProvider): def publish_description(self, pr_title: str, pr_body: str): if len(pr_body) > MEX_PR_DESCRIPTION_LENGTH: - pr_body = pr_body[:MEX_PR_DESCRIPTION_LENGTH] + trunction_message = " ... (description truncated due to length limit)" + pr_body = pr_body[:MEX_PR_DESCRIPTION_LENGTH - len(trunction_message)] + trunction_message get_logger().warning( "PR description exceeds the maximum character limit of 4000. Truncating the description." ) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 9bcc35e4..e81501e9 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -91,6 +91,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): ) # 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 validate_basic_auth(request: Request): auth = request.headers.get("Authorization") if not auth: From 076d8e71878b8e7a3791c8c68781965195f5d5fb Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 17:17:25 -0500 Subject: [PATCH 06/20] fix PR code suggestions --- .../git_providers/azuredevops_provider.py | 6 ++--- .../servers/azuredevops_server_webhook.py | 27 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index d9c09d3b..3e98fbd3 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -10,7 +10,7 @@ from .git_provider import GitProvider from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo AZURE_DEVOPS_AVAILABLE = True -MEX_PR_DESCRIPTION_LENGTH = 4000-1 +MAX_PR_DESCRIPTION_LENGTH = 4000-1 try: # noinspection PyUnresolvedReferences @@ -325,9 +325,9 @@ class AzureDevopsProvider(GitProvider): ) def publish_description(self, pr_title: str, pr_body: str): - if len(pr_body) > MEX_PR_DESCRIPTION_LENGTH: + if len(pr_body) > MAX_PR_DESCRIPTION_LENGTH: trunction_message = " ... (description truncated due to length limit)" - pr_body = pr_body[:MEX_PR_DESCRIPTION_LENGTH - len(trunction_message)] + trunction_message + pr_body = pr_body[:MAX_PR_DESCRIPTION_LENGTH - len(trunction_message)] + trunction_message get_logger().warning( "PR description exceeds the maximum character limit of 4000. Truncating the description." ) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index e81501e9..f10e0de7 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -5,9 +5,9 @@ import json import os import re - import uvicorn -from fastapi import APIRouter, FastAPI +from fastapi import APIRouter, Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.encoders import jsonable_encoder from starlette import status from starlette.background import BackgroundTasks @@ -19,7 +19,6 @@ from starlette_context.middleware import RawContextMiddleware from pr_agent.agent.pr_agent import PRAgent, command2class from pr_agent.config_loader import get_settings from pr_agent.log import get_logger -import base64 router = APIRouter() available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*") @@ -93,14 +92,20 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): # 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 validate_basic_auth(request: Request): - auth = request.headers.get("Authorization") - if not auth: - return False - if not auth.startswith("Basic "): - return False - decoded_auth = base64.b64decode(auth.split(" ")[1]).decode() - username, password = decoded_auth.split(":") - return username == WEBHOOK_USERNAME and password == WEBHOOK_PASSWORD + try: + auth = request.headers.get("Authorization") + if not auth: + return False + if not auth.startswith("Basic "): + return False + security = HTTPBasic() + credentials: HTTPBasicCredentials = Depends(security) + username = credentials.username + password = credentials.password + return username == WEBHOOK_USERNAME and password == WEBHOOK_PASSWORD + except: + get_logger().error("Failed to validate basic auth") + return False def start(): app = FastAPI(middleware=[Middleware(RawContextMiddleware)]) From bc38fad4db6aa0189e29d9393671f747e0fbc2a3 Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 17:23:56 -0500 Subject: [PATCH 07/20] add support for auto events --- .../servers/azuredevops_server_webhook.py | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index f10e0de7..1f824e20 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -47,17 +47,23 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): status_code=status.HTTP_401_UNAUTHORIZED, content=json.dumps({"message": "unauthorized"}), ) - + actions = [] if data["eventType"] == "git.pullrequest.created": - body = "review" # API V1 (latest) pr_url = data["resource"]["_links"]["web"]["href"].replace("_apis/git/repositories", "_git") + if get_settings().get("github_action_config").get("auto_review") == True: + actions.append("review") + if get_settings().get("github_action_config").get("auto_improve") == True: + actions.append("improve") + if get_settings().get("github_action_config").get("describe") == True: + actions.append("describe") + elif data["eventType"] == "ms.vss-code.git-pullrequest-comment-event": if available_commands_rgx.match(data["resource"]["comment"]["content"]): if(data["resourceVersion"] == "2.0"): repo = data["resource"]["pullRequest"]["repository"]["webUrl"] pr_url = f'{repo}/pullrequest/{data["resource"]["pullRequest"]["pullRequestId"]}' - body = data["resource"]["comment"]["content"] + actions = [data["resource"]["comment"]["content"]] else: # API V1 not supported as it does not contain the PR URL return JSONResponse( @@ -77,17 +83,18 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): log_context["event"] = data["eventType"] log_context["api_url"] = pr_url - try: - handle_request(background_tasks, pr_url, body, log_context) - return JSONResponse( - status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}) - ) - 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"}), - ) + for action in actions: + try: + handle_request(background_tasks, 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"}), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "webhook triggerd successfully"}) + ) # 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 95344c7083413596977d75c58ef7998a0d6572df Mon Sep 17 00:00:00 2001 From: Yochai Lehman Date: Sun, 11 Feb 2024 17:42:06 -0500 Subject: [PATCH 08/20] fix basic auth --- .../servers/azuredevops_server_webhook.py | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 1f824e20..978163ce 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -5,8 +5,9 @@ import json import os import re +import secrets import uvicorn -from fastapi import APIRouter, Depends, FastAPI +from fastapi import APIRouter, Depends, FastAPI, HTTPException from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.encoders import jsonable_encoder from starlette import status @@ -19,7 +20,11 @@ from starlette_context.middleware import RawContextMiddleware from pr_agent.agent.pr_agent import PRAgent, command2class from pr_agent.config_loader import get_settings from pr_agent.log import get_logger +from fastapi import Request, Depends +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from pr_agent.log import get_logger +security = HTTPBasic() router = APIRouter() available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*") azuredevops_server = get_settings().get("azure_devops_server") @@ -35,18 +40,24 @@ def handle_request( background_tasks.add_task(PRAgent().handle_request, url, body) -@router.post("/") +# 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'}, + ) + +@router.post("/", dependencies=[Depends(authorize)]) async def handle_webhook(background_tasks: BackgroundTasks, request: Request): log_context = {"server_type": "azuredevops_server"} data = await request.json() get_logger().info(json.dumps(data)) - if not validate_basic_auth(request): - get_logger().error("Unauthorized webhook request") - return JSONResponse( - status_code=status.HTTP_401_UNAUTHORIZED, - content=json.dumps({"message": "unauthorized"}), - ) actions = [] if data["eventType"] == "git.pullrequest.created": # API V1 (latest) @@ -96,24 +107,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "webhook triggerd successfully"}) ) -# 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 validate_basic_auth(request: Request): - try: - auth = request.headers.get("Authorization") - if not auth: - return False - if not auth.startswith("Basic "): - return False - security = HTTPBasic() - credentials: HTTPBasicCredentials = Depends(security) - username = credentials.username - password = credentials.password - return username == WEBHOOK_USERNAME and password == WEBHOOK_PASSWORD - except: - get_logger().error("Failed to validate basic auth") - return False - + def start(): app = FastAPI(middleware=[Middleware(RawContextMiddleware)]) app.include_router(router) From 34378384da74331923460abf79a4c3ad719be29e Mon Sep 17 00:00:00 2001 From: yochail Date: Sun, 11 Feb 2024 17:59:02 -0500 Subject: [PATCH 09/20] add get endpoint for container status --- pr_agent/servers/azuredevops_server_webhook.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 978163ce..36fb9a92 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -107,11 +107,14 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "webhook triggerd successfully"}) ) - +@router.get("/") +async def root(): + return {"status": "ok"} + def start(): app = FastAPI(middleware=[Middleware(RawContextMiddleware)]) app.include_router(router) uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000"))) if __name__ == "__main__": - start() \ No newline at end of file + start() From 8fa058ff7f65677acf7f1a0ce2ffafb3a164448b Mon Sep 17 00:00:00 2001 From: yochail Date: Sun, 11 Feb 2024 18:06:56 -0500 Subject: [PATCH 10/20] add azure devops pat to secret template config --- pr_agent/settings/.secrets_template.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pr_agent/settings/.secrets_template.toml b/pr_agent/settings/.secrets_template.toml index b5ce13b6..8735d962 100644 --- a/pr_agent/settings/.secrets_template.toml +++ b/pr_agent/settings/.secrets_template.toml @@ -77,8 +77,13 @@ base_url = "" [litellm] LITELLM_TOKEN = "" # see https://docs.litellm.ai/docs/debugging/hosted_debugging for details and instructions on how to get a token +[azure_devops] +# For Azure devops personal access token +org = "" +pat = "" + [azure_devops_server] # For Azure devops Server basic auth - configured in the webhook creation # Optional, uncomment if you want to use Azure devops webhooks. Value assinged when you create the webhook # webhook_username = "" -# webhook_password = "" \ No newline at end of file +# webhook_password = "" From bbd0d62c8556a043731d0d87a6a7b4b860783f36 Mon Sep 17 00:00:00 2001 From: yochail Date: Sun, 11 Feb 2024 18:10:22 -0500 Subject: [PATCH 11/20] fix auto_describe key --- pr_agent/servers/azuredevops_server_webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 36fb9a92..27b27b0b 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -66,7 +66,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): actions.append("review") if get_settings().get("github_action_config").get("auto_improve") == True: actions.append("improve") - if get_settings().get("github_action_config").get("describe") == True: + if get_settings().get("github_action_config").get("auto_describe") == True: actions.append("describe") elif data["eventType"] == "ms.vss-code.git-pullrequest-comment-event": From e8c2ec034dbe7fbd2159a5d9777df8fbc546f644 Mon Sep 17 00:00:00 2001 From: yochail Date: Mon, 12 Feb 2024 18:38:08 -0500 Subject: [PATCH 12/20] Update azuredevops_server_webhook.py fix returned HTTP status --- pr_agent/servers/azuredevops_server_webhook.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 27b27b0b..e5ecc3fd 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -69,7 +69,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): if get_settings().get("github_action_config").get("auto_describe") == True: actions.append("describe") - elif data["eventType"] == "ms.vss-code.git-pullrequest-comment-event": + 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"]): if(data["resourceVersion"] == "2.0"): repo = data["resource"]["pullRequest"]["repository"]["webUrl"] @@ -87,7 +87,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): ) else: return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_204_NO_CONTENT, content=json.dumps({"message": "Unsupported event"}), ) @@ -104,7 +104,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): content=json.dumps({"message": "Internal server error"}), ) return JSONResponse( - status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "webhook triggerd successfully"}) + status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder({"message": "webhook triggerd successfully"}) ) @router.get("/") From 9ff62dce083393d8b7664600ff50183a66d7bc70 Mon Sep 17 00:00:00 2001 From: yochail Date: Mon, 12 Feb 2024 18:40:06 -0500 Subject: [PATCH 13/20] Add legacy url support --- pr_agent/git_providers/azuredevops_provider.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 3e98fbd3..022e6401 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -460,18 +460,23 @@ class AzureDevopsProvider(GitProvider): @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]: parsed_url = urlparse(pr_url) - path_parts = parsed_url.path.strip("/").split("/") - if len(path_parts) < 6 or path_parts[4] != "pullrequest": + # support legacy urls + # https://learn.microsoft.com/en-us/azure/devops/extend/develop/work-with-urls?view=azure-devops&tabs=http + path_offset = 0 + if "visualstudio" in pr_url: + path_offset = 1 + + if len(path_parts) < (6 - path_offset) or path_parts[4 - path_offset] != "pullrequest": raise ValueError( "The provided URL does not appear to be a Azure DevOps PR URL" ) - workspace_slug = path_parts[1] - repo_slug = path_parts[3] + workspace_slug = path_parts[1 - path_offset] + repo_slug = path_parts[3 - path_offset] try: - pr_number = int(path_parts[5]) + pr_number = int(path_parts[5 - path_offset]) except ValueError as e: raise ValueError("Unable to convert PR number to integer") from e From b833d634681a766413722aa861919535b3c08905 Mon Sep 17 00:00:00 2001 From: yochail Date: Tue, 13 Feb 2024 22:25:52 -0500 Subject: [PATCH 14/20] PR comment: change name to azure_devops_server --- Usage.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Usage.md b/Usage.md index 1969911c..87f89a2a 100644 --- a/Usage.md +++ b/Usage.md @@ -443,11 +443,12 @@ pat = "YOUR_PAT_TOKEN" ``` ##### Azure DevOps Webhook -To trigger from an Azure webhook, you need to manually [add a webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops). Use the "Pull request created" type to trigger a review, or "Pull request commented on" to trigger any supported comment with / comment on the relevant PR. Note that for the "Pull request commented on" trigger, only API v2.0 is supported. +To trigger from an Azure webhook, you need to manually [add a webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops). +Use the "Pull request created" type to trigger a review, or "Pull request commented on" to trigger any supported comment with / comment on the relevant PR. Note that for the "Pull request commented on" trigger, only API v2.0 is supported. -For webhook security, configure the webhook username and password on both the server and Azure DevOps webhook. These will be sent as basic Auth data by the webhook with each request: +For webhook security, create a sporadic username/password pair and configure the webhook username and password on both the server and Azure DevOps webhook. These will be sent as basic Auth data by the webhook with each request: ``` -[azuredevops_server] +[azure_devops_server] webhook_username = "" webhook_password = "" ``` From 1053fa84f6a683abd9d930b6d2fdc2c33c94c188 Mon Sep 17 00:00:00 2001 From: yochail Date: Tue, 13 Feb 2024 22:27:07 -0500 Subject: [PATCH 15/20] rename azure_devops_server var --- pr_agent/servers/azuredevops_server_webhook.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index e5ecc3fd..c25a9418 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -27,9 +27,9 @@ from pr_agent.log import get_logger security = HTTPBasic() router = APIRouter() available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*") -azuredevops_server = get_settings().get("azure_devops_server") -WEBHOOK_USERNAME = azuredevops_server.get("webhook_username") -WEBHOOK_PASSWORD = azuredevops_server.get("webhook_password") +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") def handle_request( background_tasks: BackgroundTasks, url: str, body: str, log_context: dict @@ -54,7 +54,7 @@ def authorize(credentials: HTTPBasicCredentials = Depends(security)): @router.post("/", dependencies=[Depends(authorize)]) async def handle_webhook(background_tasks: BackgroundTasks, request: Request): - log_context = {"server_type": "azuredevops_server"} + log_context = {"server_type": "azure_devops_server"} data = await request.json() get_logger().info(json.dumps(data)) From b402bd55912e292023bbf6a5593f1d628f647993 Mon Sep 17 00:00:00 2001 From: yochail Date: Sat, 17 Feb 2024 08:36:26 -0500 Subject: [PATCH 16/20] revert azuredevops_provider.py change --- .../git_providers/azuredevops_provider.py | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 022e6401..75fa6001 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -10,7 +10,6 @@ from .git_provider import GitProvider from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo AZURE_DEVOPS_AVAILABLE = True -MAX_PR_DESCRIPTION_LENGTH = 4000-1 try: # noinspection PyUnresolvedReferences @@ -325,12 +324,6 @@ class AzureDevopsProvider(GitProvider): ) def publish_description(self, pr_title: str, pr_body: str): - if len(pr_body) > MAX_PR_DESCRIPTION_LENGTH: - trunction_message = " ... (description truncated due to length limit)" - pr_body = pr_body[:MAX_PR_DESCRIPTION_LENGTH - len(trunction_message)] + trunction_message - get_logger().warning( - "PR description exceeds the maximum character limit of 4000. Truncating the description." - ) try: updated_pr = GitPullRequest() updated_pr.title = pr_title @@ -460,23 +453,18 @@ class AzureDevopsProvider(GitProvider): @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]: parsed_url = urlparse(pr_url) + path_parts = parsed_url.path.strip("/").split("/") - # support legacy urls - # https://learn.microsoft.com/en-us/azure/devops/extend/develop/work-with-urls?view=azure-devops&tabs=http - path_offset = 0 - if "visualstudio" in pr_url: - path_offset = 1 - - if len(path_parts) < (6 - path_offset) or path_parts[4 - path_offset] != "pullrequest": + if len(path_parts) < 6 or path_parts[4] != "pullrequest": raise ValueError( "The provided URL does not appear to be a Azure DevOps PR URL" ) - workspace_slug = path_parts[1 - path_offset] - repo_slug = path_parts[3 - path_offset] + workspace_slug = path_parts[1] + repo_slug = path_parts[3] try: - pr_number = int(path_parts[5 - path_offset]) + pr_number = int(path_parts[5]) except ValueError as e: raise ValueError("Unable to convert PR number to integer") from e From de4af313baddc383f3a754cdf719e5e0ba2c2b24 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sat, 17 Feb 2024 19:15:13 +0200 Subject: [PATCH 17/20] azure dev ops --- .../git_providers/azuredevops_provider.py | 47 ++++++++++++++++--- .../servers/azuredevops_server_webhook.py | 34 ++++++++++---- pr_agent/tools/pr_description.py | 2 +- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 75fa6001..9f8defd8 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -10,6 +10,7 @@ from .git_provider import GitProvider from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo AZURE_DEVOPS_AVAILABLE = True +MAX_PR_DESCRIPTION_AZURE_LENGTH = 4000-1 try: # noinspection PyUnresolvedReferences @@ -38,7 +39,7 @@ class AzureDevopsProvider(GitProvider): ) self.azure_devops_client = self._get_azure_devops_client() - + self.diff_files = None self.workspace_slug = None self.repo_slug = None self.repo = None @@ -124,6 +125,19 @@ class AzureDevopsProvider(GitProvider): def get_pr_description_full(self) -> str: return self.pr.description + def edit_comment(self, 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"], + 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): try: self.azure_devops_client.delete_comment( @@ -181,7 +195,7 @@ class AzureDevopsProvider(GitProvider): include_content=True, path=".pr_agent.toml", ) - return contents + return list(contents)[0] except Exception as e: if get_settings().config.verbosity_level >= 2: get_logger().error(f"Failed to get repo settings, error: {e}") @@ -206,6 +220,10 @@ class AzureDevopsProvider(GitProvider): def get_diff_files(self) -> list[FilePatchInfo]: try: + + if self.diff_files: + return self.diff_files + base_sha = self.pr.last_merge_target_commit head_sha = self.pr.last_merge_source_commit @@ -303,7 +321,7 @@ class AzureDevopsProvider(GitProvider): edit_type=edit_type, ) ) - + self.diff_files = diff_files return diff_files except Exception as e: print(f"Error: {str(e)}") @@ -318,12 +336,29 @@ 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} if is_temporary: - self.temp_comments.append( - {"thread_id": thread_response.id, "comment_id": thread_response.comments[0].id} - ) + self.temp_comments.append(response) + return response def publish_description(self, pr_title: str, pr_body: str): + if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH: + + usage_guide_text='
✨ Usage guide:
' + ind = pr_body.find(usage_guide_text) + if ind != -1: + pr_body = pr_body[:ind] + + if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH: + changes_walkthrough_text = '## **Changes walkthrough**' + ind = pr_body.find(changes_walkthrough_text) + if ind != -1: + pr_body = pr_body[:ind] + + if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH: + trunction_message = " ... (description truncated due to length limit)" + pr_body = pr_body[:MAX_PR_DESCRIPTION_AZURE_LENGTH - len(trunction_message)] + trunction_message + get_logger().warning("PR description was truncated due to length limit") try: updated_pr = GitPullRequest() updated_pr.title = pr_title diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index c25a9418..fc667cea 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -18,7 +18,9 @@ from starlette.responses import JSONResponse 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.utils import apply_repo_settings from pr_agent.log import get_logger from fastapi import Request, Depends from fastapi.security import HTTPBasic, HTTPBasicCredentials @@ -51,8 +53,25 @@ def authorize(credentials: HTTPBasicCredentials = Depends(security)): detail='Incorrect username or password.', headers={'WWW-Authenticate': 'Basic'}, ) - -@router.post("/", dependencies=[Depends(authorize)]) + +async def _perform_commands(commands_conf: str, agent: PRAgent, body: dict, api_url: str, log_context: dict): + apply_repo_settings(api_url) + commands = get_settings().get(f"azure_devops_server.{commands_conf}") + for command in commands: + split_command = command.split(" ") + command = split_command[0] + args = split_command[1:] + other_args = update_settings_from_args(args) + new_command = ' '.join([command] + other_args) + if body: + get_logger().info(body) + get_logger().info(f"Performing command: {new_command}") + with get_logger().contextualize(**log_context): + await agent.handle_request(api_url, new_command) + + +# @router.post("/", dependencies=[Depends(authorize)]) +@router.post("/") async def handle_webhook(background_tasks: BackgroundTasks, request: Request): log_context = {"server_type": "azure_devops_server"} data = await request.json() @@ -62,13 +81,10 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): if data["eventType"] == "git.pullrequest.created": # API V1 (latest) pr_url = data["resource"]["_links"]["web"]["href"].replace("_apis/git/repositories", "_git") - if get_settings().get("github_action_config").get("auto_review") == True: - actions.append("review") - if get_settings().get("github_action_config").get("auto_improve") == True: - actions.append("improve") - if get_settings().get("github_action_config").get("auto_describe") == True: - actions.append("describe") - + log_context["event"] = data["eventType"] + log_context["api_url"] = pr_url + await _perform_commands("pr_commands", PRAgent(), {}, pr_url, log_context) + return 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"]): if(data["resourceVersion"] == "2.0"): diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py index 95a69195..f5dfa1a2 100644 --- a/pr_agent/tools/pr_description.py +++ b/pr_agent/tools/pr_description.py @@ -109,7 +109,7 @@ class PRDescription: # final markdown description full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}" - get_logger().debug(f"full_markdown_description:\n{full_markdown_description}") + # get_logger().debug(f"full_markdown_description:\n{full_markdown_description}") if get_settings().config.publish_output: get_logger().info(f"Pushing answer {self.pr_id}") From 481a4fe7a1b69d9d857337086395c144749ddf83 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sat, 17 Feb 2024 19:43:34 +0200 Subject: [PATCH 18/20] revert --- pr_agent/servers/azuredevops_server_webhook.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index fc667cea..1459b78b 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -70,8 +70,7 @@ async def _perform_commands(commands_conf: str, agent: PRAgent, body: dict, api_ await agent.handle_request(api_url, new_command) -# @router.post("/", dependencies=[Depends(authorize)]) -@router.post("/") +@router.post("/", dependencies=[Depends(authorize)]) async def handle_webhook(background_tasks: BackgroundTasks, request: Request): log_context = {"server_type": "azure_devops_server"} data = await request.json() From c6cb0524b4e2b62d8092bac5a6d3ac2fa5df748f Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 18 Feb 2024 07:56:14 +0200 Subject: [PATCH 19/20] rstrip --- pr_agent/algo/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index b0d4f121..81ab5e00 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -320,7 +320,7 @@ def _fix_key_value(key: str, value: str): def load_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict: - response_text = response_text.removeprefix('```yaml').rstrip('`').rstrip(':\n') + response_text = response_text.removeprefix('```yaml').rstrip('`') try: data = yaml.safe_load(response_text) except Exception as e: @@ -361,9 +361,9 @@ def try_fix_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict: pass # third fallback - try to remove leading and trailing curly brackets - response_text_copy = response_text.strip().rstrip().removeprefix('{').removesuffix('}') + response_text_copy = response_text.strip().rstrip().removeprefix('{').removesuffix('}').rstrip(':\n') try: - data = yaml.safe_load(response_text_copy,) + data = yaml.safe_load(response_text_copy) get_logger().info(f"Successfully parsed AI prediction after removing curly brackets") return data except: @@ -374,7 +374,7 @@ def try_fix_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict: for i in range(1, len(response_text_lines)): response_text_lines_tmp = '\n'.join(response_text_lines[:-i]) try: - data = yaml.safe_load(response_text_lines_tmp,) + data = yaml.safe_load(response_text_lines_tmp) get_logger().info(f"Successfully parsed AI prediction after removing {i} lines") return data except: From 9e3b79b21bbc01cc1d119679249122160b64cb1b Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 18 Feb 2024 07:59:53 +0200 Subject: [PATCH 20/20] readme --- Usage.md | 69 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/Usage.md b/Usage.md index 87f89a2a..7e728122 100644 --- a/Usage.md +++ b/Usage.md @@ -7,6 +7,7 @@ - [GitHub App](#working-with-github-app) - [GitHub Action](#working-with-github-action) - [BitBucket App](#working-with-bitbucket-self-hosted-app) +- [Azure DevOps Provider](#azure-devops-provider) - [Additional Configurations Walkthrough](#appendix---additional-configurations-walkthrough) ### Introduction @@ -261,6 +262,45 @@ If not set, the default option is that only the `review` tool will run automatic Note that due to limitations of the bitbucket platform, the `auto_describe` tool will be able to publish a PR description only as a comment. In addition, some subsections like `PR changes walkthrough` will not appear, since they require the usage of collapsible sections, which are not supported by bitbucket. +### Azure DevOps provider + +To use Azure DevOps provider use the following settings in configuration.toml: +``` +[config] +git_provider="azure" +use_repo_settings_file=false +``` + +And use the following settings (you have to replace the values) in .secrets.toml: +``` +[azure_devops] +org = "https://dev.azure.com/YOUR_ORGANIZATION/" +pat = "YOUR_PAT_TOKEN" +``` + +##### Azure DevOps Webhook +To trigger from an Azure webhook, you need to manually [add a webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops). +Use the "Pull request created" type to trigger a review, or "Pull request commented on" to trigger any supported comment with / comment on the relevant PR. Note that for the "Pull request commented on" trigger, only API v2.0 is supported. + +To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App: +``` +[azure_devops_server] +pr_commands = [ + "/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true", + "/review --pr_reviewer.num_code_suggestions=0", + "/improve", +] +``` + +For webhook security, create a sporadic username/password pair and configure the webhook username and password on both the server and Azure DevOps webhook. These will be sent as basic Auth data by the webhook with each request: +``` +[azure_devops_server] +webhook_username = "" +webhook_password = "" +``` +> :warning: **Ensure that the webhook endpoint is only accessible over HTTPS** to mitigate the risk of credential interception when using basic authentication. + + ### Appendix - additional configurations walkthrough @@ -425,32 +465,3 @@ patch_extra_lines=3 Increasing this number provides more context to the model, but will also increase the token budget. If the PR is too large (see [PR Compression strategy](./PR_COMPRESSION.md)), PR-Agent automatically sets this number to 0, using the original git patch. - - -#### Azure DevOps provider -To use Azure DevOps provider use the following settings in configuration.toml: -``` -[config] -git_provider="azure" -use_repo_settings_file=false -``` - -And use the following settings (you have to replace the values) in .secrets.toml: -``` -[azure_devops] -org = "https://dev.azure.com/YOUR_ORGANIZATION/" -pat = "YOUR_PAT_TOKEN" -``` - -##### Azure DevOps Webhook -To trigger from an Azure webhook, you need to manually [add a webhook](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops). -Use the "Pull request created" type to trigger a review, or "Pull request commented on" to trigger any supported comment with / comment on the relevant PR. Note that for the "Pull request commented on" trigger, only API v2.0 is supported. - -For webhook security, create a sporadic username/password pair and configure the webhook username and password on both the server and Azure DevOps webhook. These will be sent as basic Auth data by the webhook with each request: -``` -[azure_devops_server] -webhook_username = "" -webhook_password = "" -``` -> :warning: **Ensure that the webhook endpoint is only accessible over HTTPS** to mitigate the risk of credential interception when using basic authentication. -