#!/usr/bin/env python3 # Quickie script to estimate mainline release dates. # # SPDX-License-Identifier: GPL-2.0-or-later # # -*- coding: utf-8 -*- # __author__ = 'Konstantin Ryabitsev ' import argparse import requests import datetime import logging import json import sys RELEASES_JSON = 'https://www.kernel.org/releases.json' WINDOW_DAYS = 14 RC_COUNT = 7 CYCLE_DAYS = WINDOW_DAYS + (RC_COUNT * 7) VERSION = '1.0' # This is where the minor version number starts looking "too big" and # we go to the "next major version dot-zero" -- Linus says that it will # likely always be after 19. The only time this went to 20 was for # 4.20, and the only explanation we can give is that everyone was too # high at the time to notice. TOOBIG = 19 logger = logging.getLogger('mainline-when') def parse_version(ver): rcn = None if ver.find('-') > 0: ver, rc = ver.split('-') rcn = int(rc[2:]) jv, nv = ver.split('.') majver = int(jv) minver = int(nv) return majver, minver, rcn def main(estnext=3, forcever=None, rjson=None): if rjson is None: rses = requests.session() headers = {'User-Agent': f'mainline-when/{VERSION}'} rses.headers.update(headers) resp = rses.get(RELEASES_JSON) resp.raise_for_status() rels = json.loads(resp.content) else: with open(rjson, 'r') as fh: content = fh.read() rels = json.loads(content) release = None for release in rels['releases']: if release['moniker'] != 'mainline': continue break if release is None: logger.critical('Could not find mainline release info in %s', RELEASES_JSON) sys.exit(1) ics = list() if forcever: majver, minver, rcn = parse_version(forcever) else: majver, minver, rcn = parse_version(release.get('version')) crel = datetime.datetime.strptime(release['released']['isodate'], '%Y-%m-%d') if rcn: logger.info(f'current status: {majver}.{minver}-rc{rcn}') mrel = crel - datetime.timedelta(days=(7*rcn)+7) if rcn < 8: frel = mrel + datetime.timedelta(days=CYCLE_DAYS) else: # Add 7 days to the latest release and hope for the best frel = crel + datetime.timedelta(days=7) else: # We're currently in a merge window minver += 1 if minver > TOOBIG: majver += 1 minver = 0 logger.info(f'current status: {majver}.{minver} merge window') mrel = crel frel = crel + datetime.timedelta(days=CYCLE_DAYS) logger.info('---') wo = mrel + datetime.timedelta(days=1) wc = mrel + datetime.timedelta(days=WINDOW_DAYS) ics.append((majver, minver, wo, wc, frel)) if rcn: logger.info(f'{majver}.{minver} window open : {wo.strftime("%Y-%m-%d")}') logger.info(f'{majver}.{minver} window close: {wc.strftime("%Y-%m-%d")}') logger.info(f'{majver}.{minver} rc{rcn} : {crel.strftime("%Y-%m-%d")} <-- you are here') else: logger.info(f'{majver}.{minver} window open : {wo.strftime("%Y-%m-%d")} <-- you are here') logger.info(f'{majver}.{minver} window close: {wc.strftime("%Y-%m-%d")}') logger.info(f'{majver}.{minver} final : {frel.strftime("%Y-%m-%d")}') # Estimate next versions for nextver in range(minver+1, minver+estnext+1): if nextver > TOOBIG: estmaj = majver + 1 estmin = nextver - (TOOBIG + 1) else: estmaj = majver estmin = nextver logger.info('---') wo = frel + datetime.timedelta(days=1) wc = frel + datetime.timedelta(days=WINDOW_DAYS) logger.info(f'{estmaj}.{estmin} window open : {wo.strftime("%Y-%m-%d")}') logger.info(f'{estmaj}.{estmin} window close: {wc.strftime("%Y-%m-%d")}') frel = frel + datetime.timedelta(days=CYCLE_DAYS) logger.info(f'{estmaj}.{estmin} final : {frel.strftime("%Y-%m-%d")}') ics.append((estmaj, estmin, wo, wc, frel)) logger.info('---') logger.info('NB: All dates set in the future are estimates.') return ics def write_ics(ics, outfile, domain): if not domain: domain = 'mainline-when.local' now = datetime.datetime.now() admonition = 'NOTE: all dates set in the future are estimates.' with open(outfile, 'w') as fh: fh.write('BEGIN:VCALENDAR\r\n') fh.write('VERSION:2.0\r\n') fh.write(f'PRODID:{domain}\r\n') fh.write('METHOD:PUBLISH\r\n') for majver, minver, wo, wc, frel in ics: # Merge window fh.write('BEGIN:VEVENT\r\n') fh.write(f'UID:kernel-v{majver}.{minver}-merge-window@{domain}\r\n') fh.write(f'SUMMARY:Kernel v{majver}.{minver} merge window\r\n') if wo > now: fh.write(f'DESCRIPTION:{admonition}\r\n') fh.write('CLASS:PUBLIC\r\n') fh.write(f'DTSTART;VALUE=DATE:{wo.strftime("%Y%m%d")}\r\n') fh.write(f'DTEND;VALUE=DATE:{wc.strftime("%Y%m%d")}\r\n') fh.write(f'CREATED:{now.strftime("%Y%m%d")}\r\n') fh.write(f'LAST-MODIFIED:{now.strftime("%Y%m%d")}\r\n') fh.write('END:VEVENT\r\n') # rc1 fh.write('BEGIN:VEVENT\r\n') fh.write(f'UID:kernel-v{majver}.{minver}-rc1@{domain}\r\n') fh.write(f'SUMMARY:Kernel v{majver}.{minver}-rc1 release\r\n') if wc > now: fh.write(f'DESCRIPTION:{admonition}\r\n') fh.write('CLASS:PUBLIC\r\n') fh.write(f'DTSTART;VALUE=DATE:{wc.strftime("%Y%m%d")}\r\n') fh.write(f'CREATED:{now.strftime("%Y%m%d")}\r\n') fh.write(f'LAST-MODIFIED:{now.strftime("%Y%m%d")}\r\n') fh.write('END:VEVENT\r\n') # final fh.write('BEGIN:VEVENT\r\n') fh.write(f'UID:kernel-v{majver}.{minver}-final@{domain}\r\n') fh.write(f'SUMMARY:Kernel v{majver}.{minver} final release\r\n') if frel > now: fh.write(f'DESCRIPTION:{admonition} If deemed necessary, this may end up being another -rc release.') fh.write('\r\n') fh.write('CLASS:PUBLIC\r\n') fh.write(f'DTSTART;VALUE=DATE:{frel.strftime("%Y%m%d")}\r\n') fh.write(f'CREATED:{now.strftime("%Y%m%d")}\r\n') fh.write(f'LAST-MODIFIED:{now.strftime("%Y%m%d")}\r\n') fh.write('END:VEVENT\r\n') fh.write('END:VCALENDAR\r\n') if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-n', '--next', type=int, default=3, help='How many versions to estimate') parser.add_argument('-i', '--ics-out', help='Write an .ics file instead') parser.add_argument('-d', '--ics-domain', help='Domain to use for ics generation') parser.add_argument('-r', '--releases-json', help='Use this local copy of releases.json') parser.add_argument('--force-version', help='Force version to be this (testing only)') cmdargs = parser.parse_args() logger.setLevel(logging.DEBUG) ch = logging.StreamHandler() formatter = logging.Formatter('%(message)s') ch.setFormatter(formatter) if cmdargs.ics_out: ch.setLevel(logging.CRITICAL) else: ch.setLevel(logging.INFO) logger.addHandler(ch) icsdata = main(estnext=cmdargs.next, forcever=cmdargs.force_version, rjson=cmdargs.releases_json) if cmdargs.ics_out: write_ics(icsdata, cmdargs.ics_out, cmdargs.ics_domain)