| #!/usr/bin/env python3 | |
| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you 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. | |
| """Generate the [VOTE] release email for an Apache OpenServerless release. | |
| Takes a version like `0.9.0-incubating-RC4` and prints, on stdout, the | |
| mail to send to the mailing list, with every reference (dist URLs, git | |
| tag, closing date, PPMC/PMC wording) filled in consistently. | |
| The `-incubating` and `-RC<n>` parts are both optional and drive the | |
| output: | |
| * `-incubating` selects the incubator dist paths | |
| (`dist/{dev,release}/incubator/openserverless`), adds the incubator | |
| disclaimer paragraph and says "Podling PMC (PPMC)". Without it the | |
| paths are `dist/{dev,release}/openserverless` and the project is | |
| treated as a top-level project (PMC). | |
| * `-RC<n>` makes it a release *candidate*: the artifacts live under | |
| `dist/dev`, the tag carries the RC suffix and the mail is a [VOTE] | |
| with a closing date. Without it the mail is the [ANNOUNCE] of the | |
| final release, whose artifacts live under `dist/release`. | |
| The KEYS file is the exception: it is shared by all releases, so it is | |
| always linked from https://downloads.apache.org, even for candidates. | |
| Usage: | |
| ./releasing.py 0.9.0-incubating-RC4 | |
| ./releasing.py 0.9.0-incubating-RC4 --hours 96 | |
| ./releasing.py 0.9.0 --manager "Michele Sciabarra" | |
| """ | |
| import argparse | |
| import re | |
| import sys | |
| from datetime import datetime, timedelta, timezone | |
| VERSION_RE = re.compile( | |
| r"^v?(?P<base>\d+\.\d+\.\d+)" | |
| r"(?P<incubating>-incubating)?" | |
| r"(?:-(?:RC|rc)(?P<rc>\d+))?$" | |
| ) | |
| VERIFY_URL = "https://github.com/apache/openserverless/blob/0.9.0/VERIFY.md" | |
| DISCLAIMER = """\ | |
| Apache OpenServerless is an effort undergoing incubation at The Apache Software | |
| Foundation (ASF), sponsored by the Apache Incubator. Incubation is required | |
| of all newly accepted projects until a further review indicates that the | |
| infrastructure, communications, and decision making process have stabilized | |
| in a manner consistent with other successful ASF projects. While incubation | |
| status is not necessarily a reflection of the completeness or stability of | |
| the code, it does indicate that the project has yet to be fully endorsed by | |
| the ASF.""" | |
| CHECKLIST = """\ | |
| [ ] Download links are valid | |
| [ ] Checksums and signatures are valid | |
| [ ] LICENSE/NOTICE files exist | |
| [ ] No unexpected binary files in source | |
| [ ] All source files have ASF headers | |
| [ ] Can compile from source""" | |
| class Release: | |
| """The pieces of a version string, and everything derived from them.""" | |
| def __init__(self, version, hours=72, now=None): | |
| match = VERSION_RE.match(version.strip()) | |
| if not match: | |
| raise ValueError( | |
| "not a valid version: %r " | |
| "(expected something like 0.9.0-incubating-RC4)" % version | |
| ) | |
| self.base = match.group("base") | |
| self.incubating = match.group("incubating") is not None | |
| self.rc = int(match.group("rc")) if match.group("rc") else None | |
| self.hours = hours | |
| self.now = now or datetime.now(timezone.utc) | |
| # -- naming ---------------------------------------------------------- | |
| @property | |
| def name(self): | |
| """Product name as it appears in prose.""" | |
| return "Apache OpenServerless (Incubating)" if self.incubating \ | |
| else "Apache OpenServerless" | |
| @property | |
| def version(self): | |
| """Version without the RC suffix: what is actually being released.""" | |
| return self.base + ("-incubating" if self.incubating else "") | |
| @property | |
| def full_version(self): | |
| """Version including the RC suffix, if any.""" | |
| return self.version + ("-RC%d" % self.rc if self.rc else "") | |
| @property | |
| def tag(self): | |
| return "v" + self.full_version | |
| @property | |
| def is_candidate(self): | |
| return self.rc is not None | |
| @property | |
| def pmc(self): | |
| return "Apache OpenServerless Podling PMC (PPMC)" if self.incubating \ | |
| else "Apache OpenServerless PMC" | |
| # -- locations ------------------------------------------------------- | |
| @property | |
| def project_path(self): | |
| """Path of the project inside the dist repo.""" | |
| return "incubator/openserverless" if self.incubating \ | |
| else "openserverless" | |
| @property | |
| def dist_root(self): | |
| """Base of the dist repo: dev for candidates, release for finals.""" | |
| area = "dev" if self.is_candidate else "release" | |
| return "https://dist.apache.org/repos/dist/%s/%s" \ | |
| % (area, self.project_path) | |
| @property | |
| def artifacts_url(self): | |
| return "%s/%s" % (self.dist_root, self.full_version) | |
| @property | |
| def keys_url(self): | |
| """KEYS is shared by all releases, so it is linked from the | |
| downloads mirror also for candidates.""" | |
| return "https://downloads.apache.org/%s/KEYS" % self.project_path | |
| @property | |
| def downloads_url(self): | |
| return "https://downloads.apache.org/%s/%s" \ | |
| % (self.project_path, self.version) | |
| @property | |
| def tag_url(self): | |
| return "https://github.com/apache/openserverless/releases/tag/%s" \ | |
| % self.tag | |
| @property | |
| def deadline(self): | |
| return self.now + timedelta(hours=self.hours) | |
| # -- rendering ------------------------------------------------------- | |
| def vote_mail(self, manager=None): | |
| signer = manager or "the release manager" | |
| paragraphs = [ | |
| "[VOTE] Release %s %s RC%d" | |
| % (self.name, self.version, self.rc), | |
| "Hi all,", | |
| "I propose the following RC to be released as the official\n" | |
| "Apache OpenServerless %s release." % self.version, | |
| ] | |
| if self.incubating: | |
| paragraphs.append(DISCLAIMER) | |
| paragraphs += [ | |
| "The artifacts for this release candidate can be found at:\n\n%s" | |
| % self.artifacts_url, | |
| "The Git tag to be voted upon is:\n\n%s\n\n%s" | |
| % (self.tag, self.tag_url), | |
| "Release artifacts are signed with the GPG key of %s." % signer, | |
| "The KEYS file is available at:\n\n%s" % self.keys_url, | |
| "Please download, verify, and test the release candidate.", | |
| "For detailed step-by-step instructions on how to verify this\n" | |
| "release, please see the file VERIFY.md within the source\n" | |
| "archive, or check:\n\n%s" % VERIFY_URL, | |
| "The vote will run for a minimum of %d hours and close no earlier\n" | |
| "than:\n\n%s" | |
| % (self.hours, self.deadline.strftime("%Y-%m-%d %H:%M UTC")), | |
| "Please vote:\n\n" | |
| "[ ] +1 Release this package as Apache OpenServerless %s\n" | |
| "[ ] +0\n" | |
| "[ ] -1 Do not release this package because... (reason required)" | |
| % self.version, | |
| "Only %s members have binding votes, but community votes are\n" | |
| "encouraged." | |
| % ("PPMC" if self.incubating else "PMC"), | |
| "Checklist for reference:\n%s" % CHECKLIST, | |
| "On behalf of the %s," % self.pmc, | |
| ] | |
| if manager: | |
| paragraphs.append(manager) | |
| return "\n\n".join(paragraphs) + "\n" | |
| def announce_mail(self, manager=None): | |
| paragraphs = [ | |
| "[ANNOUNCE] Release %s %s" % (self.name, self.version), | |
| "Hi all,", | |
| "The %s is pleased to announce the release of\n" | |
| "Apache OpenServerless %s." % (self.pmc, self.version), | |
| "The release is available for download at:\n\n%s" | |
| % self.downloads_url, | |
| "The Git tag for this release is:\n\n%s\n\n%s" | |
| % (self.tag, self.tag_url), | |
| "The KEYS file used to sign the artifacts is available at:\n\n%s" | |
| % self.keys_url, | |
| "For instructions on how to verify the release, please see the\n" | |
| "file VERIFY.md within the source archive, or check:\n\n%s" | |
| % VERIFY_URL, | |
| ] | |
| if self.incubating: | |
| paragraphs.append(DISCLAIMER) | |
| paragraphs.append("On behalf of the %s," % self.pmc) | |
| if manager: | |
| paragraphs.append(manager) | |
| return "\n\n".join(paragraphs) + "\n" | |
| def mail(self, manager=None): | |
| return self.vote_mail(manager) if self.is_candidate \ | |
| else self.announce_mail(manager) | |
| def main(argv=None): | |
| parser = argparse.ArgumentParser( | |
| description="Generate the release email for an Apache " | |
| "OpenServerless release." | |
| ) | |
| parser.add_argument( | |
| "version", | |
| help="version to release, e.g. 0.9.0-incubating-RC4; the " | |
| "-incubating and -RC<n> parts are both optional", | |
| ) | |
| parser.add_argument( | |
| "--hours", type=int, default=72, | |
| help="minimum duration of the vote, in hours (default: 72)", | |
| ) | |
| parser.add_argument( | |
| "--manager", | |
| help="name of the release manager, used to sign off the mail", | |
| ) | |
| args = parser.parse_args(argv) | |
| try: | |
| release = Release(args.version, hours=args.hours) | |
| except ValueError as err: | |
| parser.error(str(err)) | |
| sys.stdout.write(release.mail(args.manager)) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |