def process_pull_request(repository, number, installation, action, is_new=False): # 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", {}) if not pr_config.get("enabled", False): msg = "Skipping PR checks, disabled in config." logger.debug(msg) return msg # 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) # 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: if skip_fails: pr_handler.set_check( current_app.bot_username, "Skipping checks due to {0} label".format(label), status='completed', conclusion='failure') return results = {} for function, actions in PULL_REQUEST_CHECKS.items(): if actions is None or action in actions: result = function(pr_handler, repo_handler) # Ignore skipped checks if result is not None: results.update(result) # Special message for a special day not_boring = pr_handler.get_config_value('not_boring', cfg_default=True) if not_boring: # pragma: no cover special_msg = '' if is_new: # Always be snarky for new PR special_msg = insert_special_message('') else: import random tensided_dice_roll = random.randrange(10) if tensided_dice_roll == 9: # 1 out of 10 for subsequent remarks special_msg = insert_special_message('') if special_msg: pr_handler.submit_comment(special_msg) # Post each failure as a status existing_checks = pr_handler.list_checks() for context, details in sorted(results.items()): full_context = current_app.bot_username + ':' + context # TODO: Revisit if the note made for statuses still applies to checks. # NOTE: we could in principle check if the status has been posted # before, and if so not post it again, but we had this in the past # and there were some strange caching issues where GitHub would # return old status messages, so we avoid doing that. pr_handler.set_check(full_context, details['description'], details_url=details.get('target_url'), status='completed', conclusion=details['state']) # 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_checks: 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_check(current_app.bot_username + ':' + context, 'This check has been skipped', status='completed', conclusion='neutral') # Also set the general 'single' status check as a skipped check if it # is present if full_context == current_app.bot_username: pr_handler.set_check(current_app.bot_username, 'This check has been skipped', status='completed', conclusion='neutral') return 'Finished pull requests checks'
def process_pull_request(repository, number, installation, action, is_new=False): # 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", {}) if not pr_config.get("enabled", False): msg = "Skipping PR checks, disabled in config." logger.debug(msg) return msg # 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) # 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: if skip_fails: pr_handler.set_check( current_app.bot_username, title="Skipping checks due to {0} label".format(label), name=current_app.bot_username, status='completed', conclusion='failure') return results = {} for function, actions in PULL_REQUEST_CHECKS.items(): if actions is None or action in actions: result = function(pr_handler, repo_handler) # Ignore skipped checks if result is not None: # Map old plugin keys to new checks names. # It's possible that the hook returns {} for context, check in result.items(): if check is not None: title = check.pop('description', None) if title: logger.warning( f"'description' is deprecated as a key in the return value from {function}," " it will be interpreted as 'title'") check['title'] = title check['title'] = check.pop('title', title) conclusion = check.pop('state', None) if conclusion: logger.warning( f"'state' is deprecated as a key in the return value from {function}," "it will be interpreted as 'conclusion'.") check['conclusion'] = conclusion check['conclusion'] = check.pop( 'conclusion', conclusion) result[context] = check results.update(result) # Get existing checks from our app, for the 'head' commit existing_checks = pr_handler.list_checks(only_ours=True) # For each existing check, see if it needs updating or skipping new_results = copy.copy(results) for external_id, check in existing_checks.items(): if external_id in results.keys(): details = new_results.pop(external_id) # Remove skip key. details.pop("skip_if_missing", False) # Update the previous check with the new check (this includes the check_id to update) check.update(details) # Send the check to be updated pr_handler.set_check(**check) else: # If check is in existing_checks but not results mark it as skipped. check.update({ 'title': 'This check has been skipped.', 'status': 'completed', 'conclusion': 'neutral' }) pr_handler.set_check(**check) # Any keys left in results are new checks we haven't sent on this commit yet. for external_id, details in sorted(new_results.items()): skip = details.pop("skip_if_missing", False) logger.trace(f"{details} skip is {skip}") if not skip: pr_handler.set_check(external_id, status="completed", **details) # Also set the general 'single' status check as a skipped check if it # is present if current_app.bot_username in new_results.keys(): check = new_results[current_app.bot_username] check.update({ 'title': 'This check has been skipped.', 'commit_hash': 'head', 'status': 'completed', 'conclusion': 'neutral' }) pr_handler.set_check(**check) # Special message for a special day not_boring = pr_handler.get_config_value('not_boring', cfg_default=True) if not_boring: # pragma: no cover special_msg = '' if is_new: # Always be snarky for new PR special_msg = insert_special_message('') else: import random tensided_dice_roll = random.randrange(10) if tensided_dice_roll == 9: # 1 out of 10 for subsequent remarks special_msg = insert_special_message('') if special_msg: pr_handler.submit_comment(special_msg) return 'Finished pull requests checks'
class TestPullRequestHandler: def setup_class(self): self.pr = PullRequestHandler('fakerepo/doesnotexist', 1234) def test_urls(self): assert self.pr._url_pull_request == 'https://api.github.com/repos/fakerepo/doesnotexist/pulls/1234' assert self.pr._url_review_comment == 'https://api.github.com/repos/fakerepo/doesnotexist/pulls/1234/reviews' assert self.pr._url_commits == 'https://api.github.com/repos/fakerepo/doesnotexist/pulls/1234/commits' assert self.pr._url_files == 'https://api.github.com/repos/fakerepo/doesnotexist/pulls/1234/files' def test_has_modified(self): mock = MagicMock(return_value=[{ "sha": "bbcd538c8e72b8c175046e27cc8f907076331401", "filename": "file1.txt", "status": "added", "additions": 103, "deletions": 21, "changes": 124, "blob_url": "https://github.com/blah/blah/blob/hash/file1.txt", "raw_url": "https://github.com/blaht/blah/raw/hash/file1.txt", "contents_url": "https://api.github.com/repos/blah/blah/contents/file1.txt?ref=hash", "patch": "@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test" }]) with patch('baldrick.github.github_api.paged_github_json_request', mock): # noqa assert self.pr.has_modified(['file1.txt']) assert self.pr.has_modified(['file1.txt', 'notthis.txt']) assert not self.pr.has_modified(['notthis.txt']) def test_set_check(self, app): with patch("baldrick.github.github_api.PullRequestHandler.json", new_callable=PropertyMock) as json: json.return_value = { 'head': { 'sha': 987654321 }, 'base': { 'sha': 123456789 } } with patch('requests.post') as post: self.pr.set_check("baldrick-1", "hello", name="test") expected_json = { 'external_id': 'baldrick-1', 'name': 'test', 'head_sha': 987654321, 'status': 'completed', 'output': { 'title': 'hello', 'summary': '' }, 'conclusion': 'neutral' } post.assert_called_once_with( 'https://api.github.com/repos/fakerepo/doesnotexist/check-runs', headers={ 'Accept': 'application/vnd.github.antiope-preview+json' }, json=expected_json) post.reset_mock() self.pr.set_check("baldrick-1", "hello", name="test", commit_hash='base', text="hello world", summary="why hello") expected_json = { 'external_id': 'baldrick-1', 'name': 'test', 'head_sha': 123456789, 'status': 'completed', 'output': { 'title': 'hello', 'summary': 'why hello', 'text': 'hello world' }, 'conclusion': 'neutral' } post.assert_called_once_with( 'https://api.github.com/repos/fakerepo/doesnotexist/check-runs', headers={ 'Accept': 'application/vnd.github.antiope-preview+json' }, json=expected_json) post.reset_mock() self.pr.set_check("baldrick-1", "hello", name="test", commit_hash='hello', details_url="this_is_a_url") expected_json = { 'external_id': 'baldrick-1', 'name': 'test', 'head_sha': 'hello', 'details_url': 'this_is_a_url', 'status': 'completed', 'output': { 'title': 'hello', 'summary': '' }, 'conclusion': 'neutral' } post.assert_called_once_with( 'https://api.github.com/repos/fakerepo/doesnotexist/check-runs', headers={ 'Accept': 'application/vnd.github.antiope-preview+json' }, json=expected_json) post.reset_mock() self.pr.set_check("baldrick-1", "hello", name="test", status="completed", conclusion=None) expected_json = { 'external_id': 'baldrick-1', 'name': 'test', 'head_sha': 987654321, 'status': 'completed', 'output': { 'title': 'hello', 'summary': '' }, 'conclusion': 'neutral' } post.assert_called_once_with( 'https://api.github.com/repos/fakerepo/doesnotexist/check-runs', headers={ 'Accept': 'application/vnd.github.antiope-preview+json' }, json=expected_json)