This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new b1a43d2c Move Trusted Publishing fields into a separate form
b1a43d2c is described below
commit b1a43d2c10c416a23d383d5a090381c0ef1b28a7
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Mar 20 15:21:58 2026 +0000
Move Trusted Publishing fields into a separate form
---
atr/get/projects.py | 33 ++++++++++--
atr/post/projects.py | 22 ++++++++
atr/shared/projects.py | 120 ++++++++++++++++++++----------------------
atr/storage/writers/policy.py | 17 ++++--
tests/e2e/policy/conftest.py | 5 +-
tests/e2e/policy/helpers.py | 20 +++++++
tests/e2e/policy/test_post.py | 6 +--
7 files changed, 146 insertions(+), 77 deletions(-)
diff --git a/atr/get/projects.py b/atr/get/projects.py
index 9090397e..8f1215a9 100644
--- a/atr/get/projects.py
+++ b/atr/get/projects.py
@@ -202,6 +202,7 @@ async def view(
page.append(_render_compose_form(project))
page.append(_render_vote_form(project))
page.append(_render_finish_form(project))
+ page.append(_render_trusted_publishing_form(project))
else:
page.append(_render_policy_readonly(project))
@@ -358,9 +359,6 @@ def _render_compose_form(project: sql.Project) ->
htm.Element:
"source_excludes_lightweight":
"\n".join(project.policy_source_excludes_lightweight),
"source_excludes_rat":
"\n".join(project.policy_source_excludes_rat),
"binary_artifact_paths":
"\n".join(project.policy_binary_artifact_paths),
- "github_repository_name":
project.policy_github_repository_name or "",
- "github_repository_branch":
project.policy_github_repository_branch or "",
- "github_compose_workflow_path":
"\n".join(project.policy_github_compose_workflow_path),
"file_tag_mappings": atr_tag_yaml,
"strict_checking": project.policy_strict_checking,
},
@@ -427,7 +425,6 @@ def _render_finish_form(project: sql.Project) ->
htm.Element:
submit_label="Save",
defaults={
"project_key": project.key,
- "github_finish_workflow_path":
"\n".join(project.policy_github_finish_workflow_path),
"announce_release_subject":
project.policy_announce_release_subject or "",
"announce_release_template":
project.policy_announce_release_template or "",
"preserve_download_files":
project.policy_preserve_download_files,
@@ -615,6 +612,33 @@ async def _render_releases_sections(
return sections.collect()
+def _render_trusted_publishing_form(project: sql.Project) -> htm.Element:
+ card = htm.Block(htm.div, classes=".card.mb-4")
+
card.div(".card-header.bg-light.d-flex.justify-content-between.align-items-center")[
+ htm.h3(".mb-0")["Release policy - Trusted Publishing"]
+ ]
+
+ with card.block(htm.div, classes=".card-body") as card_body:
+ form.render_block(
+ card_body,
+ model_cls=shared.projects.TrustedPublishingPolicyForm,
+ action=util.as_url(post.projects.view, name=str(project.key)),
+ submit_label="Save",
+ defaults={
+ "project_key": str(project.key),
+ "github_repository_name":
project.policy_github_repository_name or "",
+ "github_repository_branch":
project.policy_github_repository_branch or "",
+ "github_compose_workflow_path":
"\n".join(project.policy_github_compose_workflow_path),
+ "github_vote_workflow_path":
"\n".join(project.policy_github_vote_workflow_path),
+ "github_finish_workflow_path":
"\n".join(project.policy_github_finish_workflow_path),
+ },
+ form_classes=".atr-canary.py-4.px-5",
+ border=True,
+ textarea_rows=5,
+ )
+ return card.collect()
+
+
def _render_vote_form(project: sql.Project) -> htm.Element:
card = htm.Block(htm.div, classes=".card.mb-4")
card.div(".card-header.bg-light.d-flex.justify-content-between.align-items-center")[
@@ -623,7 +647,6 @@ def _render_vote_form(project: sql.Project) -> htm.Element:
defaults_dict = {
"project_key": str(project.key),
- "github_vote_workflow_path":
"\n".join(project.policy_github_vote_workflow_path),
"mailto_addresses": project.policy_mailto_addresses[0]
if project.policy_mailto_addresses
else f"dev@{project.key}.apache.org",
diff --git a/atr/post/projects.py b/atr/post/projects.py
index e22c5c8b..ef11b0d4 100644
--- a/atr/post/projects.py
+++ b/atr/post/projects.py
@@ -110,6 +110,9 @@ async def view(
case shared.projects.FinishPolicyForm() as finish_form:
return await _process_finish_form(session, finish_form)
+ case shared.projects.TrustedPublishingPolicyForm() as tp_form:
+ return await _process_trusted_publishing_form(session, tp_form)
+
case shared.projects.RemoveCategoryForm() as remove_form:
return await _process_remove_category(session, remove_form)
@@ -305,6 +308,25 @@ async def _process_remove_language(
)
+async def _process_trusted_publishing_form(
+ session: web.Committer, tp_form:
shared.projects.TrustedPublishingPolicyForm
+) -> web.WerkzeugResponse:
+ project_key = tp_form.project_key
+
+ async with storage.write(session) as write:
+ wacm = await write.as_project_committee_member(project_key)
+ try:
+ await wacm.policy.edit_trusted_publishing(tp_form)
+ except storage.AccessError as e:
+ return await session.redirect(
+ get.projects.view, project_key=project_key, error=f"Error
editing Trusted Publishing policy: {e}"
+ )
+
+ return await session.redirect(
+ get.projects.view, project_key=project_key, success="Trusted
Publishing options saved successfully."
+ )
+
+
async def _process_vote_form(session: web.Committer, vote_form:
shared.projects.VotePolicyForm) -> web.WerkzeugResponse:
project_key = vote_form.project_key
diff --git a/atr/shared/projects.py b/atr/shared/projects.py
index 84b1c856..470a32f9 100644
--- a/atr/shared/projects.py
+++ b/atr/shared/projects.py
@@ -28,8 +28,9 @@ import atr.models.sql as sql
import atr.util as util
type COMPOSE = Literal["compose"]
-type VOTE = Literal["vote"]
type FINISH = Literal["finish"]
+type TRUSTED_PUBLISHING = Literal["trusted_publishing"]
+type VOTE = Literal["vote"]
type ADD_CATEGORY = Literal["add_category"]
type REMOVE_CATEGORY = Literal["remove_category"]
type ADD_LANGUAGE = Literal["add_language"]
@@ -133,19 +134,6 @@ class ComposePolicyForm(form.Form):
"Paths to binary artifacts to be included in the release.",
widget=form.Widget.TEXTAREA,
)
- github_repository_name: str = form.label(
- "GitHub repository name",
- "The name of the GitHub repository to use for the release, excluding
the apache/ prefix.",
- )
- github_repository_branch: str = form.label(
- "GitHub repository branch",
- "Branch used for release builds (for example, main or 2.5.x).
Optional.",
- )
- github_compose_workflow_path: str = form.label(
- "GitHub compose workflow paths",
- "The full paths to the GitHub workflows to use for the release,
including the .github/workflows/ prefix.",
- widget=form.Widget.TEXTAREA,
- )
file_tag_mappings: str = form.label(
"Tagging spec",
"Spec for which files should be tagged for release in specific
distribution types, YAML format",
@@ -156,38 +144,10 @@ class ComposePolicyForm(form.Form):
"If enabled, then the release cannot be voted upon unless all checks
pass.",
)
- @pydantic.model_validator(mode="after")
- def validate_github_fields(self) -> ComposePolicyForm:
- github_repository_name = self.github_repository_name.strip()
- github_repository_branch = self.github_repository_branch.strip()
- compose_raw = self.github_compose_workflow_path or ""
- compose = [p.strip() for p in compose_raw.split("\n") if p.strip()]
-
- if compose and (not github_repository_name):
- raise ValueError("GitHub repository name is required when any
workflow path is set.")
-
- if github_repository_branch and (not github_repository_name):
- raise ValueError("GitHub repository name is required when a GitHub
branch is set.")
-
- if github_repository_name and ("/" in github_repository_name):
- raise ValueError("GitHub repository name must not contain a
slash.")
-
- if compose:
- for p in compose:
- if not p.startswith(".github/workflows/"):
- raise ValueError("GitHub workflow paths must start with
'.github/workflows/'.")
-
- return self
-
class VotePolicyForm(form.Form):
variant: VOTE = form.value(VOTE)
project_key: safe.ProjectKey = form.label("Project name",
widget=form.Widget.HIDDEN)
- github_vote_workflow_path: str = form.label(
- "GitHub vote workflow paths",
- "The full paths to the GitHub workflows to use for the release,
including the .github/workflows/ prefix.",
- widget=form.Widget.TEXTAREA,
- )
mailto_addresses: form.Email = form.label(
"Email",
f"The mailing list where vote emails are sent. This is usually your
dev list. "
@@ -227,14 +187,6 @@ class VotePolicyForm(form.Form):
@pydantic.model_validator(mode="after")
def validate_vote_fields(self) -> VotePolicyForm:
- vote_raw = self.github_vote_workflow_path or ""
- vote = [p.strip() for p in vote_raw.split("\n") if p.strip()]
-
- if vote:
- for p in vote:
- if not p.startswith(".github/workflows/"):
- raise ValueError("GitHub workflow paths must start with
'.github/workflows/'.")
-
min_hours = self.min_hours
if (min_hours != 0) and ((min_hours < 72) or (min_hours > 144)):
raise ValueError("Minimum voting period must be 0 or between 72
and 144 hours inclusive.")
@@ -245,11 +197,6 @@ class VotePolicyForm(form.Form):
class FinishPolicyForm(form.Form):
variant: FINISH = form.value(FINISH)
project_key: safe.ProjectKey = form.label("Project name",
widget=form.Widget.HIDDEN)
- github_finish_workflow_path: str = form.label(
- "GitHub finish workflow paths",
- "The full paths to the GitHub workflows to use for the release,
including the .github/workflows/ prefix.",
- widget=form.Widget.TEXTAREA,
- )
announce_release_subject: str = form.label(
"Announce release subject",
widget=form.Widget.CUSTOM,
@@ -263,15 +210,61 @@ class FinishPolicyForm(form.Form):
"If enabled, existing download files will not be overwritten.",
)
+
+class TrustedPublishingPolicyForm(form.Form):
+ variant: TRUSTED_PUBLISHING = form.value(TRUSTED_PUBLISHING)
+ project_key: safe.ProjectKey = form.label("Project name",
widget=form.Widget.HIDDEN)
+ github_repository_name: str = form.label(
+ "GitHub repository name",
+ "The name of the GitHub repository to use for the release, excluding
the apache/ prefix.",
+ )
+ github_repository_branch: str = form.label(
+ "GitHub repository branch",
+ "Branch used for release builds (for example, main or 2.5.x).
Optional.",
+ )
+ # TODO: We should think about making these a list[RelUrlPath]
+ # But note that they contain .github, so that will be awkward
+ github_compose_workflow_path: str = form.label(
+ "Compose workflow paths",
+ "GitHub workflow paths for the compose phase, including the
.github/workflows/ prefix.",
+ widget=form.Widget.TEXTAREA,
+ )
+ github_vote_workflow_path: str = form.label(
+ "Vote workflow paths",
+ "GitHub workflow paths for the vote phase, including the
.github/workflows/ prefix.",
+ widget=form.Widget.TEXTAREA,
+ )
+ github_finish_workflow_path: str = form.label(
+ "Finish workflow paths",
+ "GitHub workflow paths for the finish phase, including the
.github/workflows/ prefix.",
+ widget=form.Widget.TEXTAREA,
+ )
+
@pydantic.model_validator(mode="after")
- def validate_finish_fields(self) -> FinishPolicyForm:
- finish_raw = self.github_finish_workflow_path or ""
- finish = [p.strip() for p in finish_raw.split("\n") if p.strip()]
+ def validate_trusted_publishing_fields(self) ->
TrustedPublishingPolicyForm:
+ github_repository_name = self.github_repository_name.strip()
+ github_repository_branch = self.github_repository_branch.strip()
+
+ all_paths: list[str] = []
+ for raw in (
+ self.github_compose_workflow_path,
+ self.github_vote_workflow_path,
+ self.github_finish_workflow_path,
+ ):
+ all_paths.extend(p.strip() for p in (raw or "").split("\n") if
p.strip())
- if finish:
- for p in finish:
- if not p.startswith(".github/workflows/"):
- raise ValueError("GitHub workflow paths must start with
'.github/workflows/'.")
+ if all_paths and (not github_repository_name):
+ raise ValueError("GitHub repository name is required when any
workflow path is set.")
+
+ if github_repository_branch and (not github_repository_name):
+ raise ValueError("GitHub repository name is required when a GitHub
branch is set.")
+
+ if github_repository_name and ("/" in github_repository_name):
+ raise ValueError("GitHub repository name must not contain a
slash.")
+
+ for p in all_paths:
+ if not p.startswith(".github/workflows/"):
+ raise ValueError("GitHub workflow paths must start with
'.github/workflows/'.")
return self
@@ -311,8 +304,9 @@ class DeleteSelectedProject(form.Form):
type ProjectViewForm = Annotated[
ComposePolicyForm
- | VotePolicyForm
| FinishPolicyForm
+ | TrustedPublishingPolicyForm
+ | VotePolicyForm
| AddCategoryForm
| RemoveCategoryForm
| AddLanguageForm
diff --git a/atr/storage/writers/policy.py b/atr/storage/writers/policy.py
index 7c694d24..8d56fad8 100644
--- a/atr/storage/writers/policy.py
+++ b/atr/storage/writers/policy.py
@@ -123,9 +123,6 @@ class CommitteeMember(CommitteeParticipant):
release_policy.source_excludes_lightweight =
_split_lines_verbatim(form.source_excludes_lightweight)
release_policy.source_excludes_rat =
_split_lines_verbatim(form.source_excludes_rat)
release_policy.binary_artifact_paths =
_split_lines(form.binary_artifact_paths)
- release_policy.github_repository_name =
form.github_repository_name.strip()
- release_policy.github_repository_branch =
form.github_repository_branch.strip()
- release_policy.github_compose_workflow_path =
_split_lines(form.github_compose_workflow_path)
release_policy.file_tag_mappings = atr_tags_dict
release_policy.strict_checking = form.strict_checking
@@ -135,13 +132,24 @@ class CommitteeMember(CommitteeParticipant):
project_key = form.project_key
project, release_policy = await
self.__get_or_create_policy(project_key)
- release_policy.github_finish_workflow_path =
_split_lines(form.github_finish_workflow_path)
self.__set_announce_release_subject(form.announce_release_subject or
"", project, release_policy)
self.__set_announce_release_template(form.announce_release_template or
"", project, release_policy)
release_policy.preserve_download_files = form.preserve_download_files
await self.__commit_and_log(str(project_key))
+ async def edit_trusted_publishing(self, form:
shared.projects.TrustedPublishingPolicyForm) -> None:
+ project_key = form.project_key
+ _, release_policy = await self.__get_or_create_policy(project_key)
+
+ release_policy.github_repository_name =
form.github_repository_name.strip()
+ release_policy.github_repository_branch =
form.github_repository_branch.strip()
+ release_policy.github_compose_workflow_path =
_split_lines(form.github_compose_workflow_path)
+ release_policy.github_vote_workflow_path =
_split_lines(form.github_vote_workflow_path)
+ release_policy.github_finish_workflow_path =
_split_lines(form.github_finish_workflow_path)
+
+ await self.__commit_and_log(str(project_key))
+
async def edit_vote(self, form: shared.projects.VotePolicyForm) -> None:
project_key = form.project_key
project, release_policy = await
self.__get_or_create_policy(project_key)
@@ -149,7 +157,6 @@ class CommitteeMember(CommitteeParticipant):
release_policy.manual_vote = form.manual_vote
if not release_policy.manual_vote:
- release_policy.github_vote_workflow_path =
_split_lines(form.github_vote_workflow_path)
release_policy.mailto_addresses = [form.mailto_addresses]
self.__set_min_hours(form.min_hours, project, release_policy)
release_policy.pause_for_rm = form.pause_for_rm
diff --git a/tests/e2e/policy/conftest.py b/tests/e2e/policy/conftest.py
index 61affc80..e3cc5b67 100644
--- a/tests/e2e/policy/conftest.py
+++ b/tests/e2e/policy/conftest.py
@@ -44,5 +44,8 @@ def _clear_policy_excludes(page: Page) -> None:
policy_helpers.textarea_source_excludes_rat(page).fill("")
policy_helpers.input_github_repository_name(page).fill("")
policy_helpers.input_github_repository_branch(page).fill("")
- policy_helpers.compose_form_save_button(page).click()
+ policy_helpers.textarea_github_compose_workflow_path(page).fill("")
+ policy_helpers.textarea_github_vote_workflow_path(page).fill("")
+ policy_helpers.textarea_github_finish_workflow_path(page).fill("")
+ policy_helpers.trusted_publishing_form_save_button(page).click()
page.wait_for_load_state()
diff --git a/tests/e2e/policy/helpers.py b/tests/e2e/policy/helpers.py
index 9ac36f6f..73162174 100644
--- a/tests/e2e/policy/helpers.py
+++ b/tests/e2e/policy/helpers.py
@@ -35,9 +35,29 @@ def input_github_repository_name(page: Page) -> Locator:
return page.locator('input[name="github_repository_name"]')
+def textarea_github_compose_workflow_path(page: Page) -> Locator:
+ return page.locator('textarea[name="github_compose_workflow_path"]')
+
+
+def textarea_github_finish_workflow_path(page: Page) -> Locator:
+ return page.locator('textarea[name="github_finish_workflow_path"]')
+
+
+def textarea_github_vote_workflow_path(page: Page) -> Locator:
+ return page.locator('textarea[name="github_vote_workflow_path"]')
+
+
def textarea_source_excludes_lightweight(page: Page) -> Locator:
return page.locator('textarea[name="source_excludes_lightweight"]')
def textarea_source_excludes_rat(page: Page) -> Locator:
return page.locator('textarea[name="source_excludes_rat"]')
+
+
+def trusted_publishing_form_save_button(page: Page) -> Locator:
+ return (
+ page.locator('input[name="github_repository_name"]')
+ .locator("xpath=ancestor::form")
+ .locator('button[type="submit"]')
+ )
diff --git a/tests/e2e/policy/test_post.py b/tests/e2e/policy/test_post.py
index 768a0c90..61d942f9 100644
--- a/tests/e2e/policy/test_post.py
+++ b/tests/e2e/policy/test_post.py
@@ -25,13 +25,13 @@ def
test_github_repository_branch_can_be_cleared(page_project: Page) -> None:
repo_input.fill("tooling-actions")
branch_input = helpers.input_github_repository_branch(page_project)
branch_input.fill("main")
- helpers.compose_form_save_button(page_project).click()
+ helpers.trusted_publishing_form_save_button(page_project).click()
page_project.wait_for_load_state()
root_helpers.visit(page_project, helpers.PROJECT_URL)
branch_input = helpers.input_github_repository_branch(page_project)
branch_input.fill("")
- helpers.compose_form_save_button(page_project).click()
+ helpers.trusted_publishing_form_save_button(page_project).click()
page_project.wait_for_load_state()
root_helpers.visit(page_project, helpers.PROJECT_URL)
@@ -44,7 +44,7 @@ def
test_github_repository_branch_value_persists(page_project: Page) -> None:
repo_input.fill("tooling-actions")
branch_input = helpers.input_github_repository_branch(page_project)
branch_input.fill("2.5.x")
- helpers.compose_form_save_button(page_project).click()
+ helpers.trusted_publishing_form_save_button(page_project).click()
page_project.wait_for_load_state()
root_helpers.visit(page_project, helpers.PROJECT_URL)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]