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]

Reply via email to