Esempio n. 1
0
def update_recent_deployers(opsgenie_api_key,
                            opsgenie_team_id,
                            gocd_pipelines,
                            gocd_username,
                            gocd_password,
                            gocd_url,
                            github_token,
                            recent_cutoff=30):
    """
    Update an OpsGenie team to contain only those users whose changes were recently deployed
    by a particular GoCD pipeline.
    """
    repo_tools_repo = GitHubAPI('edx', 'repo-tools-data', github_token)
    people_yaml_data = repo_tools_repo.file_contents('people.yaml')
    people_yaml = yaml.safe_load(people_yaml_data)

    email_aliases = {
        gh: [user['email']] + user.get('other_emails', [])
        for gh, user in people_yaml.items()
    }

    gocd = GoCDAPI(gocd_username, gocd_password, gocd_url)
    recent_deployers = set().union(*(gocd.recent_deployers(
        pipeline, stages, cutoff=recent_cutoff, email_aliases=email_aliases)
                                     for pipeline, stages in gocd_pipelines))

    opsgenie = OpsGenieAPI(opsgenie_api_key)

    while True:
        try:
            opsgenie.set_team_members(opsgenie_team_id, recent_deployers)
            break
        except HTTPError as exc:
            if exc.response.status_code == 422:
                message = exc.response.json().get('message')

                if message is None:
                    raise

                match = re.match(
                    r"No user exists with username \[(?P<user>.*)\]", message)

                if match is None:
                    raise

                user = match.group('user')
                click.echo(
                    click.style('Removing user {!r} and retrying'.format(user),
                                fg='red'))
                recent_deployers.remove(user)
                continue
            raise
Esempio n. 2
0
def message_pull_requests(org, repo, token, base_sha, base_ami_tags,
                          base_ami_tag_app, head_sha, head_ami_tags,
                          head_ami_tag_app, message_type, extra_text):
    u"""
    Message a range of Pull requests between the BASE and HEAD SHA specified.

    Message can be one of 3 types:
    - PR on stage
    - PR on prod
    - Release canceled

    Args:
        org (str): The github organization
        repo (str): The github repository
        token (str): The authentication token
        base_sha (str): The starting SHA
        base_ami_tags (str): An open YAML file containing the base AMI tags
        base_ami_tag_app (str): The app name to read the the base_ami_tags
        head_sha (str): The ending SHA
        head_ami_tags (str): Yaml file containing the head AMI tags
        head_ami_tag_app (str): the app name to read the head_ami_tags
        message_type (str): type of message to send
        extra_text (str): Extra text to be inserted in the PR message

    Returns:
        None
    """
    methods = {
        u'stage': u'message_pr_deployed_stage',
        u'prod': u'message_pr_deployed_prod',
        u'rollback': u'message_pr_release_canceled',
        u'broke_vagrant': u'message_pr_broke_vagrant',
    }

    if base_sha is None and base_ami_tags and base_ami_tag_app:
        base_ami_tags = yaml.safe_load(base_ami_tags)
        tag = u'version:{}'.format(base_ami_tag_app)
        version = base_ami_tags[tag]
        _, _, base_sha = version.partition(u' ')

    if head_sha is None and head_ami_tags and head_ami_tag_app:
        head_ami_tags = yaml.safe_load(head_ami_tags)
        tag = u'version:{}'.format(head_ami_tag_app)
        version = head_ami_tags[tag]
        _, _, head_sha = version.partition(u' ')

    api = GitHubAPI(org, repo, token)
    for pull_request in api.get_pr_range(base_sha, head_sha):
        LOG.info(u"Posting message type %r to %d.", message_type,
                 pull_request.number)
        getattr(api, methods[message_type])(pr_number=pull_request,
                                            extra_text=extra_text)
Esempio n. 3
0
def merge_pull_request(org,
                       repo,
                       token,
                       pr_number,
                       input_file):
    """
    Merges a pull request, specified either by number -or read from a YAML file.

    Args:
        org (str):
        repo (str):
        token (str):
        pr_number (int): Number (ID) of PR to merge.
        input_file (str): Path to a YAML file containing PR details.
          The YAML file is expected to have a 'pr_number' field containing the PR number.

    If both or neither PR number and input file are specified, then return a failure.
    If PR number is specified, attempt to merge that PR.
    If input file is specified, attempt to merge the PR number read from the 'pr_number' field.
    """
    github_api = GitHubAPI(org, repo, token)

    if not pr_number and not input_file:
        LOG.error("Neither PR number nor input file were specified - failing.")
        sys.exit(1)
    elif pr_number and input_file:
        LOG.error("Both PR number *and* input file were specified - failing.")
        sys.exit(1)

    if input_file:
        config = yaml.safe_load(io.open(input_file, 'r'))
        if not config['pr_created']:
            # The input file indicates that no PR was created, so no PR tests to check here.
            LOG.info("No PR created - so no PR to merge.")
            sys.exit(0)
        pr_number = config['pr_number']

    try:
        github_api.merge_pull_request(pr_number)
    except (GithubException, UnknownObjectException):
        LOG.error("PR #{pr} merge for org '{org}' & repo '{repo}' failed. Aborting.".format(
            pr=pr_number,
            org=org,
            repo=repo
        ))
        raise

    LOG.info("Merged PR #{pr} for org '{org}' & repo '{repo}' successfully.".format(
        pr=pr_number,
        org=org,
        repo=repo
    ))
def check_tests(
    org, repo, token, input_file, pr_number, commit_hash,
    out_file, exclude_contexts, include_contexts,
):
    """
    Check the current combined status of a GitHub PR/commit in a repo once.

    If tests have passed for the PR/commit, return a success.
    If any other status besides success (such as in-progress/pending), return a failure.

    If an input YAML file is specified, read the PR number from the file to check.
    If a PR number is specified, check that PR number's tests.
    If a commit hash is specified, check that commit hash's tests.
    """
    # Check for one and only one of the mutually-exclusive params.
    if not exactly_one_set((input_file, pr_number, commit_hash)):
        err_msg = \
            "Exactly one of input_file ({!r}), pr_number ({!r})," \
            " and commit_hash ({!r}) should be specified.".format(
                input_file,
                pr_number,
                commit_hash
            )
        LOG.error(err_msg)
        sys.exit(1)

    gh_utils = GitHubAPI(org, repo, token, exclude_contexts=exclude_contexts, include_contexts=include_contexts)

    status_success = False
    if input_file:
        input_vars = yaml.safe_load(io.open(input_file, 'r'))
        pr_number = input_vars['pr_number']
        status_success, test_statuses = gh_utils.check_combined_status_pull_request(pr_number)
        git_obj = 'PR #{}'.format(pr_number)
    elif pr_number:
        status_success, test_statuses = gh_utils.check_combined_status_pull_request(pr_number)
        git_obj = 'PR #{}'.format(pr_number)
    elif commit_hash:
        status_success, test_statuses = gh_utils.check_combined_status_commit(commit_hash)
        git_obj = 'commit hash {}'.format(commit_hash)

    LOG.info("{}: Combined status of {} is {}.".format(
        sys.argv[0], git_obj, "success" if status_success else "failed"
    ))

    dirname = os.path.dirname(out_file.name)
    if dirname:
        os.makedirs(dirname, exist_ok=True)
    yaml.safe_dump(test_statuses, stream=out_file, width=1000)

    # An exit code of 0 means success and non-zero means failure.
    sys.exit(not status_success)
Esempio n. 5
0
def poll_tests(org, repo, token, input_file, pr_number, commit_hash):
    """
    Poll the combined status of a GitHub PR/commit in a repo several times.

    If tests pass for the PR/commit during a poll -or- no PR tests to check, return a success.
    If tests fail for the PR/commit during a poll, return a failure.
    If the maximum polls have occurred -or- a timeout, return a failure.

    If an input YAML file is specified, read the PR number from the file to check.
    Else if both PR number -and- commit hash is specified, return a failure.
    Else if either PR number -or- commit hash is specified, check the tests for the specified value.
    """
    gh_utils = GitHubAPI(org, repo, token)

    if not exactly_one_set((input_file, pr_number, commit_hash)):
        err_msg = \
            "Exactly one of commit_hash ({!r}), input_file ({!r})," \
            " and pr_number ({!r}) should be specified.".format(
                commit_hash,
                input_file,
                pr_number
            )
        LOG.error(err_msg)
        sys.exit(1)

    if input_file:
        input_vars = yaml.safe_load(io.open(input_file, 'r'))
        if not input_vars['pr_created']:
            # The input file indicates that no PR was created, so no PR tests to check here.
            LOG.info("No PR created - so no PR tests require polling.")
            sys.exit(0)
        pr_number = input_vars['pr_number']
        git_obj = 'PR #{}'.format(pr_number)
        status_success = gh_utils.poll_pull_request_test_status(pr_number)
    elif pr_number:
        git_obj = 'PR #{}'.format(pr_number)
        status_success = gh_utils.poll_pull_request_test_status(pr_number)
    elif commit_hash:
        git_obj = 'commit hash {}'.format(commit_hash)
        status_success = gh_utils.poll_for_commit_successful(commit_hash)

    LOG.info(
        "{cmd}: Combined status of {obj} for org '{org}' & repo '{repo}' is {status}."
        .format(cmd=sys.argv[0],
                obj=git_obj,
                org=org,
                repo=repo,
                status="success" if status_success else "failed"))

    # An exit code of 0 means success and non-zero means failure.
    sys.exit(not status_success)
