blob: 0e20a2cc04fe8de2fe346346ec792073dc5e1faa [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.
"""Cherry-pick onto an integration.git release branch.
Inputs
- Project: The project to apply cherry-pick to.
- Revision: The revision to cherry-pick.
- Change-Id: The Gerrit change ID to cherry-pick.
Options
- Revert: Revert instead of cherry-picking.
- Strategy-theirs: Cherry-pick with --strategy-option=theirs.
- Dryrun: Run recipe without modifying anything remotely.
"""
from urlparse import urlparse
from PB.go.chromium.org.luci.buildbucket.proto import build as build_pb2
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
from PB.recipes.fuchsia.release.cherry_pick import InputProperties
DEPS = [
"fuchsia/checkout",
"fuchsia/gerrit",
"fuchsia/git",
"fuchsia/jiri",
"fuchsia/release",
"fuchsia/sso",
"fuchsia/status_check",
"recipe_engine/context",
"recipe_engine/json",
"recipe_engine/path",
"recipe_engine/properties",
"recipe_engine/python",
"recipe_engine/raw_io",
"recipe_engine/step",
]
PROPERTIES = InputProperties
COMMIT_MESSAGE = (
"{prefix} Update {project} to {revision} after {git_subcmd} {change_url}"
"\n\n{commit_msg}"
)
def RunSteps(api, props):
with api.step.nest("check inputs"):
if not props.revision or not props.change_id or not props.target_project:
api.python.failing_step(
"missing change input",
"`revision`, `change_id`, and `target_project` must be specified",
)
if not api.release.validate_branch(props.target_branch):
api.python.failing_step(
"invalid target branch input",
'`target_branch` must start with "releases"',
)
# Assert target branch exists remotely.
https_remote = api.sso.sso_to_https(props.remote)
branch_revision = api.git.get_remote_branch_head(
step_name="get target branch HEAD",
url=https_remote,
branch=props.target_branch,
)
if not branch_revision:
api.python.failing_step(
"target branch does not exist",
"target branch %s does not exist" % props.target_branch,
)
# Checkout at branch revision.
with api.step.nest("checkout fuchsia"), api.context(infra_steps=True):
checkout_root = api.path["start_dir"].join("fuchsia")
checkout = api.checkout.fuchsia_with_options(
path=checkout_root,
build_input=build_pb2.Build.Input(
gitiles_commit=common_pb2.GitilesCommit(
id=branch_revision, project="integration",
),
),
manifest=props.manifest,
remote=props.remote,
# Skip for faster checkout.
fetch_packages=False,
run_hooks=False,
)
integration_root = checkout_root.join("integration")
with api.step.nest("resolve release version"):
# Attempt to increment candidate version.
release_version = api.release.get_next_candidate_version(
ref=branch_revision, repo_path=integration_root,
)
if api.git.get_remote_tag(url=https_remote, tag=str(release_version)):
api.python.failing_step(
"release version conflict",
"attempting snap would conflict with existing tag %s, "
"indicating branch %s has already moved forward"
% (release_version, props.target_branch),
)
with api.step.nest("get project info"), api.context(cwd=integration_root):
project_info = api.jiri.project(
projects=[props.target_project], list_remote=True,
).json.output[0]
# Run integrity check: ensure `revision` is equivalent to the current
# revision for `change_id`.
gerrit_host = api.jiri.read_manifest_element(
project_info["manifest"], "project", props.target_project
)["gerrithost"]
change_details = api.gerrit.change_details(
name="get change details",
change_id=props.change_id,
host=urlparse(gerrit_host).netloc,
query_params=["CURRENT_REVISION"],
test_data=api.json.test_api.output({"current_revision": "foo"}),
).json.output
change_revision = change_details["current_revision"]
change_url = "%s/c/%s/+/%s" % (
gerrit_host,
props.target_project,
props.change_id,
)
if props.revision != change_revision:
api.python.failing_step(
"revision integrity check failed",
"specified revision %s is not equal to the resolved change "
"revision %s, indicating that %s may have been updated recently"
% (props.revision, change_revision, change_url),
)
# Apply cherry-pick or revert.
git_subcmd = "revert" if props.revert_mode else "cherry-pick"
with api.step.nest("apply %s" % git_subcmd), api.context(
cwd=checkout_root.join(project_info["path"])
):
base_revision = api.git.get_hash()
# Ensure revision is fetched.
api.git.fetch("origin", refspec=props.revision)
strategy_option = "theirs" if props.strategy_theirs else None
try:
if props.revert_mode:
api.git.revert(props.revision, strategy_option=strategy_option)
else:
api.git.cherry_pick(props.revision, strategy_option=strategy_option)
# On failure, raise a helpful exception.
except api.step.StepFailure:
api.python.failing_step(
"%s conflict" % git_subcmd,
"%s: cannot cleanly %s %s revision %s onto base revision %s on "
"branch %s"
% (
change_url,
git_subcmd,
props.target_project,
props.revision,
base_revision,
props.target_branch,
),
)
commit_msg = api.git.get_commit_message(commit=props.revision, oneline=True)
# Lazily create branch.
api.git.push(
refs=["HEAD:refs/heads/%s" % props.target_branch], dryrun=props.dryrun,
)
new_revision = api.git.get_hash()
# On successful cherry-pick, update project pin in integration.
with api.step.nest("update integration"), api.context(cwd=integration_root):
if props.target_project != "integration":
api.jiri.init()
api.jiri.edit_manifest(
manifest=project_info["manifest"],
projects=[(project_info["name"], new_revision)],
)
update_message = COMMIT_MESSAGE.format(
prefix=api.release._COMMIT_MESSAGE_PREFIX,
project=props.target_project,
revision=api.release.shorten_revision(new_revision),
git_subcmd=git_subcmd,
change_url=change_url,
commit_msg=commit_msg,
)
api.git.commit(
message=update_message, all_tracked=True,
)
with api.context(infra_steps=True):
release_revision = api.git.get_hash()
api.git.tag(str(release_version), step_name="tag release")
# Push commit and release version.
api.git.push(
step_name="push release",
refs=[
"HEAD:refs/heads/%s" % props.target_branch,
"refs/tags/%s" % release_version,
],
atomic=True,
dryrun=props.dryrun,
)
with api.step.nest("set output properties") as output:
output.presentation.properties["release_version"] = str(release_version)
output.presentation.properties["release_revision"] = release_revision
if not props.dryrun and props.downstream_builders:
api.release.lookup_builds(
builders=props.downstream_builders,
revision=release_revision,
remote=https_remote,
)
def GenTests(api):
default_revision = "foo"
default_change_id = "Ia123b"
default_project = "test-project"
default_manifest = "integration"
default_remote = "sso://fuchsia/integration"
default_branch = "releases/targetbranch"
empty_output = api.raw_io.stream_output("")
branch_does_not_exist = api.step_data(
"check inputs.get target branch HEAD", empty_output
)
branch_version = api.step_data(
"resolve release version.git describe",
api.raw_io.stream_output("releases/0.20200101.0.1"),
)
version_does_not_exist = api.step_data(
"resolve release version.get remote tag", empty_output
)
project_info = api.jiri.read_manifest_element(
"integration_root",
"project",
default_project,
test_output={
"manifest": "fuchsia/manifest",
"path": default_project,
"gerrithost": "https://fuchsia-review.googlesource.com",
},
nesting="get project info",
)
merge_conflict = api.step_data("apply cherry-pick.git cherry-pick", retcode=1)
yield (
api.status_check.test("missing_inputs", status="failure")
+ api.properties(revision=default_revision)
)
yield (
api.status_check.test("invalid_branch_input", status="failure")
+ api.properties(
revision=default_revision,
change_id=default_change_id,
target_project=default_project,
target_branch="invalidbranch",
)
)
yield (
api.status_check.test("branch_does_not_exist", status="failure")
+ api.properties(
revision=default_revision,
change_id=default_change_id,
target_project=default_project,
target_branch=default_branch,
)
+ branch_does_not_exist
)
yield (
api.status_check.test("release_version_conflict", status="failure")
+ api.properties(
revision=default_revision,
change_id=default_change_id,
target_project=default_project,
target_branch=default_branch,
manifest=default_manifest,
remote=default_remote,
)
+ branch_version
)
yield (
api.status_check.test("failed_integrity_check", status="failure")
+ api.properties(
revision="notcurrent",
change_id=default_change_id,
target_project=default_project,
target_branch=default_branch,
manifest=default_manifest,
remote=default_remote,
)
+ branch_version
+ version_does_not_exist
+ project_info
)
yield (
api.status_check.test("cherry_pick")
+ api.properties(
revision=default_revision,
change_id=default_change_id,
target_project=default_project,
target_branch=default_branch,
manifest=default_manifest,
remote=default_remote,
downstream_builders=["a/b/c"],
)
+ branch_version
+ version_does_not_exist
+ project_info
)
yield (
api.status_check.test("merge_conflict", status="failure")
+ api.properties(
revision=default_revision,
change_id=default_change_id,
target_project=default_project,
target_branch=default_branch,
manifest=default_manifest,
remote=default_remote,
)
+ branch_version
+ version_does_not_exist
+ project_info
+ merge_conflict
)
yield (
api.status_check.test("revert")
+ api.properties(
revision=default_revision,
change_id=default_change_id,
target_project=default_project,
target_branch=default_branch,
manifest=default_manifest,
remote=default_remote,
revert_mode=True,
)
+ branch_version
+ version_does_not_exist
+ project_info
)