| # 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. |
| |
| import re |
| from urllib.parse import urlparse |
| |
| from recipe_engine import post_process |
| |
| from PB.recipes.fuchsia.roll_comment import InputProperties |
| |
| DEPS = [ |
| "fuchsia/buildbucket_util", |
| "fuchsia/gerrit", |
| "fuchsia/git", |
| "fuchsia/gitiles", |
| "recipe_engine/buildbucket", |
| "recipe_engine/defer", |
| "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 = f"https://{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.empty(f'no "{ROLLED_REPO_FOOTER}" footer in commit message') |
| return |
| |
| commit_range = api.git.read_commit_footer(commit_msg, ROLLED_COMMITS_FOOTER) |
| assert ( |
| commit_range |
| ), f'found "{ROLLED_REPO_FOOTER}" footer but no "{ROLLED_COMMITS_FOOTER}" footer' |
| |
| source_log = api.gitiles.log(source_repo, commit_range, step_name="log source repo") |
| |
| gerrit_host = api.gerrit.host_from_remote_url(source_repo) |
| roll_cl_url = f"http://go/roll-cl/{commit.id}" |
| |
| if props.gerrit_host_allowlist and gerrit_host not in map( |
| api.gerrit.normalize_host, props.gerrit_host_allowlist |
| ): |
| api.step.empty("gerrit host not allow-listed", step_text=gerrit_host) |
| return |
| |
| # 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.defer.context() as defer: |
| 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 = f"no {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 = f"no {REVIEWED_ON_FOOTER} 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" |
| else: |
| m = re.search(r"/c/(.*)/[+]/", reviewed_on) |
| if m: |
| reviewed_on_project = m.group(1) |
| rolled_repo_project = urlparse(source_repo).path.strip("/") |
| if rolled_repo_project != reviewed_on_project: |
| # This may happen if one project's git history is merged |
| # into another's, and we shouldn't comment on all CLs in |
| # the history of the merged project. |
| skip_reason = "reviewed on different gerrit project" |
| else: |
| skip_reason = ( |
| f"invalid {REVIEWED_ON_FOOTER} footer: {reviewed_on!r}" |
| ) |
| |
| if skip_reason: |
| step = api.step(f"skipping {revision}", None) |
| step.presentation.step_text = skip_reason |
| continue |
| |
| change_number = urlparse(reviewed_on).path.split("/")[-1] |
| assert ( |
| change_number.isdigit() |
| ), f"Invalid change number in {REVIEWED_ON_FOOTER} URL: {reviewed_on}" |
| |
| defer( |
| api.gerrit.set_review, |
| f"comment on {revision}", |
| change_number, |
| host=gerrit_host, |
| message=f"Change has been successfully rolled: {roll_cl_url}", |
| test_data=api.json.test_api.output({}), |
| max_attempts=max_attempts, |
| ) |
| |
| |
| def GenTests(api): |
| def properties(**kwargs): |
| defaults = { |
| "max_attempts": 3, |
| "gerrit_host_allowlist": ["https://foo-review.googlesource.com"], |
| } |
| return api.properties(**{**defaults, **kwargs}) |
| |
| remote = "https://foo.googlesource.com/bar-project" |
| gerrit_project = "https://foo-review.googlesource.com/c/bar-project" |
| |
| yield ( |
| api.buildbucket_util.test("basic") |
| + properties() |
| + 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: f"{gerrit_project}/+/12345"}, |
| ) |
| ) |
| |
| yield ( |
| api.buildbucket_util.test("no_reviewed_on") |
| + properties() |
| + 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.buildbucket_util.test("no_change_ids") |
| + properties() |
| + 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.buildbucket_util.test("wrong_reviewed_on") |
| + properties() |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| ROLLED_COMMITS_FOOTER: "1000005..100000a", |
| }, |
| ) |
| + api.step_data( |
| "log source repo", |
| api.json.output( |
| [ |
| { |
| "id": "100000a", |
| "message": "\n".join( |
| [ |
| f"{CHANGE_ID_FOOTER}: Iabc", |
| # Malformed Reviewed-on footer (missing "/c/" |
| # before project name). |
| f"{REVIEWED_ON_FOOTER}: https://foo-review.googlesource.com/foo/+/12345", |
| ] |
| ), |
| }, |
| { |
| "id": "100000b", |
| "message": "\n".join( |
| [ |
| f"{CHANGE_ID_FOOTER}: Ixyz", |
| # Project name matches `Rolled-Repo`, but Gerrit |
| # host does not. |
| f"{REVIEWED_ON_FOOTER}: https://wrong-review.googlesource.com/c/bar-project/+/456", |
| ] |
| ), |
| }, |
| { |
| "id": "100000c", |
| "message": "\n".join( |
| [ |
| f"{CHANGE_ID_FOOTER}: Ilmn", |
| # Gerrit host matches `Rolled-Repo`, but project |
| # name does not. |
| f"{REVIEWED_ON_FOOTER}: https://foo-review.googlesource.com/c/wrong-project/+/789", |
| ] |
| ), |
| }, |
| ] |
| ), |
| ) |
| + api.post_process(post_process.DoesNotRunRE, r"comment on") |
| ) |
| |
| yield ( |
| api.buildbucket_util.test("copybara") |
| + properties() |
| + 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.buildbucket_util.test("no_footers") |
| + properties() |
| + 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.buildbucket_util.test("failed_comment", status="INFRA_FAILURE") |
| + properties(max_attempts=1) |
| + 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( |
| [ |
| f"{CHANGE_ID_FOOTER}: Iabc", |
| f"{REVIEWED_ON_FOOTER}: {gerrit_project}/+/12345", |
| ] |
| ), |
| }, |
| { |
| "id": "100000b", |
| "message": "\n".join( |
| [ |
| f"{CHANGE_ID_FOOTER}: Ixyz", |
| f"{REVIEWED_ON_FOOTER}: {gerrit_project}/+/5678", |
| ] |
| ), |
| }, |
| ] |
| ), |
| ) |
| + api.step_data("comment successful roll.comment on 100000a", retcode=1) |
| + api.post_process( |
| post_process.MustRun, "comment successful roll.comment on 100000b" |
| ) |
| ) |
| |
| yield ( |
| api.buildbucket_util.test("host_not_allowlisted") |
| + properties(gerrit_host_allowlist=["allowlisted-review.googlesource.com"]) |
| + api.gitiles.log( |
| "get commit message", |
| "A", |
| n=1, |
| extra_footers={ |
| ROLLED_REPO_FOOTER: remote, |
| ROLLED_COMMITS_FOOTER: "100000a..100000b", |
| }, |
| ) |
| + api.post_process(post_process.MustRun, "gerrit host not allow-listed") |
| + api.post_process(post_process.DoesNotRun, "comment successful roll") |
| ) |