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 34d5c31 Add an API endpoint to announce a release
34d5c31 is described below
commit 34d5c3114884ba8e6369a75f67cb39bd2016ccff
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 14 20:45:28 2025 +0100
Add an API endpoint to announce a release
---
atr/blueprints/api/api.py | 27 +++++++++++++
atr/models/api.py | 10 +++++
atr/routes/announce.py | 98 +++++++++++++++++++++++++++--------------------
3 files changed, 93 insertions(+), 42 deletions(-)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 973040a..e1c118e 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -40,6 +40,7 @@ import atr.models as models
import atr.models.sql as sql
import atr.revision as revision
import atr.routes as routes
+import atr.routes.announce as announce
import atr.routes.start as start
import atr.routes.voting as voting
import atr.tasks.vote as tasks_vote
@@ -54,6 +55,32 @@ import atr.util as util
# We implicitly have /api/openapi.json
[email protected]("/announce", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.Announce)
+@quart_schema.validate_response(sql.Task, 201)
+async def announce_post(data: models.api.Announce) -> tuple[Mapping, int]:
+ asf_uid = _jwt_asf_uid()
+
+ try:
+ await announce.announce(
+ data.project,
+ data.version,
+ data.revision,
+ data.email_to,
+ data.subject,
+ data.body,
+ data.path_suffix,
+ asf_uid,
+ asf_uid,
+ )
+ except announce.AnnounceError as e:
+ raise exceptions.BadRequest(str(e))
+
+ return {"success": "Announcement sent"}, 200
+
+
@api.BLUEPRINT.route("/checks/list/<project>/<version>")
@quart_schema.validate_response(list[sql.CheckResult], 200)
async def checks_list_project_version(project: str, version: str) ->
tuple[list[Mapping], int]:
diff --git a/atr/models/api.py b/atr/models/api.py
index 2a41445..76fb7c8 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -45,6 +45,16 @@ class Task(Pagination):
status: str | None = None
+class Announce(schema.Strict):
+ project: str
+ version: str
+ revision: str
+ email_to: str
+ subject: str
+ body: str
+ path_suffix: str
+
+
class AsfuidPat(schema.Strict):
asfuid: str
pat: str
diff --git a/atr/routes/announce.py b/atr/routes/announce.py
index 1166e6e..579fb06 100644
--- a/atr/routes/announce.py
+++ b/atr/routes/announce.py
@@ -17,7 +17,6 @@
import asyncio
import datetime
-import logging
import pathlib
from typing import Any, Protocol
@@ -41,6 +40,10 @@ import atr.template as template
import atr.util as util
+class AnnounceError(Exception):
+ """Exception for announce errors."""
+
+
class AnnounceFormProtocol(Protocol):
"""Protocol for the dynamically generated AnnounceForm."""
@@ -142,42 +145,72 @@ async def selected_post(
await quart.flash(error_message, "error")
return await template.render("announce-selected.html",
release=release, announce_form=announce_form)
+ recipient = str(announce_form.mailing_list.data)
+ if recipient not in permitted_recipients:
+ raise AnnounceError(f"You are not permitted to send announcements to
{recipient}")
+
subject = str(announce_form.subject.data)
body = str(announce_form.body.data)
preview_revision_number = str(announce_form.preview_revision.data)
download_path_suffix = _download_path_suffix_validated(announce_form)
+ try:
+ await announce(
+ project_name,
+ version_name,
+ preview_revision_number,
+ recipient,
+ subject,
+ body,
+ download_path_suffix,
+ session.uid,
+ session.fullname,
+ )
+ except AnnounceError as e:
+ return await session.redirect(selected, error=str(e),
project_name=project_name, version_name=version_name)
+
+ routes_release_finished = routes_release.finished # type: ignore[has-type]
+ return await session.redirect(
+ routes_release_finished,
+ success="Preview successfully announced",
+ project_name=project_name,
+ )
+
+
+async def announce(
+ project_name: str,
+ version_name: str,
+ preview_revision_number: str,
+ recipient: str,
+ subject: str,
+ body: str,
+ download_path_suffix: str,
+ uid: str,
+ fullname: str,
+) -> None:
+ if recipient not in util.permitted_recipients(uid):
+ raise AnnounceError(f"You are not permitted to send announcements to
{recipient}")
+
unfinished_dir: str = ""
finished_dir: str = ""
- async with db.session(log_queries=True) as data:
+ async with db.session() as data:
try:
- release = await session.release(
- project_name,
- version_name,
+ release = await data.release(
+ project_name=project_name,
+ version=version_name,
phase=sql.ReleasePhase.RELEASE_PREVIEW,
latest_revision_number=preview_revision_number,
- with_revisions=True,
- data=data,
- )
+ _revisions=True,
+ ).demand(RuntimeError(f"Release {project_name} {version_name}
{preview_revision_number} does not exist"))
if (committee := release.project.committee) is None:
raise ValueError("Release has no committee")
- test_list = "user-tests"
- recipient = f"{test_list}@tooling.apache.org"
- if recipient not in util.permitted_recipients(session.uid):
- return await session.redirect(
- selected,
- error=f"You are not permitted to send announcements to
{recipient}",
- project_name=project_name,
- version_name=version_name,
- )
-
body = await construct.announce_release_body(
body,
options=construct.AnnounceReleaseOptions(
- asfuid=session.uid,
- fullname=session.fullname,
+ asfuid=uid,
+ fullname=fullname,
project_name=project_name,
version_name=version_name,
),
@@ -186,7 +219,7 @@ async def selected_post(
status=sql.TaskStatus.QUEUED,
task_type=sql.TaskType.MESSAGE_SEND,
task_args=message.Send(
- email_sender=f"{session.uid}@apache.org",
+ email_sender=f"{uid}@apache.org",
email_recipient=recipient,
subject=subject,
body=body,
@@ -206,13 +239,7 @@ async def selected_post(
await data.commit()
except (routes.FlashError, Exception) as e:
- logging.exception("Error during release announcement, database
phase:")
- return await session.redirect(
- selected,
- error=f"Error announcing preview: {e!s}",
- project_name=project_name,
- version_name=version_name,
- )
+ raise AnnounceError(f"Error announcing preview: {e!s}")
async with db.session() as data:
# This must come after updating the release object
@@ -236,23 +263,10 @@ async def selected_post(
# This removes all of the prior revisions
await aioshutil.rmtree(str(unfinished_revisions_path)) # type:
ignore[call-arg]
except Exception as e:
- logging.exception("Error during release announcement, file system
phase:")
- return await session.redirect(
- selected,
- error=f"Database updated, but error moving files: {e!s}. Manual
cleanup needed.",
- project_name=project_name,
- version_name=version_name,
- )
+ raise AnnounceError(f"Database updated, but error moving files: {e!s}.
Manual cleanup needed.")
await _hard_link_downloads(committee, finished_path, download_path_suffix)
- routes_release_finished = routes_release.finished # type: ignore[has-type]
- return await session.redirect(
- routes_release_finished,
- success="Preview successfully announced",
- project_name=project_name,
- )
-
async def _create_announce_form_instance(
permitted_recipients: list[str], *, data: dict[str, Any] | None = None
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]