This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new b65a664  Improve email signatures and general formatting
b65a664 is described below

commit b65a664e52f76123e749b820897aaba778415e85
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue May 6 17:24:17 2025 +0100

    Improve email signatures and general formatting
---
 atr/construct.py       | 22 +++++++++++++++-------
 atr/routes/announce.py |  1 +
 atr/routes/draft.py    |  1 +
 atr/routes/preview.py  |  1 +
 atr/routes/resolve.py  |  5 ++++-
 atr/routes/vote.py     |  9 +++++++--
 atr/routes/voting.py   |  1 +
 atr/tasks/vote.py      |  2 ++
 8 files changed, 32 insertions(+), 10 deletions(-)

diff --git a/atr/construct.py b/atr/construct.py
index 58feee9..a41f781 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -17,6 +17,7 @@
 
 import dataclasses
 
+import aiofiles.os
 import quart
 
 import atr.config as config
@@ -28,6 +29,7 @@ import atr.util as util
 @dataclasses.dataclass
 class AnnounceReleaseOptions:
     asfuid: str
+    fullname: str
     project_name: str
     version_name: str
 
@@ -35,6 +37,7 @@ class AnnounceReleaseOptions:
 @dataclasses.dataclass
 class StartVoteOptions:
     asfuid: str
+    fullname: str
     project_name: str
     version_name: str
     vote_duration: int
@@ -72,6 +75,7 @@ async def announce_release_body(body: str, options: 
AnnounceReleaseOptions) -> s
     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
 
@@ -100,14 +104,12 @@ Downloads are available from the following URL:
 
 On behalf of the Apache [COMMITTEE] project team,
 
-[YOUR_ASF_ID]
+[YOUR_FULL_NAME] ([YOUR_ASF_ID])
 """
 
 
 async def start_vote_body(body: str, options: StartVoteOptions) -> str:
     async with db.session() as data:
-        user_key = await 
data.public_signing_key(apache_uid=options.asfuid).get()
-        user_key_fingerprint = user_key.fingerprint if user_key else 
"0000000000000000000000000000000000000000"
         # 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,
@@ -125,6 +127,11 @@ async def start_vote_body(body: str, options: 
StartVoteOptions) -> str:
     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)
@@ -135,12 +142,13 @@ async def start_vote_body(body: str, options: 
StartVoteOptions) -> str:
     # TODO: Handle the DURATION == 0 case
     body = body.replace("[COMMITTEE]", committee_name)
     body = body.replace("[DURATION]", str(options.vote_duration))
-    body = body.replace("[KEY_FINGERPRINT]", user_key_fingerprint or "(No key 
found)")
+    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
 
@@ -164,9 +172,9 @@ The release candidate page, including downloads, can be 
found at:
 
   [REVIEW_URL]
 
-The release artifacts are signed with the GPG key with fingerprint:
+The release artifacts are signed with one or more GPG keys from:
 
-  [KEY_FINGERPRINT]
+  [KEYS_FILE]
 
 Please review the release candidate and vote accordingly.
 
@@ -180,5 +188,5 @@ This vote will remain open for [DURATION] hours.
 
 [RELEASE_CHECKLIST]
 Thanks,
-[YOUR_ASF_ID]
+[YOUR_FULL_NAME] ([YOUR_ASF_ID])
 """
