blob: 38aefdca85025815b49634ad76c6ba7eedc97a6a [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/main/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/copy.star", "copy")
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 _is_default_branch(branch) 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, recipes_ref):
"""Return the set of recipes specified in ci.yaml."""
recipe_names = {}
ci_yaml = _ci_yaml(repo, branch)
for target in ci_yaml.targets:
# Only LUCI based targets have recipes
if target.scheduler not in SUPPORTED_SCHEDULERS:
continue
if target.enabled_branches and branch not in target.enabled_branches:
continue
recipe_name = _full_recipe_name(target.recipe, version, recipes_ref)
if recipe_name not in recipe_names.keys():
recipe_names[recipe_name] = target.recipe
for recipe_name, recipe in recipe_names.items():
luci.recipe(
name = recipe_name,
recipe = recipe,
cipd_package = "flutter/recipe_bundles/flutter.googlesource.com/recipes",
cipd_version = recipes_ref,
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
return 30
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
# 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
# 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 properties:
continue
xcode_version = {
"sdk_version": 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
# Special case to add runtimes for ios simultators
if platform == "mac" and "runtime_versions" in properties:
# Add to existing osx_sdk properties dict, or create new
osx_sdk = properties.get("$flutter/osx_sdk", {})
osx_sdk["runtime_versions"] = properties["runtime_versions"]
properties["$flutter/osx_sdk"] = osx_sdk
filter_list = ["caches"]
for filter in filter_list:
if filter in properties:
properties.pop(filter)
if repo:
properties["git_repo"] = repo
if branch:
properties["git_branch"] = branch
# Add `bringup` to properties.
properties["bringup"] = target.bringup
return properties
def _full_recipe_name(recipe_name, version, recipes_ref):
"""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.
recipes_ref(str): The git ref pointing to the recipes bundle to use.
Returns:
A string with the recipe's full name.
"""
ref = recipes_ref.replace("refs/heads/", "")
return "%s-%s" % (ref, recipe_name)
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 not _is_default_branch(branch):
parts = builder_name.split(" ")
parts.insert(1, branch)
builder_name = " ".join(parts)
return "%s|%s" % (builder_name, common.short_name(builder_name))
def _is_default_branch(branch):
"""Returns whether the branch is a default branch."""
return branch in ("main", "master")
def _builders(repo, branch, version, testing_ref, dimensions, properties, triggering_policy, notifies = None, recipes_ref = "refs/heads/main"):
"""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.
recipes_ref(str): The git ref pointing to the recipes bundle to use.
"""
ci_yaml = _ci_yaml(repo, branch)
# Only generate presubmit and staging builds on tip of tree
staging_view_name = "%s_staging" % repo
if _is_default_branch(branch):
list_view_name = "%s-try" % repo
luci.list_view(
name = list_view_name,
title = "%s try builders" % repo.capitalize(),
)
luci.console_view(
name = staging_view_name,
repo = repos.GIT_REMOTE[repo],
refs = [testing_ref],
)
console_view_name = repo if _is_default_branch(branch) 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
# Flaky tests should not be on releases
if not _is_default_branch(branch) and target.bringup:
continue
platform = _target_platform(target)
merged_properties = _properties(ci_yaml, target, properties, repo, branch)
target_args = {
"caches": _swarming_caches(ci_yaml, target, platform),
"category": platform.capitalize(),
"dimensions": {},
# On release branches, extend the timeout to handle goma misses.
# TODO(godofredoc): Remove after hotfix 2.5.3 is released.
"execution_timeout": target.timeout * time.minute * 3,
"name": _builder_name(target.name, branch),
"os": merged_properties["os"],
"recipe": _full_recipe_name(target.recipe, version, recipes_ref),
"repo": repos.GIT_REMOTE[repo],
"properties": merged_properties,
}
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]
# Try builds are generated for every target to enable led and presubmit runs
if _is_default_branch(branch):
presubmit_target_args = copy.deepcopy(target_args)
presubmit_target_args["list_view_name"] = list_view_name
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):
# Set customized triggering policy for engine staging builders.
# https://github.com/flutter/flutter/issues/92947
if target.bringup:
final_triggering_policy = scheduler.greedy_batching(
max_batch_size = 20,
max_concurrent_invocations = 1,
)
else:
final_triggering_policy = triggering_policy
postsubmit_target_args = helpers.merge_dicts({
"bucket": "staging" if target.bringup else "prod",
"console_view_name": staging_view_name if target.bringup else console_view_name,
"expiration_timeout": 24 * time.hour if target.bringup else None,
"pool": "luci.flutter.staging" if target.bringup else "luci.flutter.prod",
"priority": _priority(target, branch),
# Only LUCI targets need triggers to the LUCI scheduler.
"triggered_by": [trigger_name] if target.scheduler == SCHEDULER_LUCI else None,
"triggering_policy": final_triggering_policy if target.scheduler == SCHEDULER_LUCI else None,
"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 _is_default_branch(branch):
postsubmit_target_args["properties"]["upload_metrics"] = True
# Set a 2h expiration_timeout for non flaky devicelab builders.
if not target.bringup:
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."""
platform = target.name.split(" ")[0]
return platform.lower()
def _generate(repo, branch, version, testing_ref, dimensions = {}, properties = {}, triggering_policy = None, notifies = None, recipes_ref = "refs/heads/main"):
# 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 _is_default_branch(branch):
common.cq_group(repos.GIT_REMOTE[repo])
_recipes(repo, branch, version, recipes_ref)
_builders(repo, branch, version, testing_ref, dimensions, properties, triggering_policy, notifies = notifies, recipes_ref = recipes_ref)
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,
)