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-releases.git
The following commit(s) were added to refs/heads/main by this push:
new be1e8b7 Simplify the processing of release policy forms
be1e8b7 is described below
commit be1e8b7adea0d1ba0f5afe964d0212f84ef306a0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Dec 5 11:13:54 2025 +0000
Simplify the processing of release policy forms
---
atr/models/__init__.py | 4 +-
atr/models/policy.py | 99 ----------------------------
atr/post/projects.py | 86 +------------------------
atr/storage/writers/policy.py | 145 +++++++++++++++++++++++-------------------
4 files changed, 84 insertions(+), 250 deletions(-)
diff --git a/atr/models/__init__.py b/atr/models/__init__.py
index 4cb70b3..a9cb122 100644
--- a/atr/models/__init__.py
+++ b/atr/models/__init__.py
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-from . import api, distribution, helpers, policy, results, schema, sql,
tabulate
+from . import api, distribution, helpers, results, schema, sql, tabulate
# If we use .__name__, pyright gives a warning
-__all__ = ["api", "distribution", "helpers", "policy", "results", "schema",
"sql", "tabulate"]
+__all__ = ["api", "distribution", "helpers", "results", "schema", "sql",
"tabulate"]
diff --git a/atr/models/policy.py b/atr/models/policy.py
deleted file mode 100644
index 6e5e793..0000000
--- a/atr/models/policy.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-from typing import Any
-
-import pydantic
-
-from . import schema
-
-
-# TODO: Maybe it's easier to use quart_schema for all our forms
-# We can use source=DataSource.FORM
-# But do all form input types have a pydantic counterpart?
-class ReleasePolicyData(schema.Lax):
- """Pydantic model for release policy form data."""
-
- project_name: str
-
- # Compose section
- source_artifact_paths: list[str] = pydantic.Field(default_factory=list)
- binary_artifact_paths: list[str] = pydantic.Field(default_factory=list)
- github_repository_name: str = ""
- github_compose_workflow_path: list[str] =
pydantic.Field(default_factory=list)
- strict_checking: bool = False
-
- # Vote section
- mailto_addresses: list[str] = pydantic.Field(default_factory=list)
- manual_vote: bool = False
- default_min_hours_value_at_render: int = 72
- min_hours: int = 72
- pause_for_rm: bool = False
- release_checklist: str = ""
- vote_comment_template: str = ""
- default_start_vote_template_hash: str = ""
- start_vote_template: str = ""
- github_vote_workflow_path: list[str] = pydantic.Field(default_factory=list)
-
- # Finish section
- default_announce_release_template_hash: str = ""
- announce_release_template: str = ""
- github_finish_workflow_path: list[str] =
pydantic.Field(default_factory=list)
- preserve_download_files: bool = False
-
- @pydantic.field_validator(
- "source_artifact_paths",
- "binary_artifact_paths",
- "github_compose_workflow_path",
- "github_vote_workflow_path",
- "github_finish_workflow_path",
- mode="before",
- )
- @classmethod
- def parse_artifact_paths(cls, v: Any) -> list[str]:
- if (v is None) or (v == ""):
- return []
- if isinstance(v, str):
- return [path.strip() for path in v.split("\n") if path.strip()]
- if isinstance(v, list):
- return v
- return []
-
- @pydantic.field_validator("mailto_addresses", mode="before")
- @classmethod
- def parse_mailto_addresses(cls, v: Any) -> list[str]:
- if (v is None) or (v == ""):
- return []
- if isinstance(v, str):
- return [v.strip()] if v.strip() else []
- if isinstance(v, list):
- return v
- return []
-
- @pydantic.field_validator(
- "github_repository_name",
- "release_checklist",
- "vote_comment_template",
- "start_vote_template",
- "announce_release_template",
- mode="before",
- )
- @classmethod
- def unwrap_values(cls, v: Any) -> Any:
- if v is None:
- return ""
- return v
diff --git a/atr/post/projects.py b/atr/post/projects.py
index 3f86ec6..608b4b0 100644
--- a/atr/post/projects.py
+++ b/atr/post/projects.py
@@ -23,7 +23,6 @@ import quart
import atr.blueprints.post as post
import atr.db as db
import atr.get as get
-import atr.models.policy as policy
import atr.models.sql as sql
import atr.shared as shared
import atr.storage as storage
@@ -197,37 +196,10 @@ async def _process_compose_form(
) -> web.WerkzeugResponse:
project_name = compose_form.project_name
- async with db.session() as data:
- project = await data.project(name=project_name, _committee=True,
_release_policy=True).demand(
- base.ASFQuartException(f"Project {project_name} not found",
errorcode=404)
- )
-
- policy_data = policy.ReleasePolicyData(
- project_name=project_name,
- source_artifact_paths=[p.strip() for p in
compose_form.source_artifact_paths.split("\n") if p.strip()],
- binary_artifact_paths=[p.strip() for p in
compose_form.binary_artifact_paths.split("\n") if p.strip()],
- github_repository_name=compose_form.github_repository_name.strip() or
"",
- github_compose_workflow_path=[
- p.strip() for p in
compose_form.github_compose_workflow_path.split("\n") if p.strip()
- ],
- strict_checking=compose_form.strict_checking,
- github_vote_workflow_path=project.policy_github_vote_workflow_path,
- mailto_addresses=project.policy_mailto_addresses,
- manual_vote=project.policy_manual_vote,
- min_hours=project.policy_min_hours,
- pause_for_rm=project.policy_pause_for_rm,
- release_checklist=project.policy_release_checklist or "",
- vote_comment_template=project.policy_vote_comment_template or "",
- start_vote_template=project.policy_start_vote_template or "",
- github_finish_workflow_path=project.policy_github_finish_workflow_path,
- announce_release_template=project.policy_announce_release_template or
"",
- preserve_download_files=project.policy_preserve_download_files,
- )
-
async with storage.write(session) as write:
wacm = await write.as_project_committee_member(project_name)
try:
- await wacm.policy.edit(project_name, policy_data)
+ await wacm.policy.edit_compose(compose_form)
except storage.AccessError as e:
return await session.redirect(
get.projects.view, name=project_name, error=f"Error editing
compose policy: {e}"
@@ -256,37 +228,10 @@ async def _process_finish_form(
) -> web.WerkzeugResponse:
project_name = finish_form.project_name
- async with db.session() as data:
- project = await data.project(name=project_name, _committee=True,
_release_policy=True).demand(
- base.ASFQuartException(f"Project {project_name} not found",
errorcode=404)
- )
-
- policy_data = policy.ReleasePolicyData(
- project_name=project_name,
- source_artifact_paths=project.policy_source_artifact_paths,
- binary_artifact_paths=project.policy_binary_artifact_paths,
- github_repository_name=project.policy_github_repository_name or "",
-
github_compose_workflow_path=project.policy_github_compose_workflow_path,
- strict_checking=project.policy_strict_checking,
- github_vote_workflow_path=project.policy_github_vote_workflow_path,
- mailto_addresses=project.policy_mailto_addresses,
- manual_vote=project.policy_manual_vote,
- min_hours=project.policy_min_hours,
- pause_for_rm=project.policy_pause_for_rm,
- release_checklist=project.policy_release_checklist or "",
- vote_comment_template=project.policy_vote_comment_template or "",
- start_vote_template=project.policy_start_vote_template or "",
- github_finish_workflow_path=[
- p.strip() for p in
finish_form.github_finish_workflow_path.split("\n") if p.strip()
- ],
- announce_release_template=finish_form.announce_release_template or "",
- preserve_download_files=finish_form.preserve_download_files,
- )
-
async with storage.write(session) as write:
wacm = await write.as_project_committee_member(project_name)
try:
- await wacm.policy.edit(project_name, policy_data)
+ await wacm.policy.edit_finish(finish_form)
except storage.AccessError as e:
return await session.redirect(
get.projects.view, name=project_name, error=f"Error editing
finish policy: {e}"
@@ -344,35 +289,10 @@ async def _process_remove_language(
async def _process_vote_form(session: web.Committer, vote_form:
shared.projects.VotePolicyForm) -> web.WerkzeugResponse:
project_name = vote_form.project_name
- async with db.session() as data:
- project = await data.project(name=project_name, _committee=True,
_release_policy=True).demand(
- base.ASFQuartException(f"Project {project_name} not found",
errorcode=404)
- )
-
- policy_data = policy.ReleasePolicyData(
- project_name=project_name,
- source_artifact_paths=project.policy_source_artifact_paths,
- binary_artifact_paths=project.policy_binary_artifact_paths,
- github_repository_name=project.policy_github_repository_name or "",
-
github_compose_workflow_path=project.policy_github_compose_workflow_path,
- strict_checking=project.policy_strict_checking,
- github_vote_workflow_path=[p.strip() for p in
vote_form.github_vote_workflow_path.split("\n") if p.strip()],
- mailto_addresses=[vote_form.mailto_addresses],
- manual_vote=vote_form.manual_vote,
- min_hours=vote_form.min_hours,
- pause_for_rm=vote_form.pause_for_rm,
- release_checklist=vote_form.release_checklist or "",
- vote_comment_template=vote_form.vote_comment_template or "",
- start_vote_template=vote_form.start_vote_template or "",
- github_finish_workflow_path=project.policy_github_finish_workflow_path,
- announce_release_template=project.policy_announce_release_template or
"",
- preserve_download_files=project.policy_preserve_download_files,
- )
-
async with storage.write(session) as write:
wacm = await write.as_project_committee_member(project_name)
try:
- await wacm.policy.edit(project_name, policy_data)
+ await wacm.policy.edit_vote(vote_form)
except storage.AccessError as e:
return await session.redirect(get.projects.view,
name=project_name, error=f"Error editing vote policy: {e}")
diff --git a/atr/storage/writers/policy.py b/atr/storage/writers/policy.py
index 89f7bf0..e047d6a 100644
--- a/atr/storage/writers/policy.py
+++ b/atr/storage/writers/policy.py
@@ -18,11 +18,16 @@
# Removing this will cause circular imports
from __future__ import annotations
+from typing import TYPE_CHECKING
+
import atr.db as db
import atr.models as models
import atr.storage as storage
import atr.util as util
+if TYPE_CHECKING:
+ import atr.shared as shared
+
class GeneralPublic:
def __init__(
@@ -86,104 +91,112 @@ class CommitteeMember(CommitteeParticipant):
self.__asf_uid = asf_uid
self.__committee_name = committee_name
- async def edit(self, project_name: str, policy_data:
models.policy.ReleasePolicyData) -> None:
- project = await self.__data.project(
- name=project_name, status=models.sql.ProjectStatus.ACTIVE,
_release_policy=True
- ).demand(storage.AccessError(f"Project {project_name} not found"))
+ async def edit_compose(self, form: shared.projects.ComposePolicyForm) ->
None:
+ project_name = form.project_name
+ _, release_policy = await self.__get_or_create_policy(project_name)
- release_policy = project.release_policy
- if release_policy is None:
- release_policy = models.sql.ReleasePolicy(project=project)
- project.release_policy = release_policy
- self.__data.add(release_policy)
+ release_policy.source_artifact_paths =
_split_lines(form.source_artifact_paths)
+ release_policy.binary_artifact_paths =
_split_lines(form.binary_artifact_paths)
+ release_policy.github_repository_name =
form.github_repository_name.strip()
+ release_policy.github_compose_workflow_path =
_split_lines(form.github_compose_workflow_path)
+ release_policy.strict_checking = form.strict_checking
+
+ await self.__commit_and_log(project_name)
+
+ async def edit_finish(self, form: shared.projects.FinishPolicyForm) ->
None:
+ project_name = form.project_name
+ project, release_policy = await
self.__get_or_create_policy(project_name)
+
+ release_policy.github_finish_workflow_path =
_split_lines(form.github_finish_workflow_path)
+ self.__set_announce_release_template(form.announce_release_template or
"", project, release_policy)
+ release_policy.preserve_download_files = form.preserve_download_files
- # Compose section
- release_policy.source_artifact_paths =
policy_data.source_artifact_paths
- release_policy.binary_artifact_paths =
policy_data.binary_artifact_paths
- release_policy.github_repository_name =
policy_data.github_repository_name
- # TODO: Change to paths, plural
- release_policy.github_compose_workflow_path =
policy_data.github_compose_workflow_path
- release_policy.strict_checking = policy_data.strict_checking
+ await self.__commit_and_log(project_name)
+
+ async def edit_vote(self, form: shared.projects.VotePolicyForm) -> None:
+ project_name = form.project_name
+ project, release_policy = await
self.__get_or_create_policy(project_name)
+
+ release_policy.manual_vote = form.manual_vote
- # Vote section
- release_policy.manual_vote = policy_data.manual_vote
if not release_policy.manual_vote:
- release_policy.github_vote_workflow_path =
policy_data.github_vote_workflow_path
- release_policy.mailto_addresses = policy_data.mailto_addresses
- self.__set_default_min_hours(policy_data, project, release_policy)
- release_policy.pause_for_rm = policy_data.pause_for_rm
- release_policy.release_checklist = policy_data.release_checklist
- release_policy.vote_comment_template =
policy_data.vote_comment_template
- self.__set_default_start_vote_template(policy_data, project,
release_policy)
+ 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
+ release_policy.release_checklist = form.release_checklist or ""
+ release_policy.vote_comment_template = form.vote_comment_template
or ""
+ self.__set_start_vote_template(form.start_vote_template or "",
project, release_policy)
elif project.committee and project.committee.is_podling:
- # The caller ensures that project.committee is not None
raise storage.AccessError("Manual voting is not allowed for
podlings.")
- # Finish section
- release_policy.github_finish_workflow_path =
policy_data.github_finish_workflow_path
- self.__set_default_announce_release_template(policy_data, project,
release_policy)
- release_policy.preserve_download_files =
policy_data.preserve_download_files
+ await self.__commit_and_log(project_name)
+ async def __commit_and_log(self, project_name: str) -> None:
await self.__data.commit()
self.__write_as.append_to_audit_log(
asf_uid=self.__asf_uid,
project_name=project_name,
)
- def __set_default_announce_release_template(
+ async def __get_or_create_policy(self, project_name: str) ->
tuple[models.sql.Project, models.sql.ReleasePolicy]:
+ project = await self.__data.project(
+ name=project_name, status=models.sql.ProjectStatus.ACTIVE,
_release_policy=True, _committee=True
+ ).demand(storage.AccessError(f"Project {project_name} not found"))
+
+ release_policy = project.release_policy
+ if release_policy is None:
+ release_policy = models.sql.ReleasePolicy(project=project)
+ project.release_policy = release_policy
+ self.__data.add(release_policy)
+
+ return project, release_policy
+
+ def __set_announce_release_template(
self,
- policy_data: models.policy.ReleasePolicyData,
+ submitted_template: str,
project: models.sql.Project,
release_policy: models.sql.ReleasePolicy,
) -> None:
- submitted_announce_template = policy_data.announce_release_template
- submitted_announce_template =
submitted_announce_template.replace("\r\n", "\n")
- rendered_default_announce_hash =
policy_data.default_announce_release_template_hash
- current_default_announce_text = project.policy_announce_release_default
- current_default_announce_hash =
util.compute_sha3_256(current_default_announce_text.encode())
- submitted_announce_hash =
util.compute_sha3_256(submitted_announce_template.encode())
-
- if (submitted_announce_hash == rendered_default_announce_hash) or (
- submitted_announce_hash == current_default_announce_hash
- ):
+ submitted_template = submitted_template.replace("\r\n", "\n")
+ current_default_text = project.policy_announce_release_default
+ current_default_hash =
util.compute_sha3_256(current_default_text.encode())
+ submitted_hash = util.compute_sha3_256(submitted_template.encode())
+
+ if submitted_hash == current_default_hash:
release_policy.announce_release_template = ""
else:
- release_policy.announce_release_template =
submitted_announce_template
+ release_policy.announce_release_template = submitted_template
- def __set_default_min_hours(
+ def __set_min_hours(
self,
- policy_data: models.policy.ReleasePolicyData,
+ submitted_min_hours: int,
project: models.sql.Project,
release_policy: models.sql.ReleasePolicy,
) -> None:
- submitted_min_hours = policy_data.min_hours
- default_value_seen_on_page_min_hours =
policy_data.default_min_hours_value_at_render
- current_system_default_min_hours = project.policy_default_min_hours
-
- if (
- submitted_min_hours == default_value_seen_on_page_min_hours
- or submitted_min_hours == current_system_default_min_hours
- ):
+ current_system_default = project.policy_default_min_hours
+
+ if submitted_min_hours == current_system_default:
release_policy.min_hours = None
else:
release_policy.min_hours = submitted_min_hours
- def __set_default_start_vote_template(
+ def __set_start_vote_template(
self,
- policy_data: models.policy.ReleasePolicyData,
+ submitted_template: str,
project: models.sql.Project,
release_policy: models.sql.ReleasePolicy,
) -> None:
- submitted_start_template = policy_data.start_vote_template
- submitted_start_template = submitted_start_template.replace("\r\n",
"\n")
- rendered_default_start_hash =
policy_data.default_start_vote_template_hash
- current_default_start_text = project.policy_start_vote_default
- current_default_start_hash =
util.compute_sha3_256(current_default_start_text.encode())
- submitted_start_hash =
util.compute_sha3_256(submitted_start_template.encode())
-
- if (submitted_start_hash == rendered_default_start_hash) or (
- submitted_start_hash == current_default_start_hash
- ):
+ submitted_template = submitted_template.replace("\r\n", "\n")
+ current_default_text = project.policy_start_vote_default
+ current_default_hash =
util.compute_sha3_256(current_default_text.encode())
+ submitted_hash = util.compute_sha3_256(submitted_template.encode())
+
+ if submitted_hash == current_default_hash:
release_policy.start_vote_template = ""
else:
- release_policy.start_vote_template = submitted_start_template
+ release_policy.start_vote_template = submitted_template
+
+
+def _split_lines(text: str) -> list[str]:
+ return [line.strip() for line in text.split("\n") if line.strip()]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]