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

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


The following commit(s) were added to refs/heads/main by this push:
     new 23bd8b0  #508 - block announcing through any channel until tagged 
distributions have been recorded
23bd8b0 is described below

commit 23bd8b077d2530c3b1609c92ed96a18fa25615eb
Author: Alastair McFarlane <[email protected]>
AuthorDate: Tue Jan 27 11:56:49 2026 +0000

    #508 - block announcing through any channel until tagged distributions have 
been recorded
---
 atr/db/__init__.py              |  3 ++
 atr/get/announce.py             | 97 ++++++++++++++++++++++++++---------------
 atr/get/finish.py               | 34 ++++++++++++---
 atr/post/announce.py            | 26 ++++++++++-
 atr/storage/writers/announce.py | 17 ++++++++
 atr/web.py                      |  3 ++
 6 files changed, 138 insertions(+), 42 deletions(-)

diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 8250a90..4ff46d7 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -470,6 +470,7 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         _release_policy: bool = False,
         _project_release_policy: bool = False,
         _revisions: bool = False,
+        _distributions: bool = False,
     ) -> Query[sql.Release]:
         query = sqlmodel.select(sql.Release)
 
@@ -509,6 +510,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
             query = query.options(joined_load_nested(sql.Release.project, 
sql.Project.release_policy))
         if _revisions:
             query = query.options(select_in_load(sql.Release.revisions))
+        if _distributions:
+            query = query.options(joined_load(sql.Release.distributions))
 
         return Query(self, query)
 
diff --git a/atr/get/announce.py b/atr/get/announce.py
index e6c45f1..2b59a49 100644
--- a/atr/get/announce.py
+++ b/atr/get/announce.py
@@ -40,9 +40,7 @@ async def selected(session: web.Committer, project_name: str, 
version_name: str)
     """Allow the user to announce a release preview."""
     await session.check_access(project_name)
 
-    release = await session.release(
-        project_name, version_name, with_committee=True, 
phase=sql.ReleasePhase.RELEASE_PREVIEW
-    )
+    release = await _get_page_data(project_name, session, version_name)
 
     latest_revision_number = release.latest_revision_number
     if latest_revision_number is None:
@@ -104,6 +102,19 @@ async def selected(session: web.Committer, project_name: 
str, version_name: str)
     )
 
 
