| # Copyright 2020 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from urlparse import urlparse |
| |
| from recipe_engine import post_process |
| |
| DEPS = [ |
| 'fuchsia/gerrit', |
| 'fuchsia/gitiles', |
| 'fuchsia/status_check', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/json', |
| 'recipe_engine/properties', |
| 'recipe_engine/step', |
| ] |
| |
| # The value of this footer should be an https git repo URL. |
| ROLLED_REPO_FOOTER = 'Rolled-Repo' |
| |
| # This footer should have values of the form "<start_commit>..<end_commit>" |
| # (where <start_commit> is the old pinned revision, rather than the first |
| # rolled commit). For example, "9c21da0..5051fd4" includes all the commits |
| # after 9c21da0, up to and including 5051fd4. |
| ROLLED_COMMITS_FOOTER = 'Rolled-Commits' |
| |
| REVIEWED_ON_FOOTER = 'Reviewed-on' |
| CHANGE_ID_FOOTER = 'Change-Id' |
| |
| |
| def read_footer(commit_msg, footer_name): |
| footer_line_prefix = '%s: ' % footer_name |
| for line in reversed(commit_msg.splitlines()): |
| if line.startswith(footer_line_prefix): |
| return line[len(footer_line_prefix):].strip() |
| return None |
| |
| |
| def gitiles_to_gerrit_host(gitiles_url): |
| gitiles_host_parts = urlparse(gitiles_url).netloc.split('.') |
| gerrit_hostname = '.'.join([gitiles_host_parts[0] + '-review'] + |
| gitiles_host_parts[1:]) |
| return 'https://%s' % gerrit_hostname |
| |
| |
| def RunSteps(api): |
| commit = api.buildbucket.build.input.gitiles_commit |
| assert commit.host and commit.project and commit.id, ( |
| 'recipe must be triggered by a gitiles commit') |
| |
| dest_repo = 'https://%s/%s' % (commit.host, commit.project) |
| dest_log = api.gitiles.log( |
| dest_repo, commit.id, limit=1, step_name='get commit message') |
| commit_msg = dest_log[0]['message'] |
| |
| source_repo = read_footer(commit_msg, ROLLED_REPO_FOOTER) |
| if not source_repo: |
| api.step('no "%s" footer in commit message' % ROLLED_REPO_FOOTER, None) |
| return |
| |
| commit_range = read_footer(commit_msg, ROLLED_COMMITS_FOOTER) |
| assert commit_range, 'found "%s" footer but no "%s" footer' % ( |
| ROLLED_REPO_FOOTER, ROLLED_COMMITS_FOOTER) |
| |
| source_log = api.gitiles.log( |
| source_repo, commit_range, step_name='log source repo') |
| |
| api.gerrit.host = gitiles_to_gerrit_host(source_repo) |
| roll_cl_url = 'http://go/roll-cl/%s' % commit.id |
| |
| # Defer results because comment failures shouldn't prevent the roller from |
| # commenting on the rest of the changes. |
| with api.step.nest('comment successful roll'), api.step.defer_results(): |
| for rolled_commit in source_log: |
| revision = rolled_commit['id'] |
| commit_msg = rolled_commit['message'] |
| change_id = read_footer(commit_msg, CHANGE_ID_FOOTER) |
| reviewed_on = read_footer(commit_msg, REVIEWED_ON_FOOTER) |
| |
| # TODO(47289): If we make the error output of the gerrit CLI more |
| # structured and informative, we can remove this footer-checking logic. |
| # We can instead always attempt to comment on the CL, and handle a |
| # failure differently based on the cause as reported by the output. |
| skip_reason = None |
| if not change_id: |
| # If no Change-Id is in the message then commenting will fail, so don't |
| # bother trying to comment. We still pass revision hash into |
| # gerrit.set_review() because Change-Ids are sometimes copied when |
| # cherry-picking and are less reliable than a revision hash at uniquely |
| # identifying a CL. |
| skip_reason = 'no %s' % CHANGE_ID_FOOTER |
| elif reviewed_on: |
| # If this is a Gerrit commit but it was reviewed on a different host, |
| # skip it. This applies to repos that are mirrors of other |
| # Gerrit-managed repos, such as Go. It's not a perfect solution since |
| # not all Gerrit hosts add the Reviewed-on footer, but it's a decent |
| # heuristic for skipping some comments that would fail. |
| if urlparse(reviewed_on).netloc != urlparse(api.gerrit.host).netloc: |
| skip_reason = 'reviewed on different gerrit host' |
| |
| if skip_reason: |
| step = api.step('skipping %s' % revision, None).get_result() |
| step.presentation.step_text = skip_reason |
| continue |
| |
| api.gerrit.set_review( |
| 'comment on %s' % revision, |
| revision, |
| message='Change has been successfully rolled: %s' % roll_cl_url, |
| test_data=api.json.test_api.output({}), |
| ) |
| cl_link = '%s/q/%s' % (api.gerrit.host, revision) |
| api.step.active_result.presentation.links['gerrit link'] = cl_link |
| |
| |
| def GenTests(api): |
| # yapf: disable |
| yield ( |
| api.status_check.test('basic') |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| 'get commit message', |
| 'A', |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: 'https://foo.googlesource.com/bar-project', |
| ROLLED_COMMITS_FOOTER: '1000005..100000a', |
| }) |
| + api.gitiles.log('log source repo', 'B', n=6, add_change_id=True) |
| ) |
| |
| yield ( |
| api.status_check.test('no_change_ids') |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| 'get commit message', |
| 'A', |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: 'https://foo.googlesource.com/bar-project', |
| ROLLED_COMMITS_FOOTER: '1000005..100000a', |
| }) |
| + api.gitiles.log('log source repo', 'B', n=1, add_change_id=False) |
| ) |
| |
| yield ( |
| api.status_check.test('different_gerrit_host') |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| 'get commit message', |
| 'A', |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: 'https://foo.googlesource.com/bar-project', |
| ROLLED_COMMITS_FOOTER: '1000005..100000a', |
| }) |
| + api.gitiles.log( |
| 'log source repo', |
| 'B', |
| n=1, |
| add_change_id=True, |
| extra_footers={ |
| REVIEWED_ON_FOOTER: 'https://go-review.googlesource.com/go', |
| }) |
| ) |
| |
| yield ( |
| api.status_check.test('no_footers') |
| + api.buildbucket.ci_build() |
| + api.gitiles.log('get commit message', 'A', n=1) |
| ) |
| |
| # If we get a Gerrit error commenting on the first CL, we should still try |
| # comment on the second one. |
| yield ( |
| api.status_check.test('failed_comment', status='failure') |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| 'get commit message', |
| 'A', |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: 'https://foo.googlesource.com/bar-project', |
| ROLLED_COMMITS_FOOTER: '100000a..100000b', |
| }) |
| + api.step_data( |
| 'log source repo', |
| api.json.output([ |
| {'id': '100000a', 'message': 'Change-Id: Iabc'}, |
| {'id': '100000b', 'message': 'Change-Id: Ixyz'}, |
| ])) |
| + api.step_data('comment successful roll.comment on 100000a', retcode=1) |
| + api.post_process( |
| post_process.MustRun, 'comment successful roll.comment on 100000b') |
| ) |
| # yapf: enable |