Esempio n. 6
0
    def test_filter_validation(self, exclude_contexts, include_contexts,
                               expected_contexts):
        filterable_states = ['passed', 'pending', None, 'failed']

        with patch.object(Github,
                          'get_organization',
                          return_value=Mock(name='org-mock',
                                            spec=Organization)):
            with patch.object(Github,
                              'get_repo',
                              return_value=Mock(name='repo-mock',
                                                spec=Repository)) as repo_mock:
                api = GitHubAPI('test-org',
                                'test-repo',
                                token='abc123',
                                exclude_contexts=exclude_contexts,
                                include_contexts=include_contexts)
        api.log_rate_limit = Mock(return_value=None)

        mock_combined_status = Mock(name='combined-status',
                                    spec=CommitCombinedStatus)
        mock_combined_status.statuses = [
            Mock(name='{}-status'.format(state),
                 spec=CommitStatus,
                 context='{}-status'.format(state),
                 state=state) for state in filterable_states
        ]
        mock_combined_status.state = None
        mock_combined_status.url = None

        commit_mock = Mock(name='commit', spec=Commit, url="some.fake.repo/")
        commit_mock.get_combined_status.return_value = mock_combined_status
        repo_mock.return_value.get_commit.return_value = commit_mock
        commit_mock._requester = Mock(name='_requester')  # pylint: disable=protected-access
        # pylint: disable=protected-access
        commit_mock._requester.requestJsonAndCheck.return_value = ({}, {
            'check_suites': [{
                'app': {
                    'name': '{}-check'.format(state)
                },
                'conclusion': state,
                'url': 'some.fake.repo'
            } for state in filterable_states]
        })
        filtered_results = api.filter_validation_results(
            api.get_validation_results('deadbeef'))
        assert set(expected_contexts) == set(filtered_results.keys())
def cli(org, repo, token, pr_number, branch_name):
    u"""
    Check if the PR is against the specified branch,
    i.e. if the base of the PR is the specified branch.
    """
    # github.enable_console_debug_logging()
    gh_utils = GitHubAPI(org, repo, token)

    is_base = False
    if pr_number and branch_name:
        is_base = gh_utils.is_branch_base_of_pull_request(pr_number, branch_name)
        print(u"{}: Is branch '{}' the base of PR #{} ? {}!".format(
            sys.argv[0], branch_name, pr_number, u"Yes" if is_base else u"No"
        ))

    # An exit code of 0 means success and non-zero means failure.
    sys.exit(not is_base)
Esempio n. 8
0
def check_tests(org,
                repo,
                token,
                input_file,
                pr_number,
                commit_hash):
    """
    Check the current combined status of a GitHub PR/commit in a repo once.

    If tests have passed for the PR/commit, return a success.
    If any other status besides success (such as in-progress/pending), return a failure.

    If an input YAML file is specified, read the PR number from the file to check.
    Else if both PR number -and- commit hash is specified, return a failure.
    Else if either PR number -or- commit hash is specified, check the tests for the specified value.
    """
    gh_utils = GitHubAPI(org, repo, token)

    if pr_number and commit_hash:
        LOG.info("Both PR number and commit hash are specified. Only one of the two should be specified - failing.")
        sys.exit(1)

    status_success = False
    if input_file:
        input_vars = yaml.safe_load(io.open(input_file, 'r'))
        pr_number = input_vars['pr_number']
        status_success = gh_utils.check_pull_request_test_status(pr_number)
        git_obj = 'PR #{}'.format(pr_number)
    elif pr_number:
        status_success = gh_utils.check_pull_request_test_status(pr_number)
        git_obj = 'PR #{}'.format(pr_number)
    elif commit_hash:
        status_success = gh_utils.is_commit_successful(commit_hash)
        git_obj = 'commit hash {}'.format(commit_hash)

    LOG.info("{}: Combined status of {} is {}.".format(
        sys.argv[0], git_obj, "success" if status_success else "failed"
    ))

    # An exit code of 0 means success and non-zero means failure.
    sys.exit(not status_success)
Esempio n. 9
0
def message_pull_requests(org, repo, token, base_sha, base_ami_tags,
                          ami_tag_app, head_sha, message_type):
    u"""
    Message a range of Pull requests between the BASE and HEAD SHA specified.

    Message can be one of 3 types:
    - PR on stage
    - PR on prod
    - Release canceled

    Args:
        org (str): The github organization
        repo (str): The github repository
        token (str): The authentication token
        base_sha (str): The starting SHA
        base_ami_tags (file): An open YAML file containing ami tags
        ami_tag_app (str): The app to read from the base_ami_tags
        head_sha (str): The ending SHA
        message_type (str): type of message to send

    Returns:
        None
    """
    methods = {
        u'stage': u'message_pr_deployed_stage',
        u'prod': u'message_pr_deployed_prod',
        u'rollback': u'message_pr_release_canceled'
    }

    if base_sha is None and base_ami_tags and ami_tag_app:
        ami_tags = yaml.safe_load(base_ami_tags)
        tag = u'version:{}'.format(ami_tag_app)
        version = ami_tags[tag]
        _, _, base_sha = version.partition(u' ')

    api = GitHubAPI(org, repo, token)
    for pull_request in api.get_pr_range(base_sha, head_sha):
        LOG.info(u"Posting message type %r to %d.", message_type,
                 pull_request.number)
        getattr(api, methods[message_type])(pull_request.number)
Esempio n. 10
0
def get_client(org, repo, token):
    u"""
    Returns the github client, pointing at the repo specified

    Args:
        org (str): The github organization
        repo (str): The github repository
        token (str): The authentication token

    Returns:
        Returns the github client object
    """
    api = GitHubAPI(org, repo, token)
    return api
def create_private_to_public_pr(private_org, private_repo,
                                private_source_branch, public_org, public_repo,
                                public_target_branch, token, output_file,
                                reference_repo):
    u"""
    Creates a PR to merge the private source branch into the public target branch.
    Clones the repo in order to perform the proper git commands locally.
    """
    public_github_url = u'[email protected]:{}/{}.git'.format(
        public_org, public_repo)
    private_github_url = u'[email protected]:{}/{}.git'.format(
        private_org, private_repo)
    output_yaml = {
        u'private_github_url': private_github_url,
        u'private_source_branch_name': private_source_branch,
        u'public_github_url': public_github_url,
        u'public_target_branch_name': public_target_branch
    }

    LOG.info('Cloning private repo %s with branch %s.', private_github_url,
             private_source_branch)
    with LocalGitAPI.clone(private_github_url, private_source_branch,
                           reference_repo).cleanup() as local_repo:
        # Add the public repo as a remote for the private git working tree.
        local_repo.add_remote('public', public_github_url)
        # Create a new public branch with unique name.
        new_branch_name = 'private_to_public_{}'.format(
            local_repo.get_head_sha()[:7])
        # Push the private branch into the public repo.
        LOG.info('Pushing private branch %s to public repo %s as branch %s.',
                 private_source_branch, public_github_url, new_branch_name)
        local_repo.push_branch(private_source_branch, 'public',
                               new_branch_name)
        github_api = GitHubAPI(public_org, public_repo, token)
        # Create a PR from new public branch to public master.
        try:
            pull_request = github_api.create_pull_request(
                title='Mergeback PR from private to public.',
                body=
                'Merge private changes back to the public repo post-PR-merge.\n\n'
                'Please review and tag appropriate parties.',
                head=new_branch_name,
                base=public_target_branch)
        except PullRequestCreationError as exc:
            LOG.info(
                "No pull request created for merging %s into %s in '%s' repo - nothing to merge: %s",
                new_branch_name, public_target_branch, public_github_url, exc)
            output_yaml.update({
                'pr_created': False,
            })
            # Cleanup - delete the pushed branch.
            github_api.delete_branch(new_branch_name)
        else:
            LOG.info('Created PR #%s for repo %s: %s', pull_request.number,
                     public_github_url, pull_request.html_url)
            output_yaml.update({
                'pr_created':
                True,
                'pr_id':
                pull_request.id,
                'pr_number':
                pull_request.number,
                'pr_url':
                pull_request.url,
                'pr_repo_url':
                github_api.github_repo.url,
                'pr_head':
                pull_request.head.sha,
                'pr_base':
                pull_request.base.sha,
                'pr_html_url':
                pull_request.html_url,
                'pr_diff_url':
                pull_request.diff_url,
                'pr_mergable':
                pull_request.mergeable,
                'pr_state':
                pull_request.state,
                'pr_mergable_state':
                pull_request.mergeable_state,
            })

    if output_file:
        with io.open(output_file, u'w') as stream:
            yaml.safe_dump(output_yaml,
                           stream,
                           default_flow_style=False,
                           explicit_start=True)
    else:
        yaml.safe_dump(
            output_yaml,
            sys.stdout,
        )
