def test_constructor_throws_on_pr_history_window_too_large(self): with self.assertRaises(ValueError): self.wpt_github = WPTGitHub( MockHost(), user='******', token='decafbad', pr_history_window=MAX_PR_HISTORY_WINDOW + 1)
class WPTGitHubTest(unittest.TestCase): def setUp(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad') def test_init(self): self.assertEqual(self.wpt_github.user, 'rutabaga') self.assertEqual(self.wpt_github.token, 'decafbad') def test_auth_token(self): self.assertEqual(self.wpt_github.auth_token(), base64.encodestring('rutabaga:decafbad').strip()) def test_merge_pull_request_throws_merge_error_on_405(self): self.wpt_github.host.web.responses = [ { 'status_code': 200 }, { 'status_code': 405 }, ] self.wpt_github.merge_pull_request(1234) with self.assertRaises(MergeError): self.wpt_github.merge_pull_request(5678)
def __init__(self, host, gh_user, gh_token, gerrit_user, gerrit_token, dry_run=False): self.host = host self.wpt_github = WPTGitHub(host, gh_user, gh_token, pr_history_window=PR_HISTORY_WINDOW) self.gerrit = GerritAPI(self.host, gerrit_user, gerrit_token) self.dry_run = dry_run self.local_wpt = LocalWPT(self.host, gh_token) self.local_wpt.fetch()
def __init__(self, host, gh_user, gh_token, gerrit_user, gerrit_token, dry_run=False): self.host = host self.wpt_github = WPTGitHub(host, gh_user, gh_token) self.gerrit = GerritAPI(self.host, gerrit_user, gerrit_token) self.dry_run = dry_run self.local_wpt = LocalWPT(self.host, gh_token) self.local_wpt.fetch()
def main(self, argv=None): """Creates PRs for in-flight CLs and merges changes that land on master. Returns: A boolean: True if success, False if there were any patch failures. """ args = self.parse_args(argv) self.dry_run = args.dry_run configure_logging(logging_level=logging.INFO, include_time=True) credentials = read_credentials(self.host, args.credentials_json) if not (credentials['GH_USER'] and credentials['GH_TOKEN']): _log.error('Must provide both user and token for GitHub.') return False self.wpt_github = self.wpt_github or WPTGitHub(self.host, credentials['GH_USER'], credentials['GH_TOKEN']) self.gerrit = self.gerrit or GerritAPI(self.host, credentials['GERRIT_USER'], credentials['GERRIT_TOKEN']) self.local_wpt = self.local_wpt or LocalWPT(self.host, credentials['GH_TOKEN']) self.local_wpt.fetch() open_gerrit_cls = self.gerrit.query_exportable_open_cls() self.process_gerrit_cls(open_gerrit_cls) exportable_commits, errors = self.get_exportable_commits() for error in errors: _log.warn(error) self.process_chromium_commits(exportable_commits) return not bool(errors)
def main(): configure_logging() options = parse_args() host = Host() wpt_github = WPTGitHub(host) test_exporter = TestExporter(host, wpt_github, dry_run=options.dry_run) test_exporter.run()
def main(self, argv=None): """Creates PRs for in-flight CLs and merges changes that land on master. Returns: A boolean: True if success, False if there were any patch failures. """ options = self.parse_args(argv) self.dry_run = options.dry_run log_level = logging.DEBUG if options.verbose else logging.INFO configure_logging(logging_level=log_level, include_time=True) if options.verbose: # Print out the full output when executive.run_command fails. self.host.executive.error_output_limit = None credentials = read_credentials(self.host, options.credentials_json) if not (credentials.get('GH_USER') and credentials.get('GH_TOKEN')): _log.error('You must provide your GitHub credentials for this ' 'script to work.') _log.error('See https://chromium.googlesource.com/chromium/src' '/+/master/docs/testing/web_platform_tests.md' '#GitHub-credentials for instructions on how to set ' 'your credentials up.') return False self.wpt_github = self.wpt_github or WPTGitHub( self.host, credentials['GH_USER'], credentials['GH_TOKEN']) self.gerrit = self.gerrit or GerritAPI( self.host, credentials['GERRIT_USER'], credentials['GERRIT_TOKEN']) self.local_wpt = self.local_wpt or LocalWPT(self.host, credentials['GH_TOKEN']) self.local_wpt.fetch() _log.info('Searching for exportable in-flight CLs.') # The Gerrit search API is slow and easy to fail, so we wrap it in a try # statement to continue exporting landed commits when it fails. try: open_gerrit_cls = self.gerrit.query_exportable_open_cls() except GerritError as e: _log.info( 'In-flight CLs cannot be exported due to the following error:') _log.error(str(e)) gerrit_error = True else: self.process_gerrit_cls(open_gerrit_cls) gerrit_error = False _log.info('Searching for exportable Chromium commits.') exportable_commits, git_errors = self.get_exportable_commits() self.process_chromium_commits(exportable_commits) if git_errors: _log.info( 'Attention: The following errors have prevented some commits from being ' 'exported:') for error in git_errors: _log.error(error) return not (gerrit_error or git_errors)
class WPTGitHubTest(unittest.TestCase): def setUp(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad') def test_init(self): self.assertEqual(self.wpt_github.user, 'rutabaga') self.assertEqual(self.wpt_github.token, 'decafbad') def test_auth_token(self): self.assertEqual(self.wpt_github.auth_token(), base64.encodestring('rutabaga:decafbad').strip())
class WPTGitHubTest(unittest.TestCase): def setUp(self): self.host = MockHost() self.host.environ['GH_USER'] = '******' self.host.environ['GH_TOKEN'] = 'deadbeefcafe' self.wpt_github = WPTGitHub(self.host) def test_properties(self): self.assertEqual(self.wpt_github.user, 'rutabaga') self.assertEqual(self.wpt_github.token, 'deadbeefcafe') def test_auth_token(self): expected = base64.encodestring('rutabaga:deadbeefcafe').strip() self.assertEqual(self.wpt_github.auth_token(), expected)
def test_all_pull_requests_reaches_pr_history_window(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad', pr_history_window=2) self.wpt_github.host.web.responses = [ { 'status_code': 200, 'headers': { 'Link': '<https://api.github.com/resources?page=2>; rel="next"' }, 'body': json.dumps({ 'incomplete_results': False, 'items': [self.generate_pr_item(1)] }) }, { 'status_code': 200, 'headers': { 'Link': '' }, 'body': json.dumps({ 'incomplete_results': False, 'items': [self.generate_pr_item(2), self.generate_pr_item(3)] }) }, ] self.assertEqual(len(self.wpt_github.all_pull_requests()), 2)
class WPTGitHubTest(unittest.TestCase): def setUp(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad') def test_init(self): self.assertEqual(self.wpt_github.user, 'rutabaga') self.assertEqual(self.wpt_github.token, 'decafbad') def test_auth_token(self): self.assertEqual( self.wpt_github.auth_token(), string_utils.decode(base64.b64encode( string_utils.encode('rutabaga:decafbad')), target_type=str).strip())
def main(self, argv=None): """Creates PRs for in-flight CLs and merges changes that land on master. Returns: A boolean: True if success, False if there were any patch failures. """ options = self.parse_args(argv) self.dry_run = options.dry_run log_level = logging.DEBUG if options.verbose else logging.INFO configure_logging(logging_level=log_level, include_time=True) if options.verbose: # Print out the full output when executive.run_command fails. self.host.executive.error_output_limit = None credentials = read_credentials(self.host, options.credentials_json) if not (credentials['GH_USER'] and credentials['GH_TOKEN']): _log.error('Must provide both user and token for GitHub.') return False self.wpt_github = self.wpt_github or WPTGitHub( self.host, credentials['GH_USER'], credentials['GH_TOKEN']) self.gerrit = self.gerrit or GerritAPI( self.host, credentials['GERRIT_USER'], credentials['GERRIT_TOKEN']) self.local_wpt = self.local_wpt or LocalWPT(self.host, credentials['GH_TOKEN']) self.local_wpt.fetch() # The Gerrit search API is slow and easy to fail, so we wrap it in a try # statement to continue exporting landed commits when it fails. try: open_gerrit_cls = self.gerrit.query_exportable_open_cls() except GerritError as e: _log.error(str(e)) gerrit_error = True else: self.process_gerrit_cls(open_gerrit_cls) gerrit_error = False exportable_commits, git_errors = self.get_exportable_commits() for error in git_errors: _log.error(error) self.process_chromium_commits(exportable_commits) return not (gerrit_error or git_errors)
def test_requires_env_vars(self): self.assertRaises(AssertionError, lambda: WPTGitHub(self.host))
class TestExporter(object): def __init__(self, host, gh_user, gh_token, dry_run=False): self.host = host self.wpt_github = WPTGitHub(host, gh_user, gh_token) self.dry_run = dry_run self.local_wpt = LocalWPT(self.host, gh_token) self.local_wpt.fetch() def run(self): """Query in-flight pull requests, then merges PR or creates one. This script assumes it will be run on a regular interval. On each invocation, it will either attempt to merge or attempt to create a PR, never both. """ pull_requests = self.wpt_github.in_flight_pull_requests() self.merge_all_pull_requests(pull_requests) # TODO(jeffcarp): The below line will enforce draining all open PRs before # adding any more to the queue, which mirrors current behavior. After this # change lands, modify the following to: # - for each exportable commit # - check if there's a corresponding PR # - if not, create one if not pull_requests: _log.info('No in-flight PRs found, looking for exportable commits.') self.export_first_exportable_commit() def merge_all_pull_requests(self, pull_requests): for pr in pull_requests: self.merge_pull_request(pr) def merge_pull_request(self, pull_request): _log.info('In-flight PR found: %s', pull_request.title) _log.info('https://github.com/w3c/web-platform-tests/pull/%d', pull_request.number) _log.info('Attempting to merge...') if self.dry_run: _log.info('[dry_run] Would have attempted to merge PR') return branch = self.wpt_github.get_pr_branch(pull_request.number) try: self.wpt_github.merge_pull_request(pull_request.number) self.wpt_github.delete_remote_branch(branch) except MergeError: _log.info('Could not merge PR.') def export_first_exportable_commit(self): """Looks for exportable commits in Chromium, creates PR if found.""" wpt_commit, chromium_commit = self.local_wpt.most_recent_chromium_commit() assert chromium_commit, 'No Chromium commit found, this is impossible' wpt_behind_master = self.local_wpt.commits_behind_master(wpt_commit) _log.info('\nLast Chromium export commit in web-platform-tests:') _log.info('web-platform-tests@%s', wpt_commit) _log.info('(%d behind web-platform-tests@origin/master)', wpt_behind_master) _log.info('\nThe above WPT commit points to the following Chromium commit:') _log.info('chromium@%s', chromium_commit.sha) _log.info('(%d behind chromium@origin/master)', chromium_commit.num_behind_master()) exportable_commits = exportable_commits_since(chromium_commit.sha, self.host, self.local_wpt) if not exportable_commits: _log.info('No exportable commits found in Chromium, stopping.') return _log.info('Found %d exportable commits in Chromium:', len(exportable_commits)) for commit in exportable_commits: _log.info('- %s %s', commit, commit.subject()) outbound_commit = exportable_commits[0] _log.info('Picking the earliest commit and creating a PR') _log.info('- %s %s', outbound_commit.sha, outbound_commit.subject()) patch = outbound_commit.format_patch() message = outbound_commit.message() author = outbound_commit.author() if self.dry_run: _log.info('[dry_run] Stopping before creating PR') _log.info('\n\n[dry_run] message:') _log.info(message) _log.info('\n\n[dry_run] patch:') _log.info(patch) return remote_branch_name = self.local_wpt.create_branch_with_patch(message, patch, author) response_data = self.wpt_github.create_pr( remote_branch_name=remote_branch_name, desc_title=outbound_commit.subject(), body=outbound_commit.body()) _log.info('Create PR response: %s', response_data) if response_data: data, status_code = self.wpt_github.add_label(response_data['number']) _log.info('Add label response (status %s): %s', status_code, data)
def setUp(self): self.host = MockHost() self.host.environ['GH_USER'] = '******' self.host.environ['GH_TOKEN'] = 'deadbeefcafe' self.wpt_github = WPTGitHub(self.host)
def extract_metadata(self, tag, commit_body, all_matches=False): return WPTGitHub.extract_metadata(tag, commit_body, all_matches)
class TestExporter(object): def __init__(self, host, gh_user, gh_token, gerrit_user, gerrit_token, dry_run=False): self.host = host self.wpt_github = WPTGitHub(host, gh_user, gh_token, pr_history_window=PR_HISTORY_WINDOW) self.gerrit = GerritAPI(self.host, gerrit_user, gerrit_token) self.dry_run = dry_run self.local_wpt = LocalWPT(self.host, gh_token) self.local_wpt.fetch() def run(self): """For last n commits on Chromium master, create or try to merge a PR. The exporter will look in chronological order at every commit in Chromium. """ open_gerrit_cls = self.gerrit.query_exportable_open_cls() self.process_gerrit_cls(open_gerrit_cls) exportable_commits = self.get_exportable_commits(limit=COMMIT_HISTORY_WINDOW) for exportable_commit in exportable_commits: pull_request = self.corresponding_pull_request_for_commit(exportable_commit) if pull_request: if pull_request.state == 'open': self.merge_pull_request(pull_request) else: _log.info('Pull request is not open: #%d %s', pull_request.number, pull_request.title) else: self.create_pull_request(exportable_commit) def process_gerrit_cls(self, gerrit_cls): """Creates or updates PRs for Gerrit CLs.""" for cl in gerrit_cls: _log.info('Found Gerrit in-flight CL: "%s" %s', cl.subject, cl.url) # Check if CL already has a corresponding PR pull_request = self.wpt_github.pr_with_change_id(cl.change_id) if pull_request: pr_url = '{}pull/{}'.format(WPT_GH_URL, pull_request.number) _log.info('In-flight PR found: %s', pr_url) pr_cl_revision = self.wpt_github.extract_metadata(WPT_REVISION_FOOTER + ' ', pull_request.body) if cl.current_revision_sha == pr_cl_revision: _log.info('PR revision matches CL revision. Nothing to do here.') continue _log.info('New revision found, updating PR...') self.create_or_update_pull_request_from_cl(cl, pull_request) else: _log.info('No in-flight PR found for CL. Creating...') self.create_or_update_pull_request_from_cl(cl) def get_exportable_commits(self, limit): return exportable_commits_over_last_n_commits(limit, self.host, self.local_wpt) def merge_pull_request(self, pull_request): _log.info('In-flight PR found: %s', pull_request.title) _log.info('%spull/%d', WPT_GH_URL, pull_request.number) if self.dry_run: _log.info('[dry_run] Would have attempted to merge PR') return _log.info('Attempting to merge...') # This is outside of the try block because if there's a problem communicating # with the GitHub API, we should hard fail. branch = self.wpt_github.get_pr_branch(pull_request.number) try: self.wpt_github.merge_pull_request(pull_request.number) # This is in the try block because if a PR can't be merged, we shouldn't # delete its branch. self.wpt_github.delete_remote_branch(branch) change_id = self.wpt_github.extract_metadata('Change-Id: ', pull_request.body) if change_id: cl = GerritCL(data={'change_id': change_id}, api=self.gerrit) pr_url = '{}pull/{}'.format(WPT_GH_URL, pull_request.number) cl.post_comment(( 'The WPT PR for this CL has been merged upstream! {pr_url}' ).format( pr_url=pr_url )) except MergeError: _log.info('Could not merge PR.') def export_first_exportable_commit(self): """Looks for exportable commits in Chromium, creates PR if found.""" wpt_commit, chromium_commit = self.local_wpt.most_recent_chromium_commit() assert chromium_commit, 'No Chromium commit found, this is impossible' wpt_behind_master = self.local_wpt.commits_behind_master(wpt_commit) _log.info('\nLast Chromium export commit in web-platform-tests:') _log.info('web-platform-tests@%s', wpt_commit) _log.info('(%d behind web-platform-tests@origin/master)', wpt_behind_master) _log.info('\nThe above WPT commit points to the following Chromium commit:') _log.info('chromium@%s', chromium_commit.sha) _log.info('(%d behind chromium@origin/master)', chromium_commit.num_behind_master()) exportable_commits = exportable_commits_since(chromium_commit.sha, self.host, self.local_wpt) if not exportable_commits: _log.info('No exportable commits found in Chromium, stopping.') return _log.info('Found %d exportable commits in Chromium:', len(exportable_commits)) for commit in exportable_commits: _log.info('- %s %s', commit, commit.subject()) outbound_commit = exportable_commits[0] _log.info('Picking the earliest commit and creating a PR') _log.info('- %s %s', outbound_commit.sha, outbound_commit.subject()) self.create_pull_request(outbound_commit) def create_pull_request(self, outbound_commit): patch = outbound_commit.format_patch() message = outbound_commit.message() author = outbound_commit.author() if self.dry_run: _log.info('[dry_run] Stopping before creating PR') _log.info('\n\n[dry_run] message:') _log.info(message) _log.info('\n\n[dry_run] patch:') _log.info(patch) return branch_name = 'chromium-export-{sha}'.format(sha=outbound_commit.short_sha) self.local_wpt.create_branch_with_patch(branch_name, message, patch, author) response_data = self.wpt_github.create_pr( remote_branch_name=branch_name, desc_title=outbound_commit.subject(), body=outbound_commit.body()) _log.info('Create PR response: %s', response_data) if response_data: data, status_code = self.wpt_github.add_label(response_data['number']) _log.info('Add label response (status %s): %s', status_code, data) return response_data def create_or_update_pull_request_from_cl(self, cl, pull_request=None): patch = cl.get_patch() updating = bool(pull_request) action_str = 'updating' if updating else 'creating' if self.local_wpt.test_patch(patch) == '': _log.error('Gerrit CL patch did not apply cleanly.') _log.error('First 500 characters of patch: %s', patch[0:500]) return if self.dry_run: _log.info('[dry_run] Stopping before %s PR from CL', action_str) _log.info('\n\n[dry_run] subject:') _log.info(cl.subject) _log.debug('\n\n[dry_run] patch[0:500]:') _log.debug(patch[0:500]) return message = cl.latest_commit_message_with_footers() # Annotate revision footer for Exporter's later use. message = '\n'.join([line for line in message.split('\n') if WPT_REVISION_FOOTER not in line]) message += '\n{} {}'.format(WPT_REVISION_FOOTER, cl.current_revision_sha) branch_name = 'chromium-export-cl-{id}'.format(id=cl.change_id) self.local_wpt.create_branch_with_patch(branch_name, message, patch, cl.owner_email, force_push=True) if updating: response_data = self.wpt_github.update_pr(pull_request.number, cl.subject, message) _log.debug('Update PR response: %s', response_data) # TODO(jeffcarp): Turn PullRequest into a class with a .url method cl.post_comment(( 'Successfully updated WPT GitHub pull request with ' 'new revision "{subject}": {pr_url}' ).format( subject=cl.current_revision_description, pr_url='%spull/%d' % (WPT_GH_URL, pull_request.number), )) else: response_data = self.wpt_github.create_pr(branch_name, cl.subject, message) _log.debug('Create PR response: %s', response_data) data, status_code = self.wpt_github.add_label(response_data['number']) _log.info('Add label response (status %s): %s', status_code, data) cl.post_comment(( 'Exportable changes to web-platform-tests were detected in this CL ' 'and a pull request in the upstream repo has been made: {pr_url}.\n\n' 'Travis CI has been kicked off and if it fails, we will let you know here. ' 'If this CL lands and Travis CI is green, we will auto-merge the PR.' ).format( pr_url='%spull/%d' % (WPT_GH_URL, response_data['number']) )) return response_data def corresponding_pull_request_for_commit(self, exportable_commit): """Search pull requests for one that corresponds to exportable_commit. Returns the pull_request if found, else returns None. """ # Check for PRs created by commits on master. pull_request = self.wpt_github.pr_with_position(exportable_commit.position) if pull_request: return pull_request # Check for PRs created by open Gerrit CLs. change_id = exportable_commit.change_id() if change_id: return self.wpt_github.pr_with_change_id(change_id) return None
class WPTGitHubTest(unittest.TestCase): def generate_pr_item(self, pr_number, state='closed'): return { 'title': 'Foobar', 'number': pr_number, 'body': 'description', 'state': state, 'labels': [{ 'name': EXPORT_PR_LABEL }] } def setUp(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad') def test_init(self): self.assertEqual(self.wpt_github.user, 'rutabaga') self.assertEqual(self.wpt_github.token, 'decafbad') def test_constructor_throws_on_pr_history_window_too_large(self): with self.assertRaises(ValueError): self.wpt_github = WPTGitHub( MockHost(), user='******', token='decafbad', pr_history_window=MAX_PR_HISTORY_WINDOW + 1) def test_auth_token(self): self.assertEqual(self.wpt_github.auth_token(), base64.encodestring('rutabaga:decafbad').strip()) def test_extract_link_next(self): link_header = ( '<https://api.github.com/user/repos?page=1&per_page=100>; rel="first", ' '<https://api.github.com/user/repos?page=2&per_page=100>; rel="prev", ' '<https://api.github.com/user/repos?page=4&per_page=100>; rel="next", ' '<https://api.github.com/user/repos?page=50&per_page=100>; rel="last"' ) self.assertEqual(self.wpt_github.extract_link_next(link_header), '/user/repos?page=4&per_page=100') def test_extract_link_next_not_found(self): self.assertIsNone(self.wpt_github.extract_link_next('')) def test_all_pull_requests_single_page(self): self.wpt_github.host.web.responses = [ { 'status_code': 200, 'headers': { 'Link': '' }, 'body': json.dumps({ 'incomplete_results': False, 'items': [self.generate_pr_item(1)] }) }, ] self.assertEqual(len(self.wpt_github.all_pull_requests()), 1) def test_all_pull_requests_all_pages(self): self.wpt_github.host.web.responses = [ { 'status_code': 200, 'headers': { 'Link': '<https://api.github.com/resources?page=2>; rel="next"' }, 'body': json.dumps({ 'incomplete_results': False, 'items': [self.generate_pr_item(1)] }) }, { 'status_code': 200, 'headers': { 'Link': '' }, 'body': json.dumps({ 'incomplete_results': False, 'items': [self.generate_pr_item(2)] }) }, ] self.assertEqual(len(self.wpt_github.all_pull_requests()), 2) def test_all_pull_requests_reaches_pr_history_window(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad', pr_history_window=2) self.wpt_github.host.web.responses = [ { 'status_code': 200, 'headers': { 'Link': '<https://api.github.com/resources?page=2>; rel="next"' }, 'body': json.dumps({ 'incomplete_results': False, 'items': [self.generate_pr_item(1)] }) }, { 'status_code': 200, 'headers': { 'Link': '' }, 'body': json.dumps({ 'incomplete_results': False, 'items': [self.generate_pr_item(2), self.generate_pr_item(3)] }) }, ] self.assertEqual(len(self.wpt_github.all_pull_requests()), 2) def test_all_pull_requests_throws_github_error_on_non_200(self): self.wpt_github.host.web.responses = [ { 'status_code': 204 }, ] with self.assertRaises(GitHubError): self.wpt_github.all_pull_requests() def test_all_pull_requests_throws_github_error_when_incomplete(self): self.wpt_github.host.web.responses = [ { 'status_code': 200, 'body': json.dumps({ 'incomplete_results': True, 'items': [self.generate_pr_item(1)] }) }, ] with self.assertRaises(GitHubError): self.wpt_github.all_pull_requests() def test_create_pr_success(self): self.wpt_github.host.web.responses = [ { 'status_code': 201, 'body': json.dumps({'number': 1234}) }, ] self.assertEqual(self.wpt_github.create_pr('branch', 'title', 'body'), 1234) def test_create_pr_throws_github_error_on_non_201(self): self.wpt_github.host.web.responses = [ { 'status_code': 200 }, ] with self.assertRaises(GitHubError): self.wpt_github.create_pr('branch', 'title', 'body') def test_get_pr_branch(self): self.wpt_github.host.web.responses = [ { 'status_code': 200, 'body': json.dumps({'head': { 'ref': 'fake_branch' }}) }, ] self.assertEqual(self.wpt_github.get_pr_branch(1234), 'fake_branch') def test_is_pr_merged_receives_204(self): self.wpt_github.host.web.responses = [ { 'status_code': 204 }, ] self.assertTrue(self.wpt_github.is_pr_merged(1234)) def test_is_pr_merged_receives_404(self): self.wpt_github.host.web.responses = [ { 'status_code': 404 }, ] self.assertFalse(self.wpt_github.is_pr_merged(1234)) def test_merge_pr_success(self): self.wpt_github.host.web.responses = [ { 'status_code': 200 }, ] self.wpt_github.merge_pr(1234) def test_merge_pr_throws_merge_error_on_405(self): self.wpt_github.host.web.responses = [ { 'status_code': 405 }, ] with self.assertRaises(MergeError): self.wpt_github.merge_pr(5678) def test_remove_label_throws_github_error_on_non_200_or_204(self): self.wpt_github.host.web.responses = [ { 'status_code': 201 }, ] with self.assertRaises(GitHubError): self.wpt_github.remove_label(1234, 'rutabaga') def test_delete_remote_branch_throws_github_error_on_non_204(self): self.wpt_github.host.web.responses = [ { 'status_code': 200 }, ] with self.assertRaises(GitHubError): self.wpt_github.delete_remote_branch('rutabaga') def test_pr_for_chromium_commit_change_id_only(self): self.wpt_github.all_pull_requests = lambda: [ PullRequest('PR1', 1, 'body\nChange-Id: I00c0ffee', 'open', []), PullRequest('PR2', 2, 'body\nChange-Id: I00decade', 'open', []), ] chromium_commit = MockChromiumCommit( MockHost(), change_id='I00decade', position='refs/heads/master@{#10}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 2) def test_pr_for_chromium_commit_prefers_change_id(self): self.wpt_github.all_pull_requests = lambda: [ PullRequest( 'PR1', 1, 'body\nChange-Id: I00c0ffee\nCr-Commit-Position: refs/heads/master@{#10}', 'open', []), PullRequest( 'PR2', 2, 'body\nChange-Id: I00decade\nCr-Commit-Position: refs/heads/master@{#33}', 'open', []), ] chromium_commit = MockChromiumCommit( MockHost(), change_id='I00decade', position='refs/heads/master@{#10}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 2) def test_pr_for_chromium_commit_falls_back_to_commit_position(self): self.wpt_github.all_pull_requests = lambda: [ PullRequest( 'PR1', 1, 'body\nChange-Id: I00c0ffee\nCr-Commit-Position: refs/heads/master@{#10}', 'open', []), PullRequest( 'PR2', 2, 'body\nChange-Id: I00decade\nCr-Commit-Position: refs/heads/master@{#33}', 'open', []), ] chromium_commit = MockChromiumCommit( MockHost(), position='refs/heads/master@{#10}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 1) def test_pr_for_chromium_commit_multiple_change_ids(self): self.wpt_github.all_pull_requests = lambda: [ PullRequest('PR1', 1, 'body\nChange-Id: I00c0ffee\nChange-Id: I00decade', 'open', []), ] chromium_commit = MockChromiumCommit( MockHost(), change_id='I00c0ffee', position='refs/heads/master@{#10}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 1) chromium_commit = MockChromiumCommit( MockHost(), change_id='I00decade', position='refs/heads/master@{#33}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 1) def test_pr_for_chromium_commit_multiple_commit_positions(self): self.wpt_github.all_pull_requests = lambda: [ PullRequest( 'PR1', 1, 'body\nCr-Commit-Position: refs/heads/master@{#10}\n' 'Cr-Commit-Position: refs/heads/master@{#33}', 'open', []), ] chromium_commit = MockChromiumCommit( MockHost(), position='refs/heads/master@{#10}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 1) chromium_commit = MockChromiumCommit( MockHost(), position='refs/heads/master@{#33}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 1)
def test_init(self): wpt_github = WPTGitHub(MockHost(), user='******', token='deadbeefcafe') self.assertEqual(wpt_github.user, 'rutabaga') self.assertEqual(wpt_github.token, 'deadbeefcafe')
def test_auth_token(self): wpt_github = wpt_github = WPTGitHub(MockHost(), user='******', token='deadbeefcafe') self.assertEqual( wpt_github.auth_token(), base64.encodestring('rutabaga:deadbeefcafe').strip())
class WPTGitHubTest(unittest.TestCase): def setUp(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad') def test_init(self): self.assertEqual(self.wpt_github.user, 'rutabaga') self.assertEqual(self.wpt_github.token, 'decafbad') def test_auth_token(self): self.assertEqual( self.wpt_github.auth_token(), base64.encodestring('rutabaga:decafbad').strip()) def test_merge_pull_request_throws_merge_error_on_405(self): self.wpt_github.host.web.responses = [ {'status_code': 200}, {'status_code': 405}, ] self.wpt_github.merge_pull_request(1234) with self.assertRaises(MergeError): self.wpt_github.merge_pull_request(5678) def test_remove_label_throws_github_error_on_non_200_or_204(self): self.wpt_github.host.web.responses = [ {'status_code': 201}, ] with self.assertRaises(GitHubError): self.wpt_github.remove_label(1234, 'rutabaga') def test_delete_remote_branch_throws_github_error_on_non_204(self): self.wpt_github.host.web.responses = [ {'status_code': 200}, ] with self.assertRaises(GitHubError): self.wpt_github.delete_remote_branch('rutabaga') def test_pr_for_chromium_commit_prefers_change_id(self): self.wpt_github.all_pull_requests = lambda: [ PullRequest('PR1', 1, 'body\nChange-Id: I00c0ffee\nCr-Commit-Position: refs/heads/master@{#10}', 'open', []), PullRequest('PR2', 2, 'body\nChange-Id: I00decade\nCr-Commit-Position: refs/heads/master@{#33}', 'open', []), ] chromium_commit = MockChromiumCommit( MockHost(), change_id='I00decade', position='refs/heads/master@{#10}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 2) def test_pr_for_chromium_commit_falls_back_to_commit_position(self): self.wpt_github.all_pull_requests = lambda: [ PullRequest('PR1', 1, 'body\nChange-Id: I00c0ffee\nCr-Commit-Position: refs/heads/master@{#10}', 'open', []), PullRequest('PR2', 2, 'body\nChange-Id: I00decade\nCr-Commit-Position: refs/heads/master@{#33}', 'open', []), ] chromium_commit = MockChromiumCommit( MockHost(), position='refs/heads/master@{#10}') pull_request = self.wpt_github.pr_for_chromium_commit(chromium_commit) self.assertEqual(pull_request.number, 1)
class TestExporter(object): def __init__(self, host, gh_user, gh_token, gerrit_user, gerrit_token, dry_run=False): self.host = host self.wpt_github = WPTGitHub(host, gh_user, gh_token) self.gerrit = GerritAPI(self.host, gerrit_user, gerrit_token) self.dry_run = dry_run self.local_wpt = LocalWPT(self.host, gh_token) self.local_wpt.fetch() def run(self): """For last n commits on Chromium master, create or try to merge a PR. The exporter will look in chronological order at every commit in Chromium. """ open_gerrit_cls = self.gerrit.query_exportable_open_cls() self.process_gerrit_cls(open_gerrit_cls) exportable_commits = self.get_exportable_commits() for exportable_commit in exportable_commits: pull_request = self.wpt_github.pr_for_chromium_commit( exportable_commit) if pull_request: if pull_request.state == 'open': self.merge_pull_request(pull_request) else: _log.info('Pull request is not open: #%d %s', pull_request.number, pull_request.title) else: self.create_pull_request(exportable_commit) def process_gerrit_cls(self, gerrit_cls): """Creates or updates PRs for Gerrit CLs.""" for cl in gerrit_cls: _log.info('Found Gerrit in-flight CL: "%s" %s', cl.subject, cl.url) if not cl.has_review_started: _log.info('CL review has not started, skipping.') continue # Check if CL already has a corresponding PR pull_request = self.wpt_github.pr_with_change_id(cl.change_id) if pull_request: pr_url = '{}pull/{}'.format(WPT_GH_URL, pull_request.number) _log.info('In-flight PR found: %s', pr_url) pr_cl_revision = self.wpt_github.extract_metadata( WPT_REVISION_FOOTER + ' ', pull_request.body) if cl.current_revision_sha == pr_cl_revision: _log.info( 'PR revision matches CL revision. Nothing to do here.') continue _log.info('New revision found, updating PR...') self.create_or_update_pull_request_from_cl(cl, pull_request) else: _log.info('No in-flight PR found for CL. Creating...') self.create_or_update_pull_request_from_cl(cl) def get_exportable_commits(self): return exportable_commits_over_last_n_commits(self.host, self.local_wpt, self.wpt_github) def merge_pull_request(self, pull_request): _log.info('In-flight PR found: %s', pull_request.title) _log.info('%spull/%d', WPT_GH_URL, pull_request.number) if self.dry_run: _log.info('[dry_run] Would have attempted to merge PR') return if PROVISIONAL_PR_LABEL in pull_request.labels: _log.info('Removing provisional label "%s"...', PROVISIONAL_PR_LABEL) self.wpt_github.remove_label(pull_request.number, PROVISIONAL_PR_LABEL) _log.info('Attempting to merge...') # This is outside of the try block because if there's a problem communicating # with the GitHub API, we should hard fail. branch = self.wpt_github.get_pr_branch(pull_request.number) try: self.wpt_github.merge_pull_request(pull_request.number) # This is in the try block because if a PR can't be merged, we shouldn't # delete its branch. self.wpt_github.delete_remote_branch(branch) change_id = self.wpt_github.extract_metadata( 'Change-Id: ', pull_request.body) if change_id: cl = GerritCL(data={'change_id': change_id}, api=self.gerrit) pr_url = '{}pull/{}'.format(WPT_GH_URL, pull_request.number) cl.post_comment(( 'The WPT PR for this CL has been merged upstream! {pr_url}' ).format(pr_url=pr_url)) except MergeError: _log.info('Could not merge PR.') def export_first_exportable_commit(self): """Looks for exportable commits in Chromium, creates PR if found.""" wpt_commit, chromium_commit = self.local_wpt.most_recent_chromium_commit( ) assert chromium_commit, 'No Chromium commit found, this is impossible' wpt_behind_master = self.local_wpt.commits_behind_master(wpt_commit) _log.info('\nLast Chromium export commit in web-platform-tests:') _log.info('web-platform-tests@%s', wpt_commit) _log.info('(%d behind web-platform-tests@origin/master)', wpt_behind_master) _log.info( '\nThe above WPT commit points to the following Chromium commit:') _log.info('chromium@%s', chromium_commit.sha) _log.info('(%d behind chromium@origin/master)', chromium_commit.num_behind_master()) exportable_commits = exportable_commits_over_last_n_commits( chromium_commit.sha, self.host, self.local_wpt, self.wpt_github) if not exportable_commits: _log.info('No exportable commits found in Chromium, stopping.') return _log.info('Found %d exportable commits in Chromium:', len(exportable_commits)) for commit in exportable_commits: _log.info('- %s %s', commit, commit.subject()) outbound_commit = exportable_commits[0] _log.info('Picking the earliest commit and creating a PR') _log.info('- %s %s', outbound_commit.sha, outbound_commit.subject()) self.create_pull_request(outbound_commit) def create_pull_request(self, outbound_commit): patch = outbound_commit.format_patch() message = outbound_commit.message() author = outbound_commit.author() if self.dry_run: _log.info('[dry_run] Stopping before creating PR') _log.info('\n\n[dry_run] message:') _log.info(message) _log.info('\n\n[dry_run] patch:') _log.info(patch) return branch_name = 'chromium-export-{sha}'.format( sha=outbound_commit.short_sha) self.local_wpt.create_branch_with_patch(branch_name, message, patch, author) response_data = self.wpt_github.create_pr( remote_branch_name=branch_name, desc_title=outbound_commit.subject(), body=outbound_commit.body()) _log.info('Create PR response: %s', response_data) if response_data: data, status_code = self.wpt_github.add_label( response_data['number'], EXPORT_PR_LABEL) _log.info('Add label response (status %s): %s', status_code, data) return response_data def create_or_update_pull_request_from_cl(self, cl, pull_request=None): patch = cl.get_patch() updating = bool(pull_request) action_str = 'updating' if updating else 'creating' if self.local_wpt.test_patch(patch) == '': _log.error('Gerrit CL patch did not apply cleanly.') _log.error('First 500 characters of patch: %s', patch[0:500]) return if self.dry_run: _log.info('[dry_run] Stopping before %s PR from CL', action_str) _log.info('\n\n[dry_run] subject:') _log.info(cl.subject) _log.debug('\n\n[dry_run] patch[0:500]:') _log.debug(patch[0:500]) return message = cl.latest_commit_message_with_footers() # Annotate revision footer for Exporter's later use. message = '\n'.join([ line for line in message.split('\n') if WPT_REVISION_FOOTER not in line ]) message += '\n{} {}'.format(WPT_REVISION_FOOTER, cl.current_revision_sha) branch_name = 'chromium-export-cl-{id}'.format(id=cl.change_id) self.local_wpt.create_branch_with_patch(branch_name, message, patch, cl.owner_email, force_push=True) if updating: response_data = self.wpt_github.update_pr(pull_request.number, cl.subject, message) _log.debug('Update PR response: %s', response_data) # TODO(jeffcarp): Turn PullRequest into a class with a .url method cl.post_comment( ('Successfully updated WPT GitHub pull request with ' 'new revision "{subject}": {pr_url}').format( subject=cl.current_revision_description, pr_url='%spull/%d' % (WPT_GH_URL, pull_request.number), )) else: response_data = self.wpt_github.create_pr(branch_name, cl.subject, message) _log.debug('Create PR response: %s', response_data) self.wpt_github.add_label(response_data['number'], EXPORT_PR_LABEL) self.wpt_github.add_label(response_data['number'], PROVISIONAL_PR_LABEL) cl.post_comment(( 'Exportable changes to web-platform-tests were detected in this CL ' 'and a pull request in the upstream repo has been made: {pr_url}.\n\n' 'If this CL lands and Travis CI upstream is green, we will auto-merge the PR.\n\n' 'Note: Please check the Travis CI status (at the bottom of the PR) ' 'before landing this CL and only land this CL if the status is green. ' 'Otherwise a human needs to step in and resolve it manually, during which time ' 'the WPT Importer is blocked from operating.\n\n' '(There is ongoing work to 1. prevent CLs with red upstream PRs from landing ' '(https://crbug.com/711447) and 2. prevent the importer from being blocked on ' 'stuck exportable changes (https://crbug.com/734121))\n\n' 'WPT Export docs:\n' 'https://chromium.googlesource.com/chromium/src/+/master' '/docs/testing/web_platform_tests.md#Automatic-export-process' ).format(pr_url='%spull/%d' % (WPT_GH_URL, response_data['number']))) return response_data
def test_requires_gh_user_env_var(self): self.host.environ['GH_USER'] = '******' self.assertRaises(AssertionError, lambda: WPTGitHub(self.host))
def test_requires_gh_token_env_var(self): self.host.environ['GH_TOKEN'] = 'deadbeefcafe' self.assertRaises(AssertionError, lambda: WPTGitHub(self.host))
def main(self, argv=None): # TODO(robertma): Test this method! Split it to make it easier to test # if necessary. options = self.parse_args(argv) self.verbose = options.verbose log_level = logging.DEBUG if self.verbose else logging.INFO configure_logging(logging_level=log_level, include_time=True) if options.verbose: # Print out the full output when executive.run_command fails. self.host.executive.error_output_limit = None if not self.checkout_is_okay(): return 1 credentials = read_credentials(self.host, options.credentials_json) gh_user = credentials.get('GH_USER') gh_token = credentials.get('GH_TOKEN') self.wpt_github = self.wpt_github or WPTGitHub(self.host, gh_user, gh_token) self.git_cl = GitCL( self.host, auth_refresh_token_json=options.auth_refresh_token_json) _log.debug('Noting the current Chromium revision.') chromium_revision = self.chromium_git.latest_git_commit() # Instantiate Git after local_wpt.fetch() to make sure the path exists. local_wpt = LocalWPT(self.host, gh_token=gh_token) local_wpt.fetch() self.wpt_git = self.host.git(local_wpt.path) if options.revision is not None: _log.info('Checking out %s', options.revision) self.wpt_git.run(['checkout', options.revision]) _log.debug('Noting the revision we are importing.') self.wpt_revision = self.wpt_git.latest_git_commit() self.last_wpt_revision = self._get_last_imported_wpt_revision() import_commit = 'wpt@%s' % self.wpt_revision _log.info('Importing %s to Chromium %s', import_commit, chromium_revision) if options.ignore_exportable_commits: commit_message = self._commit_message(chromium_revision, import_commit) else: commits = self.apply_exportable_commits_locally(local_wpt) if commits is None: _log.error('Could not apply some exportable commits cleanly.') _log.error('Aborting import to prevent clobbering commits.') return 1 commit_message = self._commit_message( chromium_revision, import_commit, locally_applied_commits=commits) self._clear_out_dest_path() _log.info('Copying the tests from the temp repo to the destination.') test_copier = TestCopier(self.host, local_wpt.path) test_copier.do_import() # TODO(robertma): Implement `add --all` in Git (it is different from `commit --all`). self.chromium_git.run(['add', '--all', self.dest_path]) self._generate_manifest() self._delete_orphaned_baselines() # TODO(qyearsley): Consider running the imported tests with # `run-webkit-tests --reset-results external/wpt` to get some baselines # before the try jobs are started. _log.info( 'Updating TestExpectations for any removed or renamed tests.') self.update_all_test_expectations_files(self._list_deleted_tests(), self._list_renamed_tests()) if not self.chromium_git.has_working_directory_changes(): _log.info('Done: no changes to import.') return 0 if self._only_wpt_manifest_changed(): _log.info( 'Only WPT_BASE_MANIFEST.json was updated; skipping the import.' ) return 0 self._commit_changes(commit_message) _log.info('Changes imported and committed.') if not options.auto_update: return 0 self._upload_cl() _log.info('Issue: %s', self.git_cl.run(['issue']).strip()) if not self.update_expectations_for_cl(): return 1 if not self.run_commit_queue_for_cl(): return 1 if not self.send_notifications(local_wpt, options.auto_file_bugs, options.monorail_auth_json): return 1 return 0
def main(self, argv=None): options = self.parse_args(argv) self.verbose = options.verbose log_level = logging.DEBUG if self.verbose else logging.INFO logging.basicConfig(level=log_level, format='%(message)s') if not self.checkout_is_okay(): return 1 credentials = read_credentials(self.host, options.credentials_json) gh_user = credentials.get('GH_USER') gh_token = credentials.get('GH_TOKEN') self.wpt_github = self.wpt_github or WPTGitHub(self.host, gh_user, gh_token) local_wpt = LocalWPT(self.host, gh_token=gh_token) self.git_cl = GitCL( self.host, auth_refresh_token_json=options.auth_refresh_token_json) _log.debug('Noting the current Chromium commit.') # TODO(qyearsley): Use Git (self.host.git) to run git commands. _, show_ref_output = self.run(['git', 'show-ref', 'HEAD']) chromium_commit = show_ref_output.split()[0] local_wpt.fetch() if options.revision is not None: _log.info('Checking out %s', options.revision) self.run(['git', 'checkout', options.revision], cwd=local_wpt.path) _log.info('Noting the revision we are importing.') _, show_ref_output = self.run(['git', 'show-ref', 'origin/master'], cwd=local_wpt.path) import_commit = 'wpt@%s' % show_ref_output.split()[0] commit_message = self._commit_message(chromium_commit, import_commit) if not options.ignore_exportable_commits: commits = self.apply_exportable_commits_locally(local_wpt) if commits is None: _log.error('Could not apply some exportable commits cleanly.') _log.error('Aborting import to prevent clobbering commits.') return 1 commit_message = self._commit_message( chromium_commit, import_commit, locally_applied_commits=commits) dest_path = self.finder.path_from_layout_tests('external', 'wpt') self._clear_out_dest_path(dest_path) _log.info('Copying the tests from the temp repo to the destination.') test_copier = TestCopier(self.host, local_wpt.path) test_copier.do_import() self.run(['git', 'add', '--all', 'external/wpt']) self._delete_orphaned_baselines(dest_path) # TODO(qyearsley): Consider updating manifest after adding baselines. self._generate_manifest(dest_path) # TODO(qyearsley): Consider running the imported tests with # `run-webkit-tests --reset-results external/wpt` to get some baselines # before the try jobs are started. _log.info( 'Updating TestExpectations for any removed or renamed tests.') self.update_all_test_expectations_files(self._list_deleted_tests(), self._list_renamed_tests()) has_changes = self._has_changes() if not has_changes: _log.info('Done: no changes to import.') return 0 self.run(['git', 'commit', '--all', '-F', '-'], stdin=commit_message) self._commit_changes(commit_message) _log.info('Changes imported and committed.') if not options.auto_update: return 0 self._upload_cl() _log.info('Issue: %s', self.git_cl.run(['issue']).strip()) if not self.update_expectations_for_cl(): return 1 if not self.run_commit_queue_for_cl(): return 1 return 0
def setUp(self): self.wpt_github = WPTGitHub(MockHost(), user='******', token='decafbad')
class TestExporter(object): def __init__(self, host, gh_user, gh_token, dry_run=False): self.host = host self.wpt_github = WPTGitHub(host, gh_user, gh_token) self.dry_run = dry_run self.local_wpt = LocalWPT(self.host, gh_token) self.local_wpt.fetch() def run(self): """Query in-flight pull requests, then merge PR or create one. This script assumes it will be run on a regular interval. On each invocation, it will either attempt to merge or attempt to create a PR, never both. """ pull_requests = self.wpt_github.in_flight_pull_requests() if len(pull_requests) == 1: self.merge_in_flight_pull_request(pull_requests.pop()) elif len(pull_requests) > 1: _log.error(pull_requests) # TODO(jeffcarp): Print links to PRs raise Exception('More than two in-flight PRs!') else: self.export_first_exportable_commit() def merge_in_flight_pull_request(self, pull_request): """Attempt to merge an in-flight PR. Args: pull_request: a PR object returned from the GitHub API. """ _log.info('In-flight PR found: #%d', pull_request['number']) _log.info(pull_request['title']) # TODO(jeffcarp): Check the PR status here (for Travis CI, etc.) if self.dry_run: _log.info('[dry_run] Would have attempted to merge PR') return _log.info('Merging...') self.wpt_github.merge_pull_request(pull_request['number']) _log.info('PR merged! Deleting branch.') self.wpt_github.delete_remote_branch('chromium-export-try') _log.info('Branch deleted!') def export_first_exportable_commit(self): """Looks for exportable commits in Chromium, creates PR if found.""" wpt_commit, chromium_commit = self.local_wpt.most_recent_chromium_commit( ) assert chromium_commit, 'No Chromium commit found, this is impossible' wpt_behind_master = self.local_wpt.commits_behind_master(wpt_commit) _log.info('\nLast Chromium export commit in web-platform-tests:') _log.info('web-platform-tests@%s', wpt_commit) _log.info('(%d behind web-platform-tests@origin/master)', wpt_behind_master) _log.info( '\nThe above WPT commit points to the following Chromium commit:') _log.info('chromium@%s', chromium_commit.sha) _log.info('(%d behind chromium@origin/master)', chromium_commit.num_behind_master()) exportable_commits = exportable_commits_since(chromium_commit.sha, self.host, self.local_wpt) if not exportable_commits: _log.info('No exportable commits found in Chromium, stopping.') return _log.info('Found %d exportable commits in Chromium:', len(exportable_commits)) for commit in exportable_commits: _log.info('- %s %s', commit, commit.subject()) outbound_commit = exportable_commits[0] _log.info('Picking the earliest commit and creating a PR') _log.info('- %s %s', outbound_commit.sha, outbound_commit.subject()) patch = outbound_commit.format_patch() message = outbound_commit.message() author = outbound_commit.author() if self.dry_run: _log.info('[dry_run] Stopping before creating PR') _log.info('\n\n[dry_run] message:') _log.info(message) _log.info('\n\n[dry_run] patch:') _log.info(patch) return remote_branch_name = self.local_wpt.create_branch_with_patch( message, patch, author) response_data = self.wpt_github.create_pr( remote_branch_name=remote_branch_name, desc_title=outbound_commit.subject(), body=outbound_commit.body()) _log.info('Create PR response: %s', response_data) if response_data: data, status_code = self.wpt_github.add_label( response_data['number']) _log.info('Add label response (status %s): %s', status_code, data)