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)