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]