| # 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. |
| |
| """Create an integration.git release branch. |
| |
| Branching Modes |
| - Revision: Create a branch off a specific revision. |
| - LKGR: Create a branch using the LKGR of a set of builders. |
| |
| Options |
| - Experimental: Create an experimental branch using the "add track" feature. |
| See release/api.py for more details. |
| - Dryrun: Run recipe without modifying anything remotely. |
| """ |
| |
| from urllib.parse 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.recipe_engine.result import RawResult |
| from PB.recipes.fuchsia.release.create_branch import InputProperties |
| |
| DEPS = [ |
| "fuchsia/checkout", |
| "fuchsia/gerrit", |
| "fuchsia/git", |
| "fuchsia/jiri", |
| "fuchsia/lkg", |
| "fuchsia/release", |
| "fuchsia/sso", |
| "recipe_engine/context", |
| "recipe_engine/file", |
| "recipe_engine/path", |
| "recipe_engine/properties", |
| "recipe_engine/raw_io", |
| "recipe_engine/step", |
| "recipe_engine/time", |
| ] |
| |
| PROPERTIES = InputProperties |
| |
| |
| def RunSteps(api, props): |
| with api.step.nest("check inputs"): |
| if (props.revision and props.lkg_builders) or ( |
| not props.revision and not props.lkg_builders |
| ): |
| return RawResult( |
| summary_markdown="must specify exactly one of `revision` or " |
| "`lkg_builders`", |
| status=common_pb2.FAILURE, |
| ) |
| |
| if not api.release.validate_branch(props.target_branch): |
| return RawResult( |
| summary_markdown='`target_branch` must start with "releases/"', |
| status=common_pb2.FAILURE, |
| ) |
| |
| # Assert branch does not yet exist remotely. |
| https_remote = api.sso.sso_to_https(props.remote) |
| if api.git.get_remote_branch_head( |
| step_name="get remote branch", |
| url=https_remote, |
| branch=props.target_branch, |
| ): |
| return RawResult( |
| summary_markdown=( |
| f"target branch {props.target_branch} already exists" |
| ), |
| status=common_pb2.FAILURE, |
| ) |
| |
| # If `revision` was specified, use it. Otherwise, set `revision` to LKGR. |
| revision = props.revision |
| if props.lkg_builders: |
| revision = api.lkg.revision( |
| step_name="get last-known-good revision", |
| builders=props.lkg_builders, |
| test_data="lkgrevision", |
| ) |
| |
| checkout = api.checkout.fuchsia_with_options( |
| build_input=build_pb2.Build.Input( |
| gitiles_commit=common_pb2.GitilesCommit( |
| id=revision, |
| project=props.project, |
| host=urlparse(https_remote).hostname, |
| ), |
| ), |
| project=props.project, |
| manifest=props.manifest, |
| remote=props.remote, |
| # Skip for faster checkout. |
| fetch_packages=False, |
| ) |
| integration_root = checkout.project_path(props.project) |
| |
| with api.step.nest("resolve release version"): |
| # If there are existing candidates on `revision`, get the next candidate |
| # version. |
| candidate_version_increment = props.candidate_version_increment |
| release_version = api.release.get_next_candidate_version( |
| ref=revision, |
| repo_path=integration_root, |
| add_track=props.add_track, |
| candidate_version_increment=candidate_version_increment, |
| ) |
| # Otherwise, `revision` has no release history yet, so initialize a |
| # release version with the current date. |
| if not release_version: |
| release_version = api.release.get_initial_release_version( |
| date=api.time.utcnow().date().strftime("%Y%m%d"), |
| repo_path=integration_root, |
| release_version_initialization=props.release_version_initialization, |
| candidate_version_increment=candidate_version_increment, |
| ) |
| |
| # Assert release version does not yet exist remotely. |
| if api.git.get_remote_tag( |
| url=https_remote, |
| tag=release_version.tag_name, |
| ): |
| return RawResult( |
| summary_markdown=( |
| f"release version conflict: attempting creation would " |
| f"conflict with existing version " |
| f"{release_version.tag_name}, indicating source revision " |
| f"{revision} has already moved forward" |
| ), |
| status=common_pb2.FAILURE, |
| ) |
| |
| with api.context(cwd=integration_root), api.step.nest( |
| "create release branch" |
| ) as presentation: |
| # Construct commit message. |
| create_message = ( |
| f"[release] Initialize branch {props.target_branch} to revision " |
| f"{revision}" |
| ) |
| # If the source revision is a release version, make a note in the commit |
| # message. |
| source_version = api.release.ref_to_release_version( |
| ref=revision, repo_path=integration_root |
| ) |
| if source_version: |
| create_message = f"{create_message} (equal to {source_version})" |
| |
| if props.incremental_settings_path: |
| disable_builder_settings( |
| api, |
| settings_name="incremental", |
| settings_path=integration_root / props.incremental_settings_path, |
| ) |
| if props.size_creep_settings_path: |
| disable_builder_settings( |
| api, |
| settings_name="size creep", |
| settings_path=integration_root / props.size_creep_settings_path, |
| ) |
| if props.incremental_settings_path or props.size_creep_settings_path: |
| api.step( |
| "generate configs", |
| [integration_root / props.config_generator_path], |
| ) |
| |
| # Create commit to mark the creation of the branch, possibly including |
| # any config changes from above. |
| api.git.commit( |
| step_name="create release commit", |
| message=create_message, |
| allow_empty=True, |
| all_files=True, |
| ) |
| api.git.tag(step_name="tag release", name=release_version.tag_name) |
| release_revision = api.git.get_hash() |
| api.git.push( |
| step_name="push release", |
| refs=[ |
| f"HEAD:refs/heads/{props.target_branch}", |
| f"refs/tags/{release_version.tag_name}", |
| ], |
| atomic=True, |
| dryrun=props.dryrun, |
| ) |
| api.release.set_output_properties( |
| presentation, release_revision, release_version, https_remote |
| ) |
| |
| if props.projects_to_initialize: |
| with api.step.nest("initialize project branches"), api.context( |
| cwd=integration_root |
| ): |
| initialize_project_branches( |
| api, |
| props.target_branch, |
| props.projects_to_initialize, |
| props.dryrun, |
| ) |
| |
| if not props.dryrun and props.downstream_builders: |
| api.release.lookup_builds( |
| builders=props.downstream_builders, |
| revision=release_revision, |
| remote=https_remote, |
| ) |
| |
| |
| def disable_builder_settings(api, settings_name, settings_path): |
| """Disable builder settings for a single settings file.""" |
| api.path.mock_add_paths(settings_path) |
| # The settings file may not exist depending on the branch point. |
| if api.path.exists(settings_path): |
| settings = api.file.read_json( |
| f"read {settings_name} settings", |
| settings_path, |
| ) |
| settings["disabled"] = True |
| api.file.write_json( |
| f"write {settings_name} settings", |
| settings_path, |
| settings, |
| ) |
| |
| |
| def initialize_project_branches(api, target_branch, projects, dryrun): |
| """Initialize a branch for each project. |
| |
| The base revision for each branch is the revision pinned by its integration |
| manifest. |
| """ |
| for project in projects: |
| with api.step.nest(project): |
| project_info_list = api.jiri.project( |
| projects=[project], |
| list_remote=True, |
| ).json.output |
| for project_info in project_info_list: |
| gerrit_host = api.jiri.read_manifest_element( |
| project_info["manifest"], "project", project |
| )["gerrithost"] |
| if not dryrun: |
| api.gerrit.create_branch( |
| name="create branch", |
| host=gerrit_host, |
| project=project, |
| ref=target_branch, |
| revision=project_info["revision"], |
| ) |
| |
| |
| def GenTests(api): |
| default_revision = "foo" |
| default_project = "integration" |
| default_manifest = "integration" |
| default_remote = "sso://fuchsia/integration" |
| default_branch = "releases/newbranch" |
| empty_output = api.raw_io.stream_output_text("") |
| branch_does_not_exist = api.step_data( |
| "check inputs.get remote branch", empty_output |
| ) |
| revision_has_no_release_history = api.release.ref_to_release_version( |
| "", |
| nesting="resolve release version", |
| retcode=1, |
| ) |
| source_version = api.release.ref_to_release_version( |
| "releases/0.20200102.0.1", |
| nesting="resolve release version", |
| ) |
| version_does_not_exist = api.step_data( |
| "resolve release version.get remote tag", empty_output |
| ) |
| revision_is_release_version = api.release.ref_to_release_version( |
| "releases/0.20200101.0.1", |
| nesting="create release branch", |
| ) |
| no_matching_version = api.release.ref_to_release_version( |
| "", |
| nesting="create release branch", |
| retcode=1, |
| ) |
| |
| yield ( |
| api.test("mutually_exclusive_rev_input", status="FAILURE") |
| + api.properties( |
| revision=default_revision, lkg_builders=["project/bucket/builder"] |
| ) |
| ) |
| |
| yield api.test("missing_rev_input", status="FAILURE") |
| |
| yield ( |
| api.test("invalid_branch_input", status="FAILURE") |
| + api.properties(revision=default_revision, target_branch="invalidbranch") |
| ) |
| |
| yield ( |
| api.test("branch_already_exists", status="FAILURE") |
| + api.properties(revision=default_revision, target_branch=default_branch) |
| ) |
| |
| yield ( |
| api.test("release_version_conflict", status="FAILURE") |
| + api.properties( |
| revision=default_revision, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| ) |
| + branch_does_not_exist |
| + revision_has_no_release_history |
| ) |
| |
| yield ( |
| api.test("revision_from_main") |
| + api.properties( |
| revision=default_revision, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| downstream_builders=["a/b/c"], |
| incremental_settings_path="incremental_settings.json", |
| size_creep_settings_path="size_creep_settings.json", |
| config_generator_path="gen.sh", |
| ) |
| + api.step_data( |
| "create release branch.read incremental settings", |
| api.file.read_json({"disabled": False}), |
| ) |
| + api.step_data( |
| "create release branch.read size creep settings", |
| api.file.read_json({"disabled": False}), |
| ) |
| + branch_does_not_exist |
| + revision_has_no_release_history |
| + version_does_not_exist |
| + no_matching_version |
| ) |
| |
| yield ( |
| api.test("lkgr_from_main") |
| + api.properties( |
| lkg_builders=["project/bucket/builder"], |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| projects_to_initialize=[ |
| "test-project", |
| ], |
| ) |
| + branch_does_not_exist |
| + revision_has_no_release_history |
| + version_does_not_exist |
| + no_matching_version |
| + api.step_data( |
| "initialize project branches.test-project.jiri project", |
| api.jiri.project( |
| projects=[ |
| { |
| "name": "test-project", |
| "revision": "abcdef", |
| "manifest": "flower", |
| } |
| ] |
| ), |
| ) |
| + api.jiri.read_manifest_element( |
| "test-project", |
| test_output={ |
| "gerrithost": "https://fuchsia-review.googlesource.com", |
| }, |
| nesting="initialize project branches.test-project", |
| ) |
| ) |
| |
| yield ( |
| api.test("branch_from_release_version") |
| + api.properties( |
| revision=default_revision, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| ) |
| + branch_does_not_exist |
| + source_version |
| + version_does_not_exist |
| + revision_is_release_version |
| ) |