Esempio n. 12
0
def pr_table(token, jira_url, delta):
    u"""
    Return an Element that renders all changes in `delta` as a table listing merged PRs.abs

    Arguments:
        token: The github token to access the github API with.
        jira_url: The base url of the JIRA instance to link JIRA tickets to.
        delta (VersionDelta): The AMIs to compare.
    """
    version = delta.new or delta.base
    match = re.search(u"github.com/(?P<org>[^/]*)/(?P<repo>.*)", version.repo)
    api = GitHubAPI(match.group(u'org'), match.group(u'repo'), token)

    try:
        prs = api.get_pr_range(delta.base.sha, delta.new.sha)

        change_details = E.TABLE(
            E.CLASS(u"wrapped"),
            E.TBODY(
                E.TR(
                    E.TH(u"Merged By"),
                    E.TH(u"Author"),
                    E.TH(u"Title"),
                    E.TH(u"PR"),
                    E.TH(u"JIRA"),
                    E.TH(u"Release Notes?"),
                ), *[
                    E.TR(
                        E.TD(
                            E.A(
                                pull_request.merged_by.login,
                                href=pull_request.merged_by.html_url,
                            )),
                        E.TD(
                            E.A(
                                pull_request.user.login,
                                href=pull_request.user.html_url,
                            )),
                        E.TD(pull_request.title),
                        E.TD(
                            E.A(
                                str(pull_request.number),
                                href=pull_request.html_url,
                            )),
                        E.TD(
                            format_jira_references(jira_url,
                                                   pull_request.body)),
                        E.TD(u""),
                    ) for pull_request in sorted(
                        prs, key=lambda pr: pr.merged_by.login)
                ]))
    except Exception:  # pylint: disable=broad-except
        LOGGER.exception(u'Unable to get PRs for %r', delta)
        change_details = E.P("Unable to list changes")

    return SECTION(
        E.H3(u"Changes for {} (".format(delta.app),
             E.A(GITHUB_PREFIX.sub(u'', version.repo), href=version.repo),
             ")"),
        E.P(E.STRONG(u"Before: "),
            E.A(delta.base.sha, href=format_commit_url(delta.base))),
        E.P(E.STRONG(u"After: "),
            E.A(delta.new.sha, href=format_commit_url(delta.new))),
        change_details,
    )
