예제 #1
0
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'
예제 #2
0
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'
예제 #3
0
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)