Skip to content

Commit dfe13bf

Browse files
committed
feat: auto-trigger commands when bot is assigned as reviewer on GitLab MR
Add support for detecting when the PR-Agent bot is assigned as a reviewer on a GitLab merge request via the webhook payload, and automatically running configured commands (e.g. /review) in response. - Add handle_reviewer_assignment toggle and reviewer_commands config under [gitlab] in configuration.toml (disabled by default) - Add is_bot_assigned_as_reviewer() to parse changes.reviewers from the webhook payload and detect initial bot assignment - Add _get_bot_user_id() to auto-resolve the bot's GitLab user ID via API, with caching keyed by (url, token) for multi-tenant safety - Mirror GitLabProvider auth: respect GITLAB.AUTH_TYPE (oauth/private) and GITLAB.SSL_VERIFY when creating the client - Add draft MR guard and defensive isinstance checks on payload fields - Check handle_reviewer_assignment before API calls to avoid wasted work when the feature is disabled - Call apply_repo_settings() before reading the toggle, enabling per-project configuration via .pr_agent.toml Assisted-by: opencode:deepseek-v4-pro
1 parent 9ab2636 commit dfe13bf

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

pr_agent/servers/gitlab_webhook.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import re
55
from datetime import datetime
66

7+
import hashlib
8+
79
import uvicorn
810
from fastapi import APIRouter, FastAPI, Request, status
911
from fastapi.encoders import jsonable_encoder
@@ -108,6 +110,65 @@ def is_draft_ready(data) -> bool:
108110
get_logger().error(f"Failed 'is_draft_ready' logic: {e}")
109111
return False
110112

113+
_bot_user_id_cache = {}
114+
115+
def _get_bot_user_id():
116+
gitlab_url = get_settings().get("GITLAB.URL", "https://gitlab.com")
117+
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
118+
if not gitlab_token:
119+
get_logger().error("No GitLab token available for bot user ID resolution")
120+
return None
121+
122+
cache_key = hashlib.sha256(f"{gitlab_url}:{gitlab_token}".encode()).hexdigest()
123+
124+
cached = _bot_user_id_cache.get(cache_key)
125+
if cached is not None:
126+
return cached if cached != -1 else None
127+
128+
try:
129+
import gitlab
130+
131+
ssl_verify = get_settings().get("GITLAB.SSL_VERIFY", True)
132+
auth_method = get_settings().get("GITLAB.AUTH_TYPE", "oauth_token")
133+
kwargs = {"url": gitlab_url, "ssl_verify": ssl_verify}
134+
if auth_method == "oauth_token":
135+
kwargs["oauth_token"] = gitlab_token
136+
else:
137+
kwargs["private_token"] = gitlab_token
138+
139+
gl = gitlab.Gitlab(**kwargs)
140+
gl.auth()
141+
user_id = gl.user.id
142+
_bot_user_id_cache[cache_key] = user_id
143+
get_logger().info(f"Bot user ID resolved via API: {user_id}")
144+
return user_id
145+
except Exception as e:
146+
_bot_user_id_cache[cache_key] = -1
147+
get_logger().error(f"Failed to resolve bot user ID: {e}")
148+
return None
149+
150+
def is_bot_assigned_as_reviewer(data) -> bool:
151+
try:
152+
changes = data.get('changes')
153+
if not isinstance(changes, dict):
154+
return False
155+
if 'reviewers' not in changes:
156+
return False
157+
reviewers_change = changes['reviewers']
158+
if not isinstance(reviewers_change, dict):
159+
return False
160+
previous = reviewers_change.get('previous', [])
161+
current = reviewers_change.get('current', [])
162+
bot_user_id = _get_bot_user_id()
163+
if bot_user_id is None:
164+
return False
165+
previous_ids = {r.get('id') for r in previous if isinstance(r, dict)}
166+
current_ids = {r.get('id') for r in current if isinstance(r, dict)}
167+
return bot_user_id in current_ids and bot_user_id not in previous_ids
168+
except Exception as e:
169+
get_logger().error(f"Failed 'is_bot_assigned_as_reviewer' logic: {e}")
170+
return False
171+
111172
def should_process_pr_logic(data) -> bool:
112173
try:
113174
if not data.get('object_attributes', {}):
@@ -256,6 +317,21 @@ async def inner(data: dict):
256317
# same as open MR
257318
await _perform_commands_gitlab("pr_commands", PRAgent(), url, log_context, data)
258319

320+
# for reviewer assignment triggered merge requests
321+
elif object_attributes.get('action') == 'update' and not object_attributes.get('oldrev'):
322+
url = object_attributes.get('url')
323+
apply_repo_settings(url)
324+
if not get_settings().gitlab.get('handle_reviewer_assignment', False):
325+
return JSONResponse(status_code=status.HTTP_200_OK,
326+
content=jsonable_encoder({"message": "success"}))
327+
if is_draft(data):
328+
get_logger().info(f"Skipping draft MR reviewer assignment: {url}")
329+
return JSONResponse(status_code=status.HTTP_200_OK,
330+
content=jsonable_encoder({"message": "success"}))
331+
if is_bot_assigned_as_reviewer(data):
332+
get_logger().info(f"Bot was assigned as reviewer on MR: {url}")
333+
await _perform_commands_gitlab("reviewer_commands", PRAgent(), url, log_context, data)
334+
259335
elif data.get('object_kind') == 'note' and data.get('event_type') == 'note': # comment on MR
260336
if 'merge_request' in data:
261337
mr = data['merge_request']

pr_agent/settings/configuration.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ push_commands = [
269269
]
270270
# Configure SSL validation for GitLab. Can be either set to the path of a custom CA or disabled entirely.
271271
# ssl_verify = true
272+
# Auto-trigger commands when the bot is assigned as a reviewer on an MR
273+
handle_reviewer_assignment = false
274+
reviewer_commands = [
275+
"/review",
276+
]
272277

273278
[gitea]
274279
url = "https://gitea.com"

0 commit comments

Comments
 (0)