diff --git a/atr/routes/announce.py b/atr/routes/announce.py
index c4c713e..d4963dd 100644
--- a/atr/routes/announce.py
+++ b/atr/routes/announce.py
@@ -147,6 +147,7 @@ async def selected_post(
                 body,
                 options=construct.AnnounceReleaseOptions(
                     asfuid=session.uid,
+                    fullname=session.fullname,
                     project_name=project_name,
                     version_name=version_name,
                 ),
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index c6ac1ae..c139e9b 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -487,6 +487,7 @@ async def vote_preview(
         form_body,
         construct.StartVoteOptions(
             asfuid=asfuid,
+            fullname=session.fullname,
             project_name=project_name,
             version_name=version_name,
             vote_duration=vote_duration,
diff --git a/atr/routes/preview.py b/atr/routes/preview.py
index 3eb731e..669b8f1 100644
--- a/atr/routes/preview.py
+++ b/atr/routes/preview.py
@@ -78,6 +78,7 @@ async def announce_preview(
         # Construct options and generate body
         options = construct.AnnounceReleaseOptions(
             asfuid=session.uid,
+            fullname=session.fullname,
             project_name=project_name,
             version_name=version_name,
         )
diff --git a/atr/routes/resolve.py b/atr/routes/resolve.py
index 07b3041..90b0b3e 100644
--- a/atr/routes/resolve.py
+++ b/atr/routes/resolve.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import json
+import os
 
 import quart
 import werkzeug.wrappers.response as response
@@ -128,6 +129,8 @@ async def selected_post(
 
 
 def task_mid_get(latest_vote_task: models.Task) -> str | None:
+    if "LOCAL_DEBUG" in os.environ:
+        return "[email protected]"
     # TODO: Improve this
     task_mid = None
 
@@ -184,7 +187,7 @@ async def _send_resolution(
     email_recipient = latest_vote_task.task_args["email_to"]
     email_sender = f"{session.uid}@apache.org"
     subject = f"[VOTE] [RESULT] Release {release.project.display_name} 
{release.version} {resolution.upper()}"
-    body = f"{body}\n\n--{session.uid}"
+    body = f"{body}\n\n-- \n{session.fullname} ({session.uid})"
     in_reply_to = vote_thread_mid
 
     task = models.Task(
diff --git a/atr/routes/vote.py b/atr/routes/vote.py
index dd7fea6..a893bf9 100644
--- a/atr/routes/vote.py
+++ b/atr/routes/vote.py
@@ -129,7 +129,12 @@ async def _send_vote(
     email_recipient = latest_vote_task.task_args["email_to"]
     email_sender = f"{session.uid}@apache.org"
     subject = f"Re: {original_subject}"
-    body = f"{vote}{('\n\n' + comment) if comment else ''}\n\n--{session.uid}"
+    body = [f"{vote.lower()} ({session.uid}) {session.fullname}"]
+    if comment:
+        body.append(f"{comment}")
+        # Only include the signature if there is a comment
+        body.append(f"-- \n{session.fullname} ({session.uid})")
+    body_text = "\n\n".join(body)
     in_reply_to = vote_thread_mid
 
     task = models.Task(
@@ -139,7 +144,7 @@ async def _send_vote(
             email_sender=email_sender,
             email_recipient=email_recipient,
             subject=subject,
-            body=body,
+            body=body_text,
             in_reply_to=in_reply_to,
         ).model_dump(),
         release_name=release.name,
diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index 7052ec9..6c26d5f 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -138,6 +138,7 @@ async def selected_revision(
                     email_to=email_to,
                     vote_duration=vote_duration_choice,
                     initiator_id=session.uid,
+                    initiator_fullname=session.fullname,
                     subject=subject_data,
                     body=body_data,
                 ).model_dump(),
diff --git a/atr/tasks/vote.py b/atr/tasks/vote.py
index 0ed5ee7..0e01322 100644
--- a/atr/tasks/vote.py
+++ b/atr/tasks/vote.py
@@ -41,6 +41,7 @@ class Initiate(pydantic.BaseModel):
     email_to: str = pydantic.Field(..., description="The mailing list address 
to send the vote email to")
     vote_duration: int = pydantic.Field(..., description="Duration of the vote 
in hours")
     initiator_id: str = pydantic.Field(..., description="ASF ID of the vote 
initiator")
+    initiator_fullname: str = pydantic.Field(..., description="Full name of 
the vote initiator")
     subject: str = pydantic.Field(..., description="Subject line for the vote 
email")
     body: str = pydantic.Field(..., description="Body content for the vote 
email")
 
@@ -107,6 +108,7 @@ async def _initiate_core_logic(args: Initiate) -> dict[str, 
Any]:
         args.body,
         construct.StartVoteOptions(
             asfuid=args.initiator_id,
+            fullname=args.initiator_fullname,
             project_name=release.project.name,
             version_name=release.version,
             vote_duration=args.vote_duration,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to