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, )
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)
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)
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)
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