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]