blob: 24e851105c83efd016a814a02fd45ed6c59a0bd5 [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.
"""Update project or package(s) on an integration.git 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 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/checkout",
"fuchsia/git",
"fuchsia/jiri",
"fuchsia/release",
"fuchsia/sso",
"fuchsia/status_check",
"recipe_engine/context",
"recipe_engine/properties",
"recipe_engine/python",
"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.python.failing_step(
"missing project or package input",
"at least one of `project` or `packages` must be specified",
)
if (props.revision and not props.target_project) or (
props.target_project and not props.revision
):
api.python.failing_step(
"invalid project input",
"`revision` and `project` must be specified together",
)
if (props.version and not props.packages) or (
props.packages and not props.version
):
api.python.failing_step(
"invalid package input",
"`version` and `packages` must be specified together",
)
if props.packages and not props.lockfiles:
api.python.failing_step(
"missing lockfiles input",
"`lockfiles` must be specified if `packages` are",
)
if not props.bug:
api.python.failing_step(
"missing bug input",
"`bug` 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.
checkout = api.checkout.fuchsia_with_options(
build_input=build_pb2.Build.Input(
gitiles_commit=common_pb2.GitilesCommit(
id=branch_revision,
project=props.project,
),
),
project=props.project,
manifest=props.manifest,
remote=props.remote,
# Skip for faster checkout.
fetch_packages=False,
)
integration_root = checkout.root_dir.join(props.project)
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),
)
# 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.python.failing_step(
"invalid project input",
"project %s does not exist in the checkout"
% props.target_project,
)
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)).json.output
# Ensure we have found every package.
found_package_names = set()
found_packages = []
for package in package_info:
# Only record latest or non-latest packages depending on
# `latest_packages`.
is_latest_manifest = package["manifest"].endswith("_latest")
if (props.latest_packages and is_latest_manifest) or (
not props.latest_packages and not is_latest_manifest
):
found_package_names.add(package["name"])
found_packages.append(package)
for package_name in props.packages:
if package_name not in found_package_names:
api.python.failing_step(
"invalid package input",
"package %s does not exist in branch %s"
% (package_name, props.target_branch),
)
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 and push integration.
with api.context(cwd=integration_root), api.step.nest(
"update integration"
) as presentation:
commit_message = COMMIT_MESSAGE.format(
prefix=api.release._COMMIT_MESSAGE_PREFIX,
project_or_packages=props.target_project or ", ".join(found_package_names),
revision_or_version=props.revision or props.version,
bug=props.bug,
)
if not api.git.ls_files(step_name="check for changes", modified=True).stdout:
api.python.failing_step(
"no changes to apply",
"the update operation is a no-op, indicating that the target "
"branch already matches the requested change input",
)
with api.context(infra_steps=True):
api.git.commit(
message=commit_message,
all_tracked=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,
)
api.release.set_output_properties(
presentation, release_revision, release_version
)
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_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("")
branch_does_not_exist = api.step_data(
"check inputs.get target branch HEAD", empty_output
)
branch_version = api.release.ref_to_release_version(
"releases/0.20200101.0.1",
nesting="resolve release version",
)
version_does_not_exist = api.step_data(
"resolve release version.get remote tag", empty_output
)
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",
},
]
),
)
modified_files = api.step_data(
"update integration.check for changes",
api.raw_io.stream_output("foo/bar"),
)
no_modified_files = api.step_data(
"update integration.check for changes", empty_output
)
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.status_check.test("missing_project_or_package_input", status="failure"))
yield (
api.status_check.test("missing_revision_input", status="failure")
+ api.properties(target_project=default_target_project)
)
yield (
api.status_check.test("missing_project_input", status="failure")
+ api.properties(revision=default_revision)
)
yield (
api.status_check.test("missing_version_input", status="failure")
+ api.properties(packages=default_packages)
)
yield (
api.status_check.test("missing_packages_input", status="failure")
+ api.properties(revision=default_version)
)
yield (
api.status_check.test("missing_lockfiles_input", status="failure")
+ api.properties(packages=default_packages, version=default_version)
)
yield (
api.status_check.test("missing_bug_input", status="failure")
+ api.properties(
packages=default_packages,
version=default_version,
lockfiles=default_lockfiles,
)
)
yield (
api.status_check.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.status_check.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,
)
+ branch_does_not_exist
)
yield (
api.status_check.test("release_version_conflict", status="failure")
+ 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,
)
+ branch_version
)
yield (
api.status_check.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,
)
+ branch_version
+ version_does_not_exist
+ api.step_data("update project.jiri project", api.jiri.project([]))
)
yield (
api.status_check.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,
downstream_builders=["a/b/c"],
)
+ branch_version
+ version_does_not_exist
+ project_info
+ modified_files
)
yield (
api.status_check.test("invalid_package_input", status="failure")
+ api.properties(
packages=["invalid/package"],
version=default_version,
lockfiles=default_lockfiles,
bug=default_bug,
target_branch=default_branch,
project=default_project,
manifest=default_manifest,
remote=default_remote,
)
+ branch_version
+ version_does_not_exist
+ package_info
)
yield (
api.status_check.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,
)
+ branch_version
+ version_does_not_exist
+ package_info
+ modified_files
)
yield (
api.status_check.test("no_changes_to_apply", status="failure")
+ 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,
)
+ branch_version
+ version_does_not_exist
+ package_info
+ no_modified_files
)
yield (
api.status_check.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,
)
+ branch_version
+ version_does_not_exist
+ package_info
+ modified_files
)