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