| # Copyright 2025 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Split combined triggers into separate builds by branch.""" |
| |
| from __future__ import annotations |
| |
| import dataclasses |
| import urllib.parse |
| from typing import Any |
| |
| from recipe_engine import recipe_api |
| |
| from PB.go.chromium.org.luci.buildbucket.proto import ( |
| builds_service as builds_service_pb, |
| ) |
| from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb |
| from PB.go.chromium.org.luci.scheduler.api.scheduler.v1 import ( |
| triggers as triggers_pb, |
| ) |
| from PB.recipe_engine import result as result_pb |
| from PB.recipe_modules.pigweed.split_triggers.properties import InputProperties |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class RemoteBranch: |
| remote: str |
| ref: str |
| |
| |
| class SplitTriggersApi(recipe_api.RecipeApi): |
| """Split combined triggers into separate builds by branch.""" |
| |
| def __init__(self, props: InputProperties, *args: Any, **kwargs: Any): |
| super().__init__(*args, **kwargs) |
| self._props = props |
| |
| def __call__(self) -> result_pb.RawResult | None: |
| """Split combined triggers into separate builds by branch.""" |
| with self.m.step.nest('split triggers'): |
| return self._split() |
| |
| def _split(self) -> result_pb.RawResult | None: |
| pres = self.m.step.empty('properties').presentation |
| pres.step_summary_text = repr(self.m.properties.thaw()) |
| |
| with self.m.step.nest('processing triggers'): |
| triggers_by_branch: dict[ |
| RemoteBranch, list[triggers_pb.Trigger] |
| ] = {} |
| for trigger in self.m.scheduler.triggers: |
| if trigger.WhichOneof('payload') != 'gitiles': |
| self.m.step.empty('one non-gitiles trigger') |
| continue |
| |
| key = RemoteBranch(trigger.gitiles.repo, trigger.gitiles.ref) |
| self.m.step.empty( |
| f'one trigger for {key.remote} {key.ref} ' |
| f'{trigger.gitiles.revision}', |
| ) |
| triggers_by_branch.setdefault(key, []) |
| triggers_by_branch[key].append(trigger) |
| |
| requests: list[builds_service_pb.ScheduleBuildRequest] = [] |
| |
| schedule_kwargs = {} |
| schedule_kwargs['builder'] = self.m.buildbucket.build.builder.builder |
| schedule_kwargs['inherit_buildsets'] = False |
| schedule_kwargs['experiments'] = {'pigweed.disable_git_cache': True} |
| schedule_kwargs['can_outlive_parent'] = True |
| |
| num_branches = len(triggers_by_branch) |
| |
| if num_branches == 0: |
| self.m.step.empty('no branches') |
| return None |
| |
| if num_branches == 1: |
| self.m.step.empty('only one branch') |
| return None |
| |
| with self.m.step.nest('filtering'): |
| for triggers in triggers_by_branch.values(): |
| # The most recent commit on a branch shows up last in the |
| # list of commits, so we only need to trigger the build on |
| # it. |
| trigger = triggers[-1].gitiles |
| parsed = urllib.parse.urlparse(trigger.repo) |
| |
| commit = common_pb.GitilesCommit( |
| host=parsed.hostname, |
| project=parsed.path.lstrip('/'), |
| id=trigger.revision, |
| ref=trigger.ref, |
| ) |
| |
| requests.append( |
| self.m.buildbucket.schedule_request( |
| gitiles_commit=commit, |
| **schedule_kwargs, |
| ) |
| ) |
| self.m.step.empty( |
| f'selected {trigger.repo} {trigger.ref} {trigger.revision}', |
| ) |
| |
| with self.m.step.nest(f'launching {len(requests)} builds'): |
| if self._props.enabled: |
| summary = [ |
| f'Split the triggers into {len(requests)} based on trigger ' |
| 'branches', |
| ] |
| builds = self.m.buildbucket.schedule(requests) |
| |
| for build in builds: |
| branch = build.input.gitiles_commit.ref.removeprefix( |
| 'refs/heads/', |
| ) |
| summary.append( |
| f'* {branch}: {self.m.buildbucket.build_url(build=build)}', |
| ) |
| |
| return result_pb.RawResult( |
| summary_markdown='\n'.join(summary), |
| status=common_pb.SUCCESS, |
| ) |
| |
| else: |
| self.m.step.empty('dry-run') |
| |
| for request in requests: |
| branch = request.gitiles_commit.ref.removeprefix( |
| 'refs/heads/', |
| ) |
| self.m.step.empty(branch) |
| |
| return None |