blob: bfc27e56ddc3a78183442bac2cd58fe6c543caf0 [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.
"""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 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
PYTHON_VERSION_COMPATIBILITY = "PY3"
DEPS = [
"fuchsia/checkout",
"fuchsia/gerrit",
"fuchsia/git",
"fuchsia/jiri",
"fuchsia/lkg",
"fuchsia/release",
"fuchsia/sso",
"fuchsia/status_check",
"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="target branch %s already exists"
% props.target_branch,
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,
),
),
project=props.project,
manifest=props.manifest,
remote=props.remote,
# Skip for faster checkout.
fetch_packages=False,
)
integration_root = api.release.get_project_path(checkout.root_dir, props.project)
with api.step.nest("resolve release version"):
# If there are existing candidates on `revision`, get the next candidate
# version.
release_version = api.release.get_next_candidate_version(
ref=revision, repo_path=integration_root, add_track=props.add_track
)
# 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,
)
# 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="release version conflict: attempting "
"creation would conflict with existing version %s, indicating "
"source revision %s has already moved forward"
% (release_version.tag_name, revision),
status=common_pb2.FAILURE,
)
with api.context(cwd=integration_root), api.step.nest(
"create release branch"
) as presentation:
# Construct commit message.
create_message = "[release] Initialize branch %s to revision %s" % (
props.target_branch,
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 = "%s (equal to %s)" % (create_message, source_version)
if props.incremental_settings_path:
incremental_settings_path = integration_root.join(
props.incremental_settings_path
)
api.path.mock_add_paths(incremental_settings_path)
# The incremental settings file may not exist depending on the
# branch point.
if api.path.exists(incremental_settings_path):
incremental_settings = api.file.read_json(
"read incremental settings",
incremental_settings_path,
)
# Disable incremental builds on branches since they thrash the
# cache and we do not yet have branch-specific caches to isolate
# from main.
incremental_settings["disabled"] = True
api.file.write_json(
"write incremental settings",
incremental_settings_path,
incremental_settings,
)
api.step(
"generate configs",
[integration_root.join(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=[
"HEAD:refs/heads/%s" % props.target_branch,
"refs/tags/%s" % release_version.tag_name,
],
atomic=True,
dryrun=props.dryrun,
)
api.release.set_output_properties(
presentation, release_revision, release_version
)
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 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.status_check.test("mutually_exclusive_rev_input", status="failure")
+ api.properties(
revision=default_revision, lkg_builders=["project/bucket/builder"]
)
)
yield (api.status_check.test("missing_rev_input", status="failure"))
yield (
api.status_check.test("invalid_branch_input", status="failure")
+ api.properties(revision=default_revision, target_branch="invalidbranch")
)
yield (
api.status_check.test("branch_already_exists", status="failure")
+ api.properties(revision=default_revision, target_branch=default_branch)
)
yield (
api.status_check.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.status_check.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",
config_generator_path="gen.sh",
)
+ api.step_data(
"create release branch.read incremental 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.status_check.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(
"flower",
"project",
"test-project",
test_output={
"gerrithost": "https://fuchsia-review.googlesource.com",
},
nesting="initialize project branches.test-project",
)
)
yield (
api.status_check.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
)