go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/luci/rules/cq_group.star (about)

     1  # Copyright 2019 The LUCI Authors.
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #      http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  
    15  """Defines luci.cq_group(...) rule."""
    16  
    17  load("@stdlib//internal/graph.star", "graph")
    18  load("@stdlib//internal/lucicfg.star", "lucicfg")
    19  load("@stdlib//internal/validate.star", "validate")
    20  load("@stdlib//internal/luci/common.star", "keys", "kinds")
    21  load("@stdlib//internal/luci/lib/acl.star", "acl", "aclimpl")
    22  load("@stdlib//internal/luci/lib/cq.star", "cq", "cqimpl")
    23  load("@stdlib//internal/luci/rules/cq_tryjob_verifier.star", "cq_tryjob_verifier")
    24  
    25  def _cq_group(
    26          ctx,  # @unused
    27          *,
    28          name = None,
    29          watch = None,
    30          acls = None,
    31          allow_submit_with_open_deps = None,
    32          allow_owner_if_submittable = None,
    33          trust_dry_runner_deps = None,
    34          allow_non_owner_dry_runner = None,
    35          tree_status_host = None,
    36          retry_config = None,
    37          cancel_stale_tryjobs = None,  # @unused
    38          verifiers = None,
    39          additional_modes = None,
    40          user_limits = None,
    41          user_limit_default = None,
    42          post_actions = None,
    43          tryjob_experiments = None):
    44      """Defines a set of refs to watch and a set of verifier to run.
    45  
    46      The CQ will run given verifiers whenever there's a pending approved CL for
    47      a ref in the watched set.
    48  
    49      Pro-tip: a command line tool exists to validate a locally generated .cfg
    50      file and verify that it matches arbitrary given CLs as expected.
    51      See https://chromium.googlesource.com/infra/luci/luci-go/+/refs/heads/main/cv/#luci-cv-command-line-utils
    52  
    53      **NOTE**: if you are configuring a luci.cq_group for a new Gerrit host,
    54      follow instructions at http://go/luci/cv/gerrit-pubsub to ensure that
    55      pub/sub integration is enabled for the Gerrit host.
    56  
    57      Args:
    58        ctx: the implicit rule context, see lucicfg.rule(...).
    59        name: a human- and machine-readable name this CQ group. Must be unique
    60          within this project. This is used in messages posted to users and in
    61          monitoring data. Must match regex `^[a-zA-Z][a-zA-Z0-9_-]*$`.
    62        watch: either a single cq.refset(...) or a list of cq.refset(...) (one per
    63          repo), defining what set of refs the CQ should monitor for pending CLs.
    64          Required.
    65        acls: list of acl.entry(...) objects with ACLs specific for this CQ group.
    66          Only `acl.CQ_*` roles are allowed here. By default ACLs are inherited
    67          from luci.project(...) definition. At least one `acl.CQ_COMMITTER` entry
    68          should be provided somewhere (either here or in luci.project(...)).
    69        allow_submit_with_open_deps: controls how a CQ full run behaves when the
    70          current Gerrit CL has open dependencies (not yet submitted CLs on which
    71          *this* CL depends). If set to False (default), the CQ will abort a full
    72          run attempt immediately if open dependencies are detected. If set to
    73          True, then the CQ will not abort a full run, and upon passing all other
    74          verifiers, the CQ will attempt to submit the CL regardless of open
    75          dependencies and whether the CQ verified those open dependencies. In
    76          turn, if the Gerrit project config allows this, Gerrit will submit all
    77          dependent CLs first and then this CL.
    78        allow_owner_if_submittable: allow CL owner to trigger CQ after getting
    79          `Code-Review` and other approvals regardless of `acl.CQ_COMMITTER` or
    80          `acl.CQ_DRY_RUNNER` roles. Only `cq.ACTION_*` are allowed here. Default
    81          is `cq.ACTION_NONE` which grants no additional permissions. CL owner is
    82          user owning a CL, i.e. its first patchset uploader, not to be confused
    83          with OWNERS files. **WARNING**: using this option is not recommended if
    84          you have sticky `Code-Review` label because this allows a malicious
    85          developer to upload a good looking patchset at first, get code review
    86          approval, and then upload a bad patchset and CQ it right away.
    87        trust_dry_runner_deps: consider CL dependencies that are owned by members
    88          of the `acl.CQ_DRY_RUNNER` role as trusted, even if they are not
    89          approved. By default, unapproved dependencies are only trusted if they
    90          are owned by members of the `acl.CQ_COMMITER` role. This allows CQ dry
    91          run on CLs with unapproved dependencies owned by members of
    92          `acl.CQ_DRY_RUNNER` role.
    93        allow_non_owner_dry_runner: allow members of the `acl.CQ_DRY_RUNNER` role
    94          to trigger DRY_RUN CQ on CLs that are owned by someone else, if all the
    95          CL dependencies are trusted.
    96        tree_status_host: a hostname of the project tree status app (if any). It
    97          is used by the CQ to check the tree status before committing a CL. If
    98          the tree is closed, then the CQ will wait until it is reopened.
    99        retry_config: a new cq.retry_config(...) struct or one of `cq.RETRY_*`
   100          constants that define how CQ should retry failed builds. See
   101          [CQ](#cq-doc) for more info. Default is `cq.RETRY_TRANSIENT_FAILURES`.
   102        cancel_stale_tryjobs: unused anymore, but kept for backward compatibility.
   103        verifiers: a list of luci.cq_tryjob_verifier(...) specifying what checks
   104          to run on a pending CL. See luci.cq_tryjob_verifier(...) for all
   105          details. As a shortcut, each entry can also either be a dict or a
   106          string. A dict is an alias for `luci.cq_tryjob_verifier(**entry)` and
   107          a string is an alias for `luci.cq_tryjob_verifier(builder = entry)`.
   108        additional_modes: either a single cq.run_mode(...) or a list of
   109          cq.run_mode(...) defining additional run modes supported by this CQ
   110          group apart from standard DRY_RUN and FULL_RUN. If specified, CQ will
   111          create the Run with the first mode for which triggering conditions are
   112          fulfilled. If there is no such mode, CQ will fallback to standard
   113          DRY_RUN or FULL_RUN.
   114        user_limits: a list of cq.user_limit(...) or None. **WARNING**: Please
   115          contact luci-eng@ before setting this param. They specify per-user
   116          limits/quotas for given principals. At the time of a Run start, CV looks
   117          up and applies the first matching cq.user_limit(...) to the Run, and
   118          postpones the start if limits were reached already. If none of the
   119          user_limit(s) were applicable, `user_limit_default` will be applied
   120          instead. Each cq.user_limit(...) must specify at least one user or
   121          group.
   122        user_limit_default: cq.user_limit(...) or None. **WARNING*:: Please
   123          contact luci-eng@ before setting this param. If none of limits in
   124          `user_limits` are applicable and `user_limit_default` is not specified,
   125          the user is granted unlimited runs and tryjobs. `user_limit_default`
   126          must not specify users and groups.
   127        post_actions: a list of post actions or None.
   128          Please refer to cq.post_action_* for all the available post actions.
   129          e.g., cq.post_action_gerrit_label_votes(...)
   130        tryjob_experiments: a list of cq.tryjob_experiment(...) or None. The
   131          experiments will be enabled when launching Tryjobs if condition is met.
   132      """
   133      key = keys.cq_group(validate.string("name", name))
   134  
   135      # Accept cq.refset passed as is (not wrapped in a list). Most CQ configs use
   136      # a single cq.refset.
   137      if watch and type(watch) != "list":
   138          watch = [watch]
   139      for w in validate.list("watch", watch, required = True):
   140          cqimpl.validate_refset("watch", w)
   141  
   142      # Accept cq.run_mode passed as is (not wrapped in a list).
   143      if additional_modes:
   144          if type(additional_modes) != "list":
   145              additional_modes = [additional_modes]
   146          validate.list("additional_modes", additional_modes, required = False)
   147          for m in additional_modes:
   148              cqimpl.validate_run_mode("run_mode", m)
   149  
   150      limit_names = dict()
   151      user_limits = validate.list("user_limits", user_limits)
   152      for i, lim in enumerate(user_limits):
   153          lim = cqimpl.validate_user_limit("user_limits[%d]" % i, lim, required = True)
   154          if lim.name in limit_names:
   155              fail("user_limits[%d]: duplicate limit name '%s'" % (i, lim.name))
   156          if not lim.principals:
   157              fail("user_limits[%d]: must specify at least one user or group" % i)
   158          limit_names[lim.name] = None
   159  
   160      user_limit_default = cqimpl.validate_user_limit(
   161          "user_limit_default",
   162          user_limit_default,
   163          required = False,
   164      )
   165  
   166      # TODO(crbug.com/1346143): make user_limit_default required.
   167      if user_limit_default != None:
   168          if user_limit_default.name in limit_names:
   169              fail("user_limit_default: limit name '%s' is already used in user_limits" % user_limit_default.name)
   170          if user_limit_default.principals:
   171              fail("user_limit_default: must not specify user or group")
   172  
   173      validate.list("post_actions", post_actions, required = False)
   174      known_action_names = dict()
   175      for i, pa in enumerate(post_actions or []):
   176          cqimpl.validate_post_action("post_actions[%d]" % i, pa, required = True)
   177          if pa.name in known_action_names:
   178              fail("post_action[%d]: duplicate post_action name '%s'" % (i, pa.name))
   179          known_action_names[pa.name] = i
   180  
   181      validate.list("tryjob_experiments", tryjob_experiments, required = False)
   182      known_exp_names = dict()
   183      for i, te in enumerate(tryjob_experiments or []):
   184          cqimpl.validate_tryjob_experiment(
   185              "tryjob_experiments[%d]" % i,
   186              te,
   187              required = True,
   188          )
   189          if te.name in known_exp_names:
   190              fail("tryjob_experiments[%d]: duplicate experiment name '%s'" % (i, te.name))
   191          known_exp_names[te.name] = i
   192  
   193      # TODO(vadimsh): Convert `acls` to luci.binding(...). Need to figure out
   194      # what realm to use for them. This probably depends on a design of
   195      # Realms + CQ which doesn't exist yet.
   196  
   197      graph.add_node(key, props = {
   198          "watch": watch,
   199          "acls": aclimpl.validate_acls(acls, allowed_roles = [acl.CQ_COMMITTER, acl.CQ_DRY_RUNNER, acl.CQ_NEW_PATCHSET_RUN_TRIGGERER]),
   200          "allow_submit_with_open_deps": validate.bool(
   201              "allow_submit_with_open_deps",
   202              allow_submit_with_open_deps,
   203              required = False,
   204          ),
   205          "allow_owner_if_submittable": validate.int(
   206              "allow_owner_if_submittable",
   207              allow_owner_if_submittable,
   208              default = cq.ACTION_NONE,
   209              required = False,
   210          ),
   211          "trust_dry_runner_deps": validate.bool(
   212              "trust_dry_runner_deps",
   213              trust_dry_runner_deps,
   214              required = False,
   215          ),
   216          "allow_non_owner_dry_runner": validate.bool(
   217              "allow_non_owner_dry_runner",
   218              allow_non_owner_dry_runner,
   219              required = False,
   220          ),
   221          "tree_status_host": validate.hostname("tree_status_host", tree_status_host, required = False),
   222          "retry_config": cqimpl.validate_retry_config(
   223              "retry_config",
   224              retry_config,
   225              default = cq.RETRY_TRANSIENT_FAILURES,
   226              required = False,
   227          ),
   228          "additional_modes": additional_modes,
   229          "user_limits": user_limits,
   230          "user_limit_default": user_limit_default,
   231          "post_actions": post_actions,
   232          "tryjob_experiments": tryjob_experiments,
   233      })
   234      graph.add_edge(keys.project(), key)
   235  
   236      # Add all verifiers, possibly instantiating them from dicts or direct
   237      # builder references (given either as strings or BUILDER_REF keysets).
   238      for v in validate.list("verifiers", verifiers):
   239          if type(v) == "dict":
   240              v = cq_tryjob_verifier(**v)
   241          elif type(v) == "string" or (graph.is_keyset(v) and v.has(kinds.BUILDER_REF)):
   242              v = cq_tryjob_verifier(builder = v)
   243          graph.add_edge(key, v.get(kinds.CQ_TRYJOB_VERIFIER))
   244  
   245      return graph.keyset(key)
   246  
   247  cq_group = lucicfg.rule(impl = _cq_group)