openSUSE-release-tools/devel-project.py
Jimmy Berry 760ddf39e6 osclib/conf: decouple from StagingAPI and always fetch remote config.
As the remote config is no longer optional for SLE and is utilized by
openSUSE to the point were it is dangerous not to load the remote config
it should be required. Currently only certain users call apply_remote()
while this will make it built-in during construction and thus makes the
usage consistent and no longer require StagingAPI.
2018-08-16 21:53:40 -05:00

293 lines
11 KiB
Python
Executable File

#!/usr/bin/python
import argparse
from datetime import datetime
import dateutil.parser
import sys
from lxml import etree as ET
import osc.conf
from osc.core import HTTPError
from osc.core import get_request_list
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 package_list_without_links
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': "@project='{}'".format(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.dashboard_content_ensure('devel_projects', out, 'devel_projects write')
def devel_projects_load(args):
api = staging_api(args)
devel_projects = api.dashboard_content_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('{} missing {}'.format(devel_project, ', '.join(desired - intersection)))
def notify(args):
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_without_links(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)
Config(apiurl, args.project) # Ensure mail-* options are loaded for mail_send().
subject = 'Packages you maintain are present in {}'.format(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)))
mail_send(args.project, email, subject, message, dry=args.dry)
print('notified {} of {} packages'.format(userid, len(package_identifiers)))
def requests(args):
apiurl = osc.conf.config['apiurl']
devel_projects = devel_projects_load(args)
for devel_project in devel_projects:
requests = get_request_list(apiurl, devel_project,
req_state=('new', 'review'),
req_type='submit',
# Seems to work backwards, as it includes only.
exclude_target_projects=[devel_project],
withfullhistory=True)
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)),
'({} days old)'.format(age),
)))
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:
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)),
'({} days old)'.format(age),
)))
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(' skipping due to previous reminder from {} days ago'.format(delta.days))
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 = '{}: {}'.format(', '.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')
if __name__ == '__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 dashboard container 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)
args = parser.parse_args()
osc.conf.get_config(override_apiurl=args.apiurl)
osc.conf.config['debug'] = args.debug
sys.exit(args.func(args))