Add a tool for mailing long-inactive maintainers
authorJon Turney <jon.turney@dronecode.org.uk>
Fri, 31 May 2024 13:39:55 +0000 (14:39 +0100)
committerJon Turney <jon.turney@dronecode.org.uk>
Sun, 2 Jun 2024 13:43:54 +0000 (14:43 +0100)
calm/mail-inactive-maintainers.py [new file with mode: 0644]
calm/reports.py
calm/utils.py

diff --git a/calm/mail-inactive-maintainers.py b/calm/mail-inactive-maintainers.py
new file mode 100644 (file)
index 0000000..8e692a9
--- /dev/null
@@ -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 <cygwin-apps@cygwin.com>
+
+'''
+
+
+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)
index 240b5d9a5e3d774c04cc3575acb25dcb6bb2f1ae..cd772fe0bee2eb672ebe4fc3a1ccdea46a0da966 100644 (file)
@@ -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('<p>Maintainer activity.</p>', file=body)
 
     print('<table class="grid sortable">', file=body)
     print('<tr><th>Maintainer</th><th># packages</th><th>Last ssh</th><th>Latest package</th></tr>', 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 '<details><summary>%d</summary>%s</details>' % (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:
index 6fb93b97549f067da685f4af0feb2c8a77be7fb0..16b69a39e7e766f98bfbceb492c2735972346421 100644 (file)
@@ -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))
 
This page took 0.040704 seconds and 5 git commands to generate.