| # 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 [] |