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]