Esempio n. 13
0
class GitHubApiTestCase(TestCase):
    """
    Tests the requests creation/response handling for the Github API
    All Network calls should be mocked out.
    """
    def setUp(self):
        with patch.object(Github,
                          'get_organization',
                          return_value=Mock(spec=Organization)) as org_mock:
            with patch.object(Github,
                              'get_repo',
                              return_value=Mock(spec=Repository)) as repo_mock:
                self.org_mock = org_mock.return_value = Mock(spec=Organization)
                self.repo_mock = repo_mock.return_value = Mock(spec=Repository)
                self.api = GitHubAPI('test-org', 'test-repo', token='abc123')
        self.api.log_rate_limit = Mock(return_value=None)
        super(GitHubApiTestCase, self).setUp()

    @patch('github.Github.get_user')
    def test_user(self, mock_user_method):
        # setup the mock
        mock_user_method.return_value = Mock(spec=NamedUser)

        self.assertIsInstance(self.api.user(), NamedUser)
        mock_user_method.assert_called()

    @ddt.data(('abc', 'success'), ('123', 'failure'),
              (Mock(spec=GitCommit, **{'sha': '123'}), 'success'),
              (Mock(spec=GitCommit, **{'sha': '123'}), 'failure'))
    @ddt.unpack
    def test_get_commit_combined_statuses(self, sha, state):
        combined_status = Mock(spec=CommitCombinedStatus, state=state)
        attrs = {'get_combined_status.return_value': combined_status}
        commit_mock = Mock(spec=Commit, **attrs)
        self.repo_mock.get_commit = lambda sha: commit_mock

        status = self.api.get_commit_combined_statuses(sha)
        self.assertEqual(status.state, state)

    def test_get_commit_combined_statuses_passing_commit_obj(self):
        combined_status = Mock(spec=CommitCombinedStatus,
                               **{'state': 'success'})
        attrs = {'get_combined_status.return_value': combined_status}
        commit_mock = Mock(spec=Commit, **attrs)
        self.repo_mock.get_commit = lambda sha: commit_mock

        status = self.api.get_commit_combined_statuses(commit_mock)
        self.assertEqual(status.state, 'success')

    def test_get_commit_combined_statuses_bad_object(self):
        self.assertRaises(UnknownObjectException,
                          self.api.get_commit_combined_statuses, object())

    def test_get_commits_by_branch(self):
        self.repo_mock.get_branch.return_value = Mock(spec=Branch,
                                                      **{'commit.sha': '123'})
        self.repo_mock.get_commits.return_value = [
            Mock(spec=Commit, sha=i) for i in range(10)
        ]

        commits = self.api.get_commits_by_branch('test')

        self.repo_mock.get_branch.assert_called_with('test')
        self.repo_mock.get_commits.assert_called_with('123')
        self.assertEqual(len(commits), 10)

    def test_get_diff_url(self):
        def _check_url(org, repo, base_sha, head_sha):
            """ private method to do the comparison of the expected URL and the one we get back """
            url = self.api.get_diff_url(org, repo, base_sha, head_sha)
            expected = 'https://github.com/{}/{}/compare/{}...{}'.format(
                org, repo, base_sha, head_sha)
            self.assertEqual(url, expected)

        _check_url('org', 'repo', 'base-sha', 'head-sha')
        with self.assertRaises(InvalidUrlException):
            _check_url('org', 'repo', 'abc def', 'head-sha')

    def test_get_commits_by_branch_branch_not_found(self):
        self.repo_mock.get_branch.side_effect = GithubException(
            404, {
                'documentation_url':
                'https://developer.github.com/v3/repos/#get-branch',
                'message': 'Branch not found'
            })
        self.assertRaises(GithubException, self.api.get_commits_by_branch,
                          'blah')

    def test_delete_branch(self):
        ref_mock = Mock(spec=GitRef)
        get_git_ref_mock = Mock(return_value=ref_mock)
        self.repo_mock.get_git_ref = get_git_ref_mock
        self.api.delete_branch('blah')

        get_git_ref_mock.assert_called_with(ref='heads/blah')
        ref_mock.delete.assert_called()

    @ddt.data(('blah-candidate', 'falafel'), ('meow', 'schwarma '))
    @ddt.unpack
    def test_create_branch(self, branch_name, sha):
        create_git_ref_mock = Mock()
        self.repo_mock.create_git_ref = create_git_ref_mock

        self.api.create_branch(branch_name, sha)

        create_git_ref_mock.assert_called_with(
            ref='refs/heads/{}'.format(branch_name), sha=sha)

    @ddt.data(
        ('blah-candidate', 'release', 'test', 'test_pr'),
        ('catnip', 'release', 'My meowsome PR',
         'this PR has lots of catnip inside, go crazy!'),
    )
    @ddt.unpack
    def test_create_pull_request(self, head, base, title, body):
        self.api.create_pull_request(head=head,
                                     base=base,
                                     title=title,
                                     body=body)
        self.repo_mock.create_pull.assert_called_with(head=head,
                                                      base=base,
                                                      title=title,
                                                      body=body)

    @ddt.data('*****@*****.**', None)
    def test_create_tag(self, user_email):
        mock_user = Mock(spec=NamedUser)
        mock_user.email = user_email
        mock_user.name = 'test_name'
        with patch.object(Github, 'get_user', return_value=mock_user):
            create_tag_mock = Mock()
            create_ref_mock = Mock()
            self.repo_mock.create_git_tag = create_tag_mock
            self.repo_mock.create_git_ref = create_ref_mock

            test_tag = 'test_tag'
            test_sha = 'abc'
            self.api.create_tag(test_sha, test_tag)
            _, kwargs = create_tag_mock.call_args  # pylint: disable=unpacking-non-sequence
            self.assertEqual(kwargs['tag'], test_tag)
            self.assertEqual(kwargs['message'], '')
            self.assertEqual(kwargs['type'], 'commit')
            self.assertEqual(kwargs['object'], test_sha)
            create_ref_mock.assert_called_with(
                ref='refs/tags/{}'.format(test_tag), sha=test_sha)

    def _setup_create_tag_mocks(self, status_code, msg, return_sha):
        """
        Setup the mocks for the create_tag calls below.
        """
        mock_user = Mock(NamedUser, email='*****@*****.**')
        mock_user.name = 'test_name'
        self.repo_mock.create_git_tag = Mock()
        self.repo_mock.create_git_ref = Mock(
            side_effect=GithubException(status_code, {'message': msg}))
        self.repo_mock.get_git_ref = get_tag_mock = Mock()
        get_tag_mock.return_value = Mock(object=Mock(sha=return_sha))
        return mock_user

    def test_create_tag_which_already_exists_but_matches_sha(self):
        test_sha = 'abc'
        mock_user = self._setup_create_tag_mocks(422,
                                                 'Reference already exists',
                                                 test_sha)
        with patch.object(Github, 'get_user', return_value=mock_user):
            # No exception.
            self.api.create_tag(test_sha, 'test_tag')

    def test_create_tag_which_already_exists_and_no_sha_match(self):
        mock_user = self._setup_create_tag_mocks(422,
                                                 'Reference already exists',
                                                 'def')
        with patch.object(Github, 'get_user', return_value=mock_user):
            with self.assertRaises(GitTagMismatchError):
                self.api.create_tag('abc', 'test_tag')

    def test_create_tag_which_already_exists_and_unknown_exception(self):
        mock_user = self._setup_create_tag_mocks(421, 'Not sure what this is!',
                                                 'def')
        with patch.object(Github, 'get_user', return_value=mock_user):
            with self.assertRaises(GithubException):
                self.api.create_tag('abc', 'test_tag')

    @ddt.data(('diverged', True), ('divergent', False), ('ahead', False))
    @ddt.unpack
    def test_have_branches_diverged(self, status, expected):
        self.repo_mock.compare.return_value = Mock(spec=Comparison,
                                                   status=status)
        self.assertEqual(self.api.have_branches_diverged('base', 'head'),
                         expected)

    @ddt.data(('123', list(range(10)), 10, 'SuCcEsS', True, True),
              ('123', list(range(10)), 10, 'success', True, True),
              ('123', list(range(10)), 10, 'SUCCESS', True, True),
              ('123', list(range(10)), 10, 'pending', False, True),
              ('123', list(range(10)), 10, 'failure', False, True),
              ('123', [], 0, None, False, True),
              ('123', list(range(10)), 10, 'SuCcEsS', True, False),
              ('123', list(range(10)), 10, 'success', True, False),
              ('123', list(range(10)), 10, 'SUCCESS', True, False),
              ('123', list(range(10)), 10, 'pending', False, False),
              ('123', list(range(10)), 10, 'failure', False, False),
              ('123', [], 0, None, False, False))
    @ddt.unpack
    def test_check_combined_status_commit(self, sha, statuses,
                                          statuses_returned, state,
                                          success_expected, use_statuses):
        if use_statuses:
            mock_combined_status = Mock(spec=CommitCombinedStatus)
            mock_combined_status.statuses = [
                Mock(spec=CommitStatus, id=i, state=state) for i in statuses
            ]
            mock_combined_status.state = state

            commit_mock = Mock(spec=Commit, url="some.fake.repo/")
            commit_mock.get_combined_status.return_value = mock_combined_status
            self.repo_mock.get_commit.return_value = commit_mock
            commit_mock._requester = Mock()  # pylint: disable=protected-access
            # pylint: disable=protected-access
            commit_mock._requester.requestJsonAndCheck.return_value = ({}, {
                'check_suites': []
            })
        else:
            mock_combined_status = Mock(spec=CommitCombinedStatus)
            mock_combined_status.statuses = []
            mock_combined_status.state = None
            mock_combined_status.url = None

            commit_mock = Mock(spec=Commit, url="some.fake.repo/")
            commit_mock.get_combined_status.return_value = mock_combined_status
            self.repo_mock.get_commit.return_value = commit_mock
            commit_mock._requester = Mock()  # pylint: disable=protected-access
            # pylint: disable=protected-access
            commit_mock._requester.requestJsonAndCheck.return_value = ({}, {
                'check_suites': [{
                    'app': {
                        'name': 'App {}'.format(i)
                    },
                    'conclusion': state,
                    'url': 'some.fake.repo'
                } for i in statuses]
            })

        successful, statuses = self.api.check_combined_status_commit(sha)

        assert successful == success_expected
        assert isinstance(statuses, dict)
        assert len(statuses) == statuses_returned
        commit_mock.get_combined_status.assert_called()
        self.repo_mock.get_commit.assert_called_with(sha)

    @ddt.data(('passed', True), ('failed', False))
    @ddt.unpack
    def test_poll_commit(self, end_status, successful):
        url_dict = {'TravisCI': 'some url'}
        with patch.object(self.api,
                          '_is_commit_successful',
                          side_effect=[
                              (False, url_dict, 'pending'),
                              (successful, url_dict, end_status),
                          ]):
            result = self.api._poll_commit('some sha')  # pylint: disable=protected-access

            assert self.api._is_commit_successful.call_count == 2  # pylint: disable=protected-access
        assert result[0] == end_status
        assert result[1] == url_dict

    @ddt.data(
        (None, None, [
            '{}-{}'.format(state, valtype)
            for state in ['passed', 'pending', None, 'failed']
            for valtype in ['status', 'check']
        ]),
        ('status', None,
         ['passed-check', 'pending-check', 'None-check', 'failed-check']),
        ('check', None,
         ['passed-status', 'pending-status', 'None-status', 'failed-status']),
        ('check', 'passed', [
            'passed-status', 'passed-check', 'pending-status', 'None-status',
            'failed-status'
        ]),
        ('.*', 'passed', ['passed-status', 'passed-check']),
    )
    @ddt.unpack
    def test_filter_validation(self, exclude_contexts, include_contexts,
                               expected_contexts):
        filterable_states = ['passed', 'pending', None, 'failed']

        with patch.object(Github,
                          'get_organization',
                          return_value=Mock(name='org-mock',
                                            spec=Organization)):
            with patch.object(Github,
                              'get_repo',
                              return_value=Mock(name='repo-mock',
                                                spec=Repository)) as repo_mock:
                api = GitHubAPI('test-org',
                                'test-repo',
                                token='abc123',
                                exclude_contexts=exclude_contexts,
                                include_contexts=include_contexts)
        api.log_rate_limit = Mock(return_value=None)

        mock_combined_status = Mock(name='combined-status',
                                    spec=CommitCombinedStatus)
        mock_combined_status.statuses = [
            Mock(name='{}-status'.format(state),
                 spec=CommitStatus,
                 context='{}-status'.format(state),
                 state=state) for state in filterable_states
        ]
        mock_combined_status.state = None
        mock_combined_status.url = None

        commit_mock = Mock(name='commit', spec=Commit, url="some.fake.repo/")
        commit_mock.get_combined_status.return_value = mock_combined_status
        repo_mock.return_value.get_commit.return_value = commit_mock
        commit_mock._requester = Mock(name='_requester')  # pylint: disable=protected-access
        # pylint: disable=protected-access
        commit_mock._requester.requestJsonAndCheck.return_value = ({}, {
            'check_suites': [{
                'app': {
                    'name': '{}-check'.format(state)
                },
                'conclusion': state,
                'url': 'some.fake.repo'
            } for state in filterable_states]
        })
        filtered_results = api.filter_validation_results(
            api.get_validation_results('deadbeef'))
        assert set(expected_contexts) == set(filtered_results.keys())

    @ddt.data(
        ('release-candidate', 4),
        ('meow-candidate', 6),
        ('should-have-gone-to-law-school', 1),
    )
    @ddt.unpack
    def test_most_recent_good_commit(self, branch, good_commit_id):
        commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)]
        self.api.get_commits_by_branch = Mock(return_value=commits)

        def _side_effect(sha):
            """
            side effect returns True when the commit ID matches the current iteration
            """
            return (sha == good_commit_id, {})

        self.api._is_commit_successful = Mock(side_effect=_side_effect)  # pylint: disable=protected-access

        self.api.most_recent_good_commit(branch)
        self.assertEqual(self.api._is_commit_successful.call_count,
                         good_commit_id)  # pylint: disable=protected-access

    def test_most_recent_good_commit_no_commit(self):
        commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)]
        self.api.get_commits_by_branch = Mock(return_value=commits)

        self.api._is_commit_successful = Mock(return_value=(False, {}))  # pylint: disable=protected-access
        self.assertRaises(NoValidCommitsError,
                          self.api.most_recent_good_commit,
                          'release-candidate')

    @ddt.data(
        # 1 unique SHA should result in 1 search query and 1 PR.
        (SHAS[:1], 1, 1),
        # 18 unique SHAs should result in 1 search query and 18 PRs.
        (SHAS[:18], 1, 18),
        # 36 unique SHAs should result in 2 search queries and 36 PRs.
        (SHAS[:36], 2, 36),
        # 37 unique SHAs should result in 3 search queries and 37 PRs.
        (SHAS[:37], 3, 37),
        # 20 unique SHAs, each appearing twice, should result in 3 search queries and 20 PRs.
        (SHAS[:20] * 2, 3, 20),
    )
    @ddt.unpack
    @patch('github.Github.search_issues')
    def test_get_pr_range(self, shas, expected_search_count,
                          expected_pull_count, mock_search_issues):
        commits = [Mock(spec=Commit, sha=sha) for sha in shas]
        self.repo_mock.compare.return_value = Mock(spec=Comparison,
                                                   commits=commits)

        def search_issues_side_effect(query, **kwargs):  # pylint: disable=unused-argument
            """
            Stub implementation of GitHub issue search.
            """
            return [
                Mock(
                    spec=Issue,
                    number=TRIMMED_SHA_MAP[query_item],
                    repository=self.repo_mock,
                ) for query_item in query.split()
                if query_item in TRIMMED_SHA_MAP
            ]
            # The query is all the shas + params to narry the query to PRs and repo.
            # This shouldn't break the intent of the test because we are still pulling
            # in all the params that are relevant to this test which are the passed in
            # shas.  And it's ignoring other parameters that search_issues might add
            # to the test.

        mock_search_issues.side_effect = search_issues_side_effect

        self.repo_mock.get_pull = lambda number: Mock(spec=PullRequest,
                                                      number=number)

        start_sha, end_sha = 'abc', '123'
        pulls = self.api.get_pr_range(start_sha, end_sha)

        self.repo_mock.compare.assert_called_with(start_sha, end_sha)

        self.assertEqual(mock_search_issues.call_count, expected_search_count)
        for call_args in mock_search_issues.call_args_list:
            # Verify that the batched SHAs have been trimmed.
            self.assertLess(len(call_args[0]), 200)

        self.assertEqual(len(pulls), expected_pull_count)
        for pull in pulls:
            self.assertIsInstance(pull, PullRequest)

    @ddt.data(
        ('Deployed to PROD', [':+1:', ':+1:', ':ship: :it:'
                              ], True, IssueComment),
        ('Deployed to stage', ['wahoo', 'want BLT', 'Deployed, to PROD'
                               ], False, IssueComment),
        ('Deployed to PROD', [
            ':+1:', 'law school man', '@macdiesel Deployed to PROD'
        ], True, IssueComment),
        ('Deployed to stage', [':+1:', ':+1:', '@macdiesel dEpLoYeD tO stage'
                               ], False, None),
        ('Deployed to stage', ['@macdiesel dEpLoYeD tO stage', ':+1:', ':+1:'
                               ], False, IssueComment),
        ('Deployed to PROD', [':+1:', ':+1:', '@macdiesel Deployed to PROD'
                              ], False, None),
    )
    @ddt.unpack
    def test_message_pull_request(self, new_message, existing_messages,
                                  force_message, expected_result):
        comments = [
            Mock(spec=IssueComment, body=message)
            for message in existing_messages
        ]
        self.repo_mock.get_pull.return_value = \
            Mock(spec=PullRequest,
                 get_issue_comments=Mock(return_value=comments),
                 create_issue_comment=lambda message: Mock(spec=IssueComment, body=message))

        result = self.api.message_pull_request(1, new_message, new_message,
                                               force_message)

        self.repo_mock.get_pull.assert_called()
        if expected_result:
            self.assertIsInstance(result, IssueComment)
            self.assertEqual(result.body, new_message)
        else:
            self.assertEqual(result, expected_result)

    def test_message_pr_does_not_exist(self):
        with patch.object(self.repo_mock,
                          'get_pull',
                          side_effect=UnknownObjectException(404, '')):
            self.assertRaises(InvalidPullRequestError,
                              self.api.message_pull_request, 3, 'test', 'test')

    def test_message_pr_deployed_stage(self):
        deploy_date = github_api.default_expected_release_date()
        with patch.object(self.api, 'message_pull_request') as mock:
            self.api.message_pr_with_type(1,
                                          github_api.MessageType.stage,
                                          deploy_date=deploy_date)
            mock.assert_called_with(
                1,
                github_api.PR_MESSAGE_FORMAT.format(
                    prefix=github_api.PR_PREFIX,
                    message=github_api.MessageType.stage.value,
                    extra_text=github_api.PR_ON_STAGE_DATE_EXTRA.format(
                        date=deploy_date, extra_text='')),
                github_api.PR_MESSAGE_FILTER.format(
                    prefix=github_api.PR_PREFIX,
                    message=github_api.MessageType.stage.value), False)

    @ddt.data(
        (datetime(2017, 1, 9, 11), date(2017, 1, 10)),
        (datetime(2017, 1, 13, 11), date(2017, 1, 16)),
    )
    @ddt.unpack
    def test_message_pr_deployed_stage_weekend(self, message_date,
                                               deploy_date):
        with patch.object(self.api, 'message_pull_request') as mock:
            with patch.object(github_api, 'datetime',
                              Mock(wraps=datetime)) as mock_datetime:
                mock_datetime.now.return_value = message_date
                self.api.message_pr_with_type(1,
                                              github_api.MessageType.stage,
                                              deploy_date=deploy_date)

                mock.assert_called_with(
                    1,
                    github_api.PR_MESSAGE_FORMAT.format(
                        prefix=github_api.PR_PREFIX,
                        message=github_api.MessageType.stage.value,
                        extra_text=github_api.PR_ON_STAGE_DATE_EXTRA.format(
                            date=deploy_date, extra_text='')),
                    github_api.PR_MESSAGE_FILTER.format(
                        prefix=github_api.PR_PREFIX,
                        message=github_api.MessageType.stage.value), False)

    @ddt.data(
        (1, github_api.MessageType.prod, '', False),
        (1337, github_api.MessageType.prod, 'some extra words', False),
        (867, github_api.MessageType.prod_rollback, '', True),
        (5, github_api.MessageType.prod_rollback, 'Elmo does not approve',
         False),
    )
    @ddt.unpack
    def test_message_pr_methods(self, pr_number, message_type, extra_text,
                                force_message):
        with patch.object(self.api, 'message_pull_request') as mock:
            self.api.message_pr_with_type(pr_number,
                                          message_type,
                                          extra_text=extra_text,
                                          force_message=force_message)
            mock.assert_called_with(
                pr_number,
                github_api.PR_MESSAGE_FORMAT.format(
                    prefix=github_api.PR_PREFIX,
                    message=message_type.value,
                    extra_text=extra_text),
                github_api.PR_MESSAGE_FILTER.format(
                    prefix=github_api.PR_PREFIX, message=message_type.value),
                force_message)
