Skip to content

Commit 8bd134e

Browse files
authored
Bitbucket: Add push_commands support when PR is updated (#2085)
* Add push_commands support when PR is updated * code review
1 parent a84ba36 commit 8bd134e

File tree

1 file changed

+75
-2
lines changed

1 file changed

+75
-2
lines changed

pr_agent/servers/bitbucket_app.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,79 @@ def _get_username(data):
8383
return ""
8484

8585

86+
async def _validate_time_from_last_commit_to_pr_update(data: dict) -> bool:
87+
is_valid_push = False
88+
try:
89+
data_inner = data.get('data', {})
90+
if not data_inner:
91+
get_logger().error("No data found in the webhook payload")
92+
return True
93+
pull_request = data_inner.get('pullrequest', {})
94+
commits_api = pull_request.get('links', {}).get('commits', {}).get('href')
95+
if not commits_api:
96+
return False
97+
if not pull_request.get('updated_on'):
98+
return False
99+
bearer_token = context.get('bitbucket_bearer_token')
100+
headers = {
101+
'Authorization': f'Bearer {bearer_token}',
102+
'Accept': 'application/json'
103+
}
104+
response = requests.get(commits_api, headers=headers)
105+
if response.status_code != 200:
106+
get_logger().warning(f"Bitbucket commits API returned {response.status_code} for {commits_api}")
107+
return False
108+
109+
username =_get_username(data)
110+
commits_data = response.json() or {}
111+
values = commits_data.get('values') or []
112+
if (not values or not isinstance(values, list) or not values[0].get('author') or not values[0]['author'].get('user')
113+
or not values[0]['author']['user'].get('display_name')):
114+
get_logger().warning("No commits returned for pull request or one of the required fields missing; skipping push validation",
115+
artifact={'values': values})
116+
return False
117+
commit_username = commits_data['values'][0]['author']['user']['display_name']
118+
if username != commit_username:
119+
get_logger().warning(f"Mismatch in username {username} vs. commit_username {commit_username}")
120+
return False
121+
122+
time_pr_updated = pull_request['updated_on']
123+
time_last_commit = commits_data['values'][0]['date']
124+
from datetime import datetime
125+
ts1 = datetime.fromisoformat(time_pr_updated)
126+
ts2 = datetime.fromisoformat(time_last_commit)
127+
diff = (ts1 - ts2).total_seconds()
128+
max_delta_seconds = 15
129+
if diff > 0 and diff < max_delta_seconds:
130+
is_valid_push = True
131+
else:
132+
get_logger().debug(f"Too much time passed since last commit",
133+
artifact={'updated': time_pr_updated, 'last_commit': time_last_commit})
134+
except Exception as e:
135+
get_logger().exception(f"Failed to validate time difference between last commit and PR update",
136+
artifact={'error': e, 'data': data})
137+
return is_valid_push
138+
86139
async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict, data: dict):
87140
apply_repo_settings(api_url)
88141
if commands_conf == "pr_commands" and get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
89142
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}")
90143
return
144+
if commands_conf == "push_commands":
145+
if not get_settings().get("bitbucket_app.handle_push_trigger"):
146+
get_logger().info(
147+
"Bitbucket push trigger handling disabled via config; skipping push commands")
148+
return
91149
if data.get("event", "") == "pullrequest:created":
92150
if not should_process_pr_logic(data):
93151
return
94152
commands = get_settings().get(f"bitbucket_app.{commands_conf}", {})
95153
get_settings().set("config.is_auto_command", True)
154+
if commands_conf == "push_commands":
155+
is_valid_push = await _validate_time_from_last_commit_to_pr_update(data)
156+
if not is_valid_push:
157+
get_logger().info(f"Bitbucket skipping 'pullrequest:updated' for push commands")
158+
return
96159
for command in commands:
97160
try:
98161
split_command = command.split(" ")
@@ -215,11 +278,21 @@ async def inner():
215278
log_context["event"] = "pull_request"
216279
if pr_url:
217280
with get_logger().contextualize(**log_context):
218-
apply_repo_settings(pr_url)
219281
if get_identity_provider().verify_eligibility("bitbucket",
220282
sender_id, pr_url) is not Eligibility.NOT_ELIGIBLE:
221283
if get_settings().get("bitbucket_app.pr_commands"):
222-
await _perform_commands_bitbucket("pr_commands", PRAgent(), pr_url, log_context, data)
284+
await _perform_commands_bitbucket("pr_commands", agent, pr_url, log_context, data)
285+
elif event == "pullrequest:updated": # PR updated, might be from a push (we will validate this later)
286+
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
287+
log_context["api_url"] = pr_url
288+
log_context["event"] = "pull_request"
289+
if pr_url:
290+
with get_logger().contextualize(**log_context):
291+
if get_identity_provider().verify_eligibility("bitbucket",
292+
sender_id, pr_url) is not Eligibility.NOT_ELIGIBLE:
293+
294+
if get_settings().get("bitbucket_app.push_commands"):
295+
await _perform_commands_bitbucket("push_commands", agent, pr_url, log_context, data)
223296
elif event == "pullrequest:comment_created":
224297
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
225298
log_context["api_url"] = pr_url

0 commit comments

Comments
 (0)