blob: fb8898a8b148f2816115421f30b24ab4688f4b0a [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.
This recipe rolls Flutter and related packages not to HEAD, but to a version
specified by a file that appears in a Fuchsia checkout. Essentially, it looks up
the version of Flutter used by a particular prebuilt package and rolls to that.
We use the fact that the prebuilt package successfully released and rolled into
fuchisa.git as a signal that the version of Flutter it uses is good.
In this recipe we are calling the file that indicates the desired versions of
Flutter (and dependencies) the "orchestrator file".
The orchestrator file is a json file of the following form:
{
"flutter_version": "flutter_version_xyz",
"engine_version": "engine_version_xyz",
"skia_version": "skia_version_xyz",
"dart_version": "dart_version_xyz"
}
For each package and project listed in the `rolling_packages_projects`
parameter, this recipe updates its jiri manifest to match the correct version
from the orchestrator file. If everything is already at the correct version, it
simply bails out with "nothing to roll".
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.
"""
from recipe_engine.config import List
from recipe_engine.recipe_api import Property
DEPS = [
"fuchsia/auto_roller",
"fuchsia/buildbucket_util",
"fuchsia/debug_symbols",
"fuchsia/gerrit",
"fuchsia/git",
"fuchsia/jiri",
"fuchsia/status_check",
"recipe_engine/buildbucket",
"recipe_engine/context",
"recipe_engine/file",
"recipe_engine/json",
"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"}]'
),
),
"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"}]'
),
),
"orchestrator_file_path": Property(
kind=str,
help=(
"A string with the path to the orchestrator file inside a "
"checked-out fuchsia build."
),
),
"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,
owners,
orchestrator_file_path,
rolling_packages_projects,
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()
version_dict = api.file.read_json(
"read versions", api.path["start_dir"].join(orchestrator_file_path)
)
# Roll all the dependencies
package_msgs = []
package_msg_template = "%s %s rolled from %s to %s"
updated_packages_projects = set()
with api.step.nest("edit jiri manifests"):
for package_project in rolling_packages_projects:
if package_project["type"] == "package":
expected_version = (
"git_revision:%s" % version_dict[package_project["version_tag"]]
)
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"]:
updated_packages_projects.add(package_project["name"])
old_version = changes["packages"][0]["old_version"]
package_msgs.append(
package_msg_template
% (
"package",
package_project["name"],
old_version,
expected_version,
)
)
else:
expected_version = 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"]:
updated_packages_projects.add(package_project["name"])
old_version = changes["projects"][0]["old_revision"]
package_msgs.append(
package_msg_template
% (
"project",
package_project["name"],
old_version,
expected_version,
)
)
if not updated_packages_projects:
return api.auto_roller.nothing_to_roll()
# Update fuchsia's third-party dart packages (including flutter dependencies),
# only if we are rolling a new version of dart-sdk.
if "fuchsia/dart-sdk/${platform}" in updated_packages_projects:
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=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"]]
)
message = COMMIT_MESSAGE.format(
roller=api.buildbucket.builder_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" in updated_packages_projects
and debug_symbol_packages
and not dry_run
):
api.debug_symbols.fetch_and_upload(
packages=debug_symbol_packages,
version=("git_revision:%s" % version_dict["engine_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/main 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/main")
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"}],
"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",
},
],
"orchestrator_file_path": "some_prebuilt/flutter/versions.json",
"owners": ["abc@gmail.com"],
"dry_run": False,
"debug_symbol_packages": ["flutter/fuchsia-debug-symbols-x64"],
"debug_symbol_gcs_buckets": ["fuchsia-debug-symbols-shortlived"],
}
jiri_package_test_data = [
{
"name": "fuchsia/orchestrator",
"path": "fuchsia/orchestrator",
"version": "git_revision:revision_abc",
"manifest": "manifest1",
},
]
default_properties = api.properties(**properties)
# Flutter version is the same as the one in the tree.
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",
},
]
jiri_no_changes_output = api.json.output(api.jiri.example_edit([], [], []))
yield (
api.status_check.test("nothing_to_roll")
+ default_properties
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data("edit jiri manifests.dart/sdk", jiri_no_changes_output)
+ api.step_data("edit jiri manifests.skia", jiri_no_changes_output)
+ api.step_data("edit jiri manifests.flutter/fuchsia", jiri_no_changes_output)
+ api.step_data(
"edit jiri manifests.fuchsia/dart-sdk/${platform}", jiri_no_changes_output
)
)
yield (
api.status_check.test("everything_to_roll")
+ default_properties
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.buildbucket.build(
api.buildbucket.ci_build_message(builder="flutter-dependents-roller")
)
+ api.auto_roller.success()
+ api.auto_roller.success(
name="third-party dart packages.check for completion.check if done (0)"
)
)
yield (
api.status_check.test("roll_neither_flutter_nor_dart")
+ default_properties
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data("edit jiri manifests.dart/sdk", jiri_no_changes_output)
# Default response (an update happened) for skia.
+ api.step_data("edit jiri manifests.flutter/fuchsia", jiri_no_changes_output)
+ api.step_data(
"edit jiri manifests.fuchsia/dart-sdk/${platform}", jiri_no_changes_output
)
+ api.auto_roller.success()
)
yield (
api.status_check.test("roll_flutter_package_alone")
+ default_properties
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data("edit jiri manifests.dart/sdk", jiri_no_changes_output)
+ api.step_data("edit jiri manifests.skia", jiri_no_changes_output)
# Default response (an update happened) for flutter/fuchsia.
+ api.step_data(
"edit jiri manifests.fuchsia/dart-sdk/${platform}", jiri_no_changes_output
)
+ api.auto_roller.success()
)
yield (
api.status_check.test("roll_dart_sdk_package_alone")
+ default_properties
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.step_data("edit jiri manifests.dart/sdk", jiri_no_changes_output)
+ api.step_data("edit jiri manifests.skia", jiri_no_changes_output)
+ api.step_data("edit jiri manifests.flutter/fuchsia", jiri_no_changes_output)
# Default response (an update happened) for fuchsia/dart-sdk/${platform}.
+ api.auto_roller.success()
+ api.auto_roller.success(
name="third-party dart packages.check for completion.check if done (0)"
)
)
yield (
api.status_check.test("dry_run")
+ api.properties(**dict(properties.items() + [("dry_run", True)]))
+ api.step_data(
"read versions", api.file.read_json(json_content=package_versions)
)
+ api.auto_roller.success()
+ api.auto_roller.success(
name="third-party dart packages.check for completion.check if done (0)"
)
)