+async def _get_page_data(project_name: str, session: web.Committer, 
version_name: str) -> sql.Release:
+    release = await session.release(
+        project_name,
+        version_name,
+        with_committee=True,
+        phase=sql.ReleasePhase.RELEASE_PREVIEW,
+        with_distributions=True,
+        with_release_policy=True,
+        with_project_release_policy=True,
+    )
+    return release
+
+
 def _render_body_field(default_body: str, project_name: str) -> htm.Element:
     """Render the body textarea with a link to edit the template."""
     textarea = htpy.textarea(
@@ -209,37 +220,55 @@ async def _render_page(
     ]
     page.append(_render_release_card(release))
     page.h2["Announce this release"]
-    page.p[f"This form will send an announcement to the ASF 
{util.USER_TESTS_ADDRESS} mailing list."]
-
-    custom_subject_widget = _render_subject_field(default_subject, 
release.project.name)
-    custom_body_widget = _render_body_field(default_body, release.project.name)
-    custom_mailing_list_widget = 
_render_mailing_list_with_warning(mailing_list_choices, util.USER_TESTS_ADDRESS)
-
-    # Custom widget for download_path_suffix with custom documentation
-    download_path_widget = 
_render_download_path_field(default_download_path_suffix, 
download_path_description)
-
-    defaults_dict = {
-        "revision_number": release.unwrap_revision_number,
-        "subject_template_hash": subject_template_hash,
-        "body": default_body,
-    }
-
-    form.render_block(
-        page,
-        model_cls=shared.announce.AnnounceForm,
-        action=util.as_url(post.announce.selected, 
project_name=release.project.name, version_name=release.version),
-        submit_label="Send announcement email",
-        defaults=defaults_dict,
-        custom={
-            "subject": custom_subject_widget,
-            "body": custom_body_widget,
-            "mailing_list": custom_mailing_list_widget,
-            "download_path_suffix": download_path_widget,
-        },
-        form_classes=".atr-canary.py-4.px-5.mb-4.border.rounded",
-        border=True,
-        wider_widgets=True,
-    )
+
+    announce_msg = ""
+    policy = release.release_policy or release.project.release_policy
+    if policy and policy.file_tag_mappings:
+        missing = []
+        tags = policy.file_tag_mappings.keys()
+        distributions = [d.platform.value.gh_slug for d in 
release.distributions]
+        for tag in tags:
+            if tag not in distributions:
+                missing.append(tag)
+        if missing:
+            announce_msg = f"This release cannot be announced until the 
following distributions have been recorded: {
+                ', '.join(missing)
+            }"
+
+    if not announce_msg:
+        page.p[f"This form will send an announcement to the ASF 
{util.USER_TESTS_ADDRESS} mailing list."]
+
+        custom_subject_widget = _render_subject_field(default_subject, 
release.project.name)
+        custom_body_widget = _render_body_field(default_body, 
release.project.name)
+        custom_mailing_list_widget = 
_render_mailing_list_with_warning(mailing_list_choices, util.USER_TESTS_ADDRESS)
+
+        # Custom widget for download_path_suffix with custom documentation
+        download_path_widget = 
_render_download_path_field(default_download_path_suffix, 
download_path_description)
+
+        defaults_dict = {
+            "revision_number": release.unwrap_revision_number,
+            "subject_template_hash": subject_template_hash,
+            "body": default_body,
+        }
+
+        form.render_block(
+            page,
+            model_cls=shared.announce.AnnounceForm,
+            action=util.as_url(post.announce.selected, 
project_name=release.project.name, version_name=release.version),
+            submit_label="Send announcement email",
+            defaults=defaults_dict,
+            custom={
+                "subject": custom_subject_widget,
+                "body": custom_body_widget,
+                "mailing_list": custom_mailing_list_widget,
+                "download_path_suffix": download_path_widget,
+            },
+            form_classes=".atr-canary.py-4.px-5.mb-4.border.rounded",
+            border=True,
+            wider_widgets=True,
+        )
+    else:
+        page.p[htm.strong[announce_msg]]
 
     return page.collect()
 
diff --git a/atr/get/finish.py b/atr/get/finish.py
index d82ded7..e3e17be 100644
--- a/atr/get/finish.py
+++ b/atr/get/finish.py
@@ -81,6 +81,19 @@ async def selected(
         await quart.flash("Preview revision directory not found.", "error")
         return await session.redirect(root.index)
 
+    announce_msg = ""
+    if release.release_policy and release.release_policy.file_tag_mappings:
+        missing = []
+        tags = release.release_policy.file_tag_mappings.keys()
+        distributions = [d.platform.value.gh_slug for d in 
release.distributions]
+        for tag in tags:
+            if tag not in distributions:
+                missing.append(tag)
+        if missing:
+            announce_msg = f"This release cannot be announced until the 
following distributions have been recorded: {
+                ', '.join(missing)
+            }"
+
     return await _render_page(
         release=release,
         source_files_rel=source_files_rel,
@@ -88,6 +101,7 @@ async def selected(
         deletable_dirs=deletable_dirs,
         rc_analysis=rc_analysis,
         distribution_tasks=tasks,
+        announce_disable_message=announce_msg,
     )
 
 
@@ -140,9 +154,7 @@ async def _get_page_data(
     async with db.session() as data:
         via = sql.validate_instrumented_attribute
         release = await data.release(
-            project_name=project_name,
-            version=version_name,
-            _committee=True,
+            project_name=project_name, version=version_name, _committee=True, 
_release_policy=True, _distributions=True
         ).demand(base.ASFQuartException("Release does not exist", 
errorcode=404))
         tasks = [
             t
@@ -343,6 +355,7 @@ async def _render_page(
     deletable_dirs: list[tuple[str, str]],
     rc_analysis: RCTagAnalysisResult,
     distribution_tasks: Sequence[sql.Task],
+    announce_disable_message: str,
 ) -> str:
     """Render the finish page using htm.py."""
     page = htm.Block()
@@ -363,8 +376,9 @@ async def _render_page(
     ]
 
     # Release info card
-    page.append(_render_release_card(release))
+    page.append(_render_release_card(release, announce_disable_message))
 
+    page.h2["Distributions"]
     # Information paragraph
     page.p[
         "During this phase you should distribute release artifacts to your 
package distribution networks "
@@ -481,8 +495,11 @@ def _render_rc_tags_section(rc_analysis: 
RCTagAnalysisResult) -> htm.Element:
     return section.collect()
 
 
-def _render_release_card(release: sql.Release) -> htm.Element:
+def _render_release_card(release: sql.Release, announce_disable_message: str) 
-> htm.Element:
     """Render the release information card."""
+    announce_classes = ".btn-success"
+    if announce_disable_message:
+        announce_classes += ".disabled"
     card = htm.div(".card.mb-4.shadow-sm", id=release.name)[
         htm.div(".card-header.bg-light")[htm.h3(".card-title.mb-0")["About 
this release preview"]],
         htm.div(".card-body")[
@@ -528,17 +545,20 @@ def _render_release_card(release: sql.Release) -> 
htm.Element:
                     " Show revisions",
                 ],
                 htm.a(
-                    ".btn.btn-success",
+                    f".btn{announce_classes}.me-2",
                     title=f"Announce and distribute {release.name}",
                     href=util.as_url(
                         announce.selected,
                         project_name=release.project.name,
                         version_name=release.version,
-                    ),
+                    )
+                    if not announce_disable_message
+                    else None,
                 )[
                     htm.icon("check-circle"),
                     " Announce and distribute",
                 ],
+                
htm.span(".page-preview-meta-item.page-extra-muted")[f"{announce_disable_message}"],
             ],
         ],
     ]
diff --git a/atr/post/announce.py b/atr/post/announce.py
index 44ac948..f91f498 100644
--- a/atr/post/announce.py
+++ b/atr/post/announce.py
@@ -47,7 +47,13 @@ async def selected(
 
     # Get the release to find the revision number
     release = await session.release(
-        project_name, version_name, with_committee=True, 
phase=sql.ReleasePhase.RELEASE_PREVIEW
+        project_name,
+        version_name,
+        with_committee=True,
+        phase=sql.ReleasePhase.RELEASE_PREVIEW,
+        with_distributions=True,
+        with_release_policy=True,
+        with_project_release_policy=True,
     )
     preview_revision_number = release.unwrap_revision_number
 
@@ -61,6 +67,24 @@ async def selected(
             version_name=version_name,
         )
 
+    policy = release.release_policy or release.project.release_policy
+    if policy and policy.file_tag_mappings:
+        missing = []
+        tags = policy.file_tag_mappings.keys()
+        distributions = [d.platform.value.gh_slug for d in 
release.distributions]
+        for tag in tags:
+            if tag not in distributions:
+                missing.append(tag)
+        if missing:
+            return await session.redirect(
+                get.announce.selected,
+                error=f"This release cannot be announced until the following 
distributions have been recorded: {
+                    ', '.join(missing)
+                }",
+                project_name=project_name,
+                version_name=version_name,
+            )
+
     # Validate that the subject template hasn't changed
     subject_template = await 
construct.announce_release_subject_default(project_name)
     current_hash = construct.template_hash(subject_template)
diff --git a/atr/storage/writers/announce.py b/atr/storage/writers/announce.py
index 647a1d6..7edbf49 100644
--- a/atr/storage/writers/announce.py
+++ b/atr/storage/writers/announce.py
@@ -125,6 +125,8 @@ class CommitteeMember(CommitteeParticipant):
             latest_revision_number=preview_revision_number,
             _project_release_policy=True,
             _revisions=True,
+            _distributions=True,
+            _release_policy=True,
         ).demand(
             storage.AccessError(
                 f"Release {project_name} {version_name} 
{preview_revision_number} does not exist",
@@ -133,6 +135,21 @@ class CommitteeMember(CommitteeParticipant):
         if (committee := release.project.committee) is None:
             raise storage.AccessError("Release has no committee - Invalid 
state")
 
+        policy = release.release_policy or release.project.release_policy
+        if policy and policy.file_tag_mappings:
+            missing = []
+            tags = policy.file_tag_mappings.keys()
+            distributions = [d.platform.value.gh_slug for d in 
release.distributions]
+            for tag in tags:
+                if tag not in distributions:
+                    missing.append(tag)
+            if missing:
+                raise storage.AccessError(
+                    f"This release cannot be announced until the following 
distributions have been recorded: {
+                        ', '.join(missing)
+                    }"
+                )
+
         # Fetch the current subject template and verify the hash
         subject_template = await 
construct.announce_release_subject_default(project_name)
         if subject_template_hash is not None:
diff --git a/atr/web.py b/atr/web.py
index bb075b1..4d7d8c9 100644
--- a/atr/web.py
+++ b/atr/web.py
@@ -170,6 +170,7 @@ class Committer:
         with_release_policy: bool = False,
         with_project_release_policy: bool = False,
         with_revisions: bool = False,
+        with_distributions: bool = False,
     ) -> sql.Release:
         # We reuse db.NOT_SET as an entirely different sentinel
         # TODO: We probably shouldn't do that, or should make it clearer
@@ -191,6 +192,7 @@ class Committer:
                     _release_policy=with_release_policy,
                     _project_release_policy=with_project_release_policy,
                     _revisions=with_revisions,
+                    _distributions=with_distributions,
                 ).demand(base.ASFQuartException("Release does not exist", 
errorcode=404))
         else:
             release = await data.release(
@@ -202,6 +204,7 @@ class Committer:
                 _release_policy=with_release_policy,
                 _project_release_policy=with_project_release_policy,
                 _revisions=with_revisions,
+                _distributions=with_distributions,
             ).demand(base.ASFQuartException("Release does not exist", 
errorcode=404))
         return release
 


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

Reply via email to