blob: cdf26eb1fa5239ec163f3a3a313ff305f871c0e0 [file] [log] [blame]
# Copyright (c) 2020, the Dart project 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 recipe_engine import recipe_api
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
from PB.go.chromium.org.luci.buildbucket.proto import rpc as rpc_pb2
class BisectApi(recipe_api.RecipeApi):
def _get_reason(self):
return self.m.properties['bisect_reason']
def is_bisecting(self):
return 'bisect_reason' in self.m.properties
def schedule(self, repo_url, reason):
if self.is_bisecting():
bisect_reason = self._get_reason()
if bisect_reason == reason:
self._bisect_older(bisect_reason)
else:
self._bisect_newer(bisect_reason)
# The build failed for a different reason, fan out to find the root
# cause of that failure as well.
self._bisect_older(reason)
else:
self._start_bisection(repo_url, reason)
def _start_bisection(self, repo_url, reason):
current_rev = self.m.buildbucket.gitiles_commit.id
if not current_rev:
return
# Search for previous builds created by the Luci scheduler (to exclude
# bisection builds). The TimeRange includes all builds from start_time
# (defaults to 0) to end_time (exclusive). Because builds are ordered by
# create_time, the first result will be the previous build.
create_time = common_pb2.TimeRange(
end_time=self.m.buildbucket.build.create_time)
builder = self.m.buildbucket.build.builder
search_predicate = rpc_pb2.BuildPredicate(
builder=builder,
create_time=create_time,
tags=[common_pb2.StringPair(key='user_agent', value='luci-scheduler')])
result = self.m.buildbucket.search(
search_predicate, limit=1, step_name='fetch previous build')
if not result or len(result) == 0:
# There is no previous build: do not bisect on new builders.
return
previous_build = result[0]
if previous_build.status == common_pb2.FAILURE:
# Do not bisect if the previous build failed.
# TODO(athom): Check if the failure reason is the same by adding
# 'builds.*.summaryMarkdown' to the field mask.
return
previous_rev = previous_build.input.gitiles_commit.id
# We're intentionally not paging through the log to avoid bisecting an
# excessive number of commits.
commits, _ = self.m.gitiles.log(
url=repo_url, ref='%s..%s' % (previous_rev, current_rev))
commits = commits[1:] # The first commit is the current_rev
commits = [commit['commit'] for commit in commits]
self._bisect(commits, reason)
def _bisect(self, commits, reason):
if len(commits) == 0:
# Nothing more to bisect.
return
middle_index = len(commits) // 2
newer = commits[:middle_index]
middle = commits[middle_index]
older = commits[middle_index + 1:]
commit = self.m.buildbucket.gitiles_commit
middle_commit = common_pb2.GitilesCommit()
middle_commit.CopyFrom(commit)
middle_commit.id = middle
builder = self.m.buildbucket.build.builder
request = self.m.buildbucket.schedule_request(
builder=builder.builder,
project=builder.project,
bucket=builder.bucket,
properties={
'bisect_newer': newer,
'bisect_older': older,
'bisect_reason': reason
},
gitiles_commit=middle_commit,
inherit_buildsets=False,
)
self.m.buildbucket.schedule([request],
step_name='schedule bisect (%s)' % middle)
def _bisect_newer(self, reason):
self._bisect(list(self.m.properties.get('bisect_newer', [])), reason)
def _bisect_older(self, reason):
self._bisect(list(self.m.properties.get('bisect_older', [])), reason)