blob: 249725b14763093ef3752fac0507e4971974da7a [file]
#!/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`.
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 dist_root(self):
"""Base of the dist repo: dev for candidates, release for finals."""
area = "dev" if self.is_candidate else "release"
project = "incubator/openserverless" if self.incubating \
else "openserverless"
return "https://dist.apache.org/repos/dist/%s/%s" % (area, project)
@property
def artifacts_url(self):
return "%s/%s" % (self.dist_root, self.full_version)
@property
def keys_url(self):
return "%s/KEYS" % self.dist_root
@property
def downloads_url(self):
project = "incubator/openserverless" if self.incubating \
else "openserverless"
return "https://downloads.apache.org/%s/%s" % (project, 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())