-
Notifications
You must be signed in to change notification settings - Fork 96
/
devel-project.py
executable file
·321 lines (249 loc) · 11.4 KB
/
devel-project.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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#!/usr/bin/python3
import argparse
from datetime import datetime
import sys
from lxml import etree as ET
import osc.conf
from osc.core import HTTPError
from osc.core import get_review_list
from osc.core import show_package_meta
from osc.core import show_project_meta
from osclib.comments import CommentAPI
from osclib.conf import Config
from osclib.core import devel_project_fallback
from osclib.core import entity_email
from osclib.core import get_request_list_with_history
from osclib.core import package_list_kind_filtered
from osclib.core import request_age
from osclib.stagingapi import StagingAPI
from osclib.util import mail_send
BOT_NAME = 'devel-project'
REMINDER = 'review reminder'
def search(apiurl, queries=None, **kwargs):
if 'request' in kwargs:
# get_review_list() does not support withfullhistory, but search() does.
if queries is None:
queries = {}
request = queries.get('request', {})
request['withfullhistory'] = 1
queries['request'] = request
return osc.core._search(apiurl, queries, **kwargs)
osc.core._search = osc.core.search
osc.core.search = search
def staging_api(args):
apiurl = osc.conf.config['apiurl']
Config(apiurl, args.project)
return StagingAPI(apiurl, args.project)
def devel_projects_get(apiurl, project):
"""
Returns a sorted list of devel projects for a given project.
Loads all packages for a given project, checks them for a devel link and
keeps a list of unique devel projects.
"""
devel_projects = {}
root = search(apiurl, **{'package': f"@project='{project}'"})['package']
for devel in root.findall('package/devel[@project]'):
devel_projects[devel.attrib['project']] = True
# Ensure self does not end up in list.
if project in devel_projects:
del devel_projects[project]
return sorted(devel_projects)
def list(args):
devel_projects = devel_projects_get(osc.conf.config['apiurl'], args.project)
if len(devel_projects) == 0:
print('no devel projects found')
else:
out = '\n'.join(devel_projects)
print(out)
if args.write:
api = staging_api(args)
api.pseudometa_file_ensure('devel_projects', out, 'devel_projects write')
def devel_projects_load(args):
api = staging_api(args)
devel_projects = api.pseudometa_file_load('devel_projects')
if devel_projects:
return devel_projects.splitlines()
raise Exception('no devel projects found')
def maintainer(args):
if args.group is None:
# Default is appended to rather than overridden (upstream bug).
args.group = ['factory-maintainers', 'factory-staging']
desired = set(args.group)
apiurl = osc.conf.config['apiurl']
devel_projects = devel_projects_load(args)
for devel_project in devel_projects:
meta = ET.fromstringlist(show_project_meta(apiurl, devel_project))
groups = meta.xpath('group[@role="maintainer"]/@groupid')
intersection = set(groups).intersection(desired)
if len(intersection) != len(desired):
print(f"{devel_project} missing {', '.join(desired - intersection)}")
def notify(args):
import smtplib
apiurl = osc.conf.config['apiurl']
# devel_projects_get() only works for Factory as such
# devel_project_fallback() must be used on a per package basis.
packages = args.packages
if not packages:
packages = package_list_kind_filtered(apiurl, args.project)
maintainer_map = {}
for package in packages:
devel_project, devel_package = devel_project_fallback(apiurl, args.project, package)
if devel_project and devel_package:
devel_package_identifier = '/'.join([devel_project, devel_package])
userids = maintainers_get(apiurl, devel_project, devel_package)
for userid in userids:
maintainer_map.setdefault(userid, set())
maintainer_map[userid].add(devel_package_identifier)
subject = f'Packages you maintain are present in {args.project}'
for userid, package_identifiers in maintainer_map.items():
email = entity_email(apiurl, userid)
message = """This is a friendly reminder about your packages in {}.
Please verify that the included packages are working as intended and
have versions appropriate for a stable release. Changes may be submitted until
April 26th [at the latest].
Keep in mind that some packages may be shared with SUSE Linux
Enterprise. Concerns with those should be raised via Bugzilla.
Please contact opensuse-releaseteam@opensuse.org if your package
needs special attention by the release team.
According to the information in OBS ("osc maintainer") you are
in charge of the following packages:
- {}""".format(
args.project, '\n- '.join(sorted(package_identifiers)))
log = f'notified {userid} of {len(package_identifiers)} packages'
try:
mail_send(apiurl, args.project, email, subject, message, dry=args.dry)
print(log)
except smtplib.SMTPRecipientsRefused:
print(f'[FAILED ADDRESS] {log} ({email})')
except smtplib.SMTPException as e:
print(f'[FAILED SMTP] {log} ({e})')
def requests(args):
apiurl = osc.conf.config['apiurl']
devel_projects = devel_projects_load(args)
# Disable including source project in get_request_list() query.
osc.conf.config['include_request_from_project'] = False
for devel_project in devel_projects:
requests = get_request_list_with_history(
apiurl, devel_project, req_state=('new', 'review'),
req_type='submit')
for request in requests:
action = request.actions[0]
age = request_age(request).days
if age < args.min_age:
continue
print(' '.join((
request.reqid,
'/'.join((action.tgt_project, action.tgt_package)),
'/'.join((action.src_project, action.src_package)),
f'({age} days old)',
)))
if args.remind:
remind_comment(apiurl, args.repeat_age, request.reqid, action.tgt_project, action.tgt_package)
def reviews(args):
apiurl = osc.conf.config['apiurl']
devel_projects = devel_projects_load(args)
for devel_project in devel_projects:
requests = get_review_list(apiurl, byproject=devel_project)
for request in requests:
# get_review_list() behavior has been changed in osc
# https://github.com/openSUSE/osc/commit/00decd25d1a2c775e455f8865359e0d21872a0a5
if request.state.name != 'review':
continue
action = request.actions[0]
if action.type != 'submit':
continue
age = request_age(request).days
if age < args.min_age:
continue
for review in request.reviews:
if review.by_project == devel_project:
break
print(' '.join((
request.reqid,
'/'.join((review.by_project, review.by_package)) if review.by_package else review.by_project,
'/'.join((action.tgt_project, action.tgt_package)),
f'({age} days old)',
)))
if args.remind:
remind_comment(apiurl, args.repeat_age, request.reqid, review.by_project, review.by_package)
def maintainers_get(apiurl, project, package=None):
if package:
try:
meta = show_package_meta(apiurl, project, package)
except HTTPError as e:
if e.code == 404:
# Fallback to project in the case of new package.
meta = show_project_meta(apiurl, project)
else:
meta = show_project_meta(apiurl, project)
meta = ET.fromstringlist(meta)
userids = []
for person in meta.findall('person[@role="maintainer"]'):
userids.append(person.get('userid'))
if len(userids) == 0 and package is not None:
# Fallback to project if package has no maintainers.
return maintainers_get(apiurl, project)
return userids
def remind_comment(apiurl, repeat_age, request_id, project, package=None):
comment_api = CommentAPI(apiurl)
comments = comment_api.get_comments(request_id=request_id)
comment, _ = comment_api.comment_find(comments, BOT_NAME)
if comment:
delta = datetime.utcnow() - comment['when']
if delta.days < repeat_age:
print(f' skipping due to previous reminder from {delta.days} days ago')
return
# Repeat notification so remove old comment.
try:
comment_api.delete(comment['id'])
except HTTPError as e:
if e.code == 403:
# Gracefully skip when previous reminder was by another user.
print(' unable to remove previous reminder')
return
raise e
userids = sorted(maintainers_get(apiurl, project, package))
if len(userids):
users = ['@' + userid for userid in userids]
message = f"{', '.join(users)}: {REMINDER}"
else:
message = REMINDER
print(' ' + message)
message = comment_api.add_marker(message, BOT_NAME)
comment_api.add_comment(request_id=request_id, comment=message)
def common_args_add(parser):
parser.add_argument('--min-age', type=int, default=0, metavar='DAYS', help='min age of requests')
parser.add_argument('--repeat-age', type=int, default=7, metavar='DAYS',
help='age after which a new reminder will be sent')
parser.add_argument('--remind', action='store_true', help='remind maintainers to review')
def main():
parser = argparse.ArgumentParser(description='Operate on devel projects for a given project.')
subparsers = parser.add_subparsers(title='subcommands')
parser.add_argument('-A', '--apiurl', metavar='URL', help='API URL')
parser.add_argument('-d', '--debug', action='store_true', help='print info useful for debuging')
parser.add_argument('-p', '--project', default='openSUSE:Factory', metavar='PROJECT',
help='project from which to source devel projects')
parser_list = subparsers.add_parser('list', help='List devel projects.')
parser_list.set_defaults(func=list)
parser_list.add_argument('-w', '--write', action='store_true', help='write to pseudometa package')
parser_maintainer = subparsers.add_parser('maintainer', help='Check for relevant groups as maintainer.')
parser_maintainer.set_defaults(func=maintainer)
parser_maintainer.add_argument('-g', '--group', action='append', help='group for which to check')
parser_notify = subparsers.add_parser('notify', help='notify maintainers of their packages')
parser_notify.set_defaults(func=notify)
parser_notify.add_argument('--dry', action='store_true', help='dry run emails')
parser_notify.add_argument("packages", nargs='*', help="packages to check")
parser_requests = subparsers.add_parser('requests', help='List open requests.')
parser_requests.set_defaults(func=requests)
common_args_add(parser_requests)
parser_reviews = subparsers.add_parser('reviews', help='List open reviews.')
parser_reviews.set_defaults(func=reviews)
common_args_add(parser_reviews)
if not sys.argv[1:]:
sys.argv.append("list")
args = parser.parse_args()
osc.conf.get_config(override_apiurl=args.apiurl)
osc.conf.config['debug'] = args.debug
sys.exit(args.func(args))
if __name__ == '__main__':
main()