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 eb02c4d  Add release policy fields for vote and finish phase GitHub 
workflows
eb02c4d is described below

commit eb02c4d986f8349795ddb1c916bf3fae1e3e1a53
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Sep 4 19:18:05 2025 +0100

    Add release policy fields for vote and finish phase GitHub workflows
---
 atr/blueprints/api/api.py       |  6 ++---
 atr/db/__init__.py              | 12 ++++++---
 atr/db/interaction.py           | 36 ++++++++++++++++++--------
 atr/models/sql.py               | 20 ++++++++++++---
 atr/routes/projects.py          | 57 ++++++++++++++++++++++++++++++-----------
 atr/templates/project-view.html | 26 ++++++++++++++++---
 6 files changed, 119 insertions(+), 38 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index e117d13..73e89dd 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -265,7 +265,7 @@ async def github_release_announce(data: 
models.api.GithubReleaseAnnounceArgs) ->
     """
     Announce a release with a corroborating GitHub OIDC JWT.
     """
-    _payload, asf_uid, project = await interaction.github_trusted_jwt(data.jwt)
+    _payload, asf_uid, project = await 
interaction.github_trusted_jwt(data.jwt, interaction.TrustedProjectPhase.FINISH)
     try:
         # TODO: Add defaults
         await announce.announce(
@@ -294,7 +294,7 @@ async def github_ssh_register(data: 
models.api.GithubSshRegisterArgs) -> DictRes
     """
     Register an SSH key sent with a corroborating GitHub OIDC JWT.
     """
-    payload, asf_uid, project = await interaction.github_trusted_jwt(data.jwt)
+    payload, asf_uid, project = await interaction.github_trusted_jwt(data.jwt, 
interaction.TrustedProjectPhase.COMPOSE)
     async with 
storage.write_as_committee_member(util.unwrap(project.committee).name, asf_uid) 
as wacm:
         fingerprint, expires = await wacm.ssh.add_workflow_key(
             payload["actor"],
@@ -318,7 +318,7 @@ async def github_vote_resolve(data: 
models.api.GithubVoteResolveArgs) -> DictRes
     Resolve a vote with a corroborating GitHub OIDC JWT.
     """
     # TODO: Need to be able to resolve and make the release immutable
-    _payload, asf_uid, project = await interaction.github_trusted_jwt(data.jwt)
+    _payload, asf_uid, project = await 
interaction.github_trusted_jwt(data.jwt, interaction.TrustedProjectPhase.VOTE)
     if project.committee is None:
         raise exceptions.NotFound("Project has no committee")
     # WARNING: This is subtly different from the /vote/resolve code
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 3df2231..5754b09 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -522,7 +522,9 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         release_checklist: Opt[str] = NOT_SET,
         pause_for_rm: Opt[bool] = NOT_SET,
         github_repository_name: Opt[str] = NOT_SET,
-        github_workflow_path: Opt[str] = NOT_SET,
+        github_compose_workflow_path: Opt[str] = NOT_SET,
+        github_vote_workflow_path: Opt[str] = NOT_SET,
+        github_finish_workflow_path: Opt[str] = NOT_SET,
         _project: bool = False,
     ) -> Query[sql.ReleasePolicy]:
         query = sqlmodel.select(sql.ReleasePolicy)
@@ -541,8 +543,12 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
             query = query.where(sql.ReleasePolicy.pause_for_rm == pause_for_rm)
         if is_defined(github_repository_name):
             query = query.where(sql.ReleasePolicy.github_repository_name == 
github_repository_name)
-        if is_defined(github_workflow_path):
-            query = query.where(sql.ReleasePolicy.github_workflow_path == 
github_workflow_path)
+        if is_defined(github_compose_workflow_path):
+            query = query.where(sql.ReleasePolicy.github_compose_workflow_path 
== github_compose_workflow_path)
+        if is_defined(github_vote_workflow_path):
+            query = query.where(sql.ReleasePolicy.github_vote_workflow_path == 
github_vote_workflow_path)
+        if is_defined(github_finish_workflow_path):
+            query = query.where(sql.ReleasePolicy.github_finish_workflow_path 
== github_finish_workflow_path)
 
         if _project:
             query = query.options(joined_load(sql.ReleasePolicy.project))
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 19b459b..292e7fa 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import contextlib
+import enum
 import pathlib
 from collections.abc import AsyncGenerator, Sequence
 from typing import Any
@@ -52,6 +53,12 @@ class PublicKeyError(RuntimeError):
     pass
 
 
+class TrustedProjectPhase(enum.Enum):
+    COMPOSE = "compose"
+    VOTE = "vote"
+    FINISH = "finish"
+
+
 async def candidate_drafts(project: sql.Project) -> list[sql.Release]:
     """Get the candidate drafts for the project."""
     return await releases_by_phase(project, 
sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT)
@@ -74,10 +81,10 @@ async def full_releases(project: sql.Project) -> 
list[sql.Release]:
     return await releases_by_phase(project, sql.ReleasePhase.RELEASE)
 
 
-async def github_trusted_jwt(jwt: str) -> tuple[dict[str, Any], str, 
sql.Project]:
+async def github_trusted_jwt(jwt: str, phase: TrustedProjectPhase) -> 
tuple[dict[str, Any], str, sql.Project]:
     payload = await jwtoken.verify_github_oidc(jwt)
     asf_uid = await ldap.github_to_apache(payload["actor_id"])
-    project = await _trusted_project(payload["repository"], 
payload["workflow_ref"])
+    project = await _trusted_project(payload["repository"], 
payload["workflow_ref"], phase)
     return payload, asf_uid, project
 
 
@@ -322,7 +329,7 @@ async def _delete_release_data_filesystem(release_dir: 
pathlib.Path, release_nam
         )
 
 
-async def _trusted_project(repository: str, workflow_ref: str) -> sql.Project:
+async def _trusted_project(repository: str, workflow_ref: str, phase: 
TrustedProjectPhase) -> sql.Project:
     # Debugging
     log.info(f"GitHub OIDC JWT payload: {repository} {workflow_ref}")
 
@@ -337,15 +344,24 @@ async def _trusted_project(repository: str, workflow_ref: 
str) -> sql.Project:
     workflow_path = workflow_path_at.rsplit("@", 1)[0]
     if not workflow_path.startswith(".github/workflows/"):
         raise InteractionError(f"Workflow path must start with 
'.github/workflows/', got {workflow_path}")
+    value_error = ValueError(
+        f"Release policy for repository {repository_name} and {phase.value} 
workflow path {workflow_path} not found"
+    )
     # TODO: If a policy is reused between projects, we can't get the project
     async with db.session() as db_data:
-        policy = await db_data.release_policy(
-            github_repository_name=repository_name, 
github_workflow_path=workflow_path
-        ).demand(
-            InteractionError(
-                f"No release policy found for repository name 
{repository_name} and workflow path {workflow_path}"
-            )
-        )
+        match phase:
+            case TrustedProjectPhase.COMPOSE:
+                policy = await db_data.release_policy(
+                    github_repository_name=repository_name, 
github_compose_workflow_path=workflow_path
+                ).demand(value_error)
+            case TrustedProjectPhase.VOTE:
+                policy = await db_data.release_policy(
+                    github_repository_name=repository_name, 
github_vote_workflow_path=workflow_path
+                ).demand(value_error)
+            case TrustedProjectPhase.FINISH:
+                policy = await db_data.release_policy(
+                    github_repository_name=repository_name, 
github_finish_workflow_path=workflow_path
+                ).demand(value_error)
         project = await db_data.project(release_policy_id=policy.id).demand(
             InteractionError(f"Project for release policy {policy.id} not 
found")
         )
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 7b79311..b178e62 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -659,10 +659,22 @@ Thanks,
         return policy.github_repository_name
 
     @property
-    def policy_github_workflow_path(self) -> str:
+    def policy_github_compose_workflow_path(self) -> str:
         if (policy := self.release_policy) is None:
             return ""
-        return policy.github_workflow_path
+        return policy.github_compose_workflow_path
+
+    @property
+    def policy_github_vote_workflow_path(self) -> str:
+        if (policy := self.release_policy) is None:
+            return ""
+        return policy.github_vote_workflow_path
+
+    @property
+    def policy_github_finish_workflow_path(self) -> str:
+        if (policy := self.release_policy) is None:
+            return ""
+        return policy.github_finish_workflow_path
 
 
 # Release: Project ReleasePolicy Revision CheckResult
@@ -969,7 +981,9 @@ class ReleasePolicy(sqlmodel.SQLModel, table=True):
     )
     strict_checking: bool = sqlmodel.Field(default=False)
     github_repository_name: str = sqlmodel.Field(default="")
-    github_workflow_path: str = sqlmodel.Field(default="")
+    github_compose_workflow_path: str = sqlmodel.Field(default="")
+    github_vote_workflow_path: str = sqlmodel.Field(default="")
+    github_finish_workflow_path: str = sqlmodel.Field(default="")
 
     # 1-1: ReleasePolicy -> Project
     # 1-1: Project -C-> ReleasePolicy
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 8ecf80f..97336eb 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -82,8 +82,8 @@ class ReleasePolicyForm(forms.Typed):
         "GitHub repository name",
         description="The name of the GitHub repository to use for the release, 
excluding the apache/ prefix.",
     )
-    github_workflow_path = forms.optional(
-        "GitHub workflow path",
+    github_compose_workflow_path = forms.optional(
+        "GitHub compose workflow path",
         description="The full path to the GitHub workflow to use for the 
release,"
         " including the .github/workflows/ prefix.",
     )
@@ -128,6 +128,11 @@ class ReleasePolicyForm(forms.Typed):
         rows=10,
         description="Email template for messages to start a vote on a 
release.",
     )
+    github_vote_workflow_path = forms.optional(
+        "GitHub vote workflow path",
+        description="The full path to the GitHub workflow to use for the 
release,"
+        " including the .github/workflows/ prefix.",
+    )
 
     # Finish section
     default_announce_release_template_hash = forms.hidden()
@@ -137,6 +142,11 @@ class ReleasePolicyForm(forms.Typed):
         rows=10,
         description="Email template for messages to announce a finished 
release.",
     )
+    github_finish_workflow_path = forms.optional(
+        "GitHub finish workflow path",
+        description="The full path to the GitHub workflow to use for the 
release,"
+        " including the .github/workflows/ prefix.",
+    )
 
     submit_policy = forms.submit("Save")
 
@@ -169,18 +179,31 @@ class ReleasePolicyForm(forms.Typed):
             # _form_setdefault_append(self, "strict_checking", [], msg)
 
         grn = self.github_repository_name.data
-        gwp = self.github_workflow_path.data
-        if (grn and (not gwp)) or ((not grn) and gwp):
-            msg = "GitHub repository name and workflow path must be set 
together."
-            _form_append(self.github_repository_name, msg)
-            _form_append(self.github_workflow_path, msg)
-        elif grn and gwp:
+        compose = self.github_compose_workflow_path.data
+        vote = self.github_vote_workflow_path.data
+        finish = self.github_finish_workflow_path.data
+
+        any_path = bool(compose or vote or finish)
+        if any_path and (not grn):
+            _form_append(
+                self.github_repository_name, "GitHub repository name is 
required when any workflow path is set."
+            )
+
+        if grn:
             if "/" in grn:
-                msg = "GitHub repository name must not contain a slash."
-                _form_append(self.github_repository_name, msg)
-            if not gwp.startswith(".github/workflows/"):
-                msg = "GitHub workflow path must start with 
'.github/workflows/'."
-                _form_append(self.github_workflow_path, msg)
+                _form_append(self.github_repository_name, "GitHub repository 
name must not contain a slash.")
+            if compose and (not compose.startswith(".github/workflows/")):
+                _form_append(
+                    self.github_compose_workflow_path, "GitHub workflow path 
must start with '.github/workflows/'."
+                )
+            if vote and (not vote.startswith(".github/workflows/")):
+                _form_append(
+                    self.github_vote_workflow_path, "GitHub workflow path must 
start with '.github/workflows/'."
+                )
+            if finish and (not finish.startswith(".github/workflows/")):
+                _form_append(
+                    self.github_finish_workflow_path, "GitHub workflow path 
must start with '.github/workflows/'."
+                )
 
         return not self.errors
 
@@ -479,7 +502,9 @@ async def _policy_edit(
             util.unwrap(policy_form.binary_artifact_paths.data)
         )
         release_policy.github_repository_name = 
util.unwrap(policy_form.github_repository_name.data)
-        release_policy.github_workflow_path = 
util.unwrap(policy_form.github_workflow_path.data)
+        release_policy.github_compose_workflow_path = 
util.unwrap(policy_form.github_compose_workflow_path.data)
+        release_policy.github_vote_workflow_path = 
util.unwrap(policy_form.github_vote_workflow_path.data)
+        release_policy.github_finish_workflow_path = 
util.unwrap(policy_form.github_finish_workflow_path.data)
         release_policy.strict_checking = 
util.unwrap(policy_form.strict_checking.data)
 
         # Vote section
@@ -524,7 +549,9 @@ async def _policy_form_create(project: sql.Project) -> 
ReleasePolicyForm:
     policy_form.pause_for_rm.data = project.policy_pause_for_rm
     policy_form.strict_checking.data = project.policy_strict_checking
     policy_form.github_repository_name.data = 
project.policy_github_repository_name
-    policy_form.github_workflow_path.data = project.policy_github_workflow_path
+    policy_form.github_compose_workflow_path.data = 
project.policy_github_compose_workflow_path
+    policy_form.github_vote_workflow_path.data = 
project.policy_github_vote_workflow_path
+    policy_form.github_finish_workflow_path.data = 
project.policy_github_finish_workflow_path
 
     # Set the hashes and value of the current defaults
     policy_form.default_start_vote_template_hash.data = util.compute_sha3_256(
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index a8d0265..7f7107a 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -116,11 +116,11 @@
             </div>
 
             <div class="mb-3 pb-3 row border-bottom">
-              {{ forms.label(policy_form.github_workflow_path, col="md3") }}
+              {{ forms.label(policy_form.github_compose_workflow_path, 
col="md3") }}
               <div class="col-sm-8">
-                {{ forms.widget(policy_form.github_workflow_path, 
classes="form-control font-monospace") }}
-                {{ forms.errors(policy_form.github_workflow_path) }}
-                {{ forms.description(policy_form.github_workflow_path) }}
+                {{ forms.widget(policy_form.github_compose_workflow_path, 
classes="form-control font-monospace") }}
+                {{ forms.errors(policy_form.github_compose_workflow_path) }}
+                {{ forms.description(policy_form.github_compose_workflow_path) 
}}
               </div>
             </div>
 
@@ -152,6 +152,15 @@
               </div>
             {% endif %}
 
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.github_vote_workflow_path, col="md3") 
}}
+              <div class="col-sm-8">
+                {{ forms.widget(policy_form.github_vote_workflow_path, 
classes="form-control font-monospace") }}
+                {{ forms.errors(policy_form.github_vote_workflow_path) }}
+                {{ forms.description(policy_form.github_vote_workflow_path) }}
+              </div>
+            </div>
+
             <div id="vote-options-extra">
 
               <div class="mb-3 pb-3 row border-bottom">
@@ -207,6 +216,15 @@
               <h3 class="col-md-3 col-form-label text-md-end fs-4">Finish 
options</h3>
             </div>
 
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.github_finish_workflow_path, 
col="md3") }}
+              <div class="col-sm-8">
+                {{ forms.widget(policy_form.github_finish_workflow_path, 
classes="form-control font-monospace") }}
+                {{ forms.errors(policy_form.github_finish_workflow_path) }}
+                {{ forms.description(policy_form.github_finish_workflow_path) 
}}
+              </div>
+            </div>
+
             <div class="mb-3 pb-3 row border-bottom">
               {{ forms.label(policy_form.announce_release_template, col="md3") 
}}
               <div class="col-sm-8">


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

Reply via email to