diff --git a/calm/mail-inactive-maintainers.py b/calm/mail-inactive-maintainers.py new file mode 100644 index 0000000..8e692a9 --- /dev/null +++ b/calm/mail-inactive-maintainers.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Jon Turney +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import argparse +import logging +import os +import sys +import time + +from . import common_constants +from . import package +from . import pkg2html +from . import reports +from . import utils + +MAINTAINER_ACTIVITY_THRESHOLD_YEARS = 10 + +template = ''' + +Hi {}, + +As a part of keeping cygwin secure, your package maintainer account has been +found to be long inactive, and will soon be disabled, and your packages moved to +'ORPHANED' status. + +The estimated date of your last packaging activity is {}. + +Any action using your ssh key is sufficient to keep your account alive, e.g.: + +* do a git pull with an ssh://cygwin@cygwin.com/ URL +* run 'ssh cygwin@cygwin.com alive' + +For reference, the list of packages you are recorded as a maintainer of is: + +{} + +Thanks for all your work on these! + +For further assistance, please contact us via email at + +''' + + +def main(args): + packages = {} + + for arch in common_constants.ARCHES: + logging.debug("reading existing packages for arch %s" % (arch)) + packages[arch], _ = package.read_packages(args.relarea, arch) + + activity_list = reports.maintainer_activity(args, packages) + + threshold = time.time() - MAINTAINER_ACTIVITY_THRESHOLD_YEARS * 365.25 * 24 * 60 * 60 + + for a in activity_list: + last_activity = max(a.last_seen, a.last_package) + if last_activity < threshold: + logging.info('%s %s %s %s', a.name, a.email, last_activity, a.pkgs) + pkg_list = [packages[arch][p].orig_name for p in a.pkgs] + + hdr = {} + hdr['To'] = a.email + hdr['From'] = 'cygwin-no-reply@cygwin.com' + hdr['Envelope-From'] = common_constants.ALWAYS_BCC # we want to see bounces + hdr['Reply-To'] = 'cygwin-apps@cygwin.com' + hdr['Bcc'] = common_constants.ALWAYS_BCC + hdr['Subject'] = 'cygwin package maintainer account for %s' % a.name + hdr['X-Calm-Inactive-Maintainer'] = '1' + + msg = template.format(a.name, pkg2html.tsformat(last_activity), '\n'.join(pkg_list)) + + msg_id = utils.sendmail(hdr, msg) + logging.info('%s', msg_id) + + +if __name__ == "__main__": + relarea_default = common_constants.FTP + homedir_default = common_constants.HOMEDIR + pkglist_default = common_constants.PKGMAINT + + parser = argparse.ArgumentParser(description='Send mail to inactive maintainers') + parser.add_argument('--homedir', action='store', metavar='DIR', help="maintainer home directory (default: " + homedir_default + ")", default=homedir_default) + parser.add_argument('--pkglist', action='store', metavar='FILE', help="package maintainer list (default: " + pkglist_default + ")", default=pkglist_default) + parser.add_argument('--releasearea', action='store', metavar='DIR', help="release directory (default: " + relarea_default + ")", default=relarea_default, dest='relarea') + + (args) = parser.parse_args() + + logging.getLogger().setLevel(logging.INFO) + logging.basicConfig(format=os.path.basename(sys.argv[0]) + ': %(message)s') + + main(args) diff --git a/calm/reports.py b/calm/reports.py index 240b5d9..cd772fe 100644 --- a/calm/reports.py +++ b/calm/reports.py @@ -281,9 +281,9 @@ def unstable(args, packages, reportlist): write_report(args, 'Packages marked as unstable', body, 'unstable.html', reportlist) -# produce a report on maintainer (in)activity +# gather data on maintainer activity # -def maintainer_activity(args, packages, reportlist): +def maintainer_activity(args, packages): activity_list = [] arch = 'x86_64' @@ -296,8 +296,12 @@ def maintainer_activity(args, packages, reportlist): a = types.SimpleNamespace() a.name = m.name + a.email = m.email a.last_seen = m.last_seen + # because last_seen hasn't been collected for very long, we also try to + # estimate by looking at packages (this isn't very good as it gets + # confused by co-mainainted packages) count = 0 mtime = 0 pkgs = [] @@ -329,13 +333,22 @@ def maintainer_activity(args, packages, reportlist): activity_list.append(a) + return activity_list + + +# produce a report on maintainer (in)activity +# +def maintainer_activity_report(args, packages, reportlist): + arch = 'x86_64' + activity_list = maintainer_activity(args, packages) + body = io.StringIO() print('

Maintainer activity.

', file=body) print('', file=body) print('', file=body) - for a in sorted(activity_list, key=lambda i: (i.last_seen, i.last_package)): + for a in sorted(activity_list, key=lambda i: max(i.last_seen, i.last_package)): def pkg_details(pkgs): return '
%d%s
' % (len(pkgs), ', '.join(linkify(p, packages[arch][p]) for p in pkgs)) @@ -529,7 +542,7 @@ def do_reports(args, packages): provides_rebuild(args, packages, 'ruby_rebuilds.html', 'ruby', reportlist) python_rebuild(args, packages, 'python_rebuilds.html', reportlist) - maintainer_activity(args, packages, reportlist) + maintainer_activity_report(args, packages, reportlist) fn = os.path.join(args.htdocs, 'reports_list.inc') with utils.open_amifc(fn) as f: diff --git a/calm/utils.py b/calm/utils.py index 6fb93b9..16b69a3 100644 --- a/calm/utils.py +++ b/calm/utils.py @@ -169,6 +169,8 @@ def sendmail(hdr, msg): if not hdr['To']: return + envelope_from = hdr.pop('Envelope-From', hdr['From']) + # build the email m = email.message.Message() @@ -195,7 +197,7 @@ def sendmail(hdr, msg): logging.debug(msg) logging.debug('-' * 40) else: - with subprocess.Popen(['/usr/sbin/sendmail', '-t', '-oi', '-f', hdr['From']], stdin=subprocess.PIPE) as p: + with subprocess.Popen(['/usr/sbin/sendmail', '-t', '-oi', '-f', envelope_from], stdin=subprocess.PIPE) as p: p.communicate(m.as_bytes()) logging.debug('sendmail: msgid %s, exit status %d' % (m['Message-Id'], p.returncode))
Maintainer# packagesLast sshLatest package