go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/luci/rules/cq_tryjob_verifier.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_tryjob_verifier(...) rule.""" 16 17 load("@stdlib//internal/graph.star", "graph") 18 load("@stdlib//internal/lucicfg.star", "lucicfg") 19 load("@stdlib//internal/re.star", "re") 20 load("@stdlib//internal/validate.star", "validate") 21 load("@stdlib//internal/luci/common.star", "keys", "kinds") 22 load("@stdlib//internal/luci/lib/cq.star", "cq", "cqimpl") 23 24 def _cq_tryjob_verifier( 25 ctx, # @unused 26 builder = None, 27 *, 28 cq_group = None, 29 includable_only = None, 30 result_visibility = None, 31 disable_reuse = None, 32 cancel_stale = None, 33 experiment_percentage = None, 34 location_filters = None, 35 owner_whitelist = None, 36 equivalent_builder = None, 37 equivalent_builder_percentage = None, 38 equivalent_builder_whitelist = None, 39 mode_allowlist = None): 40 """A verifier in a luci.cq_group(...) that triggers tryjobs to verify CLs. 41 42 When processing a CL, the CQ examines a list of registered verifiers and 43 launches new corresponding builds (called "tryjobs") if it decides this is 44 necessary (per the configuration of the verifier and the previous history 45 of this CL). 46 47 The CQ automatically retries failed tryjobs (per configured `retry_config` 48 in luci.cq_group(...)) and only allows CL to land if each builder has 49 succeeded in the latest retry. If a given tryjob result is too old (>1 day) 50 it is ignored. 51 52 #### Filtering based on files touched by a CL 53 54 The CQ can examine a set of files touched by the CL and decide to skip this 55 verifier. Touching a file means either adding, modifying or removing it. 56 57 This is controlled by the `location_filters` field. 58 59 location_filters is a list of filters, each of which includes regular 60 expressions for matching Gerrit host, project, and path. The Gerrit host, 61 Gerrit project and file path for each file in each CL are matched against 62 the filters; The last filter that matches all paterns determines whether 63 the file is considered included (not skipped) or excluded (skipped); if the 64 last matching LocationFilter has exclude set to true, then the builder is 65 skipped. If none of the LocationFilters match, then the file is considered 66 included if the first rule is an exclude rule; else the file is excluded. 67 68 The comparison is a full match. The pattern is implicitly anchored with `^` 69 and `$`, so there is no need add them. The pattern must use [Google 70 Re2](https://github.com/google/re2) library syntax, [documented 71 here](https://github.com/google/re2/wiki/Syntax). 72 73 This filtering currently cannot be used in any of the following cases: 74 75 * For verifiers in CQ groups with `allow_submit_with_open_deps = True`. 76 77 Please talk to CQ owners if these restrictions are limiting you. 78 79 ##### Examples 80 81 Enable the verifier only for all CLs touching any file in `third_party/blink` 82 directory of the `chromium/src` repo. 83 84 luci.cq_tryjob_verifier( 85 location_filters = [ 86 cq.location_filter( 87 gerrit_host_regexp = 'chromium-review.googlesource.com', 88 gerrit_project_regexp = 'chromium/src' 89 path_regexp = 'third_party/blink/.+') 90 ], 91 ) 92 93 Enable the verifier for CLs that touch files in "foo/", on any host and repo. 94 95 luci.cq_tryjob_verifier( 96 location_filters = [ 97 cq.location_filter(path_regexp = 'foo/.+') 98 ], 99 ) 100 101 Disable the verifier for CLs that *only* touches the "all/one.txt" file in 102 "repo" of "example.com". If the CL touches anything else in the same host 103 and repo, or touches any file in a different repo and/or host, the verifier 104 will be enabled. 105 106 luci.cq_tryjob_verifier( 107 location_filters = [ 108 cq.location_filter( 109 gerrit_host_regexp = 'example.com', 110 gerrit_project_regexp = 'repo', 111 path_regexp = 'all/one.txt', 112 exclude = True), 113 ], 114 ) 115 116 Match a CL which touches at least one file other than `one.txt` inside 117 `all/` directory of the Gerrit project `repo`: 118 119 luci.cq_tryjob_verifier( 120 location_filters = [ 121 cq.location_filter( 122 gerrit_host_regexp = 'example.com', 123 gerrit_project_regexp = 'repo', 124 path_regexp = 'all/.+'), 125 cq.location_filter( 126 gerrit_host_regexp = 'example.com', 127 gerrit_project_regexp = 'repo', 128 path_regexp = 'all/one.txt', 129 exclude = True), 130 ], 131 ) 132 133 #### Per-CL opt-in only builders 134 135 For builders which may be useful only for some CLs, predeclare them using 136 `includable_only=True` flag. Such builders will be triggered by CQ if and 137 only if a CL opts in via `CQ-Include-Trybots: <builder>` in its description. 138 139 For example, default verifiers may include only fast builders which skip low 140 level assertions, but for coverage of such assertions one may add slower 141 "debug" level builders into which CL authors opt-in as needed: 142 143 # triggered & required for all CLs. 144 luci.cq_tryjob_verifier(builder="win") 145 # triggered & required if only if CL opts in via 146 # `CQ-Include-Trybots: project/try/win-debug`. 147 luci.cq_tryjob_verifier(builder="win-debug", includable_only=True) 148 149 #### Declaring verifiers 150 151 `cq_tryjob_verifier` is used inline in luci.cq_group(...) declarations to 152 provide per-builder verifier parameters. `cq_group` argument can be omitted 153 in this case: 154 155 luci.cq_group( 156 name = 'Main CQ', 157 ... 158 verifiers = [ 159 luci.cq_tryjob_verifier( 160 builder = 'Presubmit', 161 disable_reuse = True, 162 ), 163 ... 164 ], 165 ) 166 167 168 It can also be associated with a luci.cq_group(...) outside of 169 luci.cq_group(...) declaration. This is in particular useful in functions. 170 For example: 171 172 luci.cq_group(name = 'Main CQ') 173 174 def try_builder(name, ...): 175 luci.builder(name = name, ...) 176 luci.cq_tryjob_verifier(builder = name, cq_group = 'Main CQ') 177 178 #### Declaring a Tricium analyzer 179 180 `cq_tryjob_verifier` can be used to declare a [Tricium] analyzer by 181 providing the builder and `mode_allowlist=[cq.MODE_ANALYZER_RUN]`. It will 182 generate the Tricium config as well as CQ config, so that no additional 183 changes should be required as Tricium is merged into CV. 184 185 However, the following restrictions apply until CV takes on Tricium: 186 187 * Most CQ features are not supported except for `location_filters` and 188 `owner_whitelist`. If provided, they must meet the following conditions: 189 * `location_filters` must specify either both host_regexp and 190 project_regexp or neither. For path_regexp, it must match file 191 extension only (e.g. `.+\\.py`) or everything. Note that, the exact 192 same set of Gerrit repos should be specified across all analyzers in 193 this cq_group and across each unique file extension. 194 * `owner_whitelist` must be the same for all analyzers declared 195 in this cq_group. 196 * Analyzers will run on changes targeting **all refs** of the Gerrit repos 197 watched by the containing cq_group (or repos derived from 198 location_filters, see above) even though refs or refs_exclude may be 199 provided. 200 * All analyzers must be declared in a single luci.cq_group(...). 201 202 For example: 203 204 luci.project(tricium="tricium-prod.appspot.com") 205 206 luci.cq_group( 207 name = 'Main CQ', 208 ... 209 verifiers = [ 210 luci.cq_tryjob_verifier( 211 builder = "spell-checker", 212 owner_whitelist = ["project-committer"], 213 mode_allowlist = [cq.MODE_ANALYZER_RUN], 214 ), 215 luci.cq_tryjob_verifier( 216 builder = "go-linter", 217 location_filters = [cq.location_filter(path_regexp = ".+\\.go")] 218 owner_whitelist = ["project-committer"], 219 mode_allowlist = [cq.MODE_ANALYZER_RUN], 220 ), 221 luci.cq_tryjob_verifier(builder = "Presubmit"), 222 ... 223 ], 224 ) 225 226 Note for migrating to lucicfg for LUCI Projects whose sole purpose is 227 to host a single Tricium config today 228 ([Example](https://fuchsia.googlesource.com/infra/config/+/HEAD/repositories/infra/recipes/tricium-prod.cfg)): 229 230 Due to the restrictions mentioned above, it is not possible to merge those 231 auxiliary Projects back to the main LUCI Project. It will be unblocked 232 after Tricium is folded into CV. To migrate, users can declare new 233 luci.cq_group(...)s in those Projects to host Tricium analyzers. However, 234 CQ config should not be generated because the config groups will overlap 235 with the config group in the main LUCI Project (i.e. watch same refs) and 236 break CQ. This can be done by asking lucicfg to track only Tricium config: 237 `lucicfg.config(tracked_files=["tricium-prod.cfg"])`. 238 239 [Tricium]: https://chromium.googlesource.com/infra/infra/+/HEAD/go/src/infra/tricium 240 241 Args: 242 ctx: the implicit rule context, see lucicfg.rule(...). 243 builder: a builder to launch when verifying a CL, see luci.builder(...). 244 Can also be a reference to a builder defined in another project. See 245 [Referring to builders in other projects](#external-builders) for more 246 details. Required. 247 cq_group: a CQ group to add the verifier to. Can be omitted if 248 `cq_tryjob_verifier` is used inline inside some luci.cq_group(...) 249 declaration. 250 result_visibility: can be used to restrict the visibility of the tryjob 251 results in comments on Gerrit. Valid values are `cq.COMMENT_LEVEL_FULL` 252 and `cq.COMMENT_LEVEL_RESTRICTED` constants. Default is to give full 253 visibility: builder name and full summary markdown are included in the 254 Gerrit comment. 255 cancel_stale: Controls whether not yet finished builds previously 256 triggered by CQ will be cancelled as soon as a substantially different 257 patchset is uploaded to a CL. Default is True, meaning CQ will cancel. 258 In LUCI Change Verifier (aka CV, successor of CQ), changing this 259 option will only take effect on newly-created Runs once config 260 propagates to CV. Ongoing Runs will retain the old behavior. 261 (TODO(crbug/1127991): refactor this doc after migration. As of 09/2020, 262 CV implementation is WIP) 263 includable_only: if True, this builder will only be triggered by CQ if it 264 is also specified via `CQ-Include-Trybots:` on CL description. Default 265 is False. See the explanation above for all details. For builders with 266 `experiment_percentage` or `location_filters`, don't specify 267 `includable_only`. Such builders can already be forcefully added via 268 `CQ-Include-Trybots:` in the CL description. 269 disable_reuse: if True, a fresh build will be required for each CQ 270 attempt. Default is False, meaning the CQ may re-use a successful build 271 triggered before the current CQ attempt started. This option is 272 typically used for verifiers which run presubmit scripts, which are 273 supposed to be quick to run and provide additional OWNERS, lint, etc. 274 checks which are useful to run against the latest revision of the CL's 275 target branch. 276 experiment_percentage: when this field is present, it marks the verifier 277 as experimental. Such verifier is only triggered on a given percentage 278 of the CLs and the outcome does not affect the decision whether a CL can 279 land or not. This is typically used to test new builders and estimate 280 their capacity requirements. 281 location_filters: a list of cq.location_filter(...). 282 owner_whitelist: a list of groups with accounts of CL owners to enable 283 this builder for. If set, only CLs owned by someone from any one of 284 these groups will be verified by this builder. 285 equivalent_builder: an optional alternative builder for the CQ to choose 286 instead. If provided, the CQ will choose only one of the equivalent 287 builders as required based purely on the given CL and CL's owner and 288 **regardless** of the possibly already completed try jobs. 289 equivalent_builder_percentage: a percentage expressing probability of the 290 CQ triggering `equivalent_builder` instead of `builder`. A choice itself 291 is made deterministically based on CL alone, hereby all CQ attempts on 292 all patchsets of a given CL will trigger the same builder, assuming CQ 293 config doesn't change in the mean time. Note that if 294 `equivalent_builder_whitelist` is also specified, the choice over which 295 of the two builders to trigger will be made only for CLs owned by the 296 accounts in the whitelisted group. Defaults to 0, meaning the equivalent 297 builder is never triggered by the CQ, but an existing build can be 298 re-used. 299 equivalent_builder_whitelist: a group name with accounts to enable the 300 equivalent builder substitution for. If set, only CLs that are owned 301 by someone from this group have a chance to be verified by the 302 equivalent builder. All other CLs are verified via the main builder. 303 mode_allowlist: a list of modes that CQ will trigger this verifier for. 304 CQ supports `cq.MODE_DRY_RUN` and `cq.MODE_FULL_RUN`, and 305 `cq.MODE_NEW_PATCHSET_RUN` out of the box. 306 Additional Run modes can be defined via 307 `luci.cq_group(additional_modes=...)`. 308 """ 309 builder = keys.builder_ref(builder, attr = "builder", allow_external = True) 310 311 location_filters = validate.list("location_filters", location_filters) 312 for lf in location_filters: 313 cqimpl.validate_location_filter("location_filters", lf) 314 315 owner_whitelist = validate.list("owner_whitelist", owner_whitelist) 316 for o in owner_whitelist: 317 validate.string("owner_whitelist", o) 318 319 # 'equivalent_builder' has same format as 'builder', except it is optional. 320 if equivalent_builder: 321 equivalent_builder = keys.builder_ref( 322 equivalent_builder, 323 attr = "equivalent_builder", 324 allow_external = True, 325 ) 326 327 equivalent_builder_percentage = validate.float( 328 "equivalent_builder_percentage", 329 equivalent_builder_percentage, 330 min = 0.0, 331 max = 100.0, 332 required = False, 333 ) 334 equivalent_builder_whitelist = validate.string( 335 "equivalent_builder_whitelist", 336 equivalent_builder_whitelist, 337 required = False, 338 ) 339 340 mode_allowlist = validate.list("mode_allowlist", mode_allowlist) 341 for m in mode_allowlist: 342 validate.string("mode_allowlist", m) 343 344 # Validate location_filters used by analyzers. 345 # TODO(crbug/1202952): Remove these restrictions after Tricium is 346 # folded into CV. 347 if cq.MODE_ANALYZER_RUN in mode_allowlist: 348 _validate_analyzer_location(location_filters) 349 350 if not equivalent_builder: 351 if equivalent_builder_percentage != None: 352 fail('"equivalent_builder_percentage" can be used only together with "equivalent_builder"') 353 if equivalent_builder_whitelist != None: 354 fail('"equivalent_builder_whitelist" can be used only together with "equivalent_builder"') 355 356 if includable_only: 357 if location_filters: 358 fail('"includable_only" can not be used together with "location_filters"') 359 if experiment_percentage: 360 fail('"includable_only" can not be used together with "experiment_percentage"') 361 if mode_allowlist: 362 fail('"includable_only" can not be used together with "mode_allowlist"') 363 364 # Note: The name of this node is important only for error messages. It 365 # doesn't show up in any generated files, and by construction it can't 366 # accidentally collide with some other name. 367 key = keys.unique(kinds.CQ_TRYJOB_VERIFIER, builder.id) 368 graph.add_node(key, props = { 369 "disable_reuse": validate.bool("disable_reuse", disable_reuse, required = False), 370 "result_visibility": validate.int( 371 "result_visibility", 372 result_visibility, 373 default = cq.COMMENT_LEVEL_UNSET, 374 required = False, 375 ), 376 "cancel_stale": validate.bool("cancel_stale", cancel_stale, required = False), 377 "includable_only": validate.bool("includable_only", includable_only, required = False), 378 "experiment_percentage": validate.float( 379 "experiment_percentage", 380 experiment_percentage, 381 min = 0.0, 382 max = 100.0, 383 required = False, 384 ), 385 "location_filters": location_filters, 386 "owner_whitelist": owner_whitelist, 387 "mode_allowlist": mode_allowlist, 388 }) 389 if cq_group: 390 graph.add_edge(parent = keys.cq_group(cq_group), child = key) 391 graph.add_edge(parent = key, child = builder) 392 393 # Need to setup a node to represent 'equivalent_builder' so that lucicfg can 394 # verify (via the graph integrity check) that such builder was actually 395 # defined somewhere. Note that we can't add 'equivalent_builder' as another 396 # child of 'cq_tryjob_verifier' node, since then it would be ambiguous which 397 # child builder_ref node is the "main one" and which is the equivalent. 398 if equivalent_builder: 399 # Note: this key is totally invisible. 400 eq_key = keys.unique( 401 kind = kinds.CQ_EQUIVALENT_BUILDER, 402 name = equivalent_builder.id, 403 ) 404 graph.add_node(eq_key, props = { 405 "percentage": equivalent_builder_percentage, 406 "whitelist": equivalent_builder_whitelist, 407 }) 408 graph.add_edge(parent = key, child = eq_key) 409 graph.add_edge(parent = eq_key, child = equivalent_builder) 410 411 # This is used to detect cq_tryjob_verifier nodes that aren't connected to 412 # any cq_group. Such orphan nodes aren't allowed. 413 graph.add_node(keys.cq_verifiers_root(), idempotent = True) 414 graph.add_edge(parent = keys.cq_verifiers_root(), child = key) 415 416 return graph.keyset(key) 417 418 def _validate_analyzer_location(location_filters): 419 """Validates location_filters for analyzers. 420 421 Some parts of Tricium config are generated from location_filters. But 422 because of the way that Tricium watches one set of repos per config and 423 uses glob path filters which (in practice) are used for file extensions, 424 not all location filters are valid for analyzers. 425 426 Specifically: Since all analyzers in a Tricium config are watching the same 427 set of Gerrit repos, we need to make sure that for each extension this 428 analyzer is watching, it MUST specify the same set of Gerrit repos it is 429 watching. This allows lucicfg to derive a homogeneous set of watching 430 Gerrit repos when generating Tricium config later. 431 432 For example, location_filters values that match repo1 and repo2 with path 433 filter *.go; and only repo1 with path filter .*.py would not be allowed, 434 because the generated Tricium config has to watch both repo1 and repo2. If 435 we allow it, Tricium will implicitly run for Python files in repo2 which is 436 not what user intended. 437 """ 438 if not location_filters: 439 return 440 441 re_for_ext_re = r"\.\+\\\.\w+" 442 ext_prefix = r".+\." 443 444 def matches_ext(s): 445 return re.submatches(re_for_ext_re, s) and s.startswith(ext_prefix) 446 447 re_for_gerrit_host_re = r"[a-z\-]+\-review\.googlesource\.com" 448 re_for_gerrit_project_re = r"[a-z0-9\.\-/]+" 449 450 ext_to_gerrit_urls = {} 451 all_gerrit_urls = [] 452 453 for f in location_filters: 454 if f.exclude: 455 fail('"analyzer currently can not be used together with exclude filters') 456 457 # Path filter must be empty (matching everything) or match only an extension. 458 empty_path = f.path_regexp in ("", ".*", ".+") 459 if not empty_path and not matches_ext(f.path_regexp): 460 fail('"location_filter" of an analyzer MUST have a path_regexp ' + 461 'that matches everything, OR a path_regexp like ".+\\.py"; ' + 462 'got "%s", expecting pattern "%s"' % (f.path_regexp, re_for_ext_re)) 463 ext = "" 464 if matches_ext(f.path_regexp): 465 ext = f.path_regexp[len(ext_prefix):] 466 467 empty_host = f.gerrit_host_regexp in ("", ".*", ".+") 468 empty_project = f.gerrit_project_regexp in ("", ".*", ".+") 469 if (not empty_host and empty_project) or (empty_host and not empty_project): 470 # Only host or project specified, not both. 471 fail('"location_filter" of an analyzer MUST have either both Gerrit host and project ' + 472 'or neither. Got "%s", "%s"' % (f.gerrit_host_regexp, f.gerrit_project_regexp)) 473 474 # gerrit_url below is a combination of host and project; both must be 475 # specified and match the expected formats. 476 gerrit_url = "" 477 if not empty_host and not empty_project: 478 gerrit_url = f.gerrit_host_regexp + "/" + f.gerrit_project_regexp 479 if not re.submatches(re_for_gerrit_host_re, f.gerrit_host_regexp): 480 fail("Gerrit host in location filter did not match expected format, " + 481 'got "%s", expecting pattern "%s"' % (f.gerrit_host_regexp, re_for_gerrit_host_re)) 482 if not re.submatches(re_for_gerrit_project_re, f.gerrit_project_regexp): 483 fail("Gerrit project in location filter did not match expected format, " + 484 'got "%s", expecting pattern "%s"' % (f.gerrit_project_regexp, re_for_gerrit_project_re)) 485 486 if ext not in ext_to_gerrit_urls: 487 ext_to_gerrit_urls[ext] = [] 488 if ((gerrit_url and "" in all_gerrit_urls) or (gerrit_url == "" and any(all_gerrit_urls))): 489 fail(r'"location_filters" of an analyzer MUST NOT mix two different formats ' + 490 r'(i.e. only extension, and extension plus gerrit host/project."') 491 ext_to_gerrit_urls[ext].append(gerrit_url) 492 all_gerrit_urls.append(gerrit_url) 493 494 ref_ext, ref_gerrit_urls = ext_to_gerrit_urls.popitem() 495 ref_gerrit_urls = sorted(ref_gerrit_urls) 496 for ext, gerrit_urls in ext_to_gerrit_urls.items(): 497 if sorted(gerrit_urls) != ref_gerrit_urls: 498 fail('each extension specified in "location_filters" of an analyzer ' + 499 "MUST have the same set of gerrit URLs; " + 500 "got %s for extension %s, but %s for extension %s." % ( 501 sorted(gerrit_urls), 502 ext, 503 ref_gerrit_urls, 504 ref_ext, 505 )) 506 507 cq_tryjob_verifier = lucicfg.rule(impl = _cq_tryjob_verifier)