def process_pull_requests(repository, installation, warn_seconds=None, close_seconds=None): now = time.time() # Find app name bot_name = get_app_name() # Get issues labeled as 'Close?' repo = RepoHandler(repository, 'master', installation) pull_requests = repo.open_pull_requests() for n in pull_requests: print(f'Checking {n}') pr = PullRequestHandler(repository, n, installation) if 'keep-open' in pr.labels: print('-> PROTECTED by label, skipping') continue commit_time = pr.last_commit_date time_since_last_commit = now - commit_time # Note: if warning time is before commit time, it's as if the warning # didn't exist since it's no longer relevant. warning_time = pr.last_comment_date(f'{bot_name}[bot]', filter_keep=is_close_warning) if warning_time is None or warning_time < commit_time: time_since_last_warning = -1. else: # We use max() here to make sure that the value is positive time_since_last_warning = max(0, now - warning_time) # We only close pull requests if there has been a warning before, and # the time since the warning exceeds the threshold specified by # close_seconds. if time_since_last_warning > close_seconds: comment_ids = pr.find_comments(f'{bot_name}[bot]', filter_keep=is_close_epilogue) if len(comment_ids) == 0: print(f'-> CLOSING pull request {n}') pr.set_labels(['closed-by-bot']) pr.submit_comment(PULL_REQUESTS_CLOSE_EPILOGUE) pr.close() else: print(f'-> Skipping pull request {n} (already closed)') elif time_since_last_commit > warn_seconds: # A negative time_since_last_warning means no warning since last commit. if time_since_last_warning < 0.: print(f'-> WARNING pull request {n}') pr.submit_comment(PULL_REQUESTS_CLOSE_WARNING.format(pasttime=naturaldelta(time_since_last_commit), futuretime=naturaldelta(close_seconds))) else: print(f'-> Skipping pull request {n} (already warned)') else: print(f'-> OK pull request {n}') print('Finished checking for stale pull requests')
def process_pull_request(repository, number, installation): # TODO: cache handlers and invalidate the internal cache of the handlers on # certain events. pr_handler = PullRequestHandler(repository, number, installation) pr_config = pr_handler.get_config_value("pull_requests", {}) post_comment = pr_config.get("post_pr_comment", False) pull_request_substring = pr_config.get('pull_request_substring', '') # Disable if the config is not present if pr_config is None: return # Don't comment on closed PR if pr_handler.is_closed: return "Pull request already closed, no need to check" repo_handler = RepoHandler(pr_handler.head_repo_name, pr_handler.head_branch, installation) def is_previous_comment(message): if len(pull_request_substring) > 0: return pull_request_substring in message else: return True # Find previous comments by this app comment_ids = pr_handler.find_comments(f'{current_app.bot_username}[bot]', filter_keep=is_previous_comment) if len(comment_ids) == 0: comment_id = None else: comment_id = comment_ids[-1] # First check whether there are labels that indicate the checks should be # skipped skip_labels = pr_config.get("skip_labels", []) skip_fails = pr_config.get("skip_fails", True) for label in pr_handler.labels: if label in skip_labels: skip_message = pr_config.get( "skip_message", "Pull request checks have " "been skipped as this pull request has been " f"labelled as **{label}**") skip_message = skip_message.format(pr_handler=pr_handler, repo_handler=repo_handler) pr_handler.submit_comment(skip_message, comment_id=comment_id) if skip_fails: pr_handler.set_status( 'failure', "Skipping checks due to {0} label".format(label), current_app.bot_username) return results = {} for function in PULL_REQUEST_CHECKS: result = function(pr_handler, repo_handler) results.update(result) failures = [ details['description'] for details in results.values() if details['state'] in ('error', 'failure') ] if post_comment: # Post all failures in a comment, and have a single status check if failures: pull_request_prologue = pr_config.get('fail_prologue', '') pull_request_epilogue = pr_config.get('fail_epilogue', '') fail_status = pr_config.get('fail_status', 'Failed some checks') message = pull_request_prologue.format(pr_handler=pr_handler, repo_handler=repo_handler) for failure in failures: message += f'* {failure}\n' message += pull_request_epilogue.format(pr_handler=pr_handler, repo_handler=repo_handler) comment_url = pr_handler.submit_comment(message, comment_id=comment_id, return_url=True) pr_handler.set_status('failure', fail_status, current_app.bot_username, target_url=comment_url) else: pass_status = pr_config.get('pass_status', 'Passed all checks') all_passed_message = pr_config.get('all_passed_message', '') all_passed_message = all_passed_message.format( pr_handler=pr_handler, repo_handler=repo_handler) if all_passed_message: pr_handler.submit_comment(all_passed_message, comment_id=comment_id) pr_handler.set_status('success', pass_status, current_app.bot_username) else: # Post each failure as a status existing_statuses = pr_handler.list_statuses() for context, details in sorted(results.items()): full_context = current_app.bot_username + ':' + context # Don't post again if status hasn't changed if full_context in existing_statuses: existing_details = existing_statuses[full_context] if (details['state'] == existing_details['state'] and details['description'] == existing_details['description'] and details.get('target_url') == existing_details.get('target_url')): continue pr_handler.set_status(details['state'], details['description'], full_context, target_url=details.get('target_url')) # For statuses that have been skipped this time but existed before, set # status to pass and set message to say skipped for full_context in existing_statuses: if full_context.startswith(current_app.bot_username + ':'): context = full_context[len(current_app.bot_username) + 1:] if context not in results: pr_handler.set_status( 'success', 'This check has been skipped', current_app.bot_username + ':' + context) return 'Finished pull requests checks'