@@ -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+
86139async 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