Esempio n. 14
0
class GitHubApiTestCase(TestCase):
    """
    Tests the requests creation/response handling for the Github API
    All Network calls should be mocked out.
    """
    def setUp(self):
        with patch.object(Github,
                          'get_organization',
                          return_value=Mock(spec=Organization)) as org_mock:
            with patch.object(Github,
                              'get_repo',
                              return_value=Mock(spec=Repository)) as repo_mock:
                self.org_mock = org_mock.return_value = Mock(spec=Organization)
                self.repo_mock = repo_mock.return_value = Mock(spec=Repository)
                self.api = GitHubAPI('test-org', 'test-repo', token='abc123')
        super(GitHubApiTestCase, self).setUp()

    @patch('github.Github.get_user')
    def test_user(self, mock_user_method):
        # setup the mock
        mock_user_method.return_value = Mock(spec=NamedUser)

        self.assertIsInstance(self.api.user(), NamedUser)
        mock_user_method.assert_called()

    @ddt.data(('abc', 'success'), ('123', 'failure'),
              (Mock(spec=GitCommit, **{'sha': '123'}), 'success'),
              (Mock(spec=GitCommit, **{'sha': '123'}), 'failure'))
    @ddt.unpack
    def test_get_commit_combined_statuses(self, sha, state):
        combined_status = Mock(spec=CommitCombinedStatus, state=state)
        attrs = {'get_combined_status.return_value': combined_status}
        commit_mock = Mock(spec=Commit, **attrs)
        self.repo_mock.get_commit = lambda sha: commit_mock

        status = self.api.get_commit_combined_statuses(sha)
        self.assertEqual(status.state, state)

    def test_get_commit_combined_statuses_passing_commit_obj(self):
        combined_status = Mock(spec=CommitCombinedStatus,
                               **{'state': 'success'})
        attrs = {'get_combined_status.return_value': combined_status}
        commit_mock = Mock(spec=Commit, **attrs)
        self.repo_mock.get_commit = lambda sha: commit_mock

        status = self.api.get_commit_combined_statuses(commit_mock)
        self.assertEqual(status.state, 'success')

    def test_get_commit_combined_statuses_bad_object(self):
        self.assertRaises(UnknownObjectException,
                          self.api.get_commit_combined_statuses, object())

    def test_get_commits_by_branch(self):
        self.repo_mock.get_branch.return_value = Mock(spec=Branch,
                                                      **{'commit.sha': '123'})
        self.repo_mock.get_commits.return_value = [
            Mock(spec=Commit, sha=i) for i in range(10)
        ]

        commits = self.api.get_commits_by_branch('test')

        self.repo_mock.get_branch.assert_called_with('test')
        self.repo_mock.get_commits.assert_called_with('123')
        self.assertEqual(len(commits), 10)

    def test_get_diff_url(self):
        def _check_url(org, repo, base_sha, head_sha):
            """ private method to do the comparison of the expected URL and the one we get back """
            url = self.api.get_diff_url(org, repo, base_sha, head_sha)
            expected = 'https://github.com/{}/{}/compare/{}...{}'.format(
                org, repo, base_sha, head_sha)
            self.assertEqual(url, expected)

        _check_url('org', 'repo', 'base-sha', 'head-sha')
        with self.assertRaises(InvalidUrlException):
            _check_url('org', 'repo', 'abc def', 'head-sha')

    def test_get_commits_by_branch_branch_not_found(self):
        self.repo_mock.get_branch.side_effect = GithubException(
            404, {
                'documentation_url':
                'https://developer.github.com/v3/repos/#get-branch',
                'message': 'Branch not found'
            })
        self.assertRaises(GithubException, self.api.get_commits_by_branch,
                          'blah')

    def test_delete_branch(self):
        ref_mock = Mock(spec=GitRef)
        get_git_ref_mock = Mock(return_value=ref_mock)
        self.repo_mock.get_git_ref = get_git_ref_mock
        self.api.delete_branch('blah')

        get_git_ref_mock.assert_called_with(ref='heads/blah')
        ref_mock.delete.assert_called()

    @ddt.data(('blah-candidate', 'falafel'), ('meow', 'schwarma '))
    @ddt.unpack
    def test_create_branch(self, branch_name, sha):
        create_git_ref_mock = Mock()
        self.repo_mock.create_git_ref = create_git_ref_mock

        self.api.create_branch(branch_name, sha)

        create_git_ref_mock.assert_called_with(
            ref='refs/heads/{}'.format(branch_name), sha=sha)

    @ddt.data(
        ('blah-candidate', 'release', 'test', 'test_pr'),
        ('catnip', 'release', 'My meowsome PR',
         'this PR has lots of catnip inside, go crazy!'),
    )
    @ddt.unpack
    def test_create_pull_request(self, head, base, title, body):
        self.api.create_pull_request(head=head,
                                     base=base,
                                     title=title,
                                     body=body)
        self.repo_mock.create_pull.assert_called_with(head=head,
                                                      base=base,
                                                      title=title,
                                                      body=body)

    @ddt.data('*****@*****.**', None)
    def test_create_tag(self, user_email):
        mock_user = Mock(spec=NamedUser)
        mock_user.email = user_email
        mock_user.name = 'test_name'
        with patch.object(Github, 'get_user', return_value=mock_user):
            create_tag_mock = Mock()
            create_ref_mock = Mock()
            self.repo_mock.create_git_tag = create_tag_mock
            self.repo_mock.create_git_ref = create_ref_mock

            test_tag = 'test_tag'
            test_sha = 'abc'
            self.api.create_tag(test_sha, test_tag)
            _, kwargs = create_tag_mock.call_args  # pylint: disable=unpacking-non-sequence
            self.assertEqual(kwargs['tag'], test_tag)
            self.assertEqual(kwargs['message'], '')
            self.assertEqual(kwargs['type'], 'commit')
            self.assertEqual(kwargs['object'], test_sha)
            create_ref_mock.assert_called_with(
                ref='refs/tags/{}'.format(test_tag), sha=test_sha)

    def _setup_create_tag_mocks(self, status_code, msg, return_sha):
        """
        Setup the mocks for the create_tag calls below.
        """
        mock_user = Mock(NamedUser, email='*****@*****.**')
        mock_user.name = 'test_name'
        self.repo_mock.create_git_tag = Mock()
        self.repo_mock.create_git_ref = Mock(
            side_effect=GithubException(status_code, {'message': msg}))
        self.repo_mock.get_git_ref = get_tag_mock = Mock()
        get_tag_mock.return_value = Mock(object=Mock(sha=return_sha))
        return mock_user

    def test_create_tag_which_already_exists_but_matches_sha(self):
        test_sha = 'abc'
        mock_user = self._setup_create_tag_mocks(422,
                                                 'Reference already exists',
                                                 test_sha)
        with patch.object(Github, 'get_user', return_value=mock_user):
            # No exception.
            self.api.create_tag(test_sha, 'test_tag')

    def test_create_tag_which_already_exists_and_no_sha_match(self):
        mock_user = self._setup_create_tag_mocks(422,
                                                 'Reference already exists',
                                                 'def')
        with patch.object(Github, 'get_user', return_value=mock_user):
            with self.assertRaises(GitTagMismatchError):
                self.api.create_tag('abc', 'test_tag')

    def test_create_tag_which_already_exists_and_unknown_exception(self):
        mock_user = self._setup_create_tag_mocks(421, 'Not sure what this is!',
                                                 'def')
        with patch.object(Github, 'get_user', return_value=mock_user):
            with self.assertRaises(GithubException):
                self.api.create_tag('abc', 'test_tag')

    @ddt.data(('diverged', True), ('divergent', False), ('ahead', False))
    @ddt.unpack
    def test_have_branches_diverged(self, status, expected):
        self.repo_mock.compare.return_value = Mock(spec=Comparison,
                                                   status=status)
        self.assertEqual(self.api.have_branches_diverged('base', 'head'),
                         expected)

    @ddt.data(('123', list(range(10)), 10, 'SuCcEsS', True),
              ('123', list(range(10)), 10, 'success', True),
              ('123', list(range(10)), 10, 'SUCCESS', True),
              ('123', list(range(10)), 10, 'pending', False),
              ('123', list(range(10)), 10, 'failure', False),
              ('123', list(range(10)), 0, None, False))
    @ddt.unpack
    def test_check_combined_status_commit(self, sha, statuses,
                                          statuses_returned, state, expected):
        mock_combined_status = Mock(spec=CommitCombinedStatus)
        mock_combined_status.statuses = [
            Mock(spec=CommitStatus, id=i) for i in statuses
        ]
        mock_combined_status.state = state

        commit_mock = Mock(spec=Commit)
        commit_mock.get_combined_status.return_value = mock_combined_status
        self.repo_mock.get_commit.return_value = commit_mock

        response, statuses = self.api.check_combined_status_commit(sha)

        self.assertEqual(response, expected)
        self.assertIsInstance(statuses, dict)
        self.assertEqual(len(statuses), statuses_returned)
        commit_mock.get_combined_status.assert_called()
        self.repo_mock.get_commit.assert_called_with(sha)

    @ddt.data(
        ('release-candidate', 4),
        ('meow-candidate', 6),
        ('should-have-gone-to-law-school', 1),
    )
    @ddt.unpack
    def test_most_recent_good_commit(self, branch, good_commit_id):
        commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)]
        self.api.get_commits_by_branch = Mock(return_value=commits)

        def _side_effect(sha):
            """
            side effect returns True when the commit ID matches the current iteration
            """
            return (sha == good_commit_id, {})

        self.api._is_commit_successful = Mock(side_effect=_side_effect)  # pylint: disable=protected-access

        self.api.most_recent_good_commit(branch)
        self.assertEqual(self.api._is_commit_successful.call_count,
                         good_commit_id)  # pylint: disable=protected-access

    def test_most_recent_good_commit_no_commit(self):
        commits = [Mock(spec=Commit, sha=i) for i in range(1, 10)]
        self.api.get_commits_by_branch = Mock(return_value=commits)

        self.api._is_commit_successful = Mock(return_value=(False, {}))  # pylint: disable=protected-access
        self.assertRaises(NoValidCommitsError,
                          self.api.most_recent_good_commit,
                          'release-candidate')

    @ddt.data(
        # 1 unique SHA should result in 1 search query and 1 PR.
        (SHAS[:1], 1, 1),
        # 18 unique SHAs should result in 1 search query and 18 PRs.
        (SHAS[:18], 1, 18),
        # 36 unique SHAs should result in 2 search queries and 36 PRs.
        (SHAS[:36], 2, 36),
        # 37 unique SHAs should result in 3 search queries and 37 PRs.
        (SHAS[:37], 3, 37),
        # 20 unique SHAs, each appearing twice, should result in 3 search queries and 20 PRs.
        (SHAS[:20] * 2, 3, 20),
    )
    @ddt.unpack
    @patch('github.Github.search_issues')
    def test_get_pr_range(self, shas, expected_search_count,
                          expected_pull_count, mock_search_issues):
        commits = [Mock(spec=Commit, sha=sha) for sha in shas]
        self.repo_mock.compare.return_value = Mock(spec=Comparison,
                                                   commits=commits)

        def search_issues_side_effect(shas, **kwargs):  # pylint: disable=unused-argument
            """
            Stub implementation of GitHub issue search.
            """
            return [
                Mock(spec=Issue, number=TRIMMED_SHA_MAP[sha])
                for sha in shas.split()
            ]

        mock_search_issues.side_effect = search_issues_side_effect

        self.repo_mock.get_pull = lambda number: Mock(spec=PullRequest,
                                                      number=number)

        start_sha, end_sha = 'abc', '123'
        pulls = self.api.get_pr_range(start_sha, end_sha)

        self.repo_mock.compare.assert_called_with(start_sha, end_sha)

        self.assertEqual(mock_search_issues.call_count, expected_search_count)
        for call_args in mock_search_issues.call_args_list:
            # Verify that the batched SHAs have been trimmed.
            self.assertLess(len(call_args[0]), 200)

        self.assertEqual(len(pulls), expected_pull_count)
        for pull in pulls:
            self.assertIsInstance(pull, PullRequest)

    @ddt.data(
        ('Deployed to PROD', [':+1:', ':+1:', ':ship: :it:'
                              ], False, IssueComment),
        ('Deployed to stage', ['wahoo', 'want BLT', 'Deployed, to PROD, JK'
                               ], False, IssueComment),
        ('Deployed to PROD', [
            ':+1:', 'law school man', '@macdiesel Deployed to PROD'
        ], False, None),
        ('Deployed to stage', [':+1:', ':+1:', '@macdiesel dEpLoYeD tO stage'
                               ], False, None),
        ('Deployed to stage', ['@macdiesel dEpLoYeD tO stage', ':+1:', ':+1:'
                               ], False, None),
        ('Deployed to PROD', [':+1:', ':+1:', '@macdiesel Deployed to PROD'
                              ], True, IssueComment),
    )
    @ddt.unpack
    def test_message_pull_request(self, new_message, existing_messages,
                                  force_message, expected_result):
        comments = [
            Mock(spec=IssueComment, body=message)
            for message in existing_messages
        ]
        self.repo_mock.get_pull.return_value = \
            Mock(spec=PullRequest,
                 get_issue_comments=Mock(return_value=comments),
                 create_issue_comment=lambda message: Mock(spec=IssueComment, body=message))

        result = self.api.message_pull_request(1, new_message, new_message,
                                               force_message)

        self.repo_mock.get_pull.assert_called()
        if expected_result:
            self.assertIsInstance(result, IssueComment)
            self.assertEqual(result.body, new_message)
        else:
            self.assertEqual(result, expected_result)

    def test_message_pr_does_not_exist(self):
        with patch.object(self.repo_mock,
                          'get_pull',
                          side_effect=UnknownObjectException(404, '')):
            self.assertRaises(InvalidPullRequestError,
                              self.api.message_pull_request, 3, 'test', 'test')

    def test_message_pr_deployed_stage(self):
        with patch.object(self.api, 'message_pull_request') as mock:
            self.api.message_pr_deployed_stage(1,
                                               deploy_date=datetime(
                                                   2017, 1, 10))
            mock.assert_called_with(
                1, (github_api.PR_ON_STAGE_BASE_MESSAGE +
                    github_api.PR_ON_STAGE_DATE_MESSAGE).format(
                        date=datetime(2017, 1, 10)),
                github_api.PR_ON_STAGE_BASE_MESSAGE, False)

    @ddt.data(
        (datetime(2017, 1, 9, 11), date(2017, 1, 10)),
        (datetime(2017, 1, 13, 11), date(2017, 1, 16)),
    )
    @ddt.unpack
    def test_message_pr_deployed_stage_weekend(self, message_date,
                                               deploy_date):
        with patch.object(self.api, 'message_pull_request') as mock:
            with patch.object(github_api, 'datetime',
                              Mock(wraps=datetime)) as mock_datetime:
                mock_datetime.now.return_value = message_date
                self.api.message_pr_deployed_stage(1)
                mock.assert_called_with(
                    1, (github_api.PR_ON_STAGE_BASE_MESSAGE +
                        github_api.PR_ON_STAGE_DATE_MESSAGE).format(
                            date=deploy_date),
                    github_api.PR_ON_STAGE_BASE_MESSAGE, False)

    def test_message_pr_deployed_prod(self):
        with patch.object(self.api, 'message_pull_request') as mock:
            self.api.message_pr_deployed_prod(1)
            mock.assert_called_with(1, github_api.PR_ON_PROD_MESSAGE,
                                    github_api.PR_ON_PROD_MESSAGE, False)

    def test_message_pr_release_canceled(self):
        with patch.object(self.api, 'message_pull_request') as mock:
            self.api.message_pr_release_canceled(1)
            mock.assert_called_with(1, github_api.PR_RELEASE_CANCELED_MESSAGE,
                                    github_api.PR_RELEASE_CANCELED_MESSAGE,
                                    False)
