17ec681f3Smrg#!/usr/bin/env python3 27ec681f3Smrg# SPDX-License-Identifier: MIT 37ec681f3Smrg 47ec681f3Smrg# Copyright © 2021 Intel Corporation 57ec681f3Smrg 67ec681f3Smrg# Permission is hereby granted, free of charge, to any person obtaining a copy 77ec681f3Smrg# of this software and associated documentation files (the "Software"), to deal 87ec681f3Smrg# in the Software without restriction, including without limitation the rights 97ec681f3Smrg# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 107ec681f3Smrg# copies of the Software, and to permit persons to whom the Software is 117ec681f3Smrg# furnished to do so, subject to the following conditions: 127ec681f3Smrg 137ec681f3Smrg# The above copyright notice and this permission notice shall be included in 147ec681f3Smrg# all copies or substantial portions of the Software. 157ec681f3Smrg 167ec681f3Smrg# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 177ec681f3Smrg# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 187ec681f3Smrg# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 197ec681f3Smrg# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 207ec681f3Smrg# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 217ec681f3Smrg# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 227ec681f3Smrg# SOFTWARE. 237ec681f3Smrg 247ec681f3Smrg"""Helper script for manipulating the release calendar.""" 257ec681f3Smrg 267ec681f3Smrgfrom __future__ import annotations 277ec681f3Smrgimport argparse 287ec681f3Smrgimport csv 297ec681f3Smrgimport contextlib 307ec681f3Smrgimport datetime 317ec681f3Smrgimport pathlib 327ec681f3Smrgimport subprocess 337ec681f3Smrgimport typing 347ec681f3Smrg 357ec681f3Smrgif typing.TYPE_CHECKING: 367ec681f3Smrg import _csv 377ec681f3Smrg from typing_extensions import Protocol 387ec681f3Smrg 397ec681f3Smrg class RCArguments(Protocol): 407ec681f3Smrg """Typing information for release-candidate command arguments.""" 417ec681f3Smrg 427ec681f3Smrg manager: str 437ec681f3Smrg 447ec681f3Smrg class FinalArguments(Protocol): 457ec681f3Smrg """Typing information for release command arguments.""" 467ec681f3Smrg 477ec681f3Smrg series: str 487ec681f3Smrg manager: str 497ec681f3Smrg zero_released: bool 507ec681f3Smrg 517ec681f3Smrg class ExtendArguments(Protocol): 527ec681f3Smrg """Typing information for extend command arguments.""" 537ec681f3Smrg 547ec681f3Smrg series: str 557ec681f3Smrg count: int 567ec681f3Smrg 577ec681f3Smrg 587ec681f3Smrg CalendarRowType = typing.Tuple[typing.Optional[str], str, str, str, typing.Optional[str]] 597ec681f3Smrg 607ec681f3Smrg 617ec681f3Smrg_ROOT = pathlib.Path(__file__).parent.parent 627ec681f3SmrgCALENDAR_CSV = _ROOT / 'docs' / 'release-calendar.csv' 637ec681f3SmrgVERSION = _ROOT / 'VERSION' 647ec681f3SmrgLAST_RELEASE = 'This is the last planned release of the {}.x series.' 657ec681f3SmrgOR_FINAL = 'Or {}.0 final.' 667ec681f3Smrg 677ec681f3Smrg 687ec681f3Smrgdef read_calendar() -> typing.List[CalendarRowType]: 697ec681f3Smrg """Read the calendar and return a list of it's rows.""" 707ec681f3Smrg with CALENDAR_CSV.open('r') as f: 717ec681f3Smrg return [typing.cast('CalendarRowType', tuple(r)) for r in csv.reader(f)] 727ec681f3Smrg 737ec681f3Smrg 747ec681f3Smrgdef commit(message: str) -> None: 757ec681f3Smrg """Commit the changes the the release-calendar.csv file.""" 767ec681f3Smrg subprocess.run(['git', 'commit', str(CALENDAR_CSV), '--message', message]) 777ec681f3Smrg 787ec681f3Smrg 797ec681f3Smrg 807ec681f3Smrgdef _calculate_release_start(major: str, minor: str) -> datetime.date: 817ec681f3Smrg """Calclulate the start of the release for release candidates. 827ec681f3Smrg 837ec681f3Smrg This is quarterly, on the second wednesday, in Januray, April, July, and Octobor. 847ec681f3Smrg """ 857ec681f3Smrg quarter = datetime.date.fromisoformat(f'20{major}-0{[1, 4, 7, 10][int(minor)]}-01') 867ec681f3Smrg 877ec681f3Smrg # Wednesday is 3 887ec681f3Smrg day = quarter.isoweekday() 897ec681f3Smrg if day > 3: 907ec681f3Smrg # this will walk back into the previous month, it's much simpler to 917ec681f3Smrg # duplicate the 14 than handle the calculations for the month and year 927ec681f3Smrg # changing. 937ec681f3Smrg return quarter.replace(day=quarter.day - day + 3 + 14) 947ec681f3Smrg elif day < 3: 957ec681f3Smrg quarter = quarter.replace(day=quarter.day + 3 - day) 967ec681f3Smrg return quarter.replace(day=quarter.day + 14) 977ec681f3Smrg 987ec681f3Smrg 997ec681f3Smrgdef release_candidate(args: RCArguments) -> None: 1007ec681f3Smrg """Add release candidate entries.""" 1017ec681f3Smrg with VERSION.open('r') as f: 1027ec681f3Smrg version = f.read().rstrip('-devel') 1037ec681f3Smrg major, minor, _ = version.split('.') 1047ec681f3Smrg date = _calculate_release_start(major, minor) 1057ec681f3Smrg 1067ec681f3Smrg data = read_calendar() 1077ec681f3Smrg 1087ec681f3Smrg with CALENDAR_CSV.open('w') as f: 1097ec681f3Smrg writer = csv.writer(f) 1107ec681f3Smrg writer.writerows(data) 1117ec681f3Smrg 1127ec681f3Smrg writer.writerow([f'{major}.{minor}', date.isoformat(), f'{major}.{minor}.0-rc1', args.manager]) 1137ec681f3Smrg for row in range(2, 4): 1147ec681f3Smrg date = date + datetime.timedelta(days=7) 1157ec681f3Smrg writer.writerow([None, date.isoformat(), f'{major}.{minor}.0-rc{row}', args.manager]) 1167ec681f3Smrg date = date + datetime.timedelta(days=7) 1177ec681f3Smrg writer.writerow([None, date.isoformat(), f'{major}.{minor}.0-rc4', args.manager, OR_FINAL.format(f'{major}.{minor}')]) 1187ec681f3Smrg 1197ec681f3Smrg commit(f'docs: Add calendar entries for {major}.{minor} release candidates.') 1207ec681f3Smrg 1217ec681f3Smrg 1227ec681f3Smrgdef _calculate_next_release_date(next_is_zero: bool) -> datetime.date: 1237ec681f3Smrg """Calculate the date of the next release. 1247ec681f3Smrg 1257ec681f3Smrg If the next is .0, we have the release in seven days, if the next is .1, 1267ec681f3Smrg then it's in 14 1277ec681f3Smrg """ 1287ec681f3Smrg date = datetime.date.today() 1297ec681f3Smrg day = date.isoweekday() 1307ec681f3Smrg if day < 3: 1317ec681f3Smrg delta = 3 - day 1327ec681f3Smrg elif day > 3: 1337ec681f3Smrg # this will walk back into the previous month, it's much simpler to 1347ec681f3Smrg # duplicate the 14 than handle the calculations for the month and year 1357ec681f3Smrg # changing. 1367ec681f3Smrg delta = (3 - day) 1377ec681f3Smrg else: 1387ec681f3Smrg delta = 0 1397ec681f3Smrg delta += 7 1407ec681f3Smrg if not next_is_zero: 1417ec681f3Smrg delta += 7 1427ec681f3Smrg return date + datetime.timedelta(days=delta) 1437ec681f3Smrg 1447ec681f3Smrg 1457ec681f3Smrgdef final_release(args: FinalArguments) -> None: 1467ec681f3Smrg """Add final release entries.""" 1477ec681f3Smrg data = read_calendar() 1487ec681f3Smrg date = _calculate_next_release_date(not args.zero_released) 1497ec681f3Smrg 1507ec681f3Smrg with CALENDAR_CSV.open('w') as f: 1517ec681f3Smrg writer = csv.writer(f) 1527ec681f3Smrg writer.writerows(data) 1537ec681f3Smrg 1547ec681f3Smrg base = 1 if args.zero_released else 0 1557ec681f3Smrg 1567ec681f3Smrg writer.writerow([args.series, date.isoformat(), f'{args.series}.{base}', args.manager]) 1577ec681f3Smrg for row in range(base + 1, 3): 1587ec681f3Smrg date = date + datetime.timedelta(days=14) 1597ec681f3Smrg writer.writerow([None, date.isoformat(), f'{args.series}.{row}', args.manager]) 1607ec681f3Smrg date = date + datetime.timedelta(days=14) 1617ec681f3Smrg writer.writerow([None, date.isoformat(), f'{args.series}.3', args.manager, LAST_RELEASE.format(args.series)]) 1627ec681f3Smrg 1637ec681f3Smrg commit(f'docs: Add calendar entries for {args.series} release.') 1647ec681f3Smrg 1657ec681f3Smrg 1667ec681f3Smrgdef extend(args: ExtendArguments) -> None: 1677ec681f3Smrg """Extend a release.""" 1687ec681f3Smrg @contextlib.contextmanager 1697ec681f3Smrg def write_existing(writer: _csv._writer, current: typing.List[CalendarRowType]) -> typing.Iterator[CalendarRowType]: 1707ec681f3Smrg """Write the orinal file, yield to insert new entries. 1717ec681f3Smrg 1727ec681f3Smrg This is a bit clever, basically what happens it writes out the 1737ec681f3Smrg original csv file until it reaches the start of the release after the 1747ec681f3Smrg one we're appending, then it yields the last row. When control is 1757ec681f3Smrg returned it writes out the rest of the original calendar data. 1767ec681f3Smrg """ 1777ec681f3Smrg last_row: typing.Optional[CalendarRowType] = None 1787ec681f3Smrg in_wanted = False 1797ec681f3Smrg for row in current: 1807ec681f3Smrg if in_wanted and row[0]: 1817ec681f3Smrg in_wanted = False 1827ec681f3Smrg assert last_row is not None 1837ec681f3Smrg yield last_row 1847ec681f3Smrg if row[0] == args.series: 1857ec681f3Smrg in_wanted = True 1867ec681f3Smrg if in_wanted and len(row) >= 5 and row[4] in {LAST_RELEASE.format(args.series), OR_FINAL.format(args.series)}: 1877ec681f3Smrg # If this was the last planned release and we're adding more, 1887ec681f3Smrg # then we need to remove that message and add it elsewhere 1897ec681f3Smrg r = list(row) 1907ec681f3Smrg r[4] = None 1917ec681f3Smrg # Mypy can't figure this out… 1927ec681f3Smrg row = typing.cast('CalendarRowType', tuple(r)) 1937ec681f3Smrg last_row = row 1947ec681f3Smrg writer.writerow(row) 1957ec681f3Smrg # If this is the only entry we can hit a case where the contextmanager 1967ec681f3Smrg # hasn't yielded 1977ec681f3Smrg if in_wanted: 1987ec681f3Smrg yield row 1997ec681f3Smrg 2007ec681f3Smrg current = read_calendar() 2017ec681f3Smrg 2027ec681f3Smrg with CALENDAR_CSV.open('w') as f: 2037ec681f3Smrg writer = csv.writer(f) 2047ec681f3Smrg with write_existing(writer, current) as row: 2057ec681f3Smrg # Get rid of -rcX as well 2067ec681f3Smrg if '-rc' in row[2]: 2077ec681f3Smrg first_point = int(row[2].split('rc')[-1]) + 1 2087ec681f3Smrg template = '{}.0-rc{}' 2097ec681f3Smrg days = 7 2107ec681f3Smrg else: 2117ec681f3Smrg first_point = int(row[2].split('-')[0].split('.')[-1]) + 1 2127ec681f3Smrg template = '{}.{}' 2137ec681f3Smrg days = 14 2147ec681f3Smrg 2157ec681f3Smrg date = datetime.date.fromisoformat(row[1]) 2167ec681f3Smrg for i in range(first_point, first_point + args.count): 2177ec681f3Smrg date = date + datetime.timedelta(days=days) 2187ec681f3Smrg r = [None, date.isoformat(), template.format(args.series, i), row[3], None] 2197ec681f3Smrg if i == first_point + args.count - 1: 2207ec681f3Smrg if days == 14: 2217ec681f3Smrg r[4] = LAST_RELEASE.format(args.series) 2227ec681f3Smrg else: 2237ec681f3Smrg r[4] = OR_FINAL.format(args.series) 2247ec681f3Smrg writer.writerow(r) 2257ec681f3Smrg 2267ec681f3Smrg commit(f'docs: Extend calendar entries for {args.series} by {args.count} releases.') 2277ec681f3Smrg 2287ec681f3Smrg 2297ec681f3Smrgdef main() -> None: 2307ec681f3Smrg parser = argparse.ArgumentParser() 2317ec681f3Smrg sub = parser.add_subparsers() 2327ec681f3Smrg 2337ec681f3Smrg rc = sub.add_parser('release-candidate', aliases=['rc'], help='Generate calendar entries for a release candidate.') 2347ec681f3Smrg rc.add_argument('manager', help="the name of the person managing the release.") 2357ec681f3Smrg rc.set_defaults(func=release_candidate) 2367ec681f3Smrg 2377ec681f3Smrg fr = sub.add_parser('release', help='Generate calendar entries for a final release.') 2387ec681f3Smrg fr.add_argument('manager', help="the name of the person managing the release.") 2397ec681f3Smrg fr.add_argument('series', help='The series to extend, such as "29.3" or "30.0".') 2407ec681f3Smrg fr.add_argument('--zero-released', action='store_true', help='The .0 release was today, the next release is .1') 2417ec681f3Smrg fr.set_defaults(func=final_release) 2427ec681f3Smrg 2437ec681f3Smrg ex = sub.add_parser('extend', help='Generate additional entries for a release.') 2447ec681f3Smrg ex.add_argument('series', help='The series to extend, such as "29.3" or "30.0".') 2457ec681f3Smrg ex.add_argument('count', type=int, help='The number of new entries to add.') 2467ec681f3Smrg ex.set_defaults(func=extend) 2477ec681f3Smrg 2487ec681f3Smrg args = parser.parse_args() 2497ec681f3Smrg args.func(args) 2507ec681f3Smrg 2517ec681f3Smrg 2527ec681f3Smrgif __name__ == "__main__": 2537ec681f3Smrg main() 254