| # 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 |
| ) |