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)