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 2160ef4  Send a resolution message when a vote is resolved
2160ef4 is described below

commit 2160ef446e5aa901da5437a4288fd47936895fba
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon May 5 15:47:29 2025 +0100

    Send a resolution message when a vote is resolved
---
 atr/routes/resolve.py             | 58 ++++++++++++++++++++++++++-
 atr/routes/vote.py                | 84 ++++++++++++++++++++++-----------------
 atr/routes/voting.py              |  5 +--
 atr/templates/check-selected.html |  4 ++
 atr/util.py                       | 15 +++++++
 5 files changed, 124 insertions(+), 42 deletions(-)

diff --git a/atr/routes/resolve.py b/atr/routes/resolve.py
index da2b35e..87eedfc 100644
--- a/atr/routes/resolve.py
+++ b/atr/routes/resolve.py
@@ -31,6 +31,7 @@ import atr.routes.compose as compose
 import atr.routes.finish as finish
 import atr.routes.vote as vote
 import atr.util as util
+from atr.tasks import message
 
 
 class ResolveForm(util.QuartFormTyped):
@@ -44,6 +45,7 @@ class ResolveForm(util.QuartFormTyped):
         choices=[("passed", "Passed"), ("failed", "Failed")],
         validators=[wtforms.validators.InputRequired("Vote result is 
required")],
     )
+    resolution_body = wtforms.TextAreaField("Resolution email body", 
validators=[wtforms.validators.Optional()])
     submit = wtforms.SubmitField("Resolve vote")
 
 
@@ -79,7 +81,7 @@ async def selected_post(
 
     candidate_name = form.candidate_name.data
     vote_result = form.vote_result.data
-
+    resolution_body = util.unwrap_type(form.resolution_body.data, str)
     if not candidate_name:
         return await session.redirect(
             vote.selected, error="Missing candidate name", 
project_name=project_name, version_name=version_name
@@ -100,7 +102,12 @@ async def selected_post(
     async with db.session() as data:
         async with data.begin():
             release = await session.release(
-                project_name, version_name, 
phase=models.ReleasePhase.RELEASE_CANDIDATE, data=data
+                project_name,
+                version_name,
+                with_tasks=True,
+                with_project=True,
+                phase=models.ReleasePhase.RELEASE_CANDIDATE,
+                data=data,
             )
 
             # Update the release phase based on vote result
@@ -114,6 +121,10 @@ async def selected_post(
                 success_message = "Vote marked as failed"
                 destination = compose.selected
 
+    error_message = await _send_resolution(session, release, vote_result, 
resolution_body)
+    if error_message is not None:
+        await quart.flash(error_message, "error")
+
     return await session.redirect(
         destination, success=success_message, project_name=project_name, 
version_name=release.version
     )
@@ -155,6 +166,49 @@ def _format_artifact_name(project_name: str, version: str, 
is_podling: bool = Fa
     return f"apache-{project_name}-{version}"
 
 
+async def _send_resolution(
+    session: routes.CommitterSession,
+    release: models.Release,
+    resolution: str,
+    body: str,
+) -> str | None:
+    # Get the email thread
+    latest_vote_task = release_latest_vote_task(release)
+    if latest_vote_task is None:
+        return "No vote task found, unable to send resolution message."
+    vote_thread_mid = task_mid_get(latest_vote_task)
+    if vote_thread_mid is None:
+        return "No vote thread found, unable to send resolution message."
+
+    # Construct the reply email
+    # original_subject = latest_vote_task.task_args["subject"]
+
+    # Arguments for the task to cast a vote
+    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}"
+    in_reply_to = vote_thread_mid
+
+    task = models.Task(
+        status=models.TaskStatus.QUEUED,
+        task_type=models.TaskType.MESSAGE_SEND,
+        task_args=message.Send(
+            email_sender=email_sender,
+            email_recipient=email_recipient,
+            subject=subject,
+            body=body,
+            in_reply_to=in_reply_to,
+        ).model_dump(),
+        release_name=release.name,
+    )
+    async with db.session() as data:
+        data.add(task)
+        await data.flush()
+        await data.commit()
+    return None
+
+
 async def _task_archive_url(task_mid: str) -> str | None:
     if "@" not in task_mid:
         return None
diff --git a/atr/routes/vote.py b/atr/routes/vote.py
index e063b1b..540af6e 100644
--- a/atr/routes/vote.py
+++ b/atr/routes/vote.py
@@ -72,46 +72,12 @@ async def selected_post(session: routes.CommitterSession, 
project_name: str, ver
 
         vote = str(form.vote_value.data)
         comment = str(form.vote_comment.data)
-
-        # Get the email thread
-        latest_vote_task = resolve.release_latest_vote_task(release)
-        if latest_vote_task is None:
-            return await session.redirect(
-                selected, project_name=project_name, 
version_name=version_name, error="No vote task found."
-            )
-        vote_thread_mid = resolve.task_mid_get(latest_vote_task)
-        if vote_thread_mid is None:
+        email_recipient, error_message = await _send_vote(session, release, 
vote, comment)
+        if error_message:
             return await session.redirect(
-                selected, project_name=project_name, 
version_name=version_name, error="No vote thread found."
+                selected, project_name=project_name, 
version_name=version_name, error=error_message
             )
 
-        # Construct the reply email
-        original_subject = latest_vote_task.task_args["subject"]
-
-        # Arguments for the task to cast a 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}"
-        in_reply_to = vote_thread_mid
-
-        task = models.Task(
-            status=models.TaskStatus.QUEUED,
-            task_type=models.TaskType.MESSAGE_SEND,
-            task_args=message.Send(
-                email_sender=email_sender,
-                email_recipient=email_recipient,
-                subject=subject,
-                body=body,
-                in_reply_to=in_reply_to,
-            ).model_dump(),
-            release_name=release.name,
-        )
-        async with db.session() as data:
-            data.add(task)
-            await data.flush()
-            await data.commit()
-
         success_message = f"Sending your vote to {email_recipient}."
         return await session.redirect(
             selected, project_name=project_name, version_name=version_name, 
success=success_message
@@ -125,3 +91,47 @@ async def selected_post(session: routes.CommitterSession, 
project_name: str, ver
         return await session.redirect(
             selected, project_name=project_name, version_name=version_name, 
error=error_message
         )
+
+
+async def _send_vote(
+    session: routes.CommitterSession,
+    release: models.Release,
+    vote: str,
+    comment: str,
+) -> tuple[str, str]:
+    # Get the email thread
+    latest_vote_task = resolve.release_latest_vote_task(release)
+    if latest_vote_task is None:
+        return "", "No vote task found."
+    vote_thread_mid = resolve.task_mid_get(latest_vote_task)
+    if vote_thread_mid is None:
+        return "", "No vote thread found."
+
+    # Construct the reply email
+    original_subject = latest_vote_task.task_args["subject"]
+
+    # Arguments for the task to cast a 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}"
+    in_reply_to = vote_thread_mid
+
+    task = models.Task(
+        status=models.TaskStatus.QUEUED,
+        task_type=models.TaskType.MESSAGE_SEND,
+        task_args=message.Send(
+            email_sender=email_sender,
+            email_recipient=email_recipient,
+            subject=subject,
+            body=body,
+            in_reply_to=in_reply_to,
+        ).model_dump(),
+        release_name=release.name,
+    )
+    async with db.session() as data:
+        data.add(task)
+        await data.flush()
+        await data.commit()
+
+    return email_recipient, ""
diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index 5d2526e..7052ec9 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -83,13 +83,12 @@ async def selected_revision(
             body = wtforms.TextAreaField("Body", 
validators=[wtforms.validators.Optional()])
             submit = wtforms.SubmitField("Send vote email")
 
+        project = release.project
         version = release.version
-        committee_display = committee.display_name
-        project_name = release.project.name if release.project else "Unknown"
 
         # The subject can be changed by the user
         # TODO: We should consider not allowing the subject to be changed
-        default_subject = f"[VOTE] Release Apache {committee_display} 
{project_name} {version}"
+        default_subject = f"[VOTE] Release {project.display_name} {version}"
         default_body = await construct.start_vote_default(project_name)
 
         form = await VoteInitiateForm.create_form(
diff --git a/atr/templates/check-selected.html 
b/atr/templates/check-selected.html
index 4ba6e9a..195109f 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -355,6 +355,10 @@
           {% endif %}
         </div>
       </div>
+      <div class="row">
+        <label class="col-sm-3 col-form-label text-sm-end fw-semibold">{{ 
resolve_form.resolution_body.label.text }}:</label>
+        <div class="col-sm-9 pt-2">{{ 
resolve_form.resolution_body(class_="form-control", rows="3") }}</div>
+      </div>
 
       <div class="row">
         <div class="col-sm-9 offset-sm-3">{{ resolve_form.submit(class_="btn 
btn-primary mt-3") }}</div>
diff --git a/atr/util.py b/atr/util.py
index 05f4d3c..515ab14 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -517,6 +517,21 @@ def unwrap(value: T | None, error_message: str = 
"unexpected None when unwrappin
         return value
 
 
+def unwrap_type(value: T | None, t: type[T], error_message: str = "unexpected 
None when unwrapping value") -> T:
+    """
+    Will unwrap the given value or raise a TypeError if it is not of the 
expected type
+
+    :param value: the optional value to unwrap
+    :param t: the expected type of the value
+    :param error_message: the error message when failing to unwrap
+    """
+    if value is None:
+        raise ValueError(error_message)
+    if not isinstance(value, t):
+        raise ValueError(f"Expected {t}, got {type(value)}")
+    return value
+
+
 async def update_atomic_symlink(link_path: pathlib.Path, target_path: 
pathlib.Path | str) -> None:
     """Atomically update or create a symbolic link at link_path pointing to 
target_path."""
     target_str = str(target_path)


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

Reply via email to