diff --git a/openstack_election/cmds/update_releases_calendar.py b/openstack_election/cmds/update_releases_calendar.py new file mode 100644 index 00000000..7d1e45da --- /dev/null +++ b/openstack_election/cmds/update_releases_calendar.py @@ -0,0 +1,176 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import datetime +import os +import sys +import textwrap + +from openstack_election import config +from openstack_election import yamlutils + +conf = config.load_conf() +rst_template = textwrap.dedent(""" + {release} TC and PTL Elections + --------------------------- + + .. _{nrl}-election-email-deadline: + + {release} Election Email Deadline + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Contributors that will be in the electorate for the upcoming election + should confirm their gerrit email addresses by this date ({deadline_date} + at {deadline_time} UTC). Electorate rolls are generated after this date and ballots will + be sent to the listed gerrit email address. + + .. _{nrl}-election-nominations: + + {release} Election Nomination Begins + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Candidates interested in serving for the next calendar year (TC), or + development cycle (PTL) should announce their candidacies and platforms during + this week. Please see the `Election site`_ for specific timing information. + + .. _{nrl}-election-campaigning: + + {release} Election Campaigning Begins + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + The electorate has time to ask candidates questions about their platforms + and debate topics before polling begins. Please see the `Election site`_ for + specific timing information. + + .. _{nrl}-election-voting: + + {release} Election Polling Begins + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Election polling for open seats on the TC and any required PTL elections. + Please see the `Election site`_ for specific timing information. + + .. _{nrl}-election-close: + + {release} Election Polling Ends + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + All polls close in the {release} Election and results announced. Please see the + `Election site`_ for specific timing information. + + .. _Election site: https://governance.openstack.org/election/ + """) # noqa + + +def next_release_letter(release_name): + ord_a = ord('a') + next_ord = (ord(release_name[0]) - ord_a + 1) % 26 + next_letter = chr(ord_a + next_ord) + return next_letter + + +def custom_strftime(format, t): + def _suffix(d): + return {1: 'st', 2: 'nd', 3: 'rd'}.get(d % 20, 'th') + return t.strftime(format).replace('{S}', str(t.day) + _suffix(t.day)) + + +def add_election_event_to_schedule(nrl, election_events, + schedule_events, schedule_data): + election_event_idx = 0 + for cycle_event in schedule_data["cycle"]: + (cycle_event_start, cycle_event_end) = ( + datetime.date.fromisoformat(cycle_event["start"]), + datetime.date.fromisoformat(cycle_event["end"])) + event_type = election_events[election_event_idx] + event_label = f"{nrl}-election-{event_type}" + (event_start, event_end) = schedule_events[event_label] + + if cycle_event_start < event_start.date(): + continue + + if "x-project" not in cycle_event: + cycle_event["x-project"] = [] + if event_label not in cycle_event["x-project"]: + cycle_event["x-project"].append(event_label) + + if event_end.date() < cycle_event_end: + election_event_idx += 1 + if election_event_idx >= len(election_events): + break + + +def main(): + if conf["election_type"] != "combined": + print("This tool currently only supports 'combined' elections", + file=sys.stderr) + return 1 + + description = ('Update development cycle in openstack/releases with ' + ' key election events') + parser = argparse.ArgumentParser(description) + parser.add_argument('--releases-repo', dest='releases_repo', + required=True, + help=('Path to a clone of the releases repo')) + + args = parser.parse_args() + + series_path = os.path.join(args.releases_repo, "data", + "series_status.yaml") + with open(series_path) as f: + series_data = yamlutils.loads(f) + + release = conf.get("release") + release_name = series_data[0]["name"] + nrl = next_release_letter(release_name) + + schedule_path = os.path.join(args.releases_repo, "doc", "source", + release_name, "schedule.yaml") + with open(schedule_path) as f: + schedule_data = yamlutils.loads(f) + + rst_path = os.path.join(args.releases_repo, "doc", "source", release_name, + "schedule.rst") + with open(rst_path) as f: + rst = f.read() + + email_deadline = conf["timeframe"]["email_deadline"] + deadline_date = custom_strftime('%B {S}, %Y', email_deadline) + deadline_time = email_deadline.strftime("%H:%M") + rst_blob = rst_template.format(**dict(release=release, + nrl=nrl, + deadline_time=deadline_time, + deadline_date=deadline_date,)) + election_canary = rst_blob.split("\n")[1] + if election_canary not in rst: + print(f"Updating {rst_path} to add {rst_blob}\n") + with open(rst_path, "w") as f: + f.write(rst) + f.write(rst_blob) + + schedule_events = {f"{nrl}-election-close": + (conf["timeframe"]["end"], conf["timeframe"]["end"]), + f"{nrl}-election-email-deadline": + (email_deadline, email_deadline), } + for event in conf["timeline"]: + event_type = event["name"].split()[-1].lower() + if event_type == "election": + event_type = "voting" + event_label = f"{nrl}-election-{event_type}" + schedule_events[event_label] = (event["start"], event["end"]) + + election_events = ["email-deadline"] + add_election_event_to_schedule(nrl, election_events, schedule_events, + schedule_data) + election_events = ["nominations", "campaigning", "voting", "close"] + add_election_event_to_schedule(nrl, election_events, schedule_events, + schedule_data) + + with open(schedule_path, "w") as f: + print(f"Updating {schedule_path}") + f.write(yamlutils.dumps(schedule_data)) diff --git a/openstack_election/yamlutils.py b/openstack_election/yamlutils.py new file mode 100644 index 00000000..7e74f00f --- /dev/null +++ b/openstack_election/yamlutils.py @@ -0,0 +1,34 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ruamel.yaml +import ruamel.yaml.compat + + +def dumps(obj): + """Dumps yaml content into a string.""" + yaml = ruamel.yaml.YAML() + yaml.width = 80 + + stream = ruamel.yaml.compat.StringIO() + yaml.explicit_start = True + yaml.indent(mapping=2, sequence=4, offset=2) + yaml.dump(obj, stream) + return stream.getvalue() + + +def loads(blob): + """Load a yaml blob and retain key ordering.""" + yaml = ruamel.yaml.YAML() + return yaml.load(blob) diff --git a/requirements.txt b/requirements.txt index acf69f4c..8910749b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ ndg-httpsclient>=0.4.2;python_version<'3.0' # BSD PrettyTable<0.8,>=0.7.1 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain Jinja2>=2.10 # BSD License (3 clause) +ruamel.yaml>=0.15 diff --git a/setup.cfg b/setup.cfg index 9ccabdcc..ac4e1fda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,3 +37,4 @@ console_scripts = owners = openstack_election.cmds.change_owners:main template-emails = openstack_election.cmds.template_emails:main setup-election-config = openstack_election.cmds.setup_election_config:main + update-releases-calendar = openstack_election.cmds.update_releases_calendar:main