| # Copyright 2020 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from PB.go.chromium.org.luci.common.proto.gerrit import gerrit as gerrit_pb2 |
| |
| from recipe_engine import recipe_api |
| |
| from RECIPE_MODULES.fuchsia.utils import pluralize |
| |
| from PB.go.chromium.org.luci.buildbucket.proto import builder as builder_pb2 |
| from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2 |
| from PB.go.chromium.org.luci.buildbucket.proto import ( |
| builds_service as builds_service_pb2, |
| ) |
| |
| CL_UTIL_PATH = "fuchsia/infra/cl-util/${platform}" |
| CL_UTIL_VERSION = "git_revision:72959a38896fc76d7fb2074f917ca2aa4b676776" |
| |
| |
| class CLUtilApi(recipe_api.RecipeApi): |
| """APIs for manipulating CLs.""" |
| |
| def __init__(self, *args, **kwargs): |
| super(CLUtilApi, self).__init__(*args, **kwargs) |
| self.gerrit_host = None |
| self.gerrit_project = None |
| |
| def create_cl(self, step_name, subject, file_edits, presentation=None): |
| """Create a CL. |
| |
| Args: |
| step_name (str): Name of the step. |
| subject (str): Commit message subject of CL. |
| file_edits (seq((str, str))): A sequence of file edits, where each |
| file edit is a pair of strings: (filepath, contents). |
| presentation (Step): Add links to this step, if specified. Otherwise, |
| add links to the step generated by this call. |
| |
| Returns: |
| gerritpb.ChangeInfo: The CL's change info. |
| """ |
| args = [ |
| "-subject", |
| subject, |
| "-json-output", |
| self.m.proto.output(gerrit_pb2.ChangeInfo, "JSONPB"), |
| ] |
| for filepath, contents in file_edits: |
| args += ["-file-edit", "%s:%s" % (filepath, contents)] |
| step = self(step_name, "create-cl", args, infra_step=True) |
| change_info = step.proto.output |
| presentation = presentation or step.presentation |
| presentation.links["gerrit_link"] = "https://%s/c/%s/+/%d" % ( |
| self.gerrit_host, |
| self.gerrit_project, |
| change_info.number, |
| ) |
| return change_info |
| |
| def trigger_cq( |
| self, step_name, change_num, wait=False, timeout_secs=3600, dry_run=False |
| ): |
| """Trigger CQ on a CL and optionally wait for completion. |
| |
| Args: |
| step_name (str): Name of the step. |
| change_num (int): Gerrit change number. |
| wait (bool): If set, wait for completion. |
| timeout (str): How long to wait for completion in seconds. |
| dry_run (bool): If set, apply CQ+1 instead of CQ+2. |
| """ |
| args = [ |
| "-change-num", |
| change_num, |
| ] |
| if wait: |
| args += [ |
| "-wait", |
| "-timeout", |
| "%ds" % timeout_secs, |
| ] |
| if dry_run: |
| args.append("-dryrun") |
| self(step_name, "trigger-cq", args) |
| |
| def abandon_cl(self, step_name, change_num): |
| """Abandon a CL. |
| |
| Args: |
| step_name (str): Name of the step. |
| change_num (int): Gerrit change number. |
| """ |
| args = [ |
| "-change-num", |
| change_num, |
| ] |
| self(step_name, "abandon-cl", args, infra_step=True) |
| |
| def trigger_tryjobs( |
| self, step_name, change_num, patchset_num, builders, gerrit_host=None |
| ): |
| """Trigger tryjobs on a change's patchset. |
| |
| Args: |
| step_name (str): Name of the step. |
| change_num (int): Gerrit change number. |
| patchset_num (int): Gerrit change's patchset number. |
| builders (seq(str)): A sequence of builder names to trigger. |
| gerrit_host (str): If specified, trigger tryjobs under this Gerrit |
| host. Otherwise, use module's Gerrit host. |
| """ |
| reqs = [] |
| for builder in builders: |
| project, bucket, builder_name = builder.split("/") |
| req = self.m.buildbucket.schedule_request( |
| builder=builder_name, |
| project=project, |
| bucket=bucket, |
| # Emulate "Choose Tryjobs" plugin. |
| gerrit_changes=[ |
| common_pb2.GerritChange( |
| host=gerrit_host or self.gerrit_host, |
| project=self.gerrit_project, |
| change=change_num, |
| patchset=patchset_num, |
| ), |
| ], |
| # Use the server-defined settings rather than inheriting the |
| # parent build's settings. The parent build's settings should |
| # not necessarily match the tryjobs' settings, e.g. when |
| # crossing infrastructures. |
| exe_cipd_version=None, |
| experimental=None, |
| inherit_buildsets=False, |
| gitiles_commit=None, |
| priority=None, |
| ) |
| reqs.append(req) |
| with self.m.step.nest(step_name): |
| return self.m.buildbucket.schedule(reqs, step_name="schedule") |
| |
| def collect_tryjobs( |
| self, |
| step_name, |
| change_num, |
| patchset_num, |
| builders=None, |
| build_ids=None, |
| gerrit_host=None, |
| timeout_secs=3600, |
| raise_on_failure=True, |
| ): |
| """Collect tryjobs for a patchset. |
| |
| Args: |
| step_name (str): Name of the step. |
| change_num (int): Gerrit change number. |
| patchset_num (int): Gerrit change's patchset number. |
| builders (seq(str)): A sequence of builder names to collect. Cannot |
| be combined with `build_ids`. |
| build_ids (seq(str)): A sequence of build ids to collect. Cannot be |
| be combined with `builders`. |
| gerrit_host (str): If specified, search tryjobs registered under this |
| Gerrit host. Otherwise, use module's Gerrit host. |
| timeout_secs (int): How long to attempt collection in seconds. |
| raise_on_failure (bool): Whether to raise on tryjob failure(s). |
| |
| Raises: |
| InfraFailure: One or more tryjobs completed with INFRA_FAILURE status. |
| StepFailure: One or more tryjobs completed with FAILURE status. |
| """ |
| with self.m.step.nest(step_name): |
| assert ( |
| builders or build_ids |
| ), "only one of builders or build_ids may be specified" |
| if builders: |
| predicates = [] |
| for builder in builders: |
| project, bucket, builder_name = builder.split("/") |
| predicate = builds_service_pb2.BuildPredicate( |
| builder=builder_pb2.BuilderID( |
| project=project, bucket=bucket, builder=builder_name, |
| ), |
| gerrit_changes=[ |
| common_pb2.GerritChange( |
| host=gerrit_host or self.gerrit_host, |
| project=self.gerrit_project, |
| change=change_num, |
| patchset=patchset_num, |
| ), |
| ], |
| ) |
| predicates.append(predicate) |
| build_ids = [ |
| b.id |
| for b in self.m.buildbucket.search( |
| step_name="search", predicate=predicates, |
| ) |
| ] |
| builds = self.m.buildbucket.collect_builds( |
| step_name="collect", build_ids=build_ids, timeout=timeout_secs, |
| ) |
| try: |
| self.m.display_util.display_builds( |
| step_name="display", |
| builds=builds.itervalues(), |
| raise_on_failure=raise_on_failure, |
| ) |
| except self.m.step.StepFailure: |
| gerrit_link = "https://%s/c/%s/+/%d" % ( |
| self.gerrit_host, |
| self.gerrit_project, |
| change_num, |
| ) |
| failed_tryjobs = [ |
| b.builder.builder |
| for b in builds.itervalues() |
| if b.status != common_pb2.SUCCESS |
| ] |
| raise self.m.step.StepFailure( |
| "%s failed: see [gerrit UI](%s):\n\n%s" |
| % ( |
| pluralize("external tryjob", len(failed_tryjobs)), |
| gerrit_link, |
| "\n".join(["- %s" % t for t in failed_tryjobs]), |
| ) |
| ) |
| return builds |
| |
| def __call__(self, step_name, subcmd_name, args, infra_step=False): |
| assert self.gerrit_host |
| assert self.gerrit_project |
| cmd = [ |
| self._cl_util_tool, |
| subcmd_name, |
| "-gerrit-host", |
| self.gerrit_host, |
| "-gerrit-project", |
| self.gerrit_project, |
| ] + args |
| return self.m.step(step_name, cmd, infra_step=infra_step) |
| |
| @property |
| def _cl_util_tool(self): |
| return self.m.cipd.ensure_tool(CL_UTIL_PATH, CL_UTIL_VERSION) |