atr/construct.py (98 lines of code) (raw):
# 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.
import dataclasses
import aiofiles.os
import quart
import atr.config as config
import atr.db as db
import atr.db.models as models
import atr.util as util
@dataclasses.dataclass
class AnnounceReleaseOptions:
asfuid: str
fullname: str
project_name: str
version_name: str
@dataclasses.dataclass
class StartVoteOptions:
asfuid: str
fullname: str
project_name: str
version_name: str
vote_duration: int
async def announce_release_body(body: str, options: AnnounceReleaseOptions) -> str:
# NOTE: The present module is imported by routes
# Therefore this must be done here to avoid a circular import
import atr.routes.release as routes_release
try:
host = quart.request.host
except RuntimeError:
host = config.get().APP_HOST
async with db.session() as data:
release = await data.release(
project_name=options.project_name,
version=options.version_name,
_project=True,
_committee=True,
phase=models.ReleasePhase.RELEASE_PREVIEW,
).demand(RuntimeError(f"Release {options.project_name} {options.version_name} not found"))
committee_name = release.committee.display_name if release.committee else release.project.display_name
routes_release_view = routes_release.view # type: ignore[has-type]
download_path = util.as_url(
routes_release_view, project_name=options.project_name, version_name=options.version_name
)
download_url = f"https://{host}{download_path}"
# Perform substitutions in the body
body = body.replace("[COMMITTEE]", committee_name)
body = body.replace("[DOWNLOAD_URL]", download_url)
body = body.replace("[PROJECT]", options.project_name)
body = body.replace("[VERSION]", options.version_name)
body = body.replace("[YOUR_ASF_ID]", options.asfuid)
body = body.replace("[YOUR_FULL_NAME]", options.fullname)
return body
async def announce_release_default(project_name: str) -> str:
async with db.session() as data:
project = await data.project(name=project_name, _release_policy=True).demand(
RuntimeError(f"Project {project_name} not found")
)
release_policy = project.release_policy
if release_policy is not None:
# NOTE: Do not use "if release_policy.announce_release_template is None"
# We want to check for the empty string too
if release_policy.announce_release_template:
return release_policy.announce_release_template
return """\
The Apache [COMMITTEE] project team is pleased to announce the
release of [PROJECT] [VERSION].
This is a stable release available for production use.
Downloads are available from the following URL:
[DOWNLOAD_URL]
On behalf of the Apache [COMMITTEE] project team,
[YOUR_FULL_NAME] ([YOUR_ASF_ID])
"""
async def start_vote_body(body: str, options: StartVoteOptions) -> str:
async with db.session() as data:
# Do not limit by phase, as it may be at RELEASE_CANDIDATE here if called by the task
release = await data.release(
project_name=options.project_name,
version=options.version_name,
_project=True,
_committee=True,
).demand(RuntimeError(f"Release {options.project_name} {options.version_name} not found"))
try:
host = quart.request.host
except RuntimeError:
host = config.get().APP_HOST
review_url = f"https://{host}/vote/{options.project_name}/{options.version_name}"
committee_name = release.committee.display_name if release.committee else release.project.display_name
project_short_display_name = release.project.short_display_name if release.project else options.project_name
keys_file = None
keys_file_path = util.get_finished_dir() / options.project_name / "KEYS"
if await aiofiles.os.path.isfile(keys_file_path):
keys_file = f"https://{host}/downloads/{options.project_name}/KEYS"
checklist_content = ""
async with db.session() as data:
release_policy = await db.get_project_release_policy(data, options.project_name)
if release_policy:
checklist_content = release_policy.release_checklist or ""
# Perform substitutions in the body
# TODO: Handle the DURATION == 0 case
body = body.replace("[COMMITTEE]", committee_name)
body = body.replace("[DURATION]", str(options.vote_duration))
body = body.replace("[KEYS_FILE]", keys_file or "[Sorry, the KEYS file is missing!]")
body = body.replace("[PROJECT]", project_short_display_name)
body = body.replace("[RELEASE_CHECKLIST]", checklist_content)
body = body.replace("[REVIEW_URL]", review_url)
body = body.replace("[VERSION]", options.version_name)
body = body.replace("[YOUR_ASF_ID]", options.asfuid)
body = body.replace("[YOUR_FULL_NAME]", options.fullname)
return body
async def start_vote_default(project_name: str) -> str:
async with db.session() as data:
release_policy = await db.get_project_release_policy(data, project_name)
if release_policy is not None:
# NOTE: Do not use "if release_policy.announce_release_template is None"
# We want to check for the empty string too
if release_policy.start_vote_template:
return release_policy.start_vote_template
return """Hello [COMMITTEE],
I'd like to call a vote on releasing the following artifacts as
Apache [PROJECT] [VERSION].
The release candidate page, including downloads, can be found at:
[REVIEW_URL]
The release artifacts are signed with one or more GPG keys from:
[KEYS_FILE]
Please review the release candidate and vote accordingly.
[ ] +1 Release this package
[ ] +0 Abstain
[ ] -1 Do not release this package (please provide specific comments)
You can vote on ATR at the URL above, or manually by replying to this email.
This vote will remain open for [DURATION] hours.
[RELEASE_CHECKLIST]
Thanks,
[YOUR_FULL_NAME] ([YOUR_ASF_ID])
"""