
Add lastcomment dashboad to the collection of third party CI tools so it can be worked on via the standard gerrit workflow. lastcomment is an alternative monitoring scoreboard that uses gerrit's REST API as the data source. It was designed to address the developer's question, is CI system X running/reliable or not? original source: https://github.com/jogo/lastcomment Change-Id: I6bb0f321e1885999c9e20f74417dad8768c24c52
236 lines
7.5 KiB
Python
Executable File
236 lines
7.5 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""Print the last time a reviewer(bot) left a comment."""
|
|
|
|
import argparse
|
|
import calendar
|
|
import collections
|
|
import datetime
|
|
import json
|
|
import sys
|
|
import yaml
|
|
|
|
import requests
|
|
|
|
|
|
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
|
|
|
|
class Comment(object):
|
|
date = None
|
|
number = None
|
|
subject = None
|
|
now = None
|
|
|
|
def __init__(self, date, number, subject, message):
|
|
super(Comment, self).__init__()
|
|
self.date = date
|
|
self.number = number
|
|
self.subject = subject
|
|
self.message = message
|
|
self.now = datetime.datetime.utcnow().replace(microsecond=0)
|
|
|
|
def __str__(self):
|
|
return ("%s (%s old) https://review.openstack.org/%s '%s' " % (
|
|
self.date.strftime(TIME_FORMAT),
|
|
self.age(),
|
|
self.number, self.subject))
|
|
|
|
def age(self):
|
|
return self.now - self.date
|
|
|
|
def __le__(self, other):
|
|
# self < other
|
|
return self.date < other.date
|
|
|
|
def __repr__(self):
|
|
# for sorting
|
|
return repr((self.date, self.number))
|
|
|
|
|
|
def get_comments(change, name):
|
|
"""Generator that returns all comments by name on a given change."""
|
|
body = None
|
|
for message in change['messages']:
|
|
if 'author' in message and message['author']['name'] == name:
|
|
if (message['message'].startswith("Uploaded patch set") and
|
|
len(message['message'].split()) is 4):
|
|
# comment is auto created from posting a new patch
|
|
continue
|
|
date = message['date']
|
|
body = message['message']
|
|
# https://review.openstack.org/Documentation/rest-api.html#timestamp
|
|
# drop nanoseconds
|
|
date = date.split('.')[0]
|
|
date = datetime.datetime.strptime(date, TIME_FORMAT)
|
|
yield date, body
|
|
|
|
|
|
def query_gerrit(name, count, project):
|
|
# Include review messages in query
|
|
search = "reviewer:\"%s\"" % name
|
|
if project:
|
|
search = search + (" AND project:\"%s\"" % project)
|
|
query = ("https://review.openstack.org/changes/?q=%s&"
|
|
"o=MESSAGES" % search)
|
|
r = requests.get(query)
|
|
try:
|
|
changes = json.loads(r.text[4:])
|
|
except ValueError:
|
|
print "query: '%s' failed with:\n%s" % (query, r.text)
|
|
sys.exit(1)
|
|
|
|
comments = []
|
|
for change in changes:
|
|
for date, message in get_comments(change, name):
|
|
if date is None:
|
|
# no comments from reviewer yet. This can happen since
|
|
# 'Uploaded patch set X.' is considered a comment.
|
|
continue
|
|
comments.append(Comment(date, change['_number'],
|
|
change['subject'], message))
|
|
|
|
return sorted(comments, key=lambda comment: comment.date,
|
|
reverse=True)[0:count]
|
|
|
|
|
|
def vote(comment, success, failure, log=False):
|
|
for line in comment.message.splitlines():
|
|
if line.startswith("* ") or line.startswith("- "):
|
|
job = line.split(' ')[1]
|
|
if " : SUCCESS" in line:
|
|
success[job] += 1
|
|
if log:
|
|
print line
|
|
if " : FAILURE" in line:
|
|
failure[job] += 1
|
|
if log:
|
|
print line
|
|
|
|
|
|
def generate_report(name, count, project):
|
|
result = {'name': name, 'project': project}
|
|
success = collections.defaultdict(int)
|
|
failure = collections.defaultdict(int)
|
|
|
|
comments = query_gerrit(name, count, project)
|
|
|
|
if len(comments) == 0:
|
|
print "didn't find anything"
|
|
return None
|
|
|
|
print "last seen: %s (%s old)" % (comments[0].date, comments[0].age())
|
|
result['last'] = epoch(comments[0].date)
|
|
|
|
for comment in comments:
|
|
vote(comment, success, failure)
|
|
|
|
total = sum(success.values()) + sum(failure.values())
|
|
if total > 0:
|
|
success_rate = str(int(sum(success.values()) /
|
|
float(total) * 100)) + "%"
|
|
result['rate'] = success_rate
|
|
print "success rate: %s" % success_rate
|
|
return result
|
|
|
|
|
|
def print_last_comments(name, count, print_message, project, votes):
|
|
success = collections.defaultdict(int)
|
|
failure = collections.defaultdict(int)
|
|
|
|
comments = query_gerrit(name, count, project)
|
|
|
|
message = "last %s comments from '%s'" % (count, name)
|
|
if project:
|
|
message += " on project '%s'" % project
|
|
print message
|
|
# sort by time
|
|
for i, comment in enumerate(comments):
|
|
print "[%d] %s" % (i, comment)
|
|
if print_message:
|
|
print "message: \"%s\"" % comment.message
|
|
print
|
|
if votes:
|
|
vote(comment, success, failure, log=True)
|
|
|
|
if votes:
|
|
print "success count by job:"
|
|
for job in success.iterkeys():
|
|
print "* %s: %d" % (job, success[job])
|
|
print "failure count by job:"
|
|
for job in failure.iterkeys():
|
|
print "* %s: %d" % (job, failure[job])
|
|
|
|
|
|
def epoch(timestamp):
|
|
return int(calendar.timegm(timestamp.timetuple()))
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='list most recent comment by '
|
|
'reviewer')
|
|
parser.add_argument('-n', '--name',
|
|
default="Elastic Recheck",
|
|
help='unique gerrit name of the reviewer')
|
|
parser.add_argument('-c', '--count',
|
|
default=10,
|
|
type=int,
|
|
help='unique gerrit name of the reviewer')
|
|
parser.add_argument('-f', '--file',
|
|
default=None,
|
|
help='yaml file containing list of names to search on'
|
|
'project: name'
|
|
' (overwrites -p and -n)')
|
|
parser.add_argument('-m', '--message',
|
|
action='store_true',
|
|
help='print comment message')
|
|
parser.add_argument('-v', '--votes',
|
|
action='store_true',
|
|
help=('Look in comments for CI Jobs and detect '
|
|
'SUCCESS/FAILURE'))
|
|
parser.add_argument('--json',
|
|
nargs='?',
|
|
const='lastcomment.json',
|
|
help=("Generate report to be stored in the json file "
|
|
"specified here. Ignores -v and -m "
|
|
"(default: 'lastcomment.json')"))
|
|
parser.add_argument('-p', '--project',
|
|
help='only list hits for a specific project')
|
|
|
|
args = parser.parse_args()
|
|
names = {args.project: [args.name]}
|
|
if args.file:
|
|
with open(args.file) as f:
|
|
names = yaml.load(f)
|
|
|
|
if args.json:
|
|
print "generating report %s" % args.json
|
|
print "report is over last %s comments" % args.count
|
|
report = {}
|
|
timestamp = epoch(datetime.datetime.utcnow())
|
|
report['timestamp'] = timestamp
|
|
report['rows'] = []
|
|
|
|
for project in names:
|
|
print 'Checking project: %s' % project
|
|
for name in names[project]:
|
|
print 'Checking name: %s' % name
|
|
try:
|
|
if args.json:
|
|
report['rows'].append(generate_report(
|
|
name, args.count, project))
|
|
else:
|
|
print_last_comments(name, args.count, args.message,
|
|
project, args.votes)
|
|
except Exception as e:
|
|
print e
|
|
pass
|
|
|
|
if args.json:
|
|
with open(args.json, 'w') as f:
|
|
json.dump(report, f)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|