Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(835)

Unified Diff: scripts/slave/recipe_modules/recipe_autoroller/api.py

Issue 2214303003: Revert of Removing old recipe code (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/build.git@master
Patch Set: Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
new file mode 100644
index 0000000000000000000000000000000000000000..2c00720c5df1d9504dabcdc08c22f1fc377368ba
--- /dev/null
+++ b/scripts/slave/recipe_modules/recipe_autoroller/api.py
@@ -0,0 +1,288 @@
+# 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)
« no previous file with comments | « scripts/slave/recipe_modules/recipe_autoroller/__init__.py ('k') | scripts/slave/recipe_modules/recipe_autoroller/test_api.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698