blob: 8dbb32813b12e9cac11cfa21869afd83e78ac6df [file] [log] [blame]
# 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 future.moves.urllib.parse import urlparse
import datetime
from recipe_engine import post_process
from PB.recipes.fuchsia.roll_comment import InputProperties
PYTHON_VERSION_COMPATIBILITY = "PY3"
DEPS = [
"fuchsia/buildbucket_util",
"fuchsia/gerrit",
"fuchsia/git",
"fuchsia/gitiles",
"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.empty('no "%s" footer in commit message' % ROLLED_REPO_FOOTER)
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")
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.buildbucket_util.test("basic")
+ default_props
+ 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.buildbucket_util.test("no_reviewed_on")
+ default_props
+ 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")
+ default_props
+ 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("different_gerrit_host")
+ default_props
+ 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.buildbucket_util.test("copybara")
+ default_props
+ 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")
+ default_props
+ 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="failure")
+ api.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(
[
"%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"
)
)