blob: 0600afc82e73124571520bf2c26a76e4f381ec4e [file] [log] [blame]
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Functions for forgiving false Pre-CQ rejections."""
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from chromite.lib import cros_logging as logging
from exonerator import gerrit_cq
from exonerator import constants
_MESSAGE_FOR_PRECQ_PICKED_UP = 'The Pre-Commit Queue has picked up your change.'
_TRYBOT_READY_LABEL = 'Trybot-Ready'
_COMMIT_QUEUE_LABEL = 'Commit-Queue'
_VERIFIED_LABEL = 'Verified'
_EXONERATOR_PRECQ_MESSAGE = """\
CL-Exonerator has triggered a Pre-CQ retry on this CL due to a Pre-CQ sanity
failure.
"""
def MaybeExonerate(change, build_id):
"""Mark a CL as Trybot-Ready only if it hasn't already been.
Args:
change: A GerritPatchTuple.
build_id: The CIDB id for the build which the change was kicked out of.
"""
helper = gerrit_cq.GetHelper(internal=change.internal)
details = helper.GetChangeDetail(change.gerrit_number)
if not details:
logging.warning('Change details was None for %s', str(change))
return False
label = _LabelIfShouldExonerate(details, change.patch_number)
if not label:
logging.info(
"Change %s was not ready to exonerate %d", str(change), build_id)
return False
try:
helper.SetReview(
change.gerrit_number,
msg=_EXONERATOR_PRECQ_MESSAGE,
labels={label: 1})
logging.info('Exonerated %s with %s', str(change), label)
return True
# Network errors or permission errors won't get retried. That's ok for now.
except Exception:
logging.exception('Encountered an exception while marking %s with %s',
str(change), label)
return False
def CanBeMarkedReady(change):
"""Whether a change is ready to be exonerated.
Args:
change: A GerritPatchTuple to test.
"""
helper = gerrit_cq.GetHelper(internal=change.internal)
details = helper.GetChangeDetail(change.gerrit_number)
if details:
return bool(_LabelIfShouldExonerate(details, change.patch_number))
else:
logging.warning('Change details was None for %s', str(change))
return False
def _LabelIfShouldExonerate(change_details, known_patch_number):
"""Returns the label to exonerate if the CL should can be exonerated.
We only want to send CLs back into the pre-cq if they haven't already been
marked Trybot-Ready+1, CQ+1, and it's not already on a newer patchset.
Args:
change_details: The change details response from Gerrit.
known_patch_number: The revision number which we want to forgive.
"""
if _CannotExonerate(change_details, known_patch_number):
return None
return _LabelToReapply(change_details)
def _CannotExonerate(change_details, known_patch_number):
"""Check various condition to make sure we can exonerate the change.
This does NOT check whether Pre-CQ approval was removed by a bot. We do
that check when finding the label to re-apply.
Args:
change_details: The change details response from Gerrit.
known_patch_number: The revision number which we want to forgive.
"""
return not (
gerrit_cq.SanityCheckChange(change_details, known_patch_number)
# Must not have Verified-1
and not any(
label < 0
for label in gerrit_cq.LabelValues(change_details, _VERIFIED_LABEL))
# It shouldn't already have Trybot-Ready+1, CQ+1 or CQ-1
and not (gerrit_cq.LabelValues(
change_details, gerrit_cq.COMMIT_QUEUE_LABEL) - {0})
and not (gerrit_cq.LabelValues(
change_details, _TRYBOT_READY_LABEL) - {0})
# Don't exonerate CLs with Code-Review-1 or Code-Review-2
and all(label >= 0
for label in gerrit_cq.LabelValues(
change_details, gerrit_cq.CODE_REVIEW_LABEL))
)
def _LabelToReapply(change_details):
"""Returns the label removed, if the Pre-CQ approval was removed by a bot.
Either Trybot-Ready+1 or Commit-Queue+1 labels can kick off pre-cq. To
exonerate a change, we want to re-apply the labels which gave Pre-CQ approval,
but only if they weren't removed by a human. If a human removes approval, it's
probably because the change is unsafe or at fault in some way.
Args:
change_details: The response from gerrit with the change's details.
"""
# Find the latest pre-cq failure message, then walk forward through the
# messages to find the next bot laebl removal, if it exists.
messages = change_details['messages']
failure_index = _FindLatestPreCQKickoff(messages)
if failure_index is None:
return None
labels_removed_by_bot = _FindLabelsToReapply(messages[failure_index+1:])
# Commit-Queue is a superset of Trybot-Ready, so just forgive by applying it.
if _COMMIT_QUEUE_LABEL in labels_removed_by_bot:
return _COMMIT_QUEUE_LABEL
elif _TRYBOT_READY_LABEL in labels_removed_by_bot:
return _TRYBOT_READY_LABEL
def _FindLatestPreCQKickoff(messages):
"""Finds the latest build kickoff message index.
Args:
messages: The gerrit comments.
Returns:
The index of the message
"""
def _IsBuildKickoffMessage(message):
return (
message['real_author']['email'] == constants.CHROMEOS_COMMIT_BOT_EMAIL
and _MESSAGE_FOR_PRECQ_PICKED_UP in message['message']
)
for i, message in reversed(list(enumerate(messages))):
if _IsBuildKickoffMessage(message):
return i
def _FindLabelsToReapply(messages):
"""Finds the gerrit labels to reapply (probably the label removed by the bot)
Args:
messages: The gerrit comments after kickoff
Returns:
A set of labels which were removed by the chromeos commit bot.
"""
labels_removed_by_bot = set()
for message in messages:
if (message['real_author']['email']
== constants.CHROMEOS_COMMIT_BOT_EMAIL):
for label in (_TRYBOT_READY_LABEL, _COMMIT_QUEUE_LABEL):
if '-' + label in message['message']:
labels_removed_by_bot.add(label)
if labels_removed_by_bot:
return labels_removed_by_bot
# Special case - Commit-Queue+2 isn't removed by the bot. If the trybot was
# kicked off by CR+2, we want to reapply Trybot-Ready+1.
# We assume that if neither the bot nor a human removed any labels, then it
# was kicked off by CR+2
any_labels_removed = False
for message in messages:
for label in (_TRYBOT_READY_LABEL, _COMMIT_QUEUE_LABEL):
if '-' + label in message['message']:
any_labels_removed = True
if not any_labels_removed:
return [_TRYBOT_READY_LABEL]
return []