Esempio n. 15
0
def create_pull_request(org, repo, token, source_branch, target_branch, title,
                        body, output_file):
    """
    Creates a pull request to merge a source branch into a target branch, if needed.
    Both the source and target branches are assumed to already exist.

    Args:
        org (str):
        repo (str):
        token (str):
        source_branch (str):
        target_branch (str):
        title (str):
        body (str):
        output_file (str):

    Outputs a yaml file with information about the newly created PR:
     ---
    pr_created: true
    pr_id: 96786312
    pr_number: 3
    pr_url: https://api.github.com/repos/macdiesel412/Rainer/pulls/3
    pr_repo_url: /repos/macdiesel412/Rainer
    pr_head: af538da6b229cf1dfa33d0171e75fbff6de4c283
    pr_base: 2a800083658e2f5d11d1d40118024f77c59d1b9a
    pr_diff_url: https://github.com/macdiesel412/Rainer/pull/3.diff
    pr_mergable: null
    pr_state: open
    pr_mergable_state: unknown

    -or-
     ---
    pr_created: false

    if no PR is required.
    """
    github_api = GitHubAPI(org, repo, token)

    # First, check to see that there are commits to merge from the source branch to
    # the target branch. If the target branch already contains all the commits from the
    # source branch, then there's no need to create a PR as there's nothing to merge.
    if not github_api.have_branches_diverged(source_branch, target_branch):
        LOG.info(
            "No Pull Request for merging {source} into {target} created - nothing to merge."
            .format(source=source_branch, target=target_branch))
        output_yaml = {
            'pr_created': False,
        }
    else:
        LOG.info(
            "Creating Pull Request for merging {source} into {target}".format(
                source=source_branch, target=target_branch))

        if title is None:
            title = "Automated merge of {source} into {target}".format(
                source=source_branch, target=target_branch)

        try:
            pull_request = github_api.create_pull_request(head=source_branch,
                                                          base=target_branch,
                                                          title=title,
                                                          body=body)
        except GithubException:
            LOG.error("Unable to create pull request. Aborting")
            raise

        output_yaml = {
            'pr_created': True,
            'pr_id': pull_request.id,
            'pr_number': pull_request.number,
            'pr_url': pull_request.url,
            'pr_repo_url': github_api.github_repo.url,
            'pr_head': pull_request.head.sha,
            'pr_base': pull_request.base.sha,
            'pr_diff_url': pull_request.diff_url,
            'pr_mergable': pull_request.mergeable,
            'pr_state': pull_request.state,
            'pr_mergable_state': pull_request.mergeable_state,
        }

    with io.open(output_file, 'w') as stream:
        yaml.safe_dump(output_yaml,
                       stream,
                       default_flow_style=False,
                       explicit_start=True)
