blob: bf7b983cb898eb1ad0628cb881b7ee93e6f9edff [file] [log] [blame]
#!/usr/bin/env lucicfg
# Copyright 2020 The Flutter Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Utility for loading Flutter infrastructure's yaml config into LUCI.
See https://github.com/flutter/cocoon/blob/master/CI_YAML.md for more information.
"""
proto.new_descriptor_set(
name = "scheduler",
blob = io.read_file("./scheduler.bin"),
).register()
load("@proto//scheduler.proto", _scheduler_pb = "scheduler")
load("//lib/common.star", "common")
load("//lib/helpers.star", "helpers")
load("//lib/repos.star", "repos")
def _ci_yaml(repo, branch):
"""Creates ci_yaml object.
ci_yamls are expected to have a generated jspb file under
//config/generated/ci_yaml with the format `$repo_config.json` for tip of
tree or `$repo_$branch_config.json` for release branches.
ci_yaml json files are autogenerated based on the .ci.yaml in $repo.
Args:
repo(str): Name of the repository to follow.
branch(str): Branch name.
"""
version = "" if branch == "master" else "_%s" % branch
return proto.from_jsonpb(
_scheduler_pb.SchedulerConfig,
io.read_file("./generated/ci_yaml/%s%s_config.json" % (repo, version)),
)
# lucicfg does not currently support enums from protos.
# To work around this, statically redeclare the values from:
# //config/lib/ci_yaml/scheduler.proto - SchedulerSytem
SCHEDULER_COCOON = 1
SCHEDULER_LUCI = 2
# lucicfg is only intended to generate LUCI based targets.
SUPPORTED_SCHEDULERS = [SCHEDULER_COCOON, SCHEDULER_LUCI]
def _recipes(repo, branch, version):
"""Return the set of recipes specified in ci.yaml."""
recipe_names = []
ci_yaml = _ci_yaml(repo, branch)
for target in ci_yaml.targets:
if target.enabled_branches and branch not in target.enabled_branches:
continue
recipe_name = _full_recipe_name(target.recipe, version)
if recipe_name not in recipe_names:
recipe_names.append(recipe_name)
for recipe in recipe_names:
luci.recipe(
name = recipe,
cipd_package = "flutter/recipe_bundles/flutter.googlesource.com/recipes",
cipd_version = "refs/heads/master",
use_bbagent = True,
)
def _add_recipes_cq(target):
if "add_recipes_cq" in target.properties:
return target.properties["add_recipes_cq"] == "true"
return False
def _priority(target, branch):
"""Return the priority of the target on branch."""
if target.bringup:
return 35
if branch == "master":
return 30
return 25
def _swarming_caches(ci_yaml, target, platform):
"""Return the equivalent swarming caches for target.
Targets inherit from their platform_properties and can
specify their own caches.
"""
swarming_caches = []
caches = []
cache_set = []
# Target specific caches
if target and "caches" in target.properties:
caches = json.decode(target.properties["caches"])
# Mark all in unique set to ensure platform does not override
for cache in caches:
cache_set.append(cache["name"])
# Platform wide caches
if "caches" in ci_yaml.platform_properties[platform].properties:
platform_caches = json.decode(ci_yaml.platform_properties[platform].properties["caches"])
# Ensure platform wide caches do not override target level caches
for platform_cache in platform_caches:
if platform_cache["name"] not in cache_set:
caches.append(platform_cache)
# Generate LUCI config caches
for cache in caches:
swarming_caches.append(swarming.cache(name = cache["name"], path = cache["path"]))
return swarming_caches
def _platform_properties(ci_yaml):
"""Gets platform_properties from ci_yaml."""
platform_properties = {}
for platform, map in dict(ci_yaml.platform_properties).items():
platform_properties[platform] = {}
for k, v in dict(ci_yaml.platform_properties[platform].properties).items():
if v == "true":
platform_properties[platform][k] = True
elif v == "false":
platform_properties[platform][k] = False
elif v.startswith("["):
platform_properties[platform][k] = json.decode(v)
else:
platform_properties[platform][k] = v
return platform_properties
def _properties(ci_yaml, target, default_properties = {}, repo = None, branch = None):
"""Creates builder properties based on ci_yaml.
Args:
ci_yaml(SchedulerConfig): Config file that contains target.
target(Target): Configuration to generate properties for.
default_properties(dict): Repo level properties.
repo(string): Name of the repo.
branch(string): Name of the branch.
"""
properties = {}
# Initialize with default repo properties
for k, v in default_properties.items():
properties[k] = v
# Update with platform specific properties
platform = _target_platform(target)
for k, v in _platform_properties(ci_yaml)[platform].items():
properties[k] = v
# Special case to pass xcode for engine
for xcode_platform in ["mac", "mac_ios", "mac_ios32"]:
if platform != xcode_platform:
continue
if "xcode" not in ci_yaml.platform_properties[platform].properties:
continue
xcode_version = {
"sdk_version": ci_yaml.platform_properties[platform].properties["xcode"],
}
# DeviceLab bots are on a slower lab network. We avoid LUCI caches on those bots
if platform in ["mac_ios", "mac_ios32"]:
properties["$flutter/devicelab_osx_sdk"] = xcode_version
else:
properties["$flutter/osx_sdk"] = xcode_version
# Finally, update with target specific properties
json_list = ["android_sdk_license", "android_sdk_preview_license"]
for k, v in dict(target.properties).items():
if k == "dependencies":
# Merge target dependencies with platform
dependencies = json.decode(v)
target_dependency_lookup_table = []
for dep in dependencies:
target_dependency_lookup_table.append(dep["dependency"])
# Add platform dependencies that are not already defined
for dep in properties[k]:
if dep["dependency"] not in target_dependency_lookup_table:
dependencies.append(dep)
properties[k] = dependencies
elif v == "true":
properties[k] = True
elif v == "false":
properties[k] = False
elif k in json_list:
# Yaml -> JSON -> Here adds an extra string escape that we unescape here
properties[k] = v.replace("\\n", "\n")
elif v.startswith("["):
properties[k] = json.decode(v)
elif v.isdigit():
properties[k] = int(v)
else:
properties[k] = v
filter_list = ("caches", "os")
for filter in filter_list:
if filter in properties:
properties.pop(filter)
if repo:
properties["git_repo"] = repo
if branch:
properties["git_branch"] = branch
return properties
def _full_recipe_name(recipe_name, version):
"""Creates a recipe name for recipe and version.
Args:
recipe_name(str): This is a string with the recipe base name.
version(str): A string with the build version. E.g. dev, beta, stable.
Returns:
A string with the recipe's full name.
"""
return recipe_name if not version else "%s_%s" % (recipe_name, version)
def _builder_name(builder_name, branch):
"""Creates a builder name that is versioned to branch.
Args:
builder_name(str): The original builder name.
branch(str): Branch this builder is for.
"""
if branch != "master":
parts = builder_name.split(" ")
parts.insert(1, branch)
builder_name = " ".join(parts)
return "%s|%s" % (builder_name, common.short_name(builder_name))
def _builders(repo, branch, version, testing_ref, dimensions, properties, triggering_policy, notifies = None):
"""Return the builders specified in ci.yaml.
Targets defined in .ci.yaml are translated to their LUCI equivalents.
Mostly, this is forward fields to the LUCI equivalent, but there
are some fields that are special cased.
Args:
repo(str): Name of the repo to create builders for.
branch(str): Name of the branch to create builders for. Either None:dev|beta|stable.
version(str): The branch number, following major_minor.
testing_ref(str): The git ref to trigger builds on.
dimensions(dict): Platform dimensions to pass.
properties(dict): Repo level properties to pass to every builder.
triggering_policy(scheduler): Scheduler policy to implement on postsubmit builds.
notifies(list): List of luci.notifier to send notifications on events from builders.
"""
ci_yaml = _ci_yaml(repo, branch)
if branch == "master":
list_view_name = "%s-try" % repo
luci.list_view(
name = list_view_name,
title = "%s try builders" % repo.capitalize(),
)
console_view_name = repo if branch == "master" else "%s_%s" % (branch, repo)
luci.console_view(
name = console_view_name,
repo = repos.GIT_REMOTE[repo],
refs = [testing_ref],
)
# Defines prod schedulers
trigger_name = "%s-gitiles-trigger-%s" % (branch, repo)
luci.gitiles_poller(
name = trigger_name,
bucket = "prod",
repo = repos.GIT_REMOTE[repo],
refs = [testing_ref],
)
# Defines default triggering policy
if not triggering_policy:
triggering_policy = scheduler.greedy_batching(
max_batch_size = 1,
max_concurrent_invocations = 3,
)
for target in ci_yaml.targets:
# Not all targets in ci.yaml are LUCI based, skip those.
if target.scheduler not in SUPPORTED_SCHEDULERS:
continue
platform = _target_platform(target)
target_args = {
"caches": _swarming_caches(ci_yaml, target, platform),
"category": "bringup" if target.bringup else platform.capitalize(),
"dimensions": {},
"execution_timeout": target.timeout * time.minute,
"name": _builder_name(target.name, branch),
"os": ci_yaml.platform_properties[platform].properties["os"],
"recipe": _full_recipe_name(target.recipe, version),
"repo": repos.GIT_REMOTE[repo],
"properties": _properties(ci_yaml, target, properties, repo, branch),
}
if platform in dimensions:
target_args["dimensions"] = dimensions[platform]
dimension_key_list = ("device_type", "device_os", "mac_model")
platform_properties = _platform_properties(ci_yaml)[platform]
for dimension_key in dimension_key_list:
if dimension_key in platform_properties:
target_args["dimensions"][dimension_key] = platform_properties[dimension_key]
# Presubmit is only based on tip of tree
if target.presubmit and branch == "master":
presubmit_target_args = helpers.merge_dicts({
"list_view_name": list_view_name,
}, target_args)
if repo == "engine":
presubmit_target_args["properties"]["no_lto"] = True
if _add_recipes_cq(target):
presubmit_target_args["add_cq"] = True
common.common_try_builder(**presubmit_target_args)
# Postsubmit only generates if the target is enabled for this branch, which defaults to true.
if target.postsubmit and (not target.enabled_branches or branch in target.enabled_branches):
postsubmit_target_args = helpers.merge_dicts({
"console_view_name": console_view_name,
"priority": _priority(target, branch),
"triggered_by": [trigger_name],
"triggering_policy": triggering_policy,
"notifies": notifies,
}, target_args)
# TODO(chillers): Remove when postsubmit properties are supported. https://github.com/flutter/flutter/issues/87675
if target.recipe == "devicelab/devicelab_drone":
if branch == "master":
postsubmit_target_args["properties"]["upload_metrics"] = True
postsubmit_target_args["expiration_timeout"] = 120 * time.minute
common.common_prod_builder(**postsubmit_target_args)
def _target_platform(target):
"""Return the target platform based on the name."""
supported_platforms = ("Linux", "Linux_android", "Mac", "Mac_android", "Mac_ios", "Mac_ios32", "Windows", "Windows_android")
platform = target.name.split(" ")[0]
if platform in supported_platforms:
return platform.lower()
fail("Failed to find platform for %s" % target.name)
def _generate(repo, branch, version, testing_ref, dimensions = {}, properties = {}, triggering_policy = None, notifies = None):
# CQ group configurations. Only FLUTTER_RECIPES is using
# LUCI CQ but we still need the CQ configurations for all
# the try configurations for led recipe tests.
if branch == "master":
common.cq_group(repos.GIT_REMOTE[repo])
_recipes(repo, branch, version)
_builders(repo, branch, version, testing_ref, dimensions, properties, triggering_policy, notifies = notifies)
ci_yaml = struct(
builder_name = _builder_name,
ci_yaml = _ci_yaml,
full_recipe_name = _full_recipe_name,
generate = _generate,
platform_properties = _platform_properties,
swarming_caches = _swarming_caches,
)