| Index: scripts/slave/recipe_modules/recipe_autoroller/api.py
|
| diff --git a/scripts/slave/recipe_modules/recipe_autoroller/api.py b/scripts/slave/recipe_modules/recipe_autoroller/api.py
|
| deleted file mode 100644
|
| index 2c00720c5df1d9504dabcdc08c22f1fc377368ba..0000000000000000000000000000000000000000
|
| --- a/scripts/slave/recipe_modules/recipe_autoroller/api.py
|
| +++ /dev/null
|
| @@ -1,288 +0,0 @@
|
| -# Copyright 2016 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -import hashlib
|
| -import json
|
| -import re
|
| -
|
| -from recipe_engine import recipe_api
|
| -
|
| -
|
| -def get_reviewers(commit_infos):
|
| - """Get a set of authors and reviewers from 'recipes.py autoroll' commit infos.
|
| - """
|
| - reviewers = set()
|
| - for project, commits in commit_infos.iteritems():
|
| - for commit in commits:
|
| - reviewers.add(commit['author'])
|
| - for field in ('R', 'TBR'):
|
| - for m in re.findall(
|
| - '^%s=(.*)' % field, commit['message'], re.MULTILINE):
|
| - for s in m.split(','):
|
| - # TODO(martiniss): infer domain for email address somehow?
|
| - parts = s.split('@')
|
| - if len(parts) != 2:
|
| - continue
|
| - # This mirrors a check in depot_tools/third_party/upload.py .
|
| - if '.' not in parts[1]:
|
| - continue
|
| - reviewers.add(s.strip())
|
| - return reviewers
|
| -
|
| -
|
| -def get_bugs(commit_infos):
|
| - """Return a set of bug IDs from 'recipes.py autoroll' commit infos.
|
| - """
|
| - bugs = set()
|
| - for project, commits in commit_infos.iteritems():
|
| - for commit in commits:
|
| - for m in re.findall('^BUG=(.*)', commit['message'], re.MULTILINE):
|
| - for s in m.split(','):
|
| - if s:
|
| - bugs.add(s.strip())
|
| - return bugs
|
| -
|
| -
|
| -def get_blame(commit_infos):
|
| - blame = []
|
| - for project, commits in commit_infos.iteritems():
|
| - blame.append('%s:' % project)
|
| - for commit in commits:
|
| - message = commit['message'].splitlines()
|
| - # TODO(phajdan.jr): truncate long messages.
|
| - message = message[0] if message else 'n/a'
|
| - blame.append(' https://crrev.com/%s %s (%s)' % (
|
| - commit['revision'], message, commit['author']))
|
| - return blame
|
| -
|
| -
|
| -COMMIT_MESSAGE_HEADER = (
|
| -"""
|
| -This is an automated CL created by the recipe roller. This CL rolls recipe
|
| -changes from upstream projects (e.g. depot_tools) into downstream projects
|
| -(e.g. tools/build).
|
| -""")
|
| -
|
| -
|
| -NON_TRIVIAL_MESSAGE = (
|
| -"""
|
| -
|
| -Please review the expectation changes, and LGTM as normal. The recipe roller
|
| -will *NOT* CQ the change itself, so you must commit the change manually.
|
| -"""
|
| -)
|
| -
|
| -COMMIT_MESSAGE_FOOTER = (
|
| -"""
|
| -
|
| -More info is at https://goo.gl/zkKdpD. Use https://goo.gl/noib3a to file a bug
|
| -(or complain)
|
| -
|
| -""")
|
| -
|
| -
|
| -TRIVIAL_ROLL_TBR_EMAILS = (
|
| - 'martiniss@chromium.org',
|
| - 'phajdan.jr@chromium.org',
|
| -)
|
| -
|
| -
|
| -# These are different results of a roll attempt:
|
| -# - success means we have a working non-empty roll
|
| -# - empty means the repo is using latest revision of its dependencies
|
| -# - failure means there are roll candidates but none of them are suitable
|
| -# for an automated roll
|
| -ROLL_SUCCESS, ROLL_EMPTY, ROLL_FAILURE = range(3)
|
| -
|
| -
|
| -def get_commit_message(roll_result):
|
| - """Construct a roll commit message from 'recipes.py autoroll' result.
|
| - """
|
| - message = 'Roll recipe dependencies (%s).\n' % (
|
| - 'trivial' if roll_result['trivial'] else 'nontrivial')
|
| - message += COMMIT_MESSAGE_HEADER
|
| - if not roll_result['trivial']:
|
| - message += NON_TRIVIAL_MESSAGE
|
| - message += COMMIT_MESSAGE_FOOTER
|
| -
|
| - commit_infos = roll_result['picked_roll_details']['commit_infos']
|
| -
|
| - message += '%s\n' % '\n'.join(get_blame(commit_infos))
|
| - message += '\n'
|
| - message += 'R=%s\n' % ','.join(get_reviewers(commit_infos))
|
| - message += 'BUG=%s\n' % ','.join(get_bugs(commit_infos))
|
| - return message
|
| -
|
| -
|
| -class RecipeAutorollerApi(recipe_api.RecipeApi):
|
| - def prepare_checkout(self): #pragma: no cover
|
| - """Creates a default checkout for the recipe autoroller."""
|
| - # Removed, but keep it here so roll succeeds
|
| - # TODO(martiniss): Delete once safe
|
| - pass
|
| -
|
| -
|
| - def roll_projects(self, projects):
|
| - """Attempts to roll each project from the provided list.
|
| -
|
| - If rolling any of the projects leads to failures, other
|
| - projects are not affected.
|
| - """
|
| - project_data = self.m.luci_config.get_projects()
|
| -
|
| - self.m.cipd.install_client()
|
| - with self.m.tempfile.temp_dir('recipes') as recipes_dir:
|
| - self.m.cipd.ensure(recipes_dir, {
|
| - 'infra/recipes-py': 'latest',
|
| - })
|
| -
|
| - results = []
|
| - with recipe_api.defer_results():
|
| - for project in projects:
|
| - with self.m.step.nest(str(project)):
|
| - results.append(self._roll_project(
|
| - project_data[project], recipes_dir))
|
| -
|
| - # We need to unwrap |DeferredResult|s.
|
| - results = [r.get_result() for r in results]
|
| -
|
| - # Failures to roll are OK as long as at least one of the repos is moving
|
| - # forward. For example, with repos with following dependencies:
|
| - #
|
| - # A <- B
|
| - # A, B <- C
|
| - #
|
| - # New commit in A repo will need to get rolled into B first. However,
|
| - # it'd also appear as a candidate for C roll, leading to a failure there.
|
| - if ROLL_FAILURE in results and ROLL_SUCCESS not in results:
|
| - self.m.python.failing_step(
|
| - 'roll result',
|
| - 'manual intervention needed: automated roll attempt failed')
|
| -
|
| - def _roll_project(self, project_data, recipes_dir):
|
| - with self.m.tempfile.temp_dir('roll_%s' % project_data['id']) as workdir:
|
| - self.m.git.checkout(
|
| - project_data['repo_url'], dir_path=workdir, submodules=False)
|
| -
|
| - # Introduce ourselves to git - also needed for git cl upload to work.
|
| - self.m.git(
|
| - 'config', 'user.email', 'recipe-roller@chromium.org', cwd=workdir)
|
| - self.m.git('config', 'user.name', 'recipe-roller', cwd=workdir)
|
| -
|
| - # git cl upload cannot work with detached HEAD, it requires a branch.
|
| - self.m.git('checkout', '-t', '-b', 'roll', 'origin/master', cwd=workdir)
|
| -
|
| - recipes_cfg_path = workdir.join('infra', 'config', 'recipes.cfg')
|
| -
|
| - # Use the recipes bootstrap to checkout coverage.
|
| - roll_step = self.m.step(
|
| - 'roll',
|
| - [recipes_dir.join('recipes.py'), '--use-bootstrap', '--package',
|
| - recipes_cfg_path, 'autoroll', '--output-json', self.m.json.output()])
|
| - roll_result = roll_step.json.output
|
| -
|
| - if roll_result['success']:
|
| - self._process_successful_roll(roll_step, roll_result, workdir)
|
| - return ROLL_SUCCESS
|
| - else:
|
| - if (not roll_result['roll_details'] and
|
| - not roll_result['rejected_candidates_details']):
|
| - roll_step.presentation.step_text += ' (already at latest revisions)'
|
| - return ROLL_EMPTY
|
| - else:
|
| - return ROLL_FAILURE
|
| -
|
| - def _process_successful_roll(self, roll_step, roll_result, workdir):
|
| - roll_step.presentation.logs['blame'] = get_blame(
|
| - roll_result['picked_roll_details']['commit_infos'])
|
| -
|
| - if roll_result['trivial']:
|
| - roll_step.presentation.step_text += ' (trivial)'
|
| - else:
|
| - roll_step.presentation.status = self.m.step.FAILURE
|
| -
|
| - # We use recipes.cfg hashes to uniquely identify changes (which might be
|
| - # rebased).
|
| - cfg_contents = roll_result['picked_roll_details']['spec']
|
| - cfg_digest = hashlib.md5(cfg_contents).hexdigest()
|
| -
|
| - # We use diff hashes to uniquely identify patchsets within a change.
|
| - self.m.git('commit', '-a', '-m', 'roll recipes.cfg', cwd=workdir)
|
| - diff_result = self.m.git(
|
| - 'show', '--format=%b',
|
| - stdout=self.m.raw_io.output(),
|
| - cwd=workdir,
|
| - step_test_data=lambda: self.m.raw_io.test_api.stream_output(
|
| - '-some line\n+some other line\n'))
|
| - diff = diff_result.stdout
|
| - diff_result.presentation.logs['output'] = diff.splitlines()
|
| - diff_digest = hashlib.md5(diff).hexdigest()
|
| -
|
| - # Check if we have uploaded this before.
|
| - need_to_upload = False
|
| - rebase = False
|
| - cat_result = self.m.gsutil.cat(
|
| - 'gs://recipe-roller-cl-uploads/%s' % cfg_digest,
|
| - stdout=self.m.raw_io.output(),
|
| - stderr=self.m.raw_io.output(),
|
| - ok_ret=(0,1))
|
| -
|
| - if cat_result.retcode:
|
| - cat_result.presentation.logs['stderr'] = [
|
| - self.m.step.active_result.stderr]
|
| - assert re.search('No URLs matched', cat_result.stderr), (
|
| - 'gsutil failed in an unexpected way; see stderr log')
|
| - # We have never uploaded this change before.
|
| - need_to_upload = True
|
| -
|
| - if not need_to_upload:
|
| - # We have uploaded before, now let's check the diff hash to see if we
|
| - # have uploaded this patchset before.
|
| - change_data = json.loads(cat_result.stdout)
|
| - if change_data['diff_digest'] != diff_digest:
|
| - self.m.git('cl', 'issue', change_data['issue'], cwd=workdir)
|
| - need_to_upload = True
|
| - rebase = True
|
| - cat_result.presentation.links['Issue %s' % change_data['issue']] = (
|
| - change_data['issue_url'])
|
| -
|
| - if need_to_upload:
|
| - commit_message = (
|
| - 'Rebase' if rebase else get_commit_message(roll_result))
|
| - if roll_result['trivial']:
|
| - # Land immediately.
|
| - upload_args = ['--use-commit-queue']
|
| - if not rebase:
|
| - commit_message += '\nTBR=%s\n' % ','.join(TRIVIAL_ROLL_TBR_EMAILS)
|
| - else:
|
| - upload_args = ['--send-mail', '--cq-dry-run']
|
| - upload_args.extend(['--bypass-hooks', '-f', '-m', commit_message])
|
| - upload_args.extend([
|
| - '--auth-refresh-token-json=/creds/refresh_tokens/recipe-roller'])
|
| - self.m.git('cl', 'upload', *upload_args, name='git cl upload', cwd=workdir)
|
| - issue_result = self.m.git(
|
| - 'cl', 'issue',
|
| - name='git cl issue', stdout=self.m.raw_io.output(),
|
| - cwd=workdir,
|
| - step_test_data=lambda: self.m.raw_io.test_api.stream_output(
|
| - 'Issue number: '
|
| - '123456789 (https://codereview.chromium.org/123456789)'))
|
| -
|
| - m = re.match('Issue number: (\d+) \((\S*)\)', issue_result.stdout.strip())
|
| - if not m:
|
| - self.m.python.failing_step(
|
| - 'git cl upload failed', 'git cl issue output "%s" is not valid' %
|
| - issue_result.stdout.strip())
|
| -
|
| - change_data = {
|
| - 'issue': m.group(1),
|
| - 'issue_url': m.group(2),
|
| - 'diff_digest': diff_digest,
|
| - }
|
| - issue_result.presentation.links['Issue %s' % change_data['issue']] = (
|
| - change_data['issue_url'])
|
| - self.m.gsutil.upload(
|
| - self.m.json.input(change_data),
|
| - 'recipe-roller-cl-uploads',
|
| - cfg_digest)
|
|
|