Esempio n. 16
0
def create_pull_request(org,
                        repo,
                        token,
                        source_branch,
                        target_branch,
                        title,
                        body,
                        output_file):
    """
    Creates a pull request to merge a source branch into a target branch, if needed.
    Both the source and target branches are assumed to already exist. Outputs a YAML
    file with information about the PR creation.
    """
    github_api = GitHubAPI(org, repo, token)

    # First, check to see that there are commits to merge from the source branch to
    # the target branch. If the target branch already contains all the commits from the
    # source branch, then there's no need to create a PR as there's nothing to merge.
    if not github_api.have_branches_diverged(source_branch, target_branch):
        LOG.info(
            "No Pull Request for merging {source} into {target} created - nothing to merge.".format(
                source=source_branch,
                target=target_branch
            )
        )
        output_yaml = {
            'pr_created': False,
        }
    else:
        LOG.info(
            "Creating Pull Request for merging {source} into {target}".format(
                source=source_branch,
                target=target_branch
            )
        )

        if title is None:
            title = "Automated merge of {source} into {target}".format(
                source=source_branch,
                target=target_branch
            )

        try:
            pull_request = github_api.create_pull_request(
                head=source_branch,
                base=target_branch,
                title=title,
                body=body
            )
        except PullRequestCreationError:
            LOG.error("Unable to create pull request. Aborting")
            raise

        output_yaml = {
            'pr_created': True,
            'pr_id': pull_request.id,
            'pr_number': pull_request.number,
            'pr_url': pull_request.url,
            'pr_repo_url': github_api.github_repo.url,
            'pr_head': pull_request.head.sha,
            'pr_base': pull_request.base.sha,
            'pr_diff_url': pull_request.diff_url,
            'pr_mergable': pull_request.mergeable,
            'pr_state': pull_request.state,
            'pr_mergable_state': pull_request.mergeable_state,
        }

    with io.open(output_file, 'w') as stream:
        yaml.safe_dump(
            output_yaml,
            stream,
            default_flow_style=False,
            explicit_start=True
        )
