blob: a852d535cef0b7c09da9b5c35f1d554f0dda719b [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.
"""Upload a CL to update project or package(s) on a release branch.
Inputs
- Project: The project to update.
- Revision: The revision to update to.
or
- Packages: The package(s) to update.
- Version: The version to update to.
Options
- 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.recipes.fuchsia.release.update_petal import InputProperties
DEPS = [
"fuchsia/auto_roller",
"fuchsia/checkout",
"fuchsia/git",
"fuchsia/jiri",
"fuchsia/release",
"fuchsia/sso",
"recipe_engine/context",
"recipe_engine/properties",
"recipe_engine/raw_io",
"recipe_engine/step",
]
PROPERTIES = InputProperties
COMMIT_MESSAGE = (
"{prefix} Update {project_or_packages} to {revision_or_version}\n\nBug: {bug}"
)
def RunSteps(api, props):
with api.step.nest("check inputs"):
if not props.target_project and not props.packages:
api.step.empty(
"missing project or package input",
step_text="at least one of `project` or `packages` must be specified",
status=api.step.FAILURE,
)
if (props.revision and not props.target_project) or (
props.target_project and not props.revision
):
api.step.empty(
"invalid project input",
step_text="`revision` and `project` must be specified together",
status=api.step.FAILURE,
)
if (props.version and not props.packages) or (
props.packages and not props.version
):
api.step.empty(
"invalid package input",
step_text="`version` and `packages` must be specified together",
status=api.step.FAILURE,
)
if props.packages and not props.lockfiles:
api.step.empty(
"missing lockfiles input",
step_text="`lockfiles` must be specified if `packages` are",
status=api.step.FAILURE,
)
if not props.bug:
api.step.empty(
"missing bug input",
step_text="`bug` must be specified",
status=api.step.FAILURE,
)
if not api.release.validate_branch(props.target_branch):
api.step.empty(
"invalid target branch input",
step_text='`target_branch` must start with "releases/"',
status=api.step.FAILURE,
)
# 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.step.empty(
"target branch does not exist",
step_text=f"target branch {props.target_branch} does not exist",
status=api.step.FAILURE,
)
# Checkout at branch revision.
checkout = api.checkout.fuchsia_with_options(
build_input=build_pb2.Build.Input(
gitiles_commit=common_pb2.GitilesCommit(
id=branch_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)
# If updating a project, move its pin in integration to `revision`.
if props.target_project:
with api.step.nest("update project"), api.context(cwd=integration_root):
# The target manifest can be directed by a property. If the target
# manifest is not specified, use a Jiri project query to identify
# the target manifest.
target_manifest = props.target_manifest
if not target_manifest:
project_info = api.jiri.project(
projects=[props.target_project],
list_remote=True,
).json.output
if not project_info:
api.step.empty(
"invalid project input",
step_text=(
f"project {props.target_project} does not exist in the checkout"
),
status=api.step.FAILURE,
)
target_manifest = project_info[0]["manifest"]
api.jiri.edit_manifest(
manifest=target_manifest,
projects=[(props.target_project, props.revision)],
)
# If updating package(s), move their pins in integration to `version` and
# update lockfiles.
else:
with api.step.nest("update packages"), api.context(cwd=integration_root):
package_info = api.jiri.package(packages=list(props.packages))
found_packages = []
for package in package_info:
# Only record latest or non-latest packages depending on
# `latest_packages`. This is a hacky but probably-safe heuristic
# based on the inclusion of certain keywords in the manifest
# filename.
is_latest_manifest = package["manifest"].endswith("_latest") or package[
"manifest"
].endswith("_canary")
if (props.latest_packages and is_latest_manifest) or (
not props.latest_packages and not is_latest_manifest
):
found_packages.append(package)
for package in found_packages:
api.jiri.edit_manifest(
manifest=package["manifest"],
packages=[(package["name"], props.version)],
)
for lock_entry in props.lockfiles:
fields = lock_entry.split("=")
manifest = fields[0]
lock = fields[1]
api.jiri.resolve(
local_manifest=True,
output=lock,
manifests=[manifest],
)
commit_message = COMMIT_MESSAGE.format(
prefix=api.release._COMMIT_MESSAGE_PREFIX,
project_or_packages=(
props.target_project or ", ".join(sorted(p["name"] for p in found_packages))
),
revision_or_version=props.revision or props.version,
bug=props.bug,
)
if props.dryrun: # pragma: no cover
return
change = api.auto_roller.attempt_roll(
api.auto_roller.Options(
remote=props.remote,
upstream_ref=props.target_branch,
reviewer_emails=[props.reviewer],
# Never attempt to submit the CL since it will require additional
# approvals.
dry_run=True,
),
repo_dir=integration_root,
commit_message=commit_message,
)
return api.auto_roller.raw_result(change)
def GenTests(api):
default_revision = "foo"
default_target_project = "test-project"
default_version = "version:abc"
default_packages = ["fuchsia/foo/linux-amd64", "fuchsia/foo/linux-arm64"]
default_bug = "123456"
default_lockfiles = ["foo/bar=baz.lock"]
default_project = "integration"
default_manifest = "integration"
default_remote = "sso://fuchsia/integration"
default_branch = "releases/targetbranch"
empty_output = api.raw_io.stream_output_text("")
project_info = api.step_data(
"update project.jiri project",
api.jiri.package(
[
{
"name": default_target_project,
"manifest": "fuchsia/manifest",
"path": "path/to/project",
"gerrithost": "https://fuchsia-review.googlesource.com",
},
]
),
)
package_info = api.step_data(
"update packages.jiri package",
api.jiri.package(
[
{
"name": "fuchsia/foo/linux-amd64",
"path": "path/linux-amd64",
"manifest": "test/manifest",
},
{
"name": "fuchsia/foo/linux-arm64",
"path": "path/linux-arm64",
"manifest": "test/manifest",
},
{
"name": "fuchsia/foo/linux-amd64",
"path": "path/linux-amd64",
"manifest": "test/manifest_latest",
},
{
"name": "fuchsia/foo/linux-arm64",
"path": "path/linux-arm64",
"manifest": "test/manifest_latest",
},
]
),
)
yield api.test("missing_project_or_package_input", status="FAILURE")
yield (
api.test("missing_revision_input", status="FAILURE")
+ api.properties(target_project=default_target_project)
)
yield (
api.test("missing_project_input", status="FAILURE")
+ api.properties(revision=default_revision)
)
yield (
api.test("missing_version_input", status="FAILURE")
+ api.properties(packages=default_packages)
)
yield (
api.test("missing_packages_input", status="FAILURE")
+ api.properties(revision=default_version)
)
yield (
api.test("missing_lockfiles_input", status="FAILURE")
+ api.properties(packages=default_packages, version=default_version)
)
yield (
api.test("missing_bug_input", status="FAILURE")
+ api.properties(
packages=default_packages,
version=default_version,
lockfiles=default_lockfiles,
)
)
yield (
api.test("invalid_branch_input", status="FAILURE")
+ api.properties(
packages=default_packages,
version=default_version,
lockfiles=default_lockfiles,
bug=default_bug,
target_branch="invalidbranch",
)
)
yield (
api.test("branch_does_not_exist", status="FAILURE")
+ api.properties(
packages=default_packages,
version=default_version,
lockfiles=default_lockfiles,
bug=default_bug,
target_branch=default_branch,
)
+ api.step_data("check inputs.get target branch HEAD", empty_output)
)
yield (
api.test("invalid_project_input", status="FAILURE")
+ api.properties(
target_project=default_target_project,
revision=default_revision,
bug=default_bug,
target_branch=default_branch,
project=default_project,
manifest=default_manifest,
remote=default_remote,
)
+ api.step_data("update project.jiri project", api.jiri.project([]))
)
yield (
api.test("update_project")
+ api.properties(
target_project=default_target_project,
revision=default_revision,
bug=default_bug,
target_branch=default_branch,
project=default_project,
manifest=default_manifest,
remote=default_remote,
reviewer="foo@google.com",
)
+ project_info
)
yield (
api.test("update_packages")
+ api.properties(
packages=default_packages,
version=default_version,
lockfiles=default_lockfiles,
bug=default_bug,
target_branch=default_branch,
project=default_project,
manifest=default_manifest,
remote=default_remote,
reviewer="foo@google.com",
)
+ package_info
)
yield (
api.test("update_latest_packages")
+ api.properties(
packages=default_packages,
version=default_version,
lockfiles=default_lockfiles,
bug=default_bug,
target_branch=default_branch,
project=default_project,
manifest=default_manifest,
remote=default_remote,
latest_packages=True,
reviewer="foo@google.com",
)
+ package_info
)