blob: b81828e854ea880ca317463de9ccb05ece7c6f84 [file] [log] [blame]
#!/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 <konstantin@linuxfoundation.org>'
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)