Esempio n. 17
0
def create_tag(org,
               repo,
               token,
               commit_sha,
               input_file,
               branch_name,
               deploy_artifact,
               tag_name,
               tag_message,
               commit_sha_variable):
    """
    Creates a tag at a specified commit SHA with a tag name/message.
    The commit SHA is passed in using *one* of these ways:
        - input_file: input YAML file containing a 'sha' key
        - commit_sha: explicitly passed-in commit SHA
        - branch_name: HEAD sha obtained from this branch name
    """
    github_api = GitHubAPI(org, repo, token)

    # Check for one and only one of the mutually-exclusive params.
    if not exactly_one_set((commit_sha, input_file, branch_name)):
        err_msg = \
            "Exactly one of commit_sha ({!r}), input_file ({!r})," \
            " and branch_name ({!r}) should be specified.".format(
                commit_sha,
                input_file,
                branch_name
            )
        LOG.error(err_msg)
        sys.exit(1)

    if input_file:
        input_vars = yaml.safe_load(io.open(input_file, 'r'))
        commit_sha = input_vars[commit_sha_variable]
    elif branch_name:
        commit_sha = github_api.get_head_commit_from_branch_name(branch_name)

    if deploy_artifact:
        deploy_vars = yaml.safe_load(open(deploy_artifact, 'r'))
        deploy_time = datetime.datetime.fromtimestamp(deploy_vars['deploy_time'], EST)
    else:
        # If no deploy artifact was given from which to extract a deploy time, use the current time.
        deploy_time = datetime.datetime.now(EST)
    # If no tag name was given, generate one using the date/time.
    if not tag_name:
        tag_name = 'release-{}'.format(
            deploy_time.strftime("%Y-%m-%d-%H.%M")
        )

    # If no tag message was given, generate one using the date/time.
    if not tag_message:
        tag_message = 'Release for {}'.format(
            deploy_time.strftime("%b %d, %Y %H:%M EST")
        )

    LOG.info(
        "Tagging commit sha {sha} as tag '{tag}' with message '{message}'".format(
            sha=commit_sha,
            tag=tag_name,
            message=tag_message
        )
    )

    try:
        github_api.create_tag(commit_sha, tag_name, tag_message)
    except GithubException:
        LOG.error("Unable to create tag. Aborting")
        raise
Esempio n. 18
0
def create_release_candidate(org, repo, source_branch, sha, target_branch,
                             token, output_file):
    """
    Creates a target "release-candidate" branch

    Args:
        org (str):
        repo (str):
        source_branch (str):
        sha (str):
        target_branch (str):
        token (str):
        output_file (str):

    Outputs a yaml file with information about the newly created branch.
    e.g.
     ---
    repo_name: edx-platform
    org_name: edx
    source_branch_name: master
    target_branch_name: release-candidate
    sha: af538da6b229cf1dfa33d0171e75fbff6de4c283
    """
    if source_branch is not None and sha is not None:
        LOG.error(
            "Please specify either --source_branch or --sha, but not both.")
        sys.exit(1)

    if source_branch is None and sha is None:
        LOG.error("Please specify at least one of --source_branch or --sha.")
        sys.exit(1)

    LOG.info("Getting GitHub token...")
    github_api = GitHubAPI(org, repo, token)

    if sha is None:
        LOG.info("Fetching commits...")
        try:
            commits = github_api.get_commits_by_branch(source_branch)
            commit = commits[0]
            sha = commit.sha
            commit_message = extract_message_summary(commit.commit.message)

        except NoValidCommitsError:
            LOG.error(
                "Couldn't find a recent commit without test failures. Aborting"
            )
            raise

        LOG.info("Branching {rc} off {sha}. ({msg})".format(
            rc=target_branch, sha=sha, msg=commit_message))
    else:
        LOG.info("Branching {rc} off {sha} (explicitly provided).".format(
            rc=target_branch,
            sha=sha,
        ))

    try:
        github_api.delete_branch(target_branch)
    except Exception:  # pylint: disable=broad-except
        LOG.error("Unable to delete branch {branch_name}. ".format(
            branch_name=target_branch))

    try:
        github_api.create_branch(target_branch, sha)
    except Exception:  # pylint: disable=broad-except
        LOG.error("Unable to create branch {branch_name}. Aborting".format(
            branch_name=target_branch))
        raise

    with io.open(output_file, 'w') as stream:
        yaml.safe_dump(
            {
                'repo_name': repo,
                'org_name': org,
                'source_branch_name': source_branch,
                'target_branch_name': target_branch,
                'sha': sha,
            },
            stream,
            default_flow_style=False,
            explicit_start=True)
Esempio n. 19
0
def create_release_candidate(org, repo, source_branch, target_branch,
                             pr_target_branch, release_date, find_commit,
                             force_commit, token, output_file):
    """
    Creates a target "release-candidate" branch and pull request

    Args:
        org (str):
        repo (str):
        source_branch (str):
        target_branch (str):
        pr_target_branch (str):
        release_date (str): in the format: YYYY-MM-DD
        find_commit (bool):
        force_commit (str):
        token (str):
        output_file (str):

    Outputs a yaml file with information about the newly created PR.
    e.g.
     ---
    pr_base: 2a800083658e2f5d11d1d40118024f77c59d1b9a
    pr_diff_url: https://github.com/macdiesel412/Rainer/pull/3.diff
    pr_head: af538da6b229cf1dfa33d0171e75fbff6de4c283
    pr_id: 96786312
    pr_number: 3
    pr_mergable: null
    pr_mergable_state: unknown
    pr_repo_url: /repos/macdiesel412/Rainer
    pr_state: open
    pr_url: https://api.github.com/repos/macdiesel412/Rainer/pulls/3

    """
    LOG.info("Getting GitHub token...")
    github_api = GitHubAPI(org, repo, token)

    if force_commit:
        commit_hash = force_commit
        commit_message = "User overide SHA"
    else:
        LOG.info("Fetching commits...")
        try:
            commit = github_api.most_recent_good_commit(source_branch)
            commit_hash = commit.sha
            commit_message = extract_message_summary(commit.commit.message)

        except NoValidCommitsError:
            LOG.error(
                "Couldn't find a recent commit without test failures. Aborting"
            )
            raise

    # Return early if we are only returning the commit hash to stdout
    if find_commit:
        LOG.info("\n\thash: {commit_hash}\n\tcommit message: {message}".format(
            commit_hash=commit_hash, message=commit_message))
        return

    LOG.info("Branching {rc} off {sha}. ({msg})".format(rc=target_branch,
                                                        sha=commit_hash,
                                                        msg=commit_message))
    try:
        github_api.delete_branch(target_branch)
    except Exception:  # pylint: disable=broad-except
        LOG.error("Unable to delete branch %s. "
                  "Will attempt to recreate", target_branch)

    try:
        github_api.create_branch(target_branch, commit_hash)
    except Exception:  # pylint: disable=broad-except
        LOG.error("Unable to recreate branch %s. Aborting", target_branch)
        raise

    LOG.info("Creating Pull Request for %s into %s", target_branch,
             pr_target_branch)

    try:
        pr_title = "Release Candidate {rc}".format(
            rc=rc_branch_name_for_date(release_date.date()))
        pull_request = github_api.create_pull_request(head=target_branch,
                                                      base=pr_target_branch,
                                                      title=pr_title)

        with io.open(output_file, 'w') as stream:
            yaml.safe_dump(
                {
                    'pr_id': pull_request.id,
                    'pr_number': pull_request.number,
                    'pr_url': pull_request.url,
                    'pr_repo_url': github_api.github_repo.url,
                    'pr_head': pull_request.head.sha,
                    'pr_base': pull_request.base.sha,
                    'pr_diff_url': pull_request.diff_url,
                    'pr_mergable': pull_request.mergeable,
                    'pr_state': pull_request.state,
                    'pr_mergable_state': pull_request.mergeable_state,
                },
                stream,
                default_flow_style=False,
                explicit_start=True)

    except GithubException:
        LOG.error("Unable to create branch. Aborting")
        raise