| # 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 |
| |
| from PB.recipes.fuchsia.roll_comment import InputProperties |
| |
| DEPS = [ |
| "fuchsia/gerrit", |
| "fuchsia/git", |
| "fuchsia/gitiles", |
| "fuchsia/status_check", |
| "recipe_engine/buildbucket", |
| "recipe_engine/json", |
| "recipe_engine/properties", |
| "recipe_engine/step", |
| ] |
| |
| PROPERTIES = InputProperties |
| |
| # 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" |
| |
| # Copybara will add one of these footers to commits copied from Piper or |
| # another Gerrit host where the copy of the commit didn't go through Gerrit. |
| COPYBARA_FOOTERS = ( |
| "GitOrigin-RevId", |
| "PiperOrigin-RevId", |
| ) |
| |
| |
| def RunSteps(api, props): |
| max_attempts = props.max_attempts or 3 |
| 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 = api.git.read_commit_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 = api.git.read_commit_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", |
| max_attempts=3, |
| timeout=30, |
| ) |
| |
| gerrit_host = api.gerrit.host_from_remote_url(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 = api.git.read_commit_footer(commit_msg, CHANGE_ID_FOOTER) |
| reviewed_on = api.git.read_commit_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 retrieve and pass the change number into |
| # gerrit.set_review() because Change-Ids are sometimes copied when |
| # cherry-picking and are less reliable than a change number at uniquely |
| # identifying a CL. |
| skip_reason = "no %s" % CHANGE_ID_FOOTER |
| elif any( |
| api.git.read_commit_footer(commit_msg, footer) |
| for footer in COPYBARA_FOOTERS |
| ): |
| # Skip any commits made by Copybara that bypassed Gerrit. |
| skip_reason = "copybara bypassed gerrit" |
| elif not reviewed_on: |
| # All Fuchsia-owned repos set the Reviewed-on footer for |
| # commits that go through Gerrit. So if a commit doesn't set |
| # the footer then either it came from a non-Fuchsia-owned repo |
| # or it didn't go through Gerrit. In either case we shouldn't |
| # try to add a comment. |
| skip_reason = "no %s footer" % REVIEWED_ON_FOOTER |
| elif urlparse(reviewed_on).netloc != gerrit_host: |
| # 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. |
| 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 |
| |
| change_number = urlparse(reviewed_on).path.split("/")[-1] |
| assert change_number.isdigit(), "Invalid change number in %s URL: %s" % ( |
| REVIEWED_ON_FOOTER, |
| reviewed_on, |
| ) |
| |
| api.gerrit.set_review( |
| "comment on %s" % revision, |
| change_number, |
| host=gerrit_host, |
| message="Change has been successfully rolled: %s" % roll_cl_url, |
| test_data=api.json.test_api.output({}), |
| max_attempts=max_attempts, |
| ) |
| |
| |
| def GenTests(api): |
| default_props = api.properties(max_attempts=3) |
| |
| remote = "https://foo.googlesource.com/bar-project" |
| gerrit_project = "https://foo-review.googlesource.com/c/bar-project" |
| |
| yield ( |
| api.status_check.test("basic") |
| + default_props |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| ROLLED_COMMITS_FOOTER: "1000005..100000a", |
| }, |
| ) |
| + api.gitiles.log( |
| "log source repo", |
| "B", |
| n=6, |
| add_change_id=True, |
| extra_footers={REVIEWED_ON_FOOTER: "%s/+/12345" % gerrit_project}, |
| ) |
| ) |
| |
| yield ( |
| api.status_check.test("no_reviewed_on") |
| + default_props |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| ROLLED_COMMITS_FOOTER: "1000005..100000a", |
| }, |
| ) |
| + api.gitiles.log("log source repo", "B", n=1, add_change_id=True) |
| ) |
| |
| yield ( |
| api.status_check.test("no_change_ids") |
| + default_props |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| 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") |
| + default_props |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| 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("copybara") |
| + default_props |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| ROLLED_COMMITS_FOOTER: "1000005..100000a", |
| }, |
| ) |
| + api.gitiles.log( |
| "log source repo", |
| "B", |
| n=1, |
| add_change_id=True, |
| extra_footers={COPYBARA_FOOTERS[0]: 1234}, |
| ) |
| + api.post_process(post_process.DoesNotRunRE, r"comment on") |
| ) |
| |
| yield ( |
| api.status_check.test("no_footers") |
| + default_props |
| + 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.properties(max_attempts=1) |
| + api.buildbucket.ci_build() |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| ROLLED_COMMITS_FOOTER: "100000a..100000b", |
| }, |
| ) |
| + api.step_data( |
| "log source repo", |
| api.json.output( |
| [ |
| { |
| "id": "100000a", |
| "message": "\n".join( |
| [ |
| "%s: Iabc" % CHANGE_ID_FOOTER, |
| "%s: %s/+/12345" % (REVIEWED_ON_FOOTER, gerrit_project), |
| ] |
| ), |
| }, |
| { |
| "id": "100000b", |
| "message": "\n".join( |
| [ |
| "%s: Ixyz" % CHANGE_ID_FOOTER, |
| "%s: %s/+/5678" % (REVIEWED_ON_FOOTER, gerrit_project), |
| ] |
| ), |
| }, |
| ] |
| ), |
| ) |
| + api.step_data("comment successful roll.comment on 100000a", retcode=1) |
| + api.post_process( |
| post_process.MustRun, "comment successful roll.comment on 100000b" |
| ) |
| ) |