blob: 81df959decef66ce210cf3b792b55088ac7932a0 [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import re
import sys
import time
import urllib.parse
import common
import gerrit_interface
# Thresholds, unit: day(s).
NO_UPDATE_THRESHOLD = 7
CQ_PLUS1_RETRY_THRESHOLD = 1
# Limits
CQ_PLUS1_LIMIT_PER_DAY = 2
CQ_PLUS1_LIMIT = 4
RE_GWSQ_REVIEWER = re.compile(rf"(\w+) is from group\({common.GWSQ_EMAIL}\)")
def parse(obj):
ret = {}
ret["cl_num"] = obj["number"]
ret["change_id"] = obj["id"]
ret["subject"] = obj["subject"]
ret["url"] = obj["url"]
# Example to parse: chromeos-6.6
ret["branch"] = obj["branch"].split("-")[1]
ret["messages"] = gerrit_interface.get_messages(
ret["change_id"], ret["branch"]
)[::-1]
for m in ret["messages"]:
if m["author"]["name"] == "gwsq":
ret["gwsq_message"] = m
break
else:
ret["gwsq_message"] = None
ret["gwsq_in_progress"] = False
max_cr = max_cq = max_v = min_v = 0
reviewers = set()
for approval in obj["currentPatchSet"]["approvals"]:
if approval["type"] == "CRVW":
max_cr = max(max_cr, int(approval["value"]))
email = approval["by"]["email"]
if (
email.endswith("@chromium.org") or email.endswith("@google.com")
) and email != common.GWSQ_EMAIL:
reviewers.add(email)
if email == common.GWSQ_EMAIL:
ret["gwsq_in_progress"] = True
if approval["type"] == "COMR":
max_cq = max(max_cq, int(approval["value"]))
if approval["type"] == "VRIF":
max_v = max(max_v, int(approval["value"]))
min_v = min(min_v, int(approval["value"]))
ret["cr"] = max_cr
ret["cq"] = max_cq
if min_v < 0:
ret["v"] = min_v
else:
ret["v"] = max_v
ret["reviewers"] = list(reviewers)
return ret
def has_elapsed(from_time, threshold):
"""Return True if `threshold` has elapsed since `from_time`."""
return (int(time.time()) - from_time) >= threshold
def send_ping_msg(obj):
"""Return True on success."""
q = [
"component=167278",
"format=PLAIN",
"type=BUG",
"priority=P2",
"severity=S2",
"hotlistIds=5435871",
]
title = f'Kernel: fix build failure for ("{obj["subject"]}")'
q.append("title=%s" % urllib.parse.quote(title))
description = f"""Fix build failure for ("{obj["subject"]}").
Link: {obj["url"]}
Branch: {obj["branch"]}
"""
q.append("description=%s" % urllib.parse.quote(description))
q = "&".join(q)
url = f"http://b.corp.google.com/createIssue?{q}"
msg = f"""
Dear Reviewer,
You have been selected because this bot believes you are the best owner for this CL.
- If you think you are not the right person for handling the CL:
- Please add chromeos-kernel-findmissing-reviewers@google.com as a reviewer.
- Please remove yourself from the reviewer list.
- If the CL is irrelevant to ChromeOS, feel free to abandon it.
- If the CL makes sense to you:
- If the CQ tests passed and the robot has marked V+1 and AS+1, please provide CR+2 and CQ+2.
- If the CQ tests failed due to build failures, please provide V-1 and file a bug via [the template]({url}).
- Otherwise, please provide CR-1 and your comments.
Thank you for contributing to ChromeOS kernel stability.
"""
return gerrit_interface.send_message(obj["cl_num"], msg)
def to_mtimestamp(msg):
"""Convert a message date to timestamp."""
mtime = time.strptime(msg["date"], "%Y-%m-%d %H:%M:%S.000000000")
return int(time.mktime(mtime))
def add_backup_reviewer(obj):
"""Add a backup reviewer from CLs (with the same Change-Id) in other branches or GWSQ.
Return True if a backup reviewer (excluding GWSQ) has added.
"""
if obj["gwsq_in_progress"]:
print("Deferred, gwsq is still in progress")
return False
if obj["gwsq_message"]:
print("gwsq has involved")
return False
cl_nums = gerrit_interface.get_cl_nums(obj["change_id"])
cl_nums.pop(obj["branch"], None)
for cl_num in cl_nums.values():
o = gerrit_interface.inspect(cl_num)[0]
o = parse(o)
# Only take chromium accounts into consideration.
chromium_reviewers = [r for r in o["reviewers"] if "@chromium.org" in r]
if chromium_reviewers:
for r in chromium_reviewers:
gerrit_interface.add_reviewer(obj["cl_num"], r)
print(f"Add {r} as a reviewer from {cl_num}")
return True
if o["gwsq_in_progress"]:
print(f"Deferred, gwsq is still in progress in {cl_num}")
return False
if o["gwsq_message"]:
match = RE_GWSQ_REVIEWER.search(o["gwsq_message"]["message"])
if match:
gwsq_reviewer = match.group(1)
gerrit_interface.add_reviewer(
obj["cl_num"], f"{gwsq_reviewer}@chromium.org"
)
print(f"Add {gwsq_reviewer} as a reviewer from {cl_num}")
return True
print("Deferred, loop in gwsq")
gerrit_interface.add_reviewer(obj["cl_num"], common.GWSQ_EMAIL)
return False
def check_no_update(obj):
for last, m in enumerate(obj["messages"]):
if m["author"]["name"] != "gwsq":
break
else:
print("Failed to find the last non-gwsq message")
return
if not has_elapsed(
to_mtimestamp(obj["messages"][last]), 86400 * NO_UPDATE_THRESHOLD
):
return
print(f"Hasn't been changed for more than {NO_UPDATE_THRESHOLD} day(s)")
# Don't try to loop in gwsq if a CL has V-1.
if obj["v"] != -1:
if obj["gwsq_message"]:
# gwsq has involved.
pass
elif not add_backup_reviewer(obj):
# gwsq is in progress.
return
if not send_ping_msg(obj):
print("Failed to send ping message")
def check_cq_plus1(obj):
if obj["cq"] != 0 or obj["v"] == 1:
print("Already handled")
return
if obj["v"] == -1:
print("Already handled (V-1)")
return
cl_num = obj["cl_num"]
has_failed = 0
for m in obj["messages"]:
msg = m["message"]
# If a new PS has uploaded, retry CQ+1 anyway.
if "Uploaded patch set" in msg or "was rebased" in msg:
print("Mark CQ+1 because of a new patchset")
gerrit_interface.label_cq(cl_num, "1")
return
if "This CL has passed the run" in msg:
# Make sure the latest result passes CQ.
if has_failed == 0:
print("CQ+1 has passed")
if obj["reviewers"] or add_backup_reviewer(obj):
print("Send ping message, V+1, and AS+1")
send_ping_msg(obj)
gerrit_interface.label_v(cl_num, "1")
gerrit_interface.label_as(cl_num, "1")
return
if "its dependencies weren't CQ-ed" in msg:
print("Dependencies weren't CQ-ed")
gerrit_interface.send_message(
cl_num,
"Won't CQ+1 again because dependencies weren't CQ-ed",
)
gerrit_interface.label_v(cl_num, "-1")
return
if "This CL has failed the run" in msg:
if "buildtest-cq" in msg:
print("CQ failed on buildtest")
if obj["reviewers"] or add_backup_reviewer(obj):
print("Send ping message, and V-1")
send_ping_msg(obj)
gerrit_interface.label_v(cl_num, "-1")
return
has_failed += 1
if has_failed >= CQ_PLUS1_LIMIT:
print("Do nothing because the retries are over the limit")
if obj["reviewers"] or add_backup_reviewer(obj):
print("Send ping message")
send_ping_msg(obj)
gerrit_interface.send_message(
cl_num,
"Won't CQ+1 again because the retries are over the limit",
)
gerrit_interface.label_v(cl_num, "-1")
return
if not has_elapsed(
to_mtimestamp(m), 86400 * CQ_PLUS1_RETRY_THRESHOLD
):
if has_failed >= CQ_PLUS1_LIMIT_PER_DAY:
print(
"Do nothing because the retries are over the limit today"
)
return
print("Mark CQ+1")
gerrit_interface.label_cq(cl_num, "1")
def ping_cl(cl_num):
obj = gerrit_interface.inspect(cl_num)[0]
obj = parse(obj)
send_ping_msg(obj)
def ping_pending_cls():
for obj in gerrit_interface.inspect():
obj = parse(obj)
print(f"Checking CL {obj['cl_num']}")
check_no_update(obj)
check_cq_plus1(obj)
if __name__ == "__main__":
if len(sys.argv) == 2:
ping_cl(sys.argv[1])
else:
ping_pending_cls()