From bc9e09360f824612468a8a8d34987bbf7f820b86 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 4 Jan 2021 16:04:45 -0800 Subject: [PATCH] bin: Add script for manipulating the release calendar Currently it only handles creating entries for a new rc. Acked-by: Eric Engestrom Part-of: --- bin/gen_calendar_entries.py | 129 +++++++++++++++++++++++++++++++ bin/gen_calendar_entries_test.py | 108 ++++++++++++++++++++++++++ docs/release-calendar.csv | 4 +- 3 files changed, 239 insertions(+), 2 deletions(-) create mode 100755 bin/gen_calendar_entries.py create mode 100644 bin/gen_calendar_entries_test.py diff --git a/bin/gen_calendar_entries.py b/bin/gen_calendar_entries.py new file mode 100755 index 00000000000..45562eefec8 --- /dev/null +++ b/bin/gen_calendar_entries.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT + +# Copyright © 2021 Intel Corporation + +# 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. + +"""Helper script for manipulating the release calendar.""" + +from __future__ import annotations +import argparse +import csv +import contextlib +import datetime +import pathlib +import subprocess +import typing + +if typing.TYPE_CHECKING: + import _csv + from typing_extensions import Protocol + + class RCArguments(Protocol): + """Typing information for release-candidate command arguments.""" + + manager: str + + class ExtendArguments(Protocol): + """Typing information for extend command arguments.""" + + series: str + count: int + + + CalendarRowType = typing.Tuple[typing.Optional[str], str, str, str, typing.Optional[str]] + + +_ROOT = pathlib.Path(__file__).parent.parent +CALENDAR_CSV = _ROOT / 'docs' / 'release-calendar.csv' +VERSION = _ROOT / 'VERSION' +LAST_RELEASE = 'This is the last planned release of the {}.x series.' +OR_FINAL = 'Or {}.0 final.' + + +def read_calendar() -> typing.List[CalendarRowType]: + """Read the calendar and return a list of it's rows.""" + with CALENDAR_CSV.open('r') as f: + return [typing.cast('CalendarRowType', tuple(r)) for r in csv.reader(f)] + + +def commit(message: str) -> None: + """Commit the changes the the release-calendar.csv file.""" + subprocess.run(['git', 'commit', str(CALENDAR_CSV), '--message', message]) + + + +def _calculate_release_start(major: str, minor: str) -> datetime.date: + """Calclulate the start of the release for release candidates. + + This is quarterly, on the second wednesday, in Januray, April, July, and Octobor. + """ + quarter = datetime.date.fromisoformat(f'20{major}-0{[1, 4, 7, 10][int(minor)]}-01') + + # Wednesday is 3 + day = quarter.isoweekday() + if day > 3: + # this will walk back into the previous month, it's much simpler to + # duplicate the 14 than handle the calculations for the month and year + # changing. + return quarter.replace(day=quarter.day - day + 3 + 14) + elif day < 3: + quarter = quarter.replace(day=quarter.day + 3 - day) + return quarter.replace(day=quarter.day + 14) + + + +def release_candidate(args: RCArguments) -> None: + """Add release candidate entries.""" + with VERSION.open('r') as f: + version = f.read().rstrip('-devel') + major, minor, _ = version.split('.') + date = _calculate_release_start(major, minor) + + data = read_calendar() + + with CALENDAR_CSV.open('w') as f: + writer = csv.writer(f) + writer.writerows(data) + + writer.writerow([f'{major}.{minor}', date.isoformat(), f'{major}.{minor}.0-rc1', args.manager]) + for row in range(2, 4): + date = date + datetime.timedelta(days=7) + writer.writerow([None, date.isoformat(), f'{major}.{minor}.0-rc{row}', args.manager]) + date = date + datetime.timedelta(days=7) + writer.writerow([None, date.isoformat(), f'{major}.{minor}.0-rc4', args.manager, OR_FINAL.format(f'{major}.{minor}')]) + + commit(f'docs: Add calendar entries for {major}.{minor} release candidates.') + + +def main() -> None: + parser = argparse.ArgumentParser() + sub = parser.add_subparsers() + + rc = sub.add_parser('release-candidate', aliases=['rc'], help='Generate calendar entries for a release candidate.') + rc.add_argument('manager', help="the name of the person managing the release.") + rc.set_defaults(func=release_candidate) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/bin/gen_calendar_entries_test.py b/bin/gen_calendar_entries_test.py new file mode 100644 index 00000000000..30df79d4008 --- /dev/null +++ b/bin/gen_calendar_entries_test.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT + +# Copyright © 2021 Intel Corporation + +# 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. + +from __future__ import annotations +from unittest import mock +import argparse +import csv +import tempfile +import os +import pathlib + +import pytest + +from . import gen_calendar_entries + + +@pytest.fixture(autouse=True, scope='module') +def disable_git_commits() -> None: + """Mock out the commit function so no git commits are made durring testing.""" + with mock.patch('bin.gen_calendar_entries.commit', mock.Mock()): + yield + + +class TestReleaseStart: + + def test_first_is_wednesday(self) -> None: + d = gen_calendar_entries._calculate_release_start('20', '0') + assert d.day == 15 + assert d.month == 1 + assert d.year == 2020 + + def test_first_is_before_wednesday(self) -> None: + d = gen_calendar_entries._calculate_release_start('19', '0') + assert d.day == 16 + assert d.month == 1 + assert d.year == 2019 + + def test_first_is_after_wednesday(self) -> None: + d = gen_calendar_entries._calculate_release_start('21', '0') + assert d.day == 13 + assert d.month == 1 + assert d.year == 2021 + + +class TestRC: + + ORIGINAL_DATA = [ + ('20.3', '2021-01-13', '20.3.3', 'Dylan Baker', ''), + ('', '2021-01-27', '20.3.4', 'Dylan Baker', 'Last planned release of the 20.3.x series'), + ] + + @pytest.fixture(autouse=True, scope='class') + def mock_version(self) -> None: + """Keep the version set at a specific value.""" + with tempfile.TemporaryDirectory() as d: + v = os.path.join(d, 'version') + with open(v, 'w') as f: + f.write('21.0.0-devel\n') + + with mock.patch('bin.gen_calendar_entries.VERSION', pathlib.Path(v)): + yield + + @pytest.fixture(autouse=True) + def mock_data(self) -> None: + """inject our test data..""" + with tempfile.TemporaryDirectory() as d: + c = os.path.join(d, 'calendar.csv') + with open(c, 'w') as f: + writer = csv.writer(f) + writer.writerows(self.ORIGINAL_DATA) + + with mock.patch('bin.gen_calendar_entries.CALENDAR_CSV', pathlib.Path(c)): + yield + + def test_basic(self) -> None: + args = argparse.Namespace() + args.manager = "Dylan Baker" + gen_calendar_entries.release_candidate(args) + + expected = self.ORIGINAL_DATA.copy() + expected.append(('21.0', '2021-01-13', f'21.0.0-rc1', 'Dylan Baker')) + expected.append(( '', '2021-01-20', f'21.0.0-rc2', 'Dylan Baker')) + expected.append(( '', '2021-01-27', f'21.0.0-rc3', 'Dylan Baker')) + expected.append(( '', '2021-02-03', f'21.0.0-rc4', 'Dylan Baker', 'Or 21.0.0 final.')) + + actual = gen_calendar_entries.read_calendar() + + assert actual == expected diff --git a/docs/release-calendar.csv b/docs/release-calendar.csv index 9eec289f955..c6de990a20f 100644 --- a/docs/release-calendar.csv +++ b/docs/release-calendar.csv @@ -1,2 +1,2 @@ -20.3, 2021-01-13, 20.3.3, Dylan Baker, - , 2021-01-27, 20.3.4, Dylan Baker, +20.3, 2021-01-13, 20.3.3, Dylan Baker, + , 2021-01-27, 20.3.4, Dylan Baker,