1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71

infra / config / validators / builder-group-triggers.star [blame]

# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A validator to enforce that triggers do not cross builder groups."""

load("@stdlib//internal/graph.star", "graph")
load("@stdlib//internal/luci/common.star", "keys", "triggerer")

# These triggers cross builder groups but predate us restricting triggers to
# within a builder group, we should attempt to remove these where feasible
_LEGACY_CROSS_BUILDER_GROUP_TRIGGERS = {
    ("ci", "Android x64 Builder (dbg)"): [
        ("ci", "android-12-x64-dbg-tests"),
        ("ci", "android-webview-12-x64-dbg-tests"),
        ("ci", "android-webview-13-x64-dbg-tests"),
    ],
    ("ci", "Mac Builder (dbg)"): [
        ("ci", "mac-osxbeta-rel"),
    ],
    ("ci", "Win x64 Builder"): [
        ("ci", "win-network-sandbox-tester"),
    ],
    ("ci", "android-12-x64-rel"): [
        ("ci", "android-12-x64-fyi-rel"),
    ],
    ("ci", "mac-arm64-rel"): [
        ("ci", "mac-fieldtrial-tester"),
    ],
}

def _check_trigger_builder_groups(ctx):
    cfg = ctx.output["luci/cr-buildbucket.cfg"]

    builder_group_by_builder = {}

    for bucket in cfg.buckets:
        if not proto.has(bucket, "swarming"):
            continue
        for builder in bucket.swarming.builders:
            builder_group = json.decode(builder.properties).get("builder_group")
            if builder_group != None:
                builder_group_by_builder[(bucket.name, builder.name)] = builder_group

    # Traverse the graph for triggering. If builder X triggers Y, the nodes will
    # be: BUILDER(X) -> TRIGGERER(X) -> BUILDER_REF(Y) -> BUILDER(Y)
    # The TRIGGERER nodes abstract things that can trigger (builders or pollers)
    # The BUILDER_REF nodes abstract out referring to a builder by simple name
    # (e.g. mac-rel) or bucket-qualified name (e.g. try/mac-rel)
    bad_triggers = []
    for (bucket_name, builder_name), builder_group in builder_group_by_builder.items():
        builder_node = graph.node(keys.builder(bucket_name, builder_name))
        legacy_triggers = _LEGACY_CROSS_BUILDER_GROUP_TRIGGERS.get((bucket_name, builder_name), [])
        for n in triggerer.targets(builder_node):
            child_bucket_name = n.key.container.id
            child_builder_name = n.key.id
            child_group = builder_group_by_builder.get((child_bucket_name, child_builder_name))
            if child_group == None:
                fail("A builder without a group is being triggered: {}/{} ({}) -> {}/{}"
                    .format(bucket_name, builder_name, builder_group, child_bucket_name, child_builder_name))
            if child_group == builder_group:
                continue
            if (child_bucket_name, child_builder_name) in legacy_triggers:
                continue

            bad_triggers.append("* {}/{} ({}) -> {}/{} ({})"
                .format(bucket_name, builder_name, builder_group, child_bucket_name, child_builder_name, child_group))

    if bad_triggers:
        fail("The following triggers cross builder groups:\n{}".format("\n".join(sorted(bad_triggers))))

lucicfg.generator(_check_trigger_builder_groups)