You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/05/11 11:05:34 UTC
(tooling-trusted-releases) 01/01: #913 - make new project data available as template variables, add project key per #1227
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 42a2e468a5eaaed83d1b27413b2f77548d29ec67
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Mon May 11 11:21:05 2026 +0100
#913 - make new project data available as template variables, add project key per #1227
---
atr/construct.py | 251 +++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 180 insertions(+), 71 deletions(-)
diff --git a/atr/construct.py b/atr/construct.py
index 9fdcf8b0..0c0f77b1 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -18,7 +18,8 @@
import dataclasses
import datetime
import hashlib
-from typing import Literal
+from collections.abc import Mapping
+from typing import Final, Literal, TypedDict, overload
import aiofiles.os
import quart
@@ -32,23 +33,96 @@ import atr.util as util
type Context = Literal["announce", "announce_subject", "checklist", "vote", "vote_subject"]
-TEMPLATE_VARIABLES: list[tuple[str, str, set[Context]]] = [
- ("CHECKLIST_URL", "URL to the release checklist", {"vote"}),
- ("COMMITTEE", "Committee display name", {"announce", "checklist", "vote", "vote_subject"}),
- ("DISCLAIMER", "Podling incubation disclaimer", {"announce"}),
- ("DOWNLOAD_URL", "URL to download the release", {"announce"}),
- ("DURATION", "Vote duration in hours", {"vote"}),
- ("KEYS_FILE", "URL to the KEYS file", {"vote"}),
- ("PROJECT", "Project display name", {"announce", "announce_subject", "checklist", "vote", "vote_subject"}),
- ("RELEASE_CHECKLIST", "Release checklist content", {"vote"}),
- ("REVIEW_URL", "URL to review the release", {"checklist", "vote"}),
- ("REVISION", "Revision number", {"announce", "checklist", "vote", "vote_subject"}),
- ("TAG", "Revision tag, if set", {"announce", "checklist", "vote", "vote_subject"}),
- ("VERSION", "Version name", {"announce", "announce_subject", "checklist", "vote", "vote_subject"}),
- ("VOTE_ENDS_UTC", "Vote end date and time in UTC", {"vote_subject"}),
- ("YOUR_ASF_ID", "Your Apache UID", {"announce", "vote"}),
- ("YOUR_FULL_NAME", "Your full name", {"announce", "vote"}),
-]
+
+class _AnnounceSubjectValues(TypedDict):
+ PROJECT_NAME: str
+ VERSION: str
+
+
+class _AnnounceValues(TypedDict):
+ BUG_DATABASE: str
+ COMMITTEE: str
+ DISCLAIMER: str
+ DOWNLOAD_PAGE: str
+ DOWNLOAD_URL: str
+ HOMEPAGE: str
+ LIFECYCLE_PAGE: str
+ MAILING_LISTS: str
+ PROJECT_NAME: str
+ PROJECT_KEY: str
+ REPOSITORY: str
+ REVISION: str
+ TAG: str
+ VERSION: str
+ YOUR_ASF_ID: str
+ YOUR_FULL_NAME: str
+
+
+class _ChecklistValues(TypedDict):
+ COMMITTEE: str
+ HOMEPAGE: str
+ PROJECT_NAME: str
+ PROJECT_KEY: str
+ REVIEW_URL: str
+ REVISION: str
+ SHORT_DESCRIPTION: str
+ TAG: str
+ VERSION: str
+
+
+class _VoteSubjectValues(TypedDict):
+ COMMITTEE: str
+ PROJECT_NAME: str
+ REVISION: str
+ TAG: str
+ VERSION: str
+ VOTE_ENDS_UTC: str
+
+
+class _VoteValues(TypedDict):
+ BUG_DATABASE: str
+ CHECKLIST_URL: str
+ COMMITTEE: str
+ DURATION: str
+ HOMEPAGE: str
+ KEYS_FILE: str
+ PROJECT_NAME: str
+ PROJECT_KEY: str
+ RELEASE_CHECKLIST: str
+ REPOSITORY: str
+ REVIEW_URL: str
+ REVISION: str
+ TAG: str
+ VERSION: str
+ YOUR_ASF_ID: str
+ YOUR_FULL_NAME: str
+
+
+TEMPLATE_DESCRIPTIONS: Final[dict[str, str]] = {
+ "BUG_DATABASE": "Bug database URL",
+ "CHECKLIST_URL": "URL to the release checklist",
+ "COMMITTEE": "Committee display name",
+ "DISCLAIMER": "Podling incubation disclaimer",
+ "DOWNLOAD_PAGE": "Download page URL",
+ "DOWNLOAD_URL": "URL to download the release",
+ "DURATION": "Vote duration in hours",
+ "HOMEPAGE": "Homepage URL",
+ "KEYS_FILE": "URL to the KEYS file",
+ "LIFECYCLE_PAGE": "Lifecycle page URL",
+ "MAILING_LISTS": "Mailing lists page URL",
+ "PROJECT_NAME": "Project display name",
+ "PROJECT_KEY": "ATR key for the project",
+ "RELEASE_CHECKLIST": "Release checklist content",
+ "REPOSITORY": "Source repository URLs",
+ "REVIEW_URL": "URL to review the release",
+ "REVISION": "Revision number",
+ "SHORT_DESCRIPTION": "Short project description",
+ "TAG": "Revision tag, if set",
+ "VERSION": "Version name",
+ "VOTE_ENDS_UTC": "Vote end date and time in UTC",
+ "YOUR_ASF_ID": "Your Apache UID",
+ "YOUR_FULL_NAME": "Your full name",
+}
@dataclasses.dataclass
@@ -104,27 +178,33 @@ async def announce_release_subject_and_body(
revision_number = revision.number if revision else ""
revision_tag = revision.tag if (revision and revision.tag) else ""
- project_display_name = release.project.short_display_name if release.project else str(options.project_key)
+ project = release.project
+ project_display_name = project.short_display_name if project else str(options.project_key)
download_url = paths.committee_downloads_url(host, committee)
if options.download_path_suffix is not None:
download_url += f"/{options.download_path_suffix!s}"
download_url += "/"
- # Perform substitutions in the subject
- subject = subject.replace("{{PROJECT}}", project_display_name)
- subject = subject.replace("{{VERSION}}", str(options.version_key))
-
- # Perform substitutions in the body
- body = body.replace("{{COMMITTEE}}", committee.display_name)
- body = body.replace("{{DISCLAIMER}}", _podling_disclaimer(release.project, committee))
- body = body.replace("{{DOWNLOAD_URL}}", download_url)
- body = body.replace("{{PROJECT}}", project_display_name)
- body = body.replace("{{REVISION}}", revision_number)
- body = body.replace("{{TAG}}", revision_tag)
- body = body.replace("{{VERSION}}", str(options.version_key))
- body = body.replace("{{YOUR_ASF_ID}}", options.asfuid)
- body = body.replace("{{YOUR_FULL_NAME}}", options.fullname)
-
+ values: _AnnounceValues = {
+ "BUG_DATABASE": project.bug_database or "",
+ "COMMITTEE": committee.display_name,
+ "DISCLAIMER": _podling_disclaimer(project, committee),
+ "DOWNLOAD_PAGE": project.download_page or "",
+ "DOWNLOAD_URL": download_url,
+ "HOMEPAGE": project.homepage or "",
+ "LIFECYCLE_PAGE": project.lifecycle_page or "",
+ "MAILING_LISTS": project.mailing_lists or "",
+ "PROJECT_NAME": project_display_name,
+ "PROJECT_KEY": project.key,
+ "REPOSITORY": "\n".join(project.repository),
+ "REVISION": revision_number,
+ "TAG": revision_tag,
+ "VERSION": str(options.version_key),
+ "YOUR_ASF_ID": options.asfuid,
+ "YOUR_FULL_NAME": options.fullname,
+ }
+ subject = _substitute(subject, values, "announce_subject")
+ body = _substitute(body, values, "announce")
return subject, body
@@ -138,11 +218,11 @@ async def announce_release_subject_default(project_key: safe.ProjectKey) -> str:
def announce_subject_template_variables() -> list[tuple[str, str]]:
- return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if "announce_subject" in contexts]
+ return [(name, TEMPLATE_DESCRIPTIONS[name]) for name in sorted(_AnnounceSubjectValues.__required_keys__)]
def announce_template_variables() -> list[tuple[str, str]]:
- return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if "announce" in contexts]
+ return [(name, TEMPLATE_DESCRIPTIONS[name]) for name in sorted(_AnnounceValues.__required_keys__)]
def checklist_body(
@@ -164,17 +244,22 @@ def checklist_body(
review_path = util.as_url(vote.selected, project_key=project.key, version_key=version_key)
review_url = f"https://{host}{review_path}"
- markdown = markdown.replace("{{COMMITTEE}}", committee.display_name)
- markdown = markdown.replace("{{PROJECT}}", project.short_display_name)
- markdown = markdown.replace("{{REVIEW_URL}}", review_url)
- markdown = markdown.replace("{{REVISION}}", revision_number)
- markdown = markdown.replace("{{TAG}}", revision_tag)
- markdown = markdown.replace("{{VERSION}}", str(version_key))
- return markdown
+ values: _ChecklistValues = {
+ "COMMITTEE": committee.display_name,
+ "HOMEPAGE": project.homepage or "",
+ "PROJECT_NAME": project.short_display_name,
+ "PROJECT_KEY": project.key,
+ "REVIEW_URL": review_url,
+ "REVISION": revision_number,
+ "SHORT_DESCRIPTION": project.short_description or "",
+ "TAG": revision_tag,
+ "VERSION": str(version_key),
+ }
+ return _substitute(markdown, values, "checklist")
def checklist_template_variables() -> list[tuple[str, str]]:
- return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if "checklist" in contexts]
+ return [(name, TEMPLATE_DESCRIPTIONS[name]) for name in sorted(_ChecklistValues.__required_keys__)]
async def start_vote_default(project_key: safe.ProjectKey) -> str:
@@ -217,7 +302,8 @@ async def start_vote_subject_and_body(subject: str, body: str, options: StartVot
checklist_url = f"https://{host}{checklist_path}"
review_path = util.as_url(vote.selected, project_key=str(options.project_key), version_key=str(options.version_key))
review_url = f"https://{host}{review_path}"
- project_display_name = release.project.short_display_name if release.project else str(options.project_key)
+ project = release.project
+ project_display_name = project.short_display_name if project else str(options.project_key)
vote_end = datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=options.vote_duration)
vote_end_str = f"{vote_end.day} {vote_end.strftime('%b %H:%M')} UTC"
@@ -234,38 +320,44 @@ async def start_vote_subject_and_body(subject: str, body: str, options: StartVot
if release_policy:
checklist_content = release_policy.release_checklist or ""
- if checklist_content and release.project:
+ if checklist_content and project:
checklist_content = checklist_body(
checklist_content,
- project=release.project,
+ project=project,
version_key=options.version_key,
committee=committee,
revision=revision,
)
- # Perform substitutions in the subject
- subject = subject.replace("{{COMMITTEE}}", committee.display_name)
- subject = subject.replace("{{PROJECT}}", str(project_display_name))
- subject = subject.replace("{{REVISION}}", revision_number)
- subject = subject.replace("{{TAG}}", revision_tag)
- subject = subject.replace("{{VERSION}}", str(options.version_key))
- subject = subject.replace("{{VOTE_ENDS_UTC}}", vote_end_str)
-
- # Perform substitutions in the body
+ subject_values: _VoteSubjectValues = {
+ "COMMITTEE": committee.display_name,
+ "PROJECT_NAME": project_display_name,
+ "REVISION": revision_number,
+ "TAG": revision_tag,
+ "VERSION": str(options.version_key),
+ "VOTE_ENDS_UTC": vote_end_str,
+ }
# TODO: Handle the DURATION == 0 case
- body = body.replace("{{CHECKLIST_URL}}", checklist_url)
- body = body.replace("{{COMMITTEE}}", committee.display_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}}", str(project_display_name))
- body = body.replace("{{RELEASE_CHECKLIST}}", checklist_content)
- body = body.replace("{{REVIEW_URL}}", review_url)
- body = body.replace("{{REVISION}}", revision_number)
- body = body.replace("{{TAG}}", revision_tag)
- body = body.replace("{{VERSION}}", str(options.version_key))
- body = body.replace("{{YOUR_ASF_ID}}", options.asfuid)
- body = body.replace("{{YOUR_FULL_NAME}}", options.fullname)
-
+ body_values: _VoteValues = {
+ "BUG_DATABASE": project.bug_database or "",
+ "CHECKLIST_URL": checklist_url,
+ "COMMITTEE": committee.display_name,
+ "DURATION": str(options.vote_duration),
+ "HOMEPAGE": project.homepage or "",
+ "KEYS_FILE": keys_file or "(Sorry, the KEYS file is missing!)",
+ "PROJECT_NAME": project_display_name,
+ "PROJECT_KEY": project.key,
+ "RELEASE_CHECKLIST": checklist_content,
+ "REPOSITORY": "\n".join(project.repository),
+ "REVIEW_URL": review_url,
+ "REVISION": revision_number,
+ "TAG": revision_tag,
+ "VERSION": str(options.version_key),
+ "YOUR_ASF_ID": options.asfuid,
+ "YOUR_FULL_NAME": options.fullname,
+ }
+ subject = _substitute(subject, subject_values, "vote_subject")
+ body = _substitute(body, body_values, "vote")
return subject, body
@@ -284,11 +376,11 @@ def template_hash(template: str) -> str:
def vote_subject_template_variables() -> list[tuple[str, str]]:
- return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if "vote_subject" in contexts]
+ return [(name, TEMPLATE_DESCRIPTIONS[name]) for name in sorted(_VoteSubjectValues.__required_keys__)]
def vote_template_variables() -> list[tuple[str, str]]:
- return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if "vote" in contexts]
+ return [(name, TEMPLATE_DESCRIPTIONS[name]) for name in sorted(_VoteValues.__required_keys__)]
def _podling_disclaimer(project: sql.Project, committee: sql.Committee) -> str:
@@ -307,3 +399,20 @@ def _podling_disclaimer(project: sql.Project, committee: sql.Committee) -> str:
"does indicate that the project has yet to be fully endorsed "
"by the ASF.\n"
)
+
+
+@overload
+def _substitute(text: str, values: _AnnounceSubjectValues, context: Literal["announce_subject"]) -> str: ...
+@overload
+def _substitute(text: str, values: _AnnounceValues, context: Literal["announce"]) -> str: ...
+@overload
+def _substitute(text: str, values: _ChecklistValues, context: Literal["checklist"]) -> str: ...
+@overload
+def _substitute(text: str, values: _VoteSubjectValues, context: Literal["vote_subject"]) -> str: ...
+@overload
+def _substitute(text: str, values: _VoteValues, context: Literal["vote"]) -> str: ...
+def _substitute(text: str, values: Mapping[str, object], context: Context) -> str:
+ _ = context # marks as unused for pyright - we're using the value to pick the right overload
+ for name, value in values.items():
+ text = text.replace(f"{{{{{name}}}}}", str(value))
+ return text
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org