/
scorecard.py
297 lines (233 loc) · 8.64 KB
/
scorecard.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import collections
import datetime
import os
import asyncio
import aiohttp
from aiohttp import web
import gidgethub.aiohttp
import random
import humanize
import logging
from hailtop.config import get_deploy_config
from gear import setup_aiohttp_session, web_maybe_authenticated_user
from web_common import setup_aiohttp_jinja2, setup_common_static_routes, render_template
log = logging.getLogger('scorecard')
deploy_config = get_deploy_config()
component_users = {
'Hail front-end (Py)': ['tpoterba', 'jigold', 'catoverdrive', 'patrick-schultz', 'chrisvittal', 'konradjk', 'johnc1231', 'iitalics'],
'Hail middle-end (Scala)': ['danking', 'tpoterba', 'jigold', 'catoverdrive', 'patrick-schultz', 'chrisvittal', 'johnc1231', 'iitalics'],
'hailctl dataproc': ['tpoterba', 'danking', 'konradjk'],
'k8s, services': ['danking', 'jigold', 'akotlar', 'johnc1231'],
'Web app (JS)': ['akotlar', 'danking'],
}
default_repo = 'hail'
repos = {
'hail': 'hail-is/hail',
}
routes = web.RouteTableDef()
data = None
timestamp = None
@routes.get('/healthcheck')
async def get_healthcheck(request): # pylint: disable=unused-argument
return web.Response()
@routes.get('')
@routes.get('/')
@web_maybe_authenticated_user
async def index(request, userdata):
user_data, unassigned, urgent_issues, updated = get_users()
component_random_user = {c: random.choice(us) for c, us in component_users.items()}
page_context = {
'unassigned': unassigned,
'user_data': user_data,
'urgent_issues': urgent_issues,
'component_user': component_random_user,
'updated': updated
}
return await render_template('scorecard', request, userdata, 'index.html', page_context)
@routes.get('/users/{user}')
@web_maybe_authenticated_user
async def html_get_user(request, userdata):
user = request.match_info['user']
user_data, updated = get_user(user)
page_context = {
'user': user,
'user_data': user_data,
'updated': updated,
}
return await render_template('scorecard', request, userdata, 'user.html', page_context)
def get_users():
cur_data = data
cur_timestamp = timestamp
unassigned = []
user_data = collections.defaultdict(
lambda: {'CHANGES_REQUESTED': [],
'NEEDS_REVIEW': [],
'ISSUES': []})
urgent_issues = []
def add_pr(pr):
state = pr['state']
if state == 'CHANGES_REQUESTED':
d = user_data[pr['user']]
d[state].append(pr)
elif state == 'NEEDS_REVIEW':
for user in pr['assignees']:
d = user_data[user]
d[state].append(pr)
else:
assert state == 'APPROVED'
def add_issue(issue):
for user in issue['assignees']:
d = user_data[user]
if issue['urgent']:
time = datetime.datetime.now() - issue['created_at']
urgent_issues.append({
'USER': user,
'ISSUE': issue,
'timedelta': time,
'AGE': humanize.naturaltime(time)})
else:
d['ISSUES'].append(issue)
for _, repo_data in cur_data.items():
for pr in repo_data['prs']:
if len(pr['assignees']) == 0:
unassigned.append(pr)
continue
add_pr(pr)
for issue in repo_data['issues']:
add_issue(issue)
list.sort(urgent_issues, key=lambda issue: issue['timedelta'], reverse=True)
updated = humanize.naturaltime(
datetime.datetime.now() - cur_timestamp)
return (user_data, unassigned, urgent_issues, updated)
def get_user(user):
global data, timestamp
cur_data = data
cur_timestamp = timestamp
user_data = {
'CHANGES_REQUESTED': [],
'NEEDS_REVIEW': [],
'FAILING': [],
'ISSUES': []
}
for _, repo_data in cur_data.items():
for pr in repo_data['prs']:
state = pr['state']
if state == 'CHANGES_REQUESTED':
if user == pr['user']:
user_data[state].append(pr)
elif state == 'NEEDS_REVIEW':
if user in pr['assignees']:
user_data[state].append(pr)
else:
assert state == 'APPROVED'
if pr['status'] == 'failure' and user == pr['user']:
user_data['FAILING'].append(pr)
for issue in repo_data['issues']:
if user in issue['assignees']:
user_data['ISSUES'].append(issue)
updated = humanize.naturaltime(
datetime.datetime.now() - cur_timestamp)
return (user_data, updated)
def get_id(repo_name, number):
if repo_name == default_repo:
return f'{number}'
return f'{repo_name}/{number}'
async def get_pr_data(gh_client, fq_repo, repo_name, pr):
assignees = [a['login'] for a in pr['assignees']]
reviews = []
async for review in gh_client.getiter(f'/repos/{fq_repo}/pulls/{pr["number"]}/reviews'):
reviews.append(review)
state = 'NEEDS_REVIEW'
for review in reversed(reviews):
review_state = review['state']
if review_state == 'CHANGES_REQUESTED':
state = review_state
break
elif review_state == 'DISMISSED':
break
elif review_state == 'APPROVED':
state = 'APPROVED'
break
else:
if review_state != 'COMMENTED':
log.warning(f'unknown review state {review_state} on review {review} in pr {pr}')
sha = pr['head']['sha']
status = await gh_client.getitem(f'/repos/{fq_repo}/commits/{sha}')
return {
'repo': repo_name,
'id': get_id(repo_name, pr['number']),
'title': pr['title'],
'user': pr['user']['login'],
'assignees': assignees,
'html_url': pr['html_url'],
'state': state,
'status': status
}
def get_issue_data(repo_name, issue):
assignees = [a['login'] for a in issue['assignees']]
return {
'repo': repo_name,
'id': get_id(repo_name, issue['number']),
'title': issue['title'],
'assignees': assignees,
'html_url': issue['html_url'],
'urgent': any(label['name'] == 'prio:high' for label in issue['labels']),
'created_at': datetime.datetime.strptime(issue['created_at'], '%Y-%m-%dT%H:%M:%SZ')
}
async def update_data(gh_client):
global data, timestamp
rate_limit = await gh_client.getitem("/rate_limit")
log.info(f'rate_limit {rate_limit}')
log.info('start updating_data')
new_data = {}
for repo_name in repos:
new_data[repo_name] = {
'prs': [],
'issues': []
}
try:
for repo_name, fq_repo in repos.items():
async for pr in gh_client.getiter(f'/repos/{fq_repo}/pulls?state=open'):
pr_data = await get_pr_data(gh_client, fq_repo, repo_name, pr)
new_data[repo_name]['prs'].append(pr_data)
async for issue in gh_client.getiter(f'/repos/{fq_repo}/issues?state=open'):
print(issue)
if 'pull_request' not in issue:
issue_data = get_issue_data(repo_name, issue)
new_data[repo_name]['issues'].append(issue_data)
except Exception: # pylint: disable=broad-except
log.exception('update failed due to except')
return
log.info('updating_data done')
now = datetime.datetime.now()
data = new_data
timestamp = now
async def poll(gh_client):
while True:
await asyncio.sleep(180)
try:
log.info('run update_data')
await update_data(gh_client)
log.info('update_data returned')
except Exception: # pylint: disable=broad-except
log.exception('update_data failed with exception')
async def on_startup(app):
token_file = os.environ.get('GITHUB_TOKEN_PATH',
'/secrets/scorecard-github-access-token.txt')
with open(token_file, 'r') as f:
token = f.read().strip()
session = aiohttp.ClientSession(
raise_for_status=True,
timeout=aiohttp.ClientTimeout(total=60))
gh_client = gidgethub.aiohttp.GitHubAPI(session, 'scorecard', oauth_token=token)
app['gh_client'] = gh_client
await update_data(gh_client)
asyncio.ensure_future(poll(gh_client))
def run():
app = web.Application()
app.on_startup.append(on_startup)
setup_aiohttp_jinja2(app, 'scorecard')
setup_aiohttp_session(app)
setup_common_static_routes(routes)
app.add_routes(routes)
web.run_app(deploy_config.prefix_application(app, 'scorecard'), host='0.0.0.0', port=5000)