blob: 2040082602ab8f022bd2eb0f63ee286ca7481bea [file] [log] [blame]
# Copyright 2019 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.
"""Recipe for rolling flutter and dependencies.
Flutter and dependencies need to get rolled based on the version
dependencies of another fuchsia prebuilt. In this recipe we are
calling the fuchsia prebuilt dictating the dependencies "orchestrator".
The orchestrator cipd package contains a json file with the dependencies
it was built with. The following is an example of the json file:
{
"flutter_version": "flutter_version_xyz",
"engine_version": "engine_version_xyz",
"skia_version": "skia_version_xyz",
"dart_version": "dart_version_xyz"
}
This recipe checks if there is a new version of the orchestrator and
if it requires different versions of the dependencies from the ones in
the source tree. If both conditions are true then it proceeds to roll
new versions of projects and packages as described in the
rolling_packages_projects parameter and using the versions from the
orchestrator dependencies json file.
Warning: If this recipe runs in dry mode it will still roll changes into
fuchsia dart 3p repository but the fuchsia tree won't pin to the latest
version. This is safe as the fuchsia dart 3p hash will be bumped only on a
successful roll of all the other dependencies.
"""
import copy
from recipe_engine.config import List
from recipe_engine.post_process import StepSuccess
from recipe_engine.recipe_api import Property
DEPS = [
"fuchsia/auto_roller",
"fuchsia/buildbucket_util",
"fuchsia/cipd_dependencies",
"fuchsia/debug_symbols",
"fuchsia/gerrit",
"fuchsia/git",
"fuchsia/jiri",
"fuchsia/status_check",
"recipe_engine/buildbucket",
"recipe_engine/cipd",
"recipe_engine/context",
"recipe_engine/file",
"recipe_engine/path",
"recipe_engine/properties",
"recipe_engine/python",
"recipe_engine/step",
]
PROPERTIES = {
"project": Property(kind=str, help="Jiri remote manifest project", default=None),
"manifests": Property(
kind=List(dict),
help=(
"A list of dictionaries with project, manifest and remote keys "
"used to signal jiri wich manifests to synchronize. "
'E.g. [{"project": "integration", "manifest": "other/dependency", '
' "remote": "sso://fuchsia/integration", '
' "lock_file": "integration/jiri.lock"}]'
),
),
"locks": Property(
kind=List(dict),
help=(
"A list of dictionaries with manifest and files keys used to "
"signal jiri which lock files to update. "
' E.g. [{"manifest": "integration/fuchsia/flower", '
' "file": "integration/fuchsia/jiri.lock"}]'
),
),
"orchestrator_package": Property(
kind=str,
help=(
"A string with the CIPD package used to validate "
"flutter and dependencies versions. This is a special "
"cipd package that contains a json file describing "
"the dependencies it was built with."
),
),
"orchestrator_ref": Property(
kind=str,
default="latest",
help="A cipd ref used to get a given version of the orchestrator package",
),
"orchestrator_import_in": Property(
kind=str,
default="",
help=(
"A string with the location where the orchestrator package "
"should be imported into the source tree."
),
),
"owners": Property(
kind=List(str),
default=(),
help=(
"The owners responsible for watching this roller "
'(example: "username@google.com").'
),
),
"rolling_packages_projects": Property(
kind=List(dict),
default=(),
help=(
"A list of dictionaries with rolling package/project, type,"
" and location to import_in. This is used to list the "
"dependencies that should be updated: "
'E.g. [{"name": "dart", "type": "package", '
' "import_in": "fuchsia/prebuilts", '
' "version_tag": "dart_version"}]'
),
),
"versions_file": Property(
kind=str,
help=(
"A string with the path to the versions file inside"
"the orchestrator CIPD package."
),
),
"validate_against_package": Property(
kind=dict,
help=(
"A dictionary with the package and tag for the version to use"
" for validation. A single dependency is used to validate if"
' a roll is needed. E.g. {"package": "flutter/fuchsia", '
' "version_tag": "flutter_version"} '
"will validate the source tree version of flutter/fuchsia "
"with the value of flutter_version from the orchestrator "
"json file."
),
),
"dry_run": Property(
kind=bool,
default=False,
help="Whether to dry-run the auto-roller (CQ+1 and abandon the change)",
),
"debug_symbol_packages": Property(
kind=List(str),
default=(),
help=(
"A list of strings with the cipd packages containing debug "
"symbols and their associated packages."
),
),
"debug_symbol_gcs_buckets": Property(
kind=List(str), default=(), help="GCS buckets to upload debug symbols to",
),
}
COMMIT_MESSAGE = """[roll] Roll {roller} packages and projects:
{packages}
Test: CQ"""
def RunSteps(
api,
project,
manifests,
locks,
orchestrator_ref,
owners,
versions_file,
rolling_packages_projects,
orchestrator_package,
orchestrator_import_in,
validate_against_package,
dry_run,
debug_symbol_packages,
debug_symbol_gcs_buckets,
):
with api.context(infra_steps=True):
if owners:
with api.step.nest("owners") as presentation:
presentation.step_summary_text = ", ".join(owners)
# Import manifests
with api.step.nest("jiri-manifests") as presentation:
api.jiri.init(use_lock_file=True)
presentation.logs["manifests"] = str(manifests)
for manifest_config in manifests:
api.jiri.import_manifest(
manifest=manifest_config["manifest"],
remote=manifest_config["remote"],
name=manifest_config["project"],
)
api.jiri.update(run_hooks=False)
with api.context(cwd=api.path["start_dir"]):
api.jiri.run_hooks()
deps_info = api.jiri.package([orchestrator_package])
cipd_description = api.cipd.describe(orchestrator_package, orchestrator_ref)
orchestrator_current_version = deps_info.json.output[0]["version"]
# Is there a new version to roll?
orchestrator_expected_version = cipd_description.tags[0].tag
if orchestrator_expected_version == orchestrator_current_version:
msg = "%s in tree version is [%s] and latest from cipd is [%s]" % (
orchestrator_package,
orchestrator_current_version,
orchestrator_expected_version,
)
api.step(
"manifest up-to-date; nothing to roll", None
).presentation.step_text = msg
return api.auto_roller.nothing_to_roll()
# Read main package versions file.
orchestrator_version_dict = api.cipd_dependencies.get_dependencies(
"read versions",
orchestrator_package,
cipd_description.pin.instance_id,
versions_file,
)
# Use version of the validation package to decide whether a roll is needed or
# not.
validation_info = api.jiri.package([validate_against_package["package"]])
local_version = validation_info.json.output[0]["version"]
expected_version = (
"git_revision:%s"
% orchestrator_version_dict[validate_against_package["version_tag"]]
)
if local_version == expected_version:
msg = "Expected version [%s] == local fuchsia tree version [%s]" % (
expected_version,
local_version,
)
api.step("validation summary", cmd=None).presentation.step_text = msg
return api.auto_roller.nothing_to_roll()
# Start rolling orchestrator
rolling_packages_projects.append(
{
"type": "package",
"name": orchestrator_package,
"version_tag": orchestrator_package,
"import_in": orchestrator_import_in,
}
)
orchestrator_version_dict[orchestrator_package] = orchestrator_expected_version
# Roll all the dependencies
package_msgs = []
flutter_fuchsia_version = None
package_msg_template = "%s %s rolled from %s to %s"
with api.step.nest("edit jiri manifests"):
for package_project in rolling_packages_projects:
package_msg = ""
if package_project["type"] == "package":
if package_project["name"] == orchestrator_package:
expected_version = orchestrator_version_dict[
package_project["version_tag"]
]
else:
expected_version = (
"git_revision:%s"
% orchestrator_version_dict[package_project["version_tag"]]
)
# Save flutter/fuchsia version, as it is the coordinating version
# for debug symbol uploads.
if package_project["name"] == "flutter/fuchsia":
flutter_fuchsia_version = str(expected_version)
changes = api.jiri.edit_manifest(
package_project["import_in"],
packages=[(package_project["name"], expected_version)],
name=package_project["name"],
)
# Sometimes only a subset of packages are updated. In those cases
# the changes list is empty.
if changes["packages"]:
old_version = changes["packages"][0]["old_version"]
package_msg = package_msg_template % (
"package",
package_project["name"],
old_version,
expected_version,
)
else:
expected_version = orchestrator_version_dict[
package_project["version_tag"]
]
changes = api.jiri.edit_manifest(
package_project["import_in"],
name=package_project["name"],
projects=[(package_project["name"], expected_version)],
)
# Sometimes only a subset of projects are updated. In those cases
# the changes list is empty.
if changes["projects"]:
old_version = changes["projects"][0]["old_revision"]
package_msg = package_msg_template % (
"project",
package_project["name"],
old_version,
expected_version,
)
if package_msg:
package_msgs.append(package_msg)
# Update fuchsia's third-party dart packages (including flutter dependencies),
# only if we are rolling a new version of dart-sdk.
rolling_package_names = [p["name"] for p in rolling_packages_projects]
if "fuchsia/dart-sdk/${platform}" in rolling_package_names:
with api.step.nest("third-party dart packages") as presentation:
checkout_root = api.path["start_dir"]
rolled_hash = update_3p_packages(
api=api,
presentation=presentation,
checkout_root=checkout_root,
flutter_revision=orchestrator_version_dict["flutter_version"],
)
changes = api.jiri.edit_manifest(
"integration/fuchsia/topaz/dart",
name="third_party/dart-pkg",
projects=[("third_party/dart-pkg", rolled_hash)],
)
# Update lock files
with api.step.nest("update jiri lock files"):
for lock in locks:
api.jiri.resolve(
local_manifest=True, output=lock["file"], manifests=[lock["manifest"]]
)
roller_name = api.buildbucket.builder_name
if roller_name.endswith("-roller"):
roller_name = api.buildbucket.builder_name[:7]
message = COMMIT_MESSAGE.format(
roller=roller_name,
packages="\n\n".join(package_msgs),
builder=api.buildbucket.builder_name,
build_id=api.buildbucket_util.id,
)
# All the changes are happening inside one of the git cloned projects.
# The "project" property points to the location of the project containing
# the changes and we need to move to that directory for the roller to pick
# up the changes.
project_dir = api.path["start_dir"].join(*project.split("/"))
with api.context(cwd=project_dir):
change = api.auto_roller.attempt_roll(
api.gerrit.host_from_remote_url(manifests[0]["remote"]),
gerrit_project=project,
repo_dir=project_dir,
commit_message=message,
dry_run=dry_run,
)
rolled = change and change.success
# Skip building and pushing Flutter debug symbols if a new version is not
# provided or running in dry run mode. For dry run mode, dependencies won't
# be available in the tree.
if (
rolled
and debug_symbol_gcs_buckets
and flutter_fuchsia_version is not None
and debug_symbol_packages
and not dry_run
):
api.debug_symbols.fetch_and_upload(
packages=debug_symbol_packages,
version=flutter_fuchsia_version,
buckets=debug_symbol_gcs_buckets,
)
return api.auto_roller.raw_result(change)
def update_3p_packages(
api, checkout_root, presentation, flutter_revision=None, dry_run=False
):
"""Updates fuchsia's third-party dart packages.
Args:
presentation (StepPresentation): A presentation to attach logs, etc. to.
checkout_root (Path): Root path to checkouts.
flutter_revision (Optional): A git hash within the flutter repo.
dry_run (bool): Whether the roll will be allowed to complete or not.
Returns:
A string with third_party/dart-pkg/pub latest commit hash.
"""
commit_msg = "[roll] Update third-party dart packages\n"
packages_root = checkout_root.join("third_party", "dart-pkg", "pub")
with api.context(cwd=packages_root):
# Make sure third_party/dart-pkg is at origin/master before running the
# update script to catch any manual commits that extend past the revision at
# integration's HEAD.
api.git("git fetch", "fetch", "origin")
api.git("git checkout", "checkout", "origin/master")
flutter_flags = (
["--flutter-revision", flutter_revision] if flutter_revision else []
)
api.python(
"update dart 3p packages",
checkout_root.join("scripts", "dart", "update_3p_packages.py"),
args=["--debug"] + flutter_flags,
)
change = api.auto_roller.attempt_roll(
"fuchsia-review.googlesource.com",
gerrit_project="third_party/dart-pkg",
repo_dir=packages_root,
commit_message=commit_msg,
commit_untracked=True,
dry_run=dry_run,
)
# If we rolled changes, the final hash of the merged CL should
# correspond to the remote HEAD. Otherwise, we can assume that local
# HEAD is up to date with remote HEAD.
current_hash = change.revision if change else api.git.get_hash()
presentation.step_text = current_hash
return current_hash
def GenTests(api):
"""Tests for cipd with dependents roller."""
properties = {
"project": "integration",
"manifests": [
{
"project": "integration",
"manifest": "other/dependency",
"remote": "sso://fuchsia/integration",
"lock_file": "integration/jiri.lock",
},
{
"project": "integration",
"manifest": "fuchsia/flower",
"remote": "sso://fuchsia/integration",
"lock_file": "integration/fuchsia/jiri.lock",
},
],
"locks": [{"manifest": "a/b", "file": "a/b/jiri.lock"}],
"orchestrator_package": "fuchsia/orchestrator",
"orchestrator_import_in": "fuchsia/prebuilts",
"validate_against_package": {
"package": "flutter/fuchsia",
"version_tag": "flutter_version",
},
"rolling_packages_projects": [
{
"name": "dart/sdk",
"type": "project",
"import_in": "fuchsia/prebuilts",
"version_tag": "dart_version",
},
{
"name": "skia",
"type": "project",
"import_in": "fuchsia/prebuilts",
"version_tag": "skia_version",
},
{
"name": "flutter/fuchsia",
"type": "package",
"import_in": "fuchsia/prebuilts",
"version_tag": "engine_version",
},
{
"import_in": "integration/fuchsia/prebuilts",
"name": "fuchsia/dart-sdk/${platform}",
"type": "package",
"version_tag": "dart_version",
},
],
"versions_file": "flutter/versions.json",
"owners": ["abc@gmail.com"],
"dry_run": True,
"debug_symbol_packages": ["flutter/fuchsia-debug-symbols-x64"],
"debug_symbol_gcs_buckets": ["fuchsia-debug-symbols-shortlived"],
}
package_no_match_test_data = api.step_data(
"cipd describe fuchsia/orchestrator",
api.cipd.example_describe(
package_name="fuchsia/orchestrator",
version="version_abc",
test_data_tags=["git_revision:revision_abc"],
),
)
jiri_package_test_data = [
{
"name": "fuchsia/orchestrator",
"path": "fuchsia/orchestrator",
"version": "git_revision:revision_abc",
"manifest": "manifest1",
},
]
default_properties = api.properties(**properties)
# Version of the cipd orchestrator package is the same as the one
# in the tree.
yield (
api.status_check.test("nothing_to_roll")
+ default_properties
+ package_no_match_test_data
+ api.step_data("jiri package", api.jiri.package(jiri_package_test_data))
)
# Version of the cipd orchestrator package is different from the one
# in the tree and ready to get rolled but flutter version is the same as the
# one in the tree.
package_match_test_data = api.step_data(
"cipd describe fuchsia/orchestrator",
api.cipd.example_describe(
package_name="fuchsia/orchestrator",
version="version_abc",
test_data_tags=["git_revision:revision_jkl"],
),
)
package_versions = {
"flutter_version": "flutter_version_xyz",
"engine_version": "engine_version_xyz",
"skia_version": "skia_version_xyz",
"dart_version": "dart_version_xyz",
}
jiri_flutter_package_test_data = [
{
"name": "flutter/fuchsia",
"path": "flutter/fuchsia",
"version": "git_revision:flutter_version_xyz",
"manifest": "manifest1",
},
]
yield (
api.status_check.test("orchestrator_dep_version_same_as_tree")
+ default_properties
+ api.step_data("jiri package", api.jiri.package(jiri_package_test_data))
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ package_match_test_data
+ api.step_data(
"jiri package (2)", api.jiri.package(jiri_flutter_package_test_data)
)
)
# Version of the validation cipd package is different from the one in the tree
# and ready to get rolled along its dependencies.
jiri_flutter_package_test_data_no_match = [
{
"name": "flutter/fuchsia",
"path": "flutter/fuchsia",
"version": "git_revision:flutter_version_xyz1",
"manifest": "manifest1",
},
]
yield (
api.status_check.test(
"orchestrator_dep_version_different_from_tree_dry_run", status="failure"
)
+ default_properties
+ package_match_test_data
+ api.step_data("jiri package", api.jiri.package(jiri_package_test_data))
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data(
"jiri package (2)",
api.jiri.package(jiri_flutter_package_test_data_no_match),
)
+ api.buildbucket.build(
api.buildbucket.ci_build_message(builder="flutter-dependents-roller")
)
+ api.auto_roller.dry_run_success(
name="third-party dart packages.check for completion.check if done ({})"
)
)
# Validate the build and upload debug symbols path.
props = copy.deepcopy(properties)
props["dry_run"] = False
no_dry_run_props = api.properties(**props)
yield (
api.status_check.test("orchestrator_dep_version_different_from_tree")
+ no_dry_run_props
+ package_match_test_data
+ api.step_data("jiri package", api.jiri.package(jiri_package_test_data))
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data(
"jiri package (2)",
api.jiri.package(jiri_flutter_package_test_data_no_match),
)
+ api.auto_roller.success()
+ api.buildbucket.build(
api.buildbucket.ci_build_message(builder="flutter-dependents-roller")
)
+ api.auto_roller.success(
name="third-party dart packages.check for completion.check if done (0)"
)
)
# Validate the dar-sdk plugins steps is not executed when dart-sdk is not
# being rolled.
rolling_packages = [
{
"name": "dart/sdk",
"type": "project",
"import_in": "fuchsia/prebuilts",
"version_tag": "dart_version",
},
{
"name": "skia",
"type": "project",
"import_in": "fuchsia/prebuilts",
"version_tag": "skia_version",
},
{
"name": "flutter/fuchsia",
"type": "package",
"import_in": "fuchsia/prebuilts",
"version_tag": "engine_version",
},
]
props = copy.deepcopy(properties)
props["dry_run"] = False
props["rolling_packages_projects"] = rolling_packages
no_dry_run_props = api.properties(**props)
yield (
api.status_check.test("do_not_roll_plugins_if_dart_not_rolled")
+ no_dry_run_props
+ package_match_test_data
+ api.step_data("jiri package", api.jiri.package(jiri_package_test_data))
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data(
"jiri package (2)",
api.jiri.package(jiri_flutter_package_test_data_no_match),
)
+ api.auto_roller.success()
+ api.buildbucket.build(
api.buildbucket.ci_build_message(builder="flutter-dependents-roller")
)
)
# Validate that uploading debug symbols is skipped when the flutter/fuchsia
# package is not rolled.
rolling_packages = [
{
"name": "dart/sdk",
"type": "project",
"import_in": "fuchsia/prebuilts",
"version_tag": "dart_version",
},
{
"name": "skia",
"type": "project",
"import_in": "fuchsia/prebuilts",
"version_tag": "skia_version",
},
]
props = copy.deepcopy(properties)
props["dry_run"] = False
props["rolling_packages_projects"] = rolling_packages
no_dry_run_props = api.properties(**props)
yield (
api.status_check.test(
"do_not_upload_debug_symbols_if_flutter_fuchsia_is_not_rolled"
)
+ no_dry_run_props
+ package_match_test_data
+ api.step_data("jiri package", api.jiri.package(jiri_package_test_data))
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data(
"jiri package (2)",
api.jiri.package(jiri_flutter_package_test_data_no_match),
)
+ api.auto_roller.success()
+ api.buildbucket.build(
api.buildbucket.ci_build_message(builder="flutter-dependents-roller")
)
)