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 0f60f8a Use a list of strings for workflow paths
0f60f8a is described below
commit 0f60f8ac8a1a97e1928a53cd95ee476d056b14bf
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Sep 8 20:34:14 2025 +0100
Use a list of strings for workflow paths
---
atr/db/__init__.py | 22 +++++-
atr/db/interaction.py | 36 ++++++----
atr/models/api.py | 1 -
atr/models/sql.py | 30 +++++----
atr/routes/projects.py | 78 ++++++++++++++--------
migrations/versions/0027_2025.09.08_69e565eb.py | 89 +++++++++++++++++++++++++
6 files changed, 199 insertions(+), 57 deletions(-)
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 5754b09..d126e13 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -522,12 +522,16 @@ 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_compose_workflow_path: Opt[str] = NOT_SET,
- github_vote_workflow_path: Opt[str] = NOT_SET,
- github_finish_workflow_path: Opt[str] = NOT_SET,
+ github_compose_workflow_path: Opt[list[str]] = NOT_SET,
+ github_vote_workflow_path: Opt[list[str]] = NOT_SET,
+ github_finish_workflow_path: Opt[list[str]] = NOT_SET,
+ github_compose_workflow_path_has: Opt[str] = NOT_SET,
+ github_vote_workflow_path_has: Opt[str] = NOT_SET,
+ github_finish_workflow_path_has: Opt[str] = NOT_SET,
_project: bool = False,
) -> Query[sql.ReleasePolicy]:
query = sqlmodel.select(sql.ReleasePolicy)
+ via = sql.validate_instrumented_attribute
if is_defined(id):
query = query.where(sql.ReleasePolicy.id == id)
@@ -545,10 +549,22 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
query = query.where(sql.ReleasePolicy.github_repository_name ==
github_repository_name)
if is_defined(github_compose_workflow_path):
query = query.where(sql.ReleasePolicy.github_compose_workflow_path
== github_compose_workflow_path)
+ if is_defined(github_compose_workflow_path_has):
+ query = query.where(
+
via(sql.ReleasePolicy.github_compose_workflow_path).contains(github_compose_workflow_path_has)
+ )
if is_defined(github_vote_workflow_path):
query = query.where(sql.ReleasePolicy.github_vote_workflow_path ==
github_vote_workflow_path)
+ if is_defined(github_vote_workflow_path_has):
+ query = query.where(
+
via(sql.ReleasePolicy.github_vote_workflow_path).contains(github_vote_workflow_path_has)
+ )
if is_defined(github_finish_workflow_path):
query = query.where(sql.ReleasePolicy.github_finish_workflow_path
== github_finish_workflow_path)
+ if is_defined(github_finish_workflow_path_has):
+ query = query.where(
+
via(sql.ReleasePolicy.github_finish_workflow_path).contains(github_finish_workflow_path_has)
+ )
if _project:
query = query.options(joined_load(sql.ReleasePolicy.project))
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 2852439..be3bd28 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -472,18 +472,8 @@ async def _delete_release_data_filesystem(release_dir:
pathlib.Path, release_nam
async def _trusted_project(repository: str, workflow_ref: str, phase:
TrustedProjectPhase) -> sql.Project:
# Debugging
log.info(f"GitHub OIDC JWT payload: {repository} {workflow_ref}")
+ repository_name, workflow_path = _trusted_project_checks(repository,
workflow_ref, phase)
- if not repository.startswith("apache/"):
- raise InteractionError("Repository must start with 'apache/'")
- repository_name = repository.removeprefix("apache/")
- if not workflow_ref.startswith(repository + "/"):
- raise InteractionError(f"Workflow ref must start with repository, got
{workflow_ref}")
- workflow_path_at = workflow_ref.removeprefix(repository + "/")
- if "@" not in workflow_path_at:
- raise InteractionError(f"Workflow path must contain '@', got
{workflow_path_at}")
- 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"
)
@@ -492,15 +482,18 @@ async def _trusted_project(repository: str, workflow_ref:
str, phase: TrustedPro
match phase:
case TrustedProjectPhase.COMPOSE:
policy = await db_data.release_policy(
- github_repository_name=repository_name,
github_compose_workflow_path=workflow_path
+ github_repository_name=repository_name,
+ github_compose_workflow_path_has=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
+ github_repository_name=repository_name,
+ github_vote_workflow_path_has=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
+ github_repository_name=repository_name,
+ github_finish_workflow_path_has=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")
@@ -512,3 +505,18 @@ async def _trusted_project(repository: str, workflow_ref:
str, phase: TrustedPro
log.info(f"Release policy: {policy}")
log.info(f"Project: {project}")
return project
+
+
+def _trusted_project_checks(repository: str, workflow_ref: str, phase:
TrustedProjectPhase) -> tuple[str, str]:
+ if not repository.startswith("apache/"):
+ raise InteractionError("Repository must start with 'apache/'")
+ repository_name = repository.removeprefix("apache/")
+ if not workflow_ref.startswith(repository + "/"):
+ raise InteractionError(f"Workflow ref must start with repository, got
{workflow_ref}")
+ workflow_path_at = workflow_ref.removeprefix(repository + "/")
+ if "@" not in workflow_path_at:
+ raise InteractionError(f"Workflow path must contain '@', got
{workflow_path_at}")
+ 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}")
+ return repository_name, workflow_path
diff --git a/atr/models/api.py b/atr/models/api.py
index adf370b..049225c 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -230,7 +230,6 @@ class ProjectsListResults(schema.Strict):
class PublisherDistributionRecordArgs(schema.Strict):
publisher: str = schema.Field(..., **example("user"))
jwt: str = schema.Field(...,
**example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI="))
- project: str = schema.Field(..., **example("example"))
version: str = schema.Field(..., **example("0.0.1"))
platform: sql.DistributionPlatform = schema.Field(...,
**example(sql.DistributionPlatform.ARTIFACT_HUB))
distribution_owner_namespace: str | None = schema.Field(default=None,
**example("example"))
diff --git a/atr/models/sql.py b/atr/models/sql.py
index b178e62..ed655a9 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -659,22 +659,22 @@ Thanks,
return policy.github_repository_name
@property
- def policy_github_compose_workflow_path(self) -> str:
+ def policy_github_compose_workflow_path(self) -> list[str]:
if (policy := self.release_policy) is None:
- return ""
- return policy.github_compose_workflow_path
+ return []
+ return policy.github_compose_workflow_path or []
@property
- def policy_github_vote_workflow_path(self) -> str:
+ def policy_github_vote_workflow_path(self) -> list[str]:
if (policy := self.release_policy) is None:
- return ""
- return policy.github_vote_workflow_path
+ return []
+ return policy.github_vote_workflow_path or []
@property
- def policy_github_finish_workflow_path(self) -> str:
+ def policy_github_finish_workflow_path(self) -> list[str]:
if (policy := self.release_policy) is None:
- return ""
- return policy.github_finish_workflow_path
+ return []
+ return policy.github_finish_workflow_path or []
# Release: Project ReleasePolicy Revision CheckResult
@@ -981,9 +981,15 @@ class ReleasePolicy(sqlmodel.SQLModel, table=True):
)
strict_checking: bool = sqlmodel.Field(default=False)
github_repository_name: 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="")
+ github_compose_workflow_path: list[str] = sqlmodel.Field(
+ default_factory=list, sa_column=sqlalchemy.Column(sqlalchemy.JSON,
nullable=False)
+ )
+ github_vote_workflow_path: list[str] = sqlmodel.Field(
+ default_factory=list, sa_column=sqlalchemy.Column(sqlalchemy.JSON,
nullable=False)
+ )
+ github_finish_workflow_path: list[str] = sqlmodel.Field(
+ default_factory=list, sa_column=sqlalchemy.Column(sqlalchemy.JSON,
nullable=False)
+ )
# 1-1: ReleasePolicy -> Project
# 1-1: Project -C-> ReleasePolicy
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 97336eb..ceb797e 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -82,9 +82,11 @@ class ReleasePolicyForm(forms.Typed):
"GitHub repository name",
description="The name of the GitHub repository to use for the release,
excluding the apache/ prefix.",
)
- github_compose_workflow_path = forms.optional(
- "GitHub compose workflow path",
- description="The full path to the GitHub workflow to use for the
release,"
+ github_compose_workflow_path = forms.textarea(
+ "GitHub compose workflow paths",
+ optional=True,
+ rows=5,
+ description="The full paths to the GitHub workflows to use for the
release,"
" including the .github/workflows/ prefix.",
)
strict_checking = forms.boolean(
@@ -128,9 +130,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,"
+ github_vote_workflow_path = forms.textarea(
+ "GitHub vote workflow paths",
+ optional=True,
+ rows=5,
+ description="The full paths to the GitHub workflows to use for the
release,"
" including the .github/workflows/ prefix.",
)
@@ -142,9 +146,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,"
+ github_finish_workflow_path = forms.textarea(
+ "GitHub finish workflow paths",
+ optional=True,
+ rows=5,
+ description="The full paths to the GitHub workflows to use for the
release,"
" including the .github/workflows/ prefix.",
)
@@ -192,18 +198,30 @@ class ReleasePolicyForm(forms.Typed):
if grn:
if "/" in grn:
_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/'."
- )
+ if compose:
+ for p in _parse_artifact_paths(compose):
+ if not p.startswith(".github/workflows/"):
+ _form_append(
+ self.github_compose_workflow_path,
+ "GitHub workflow paths must start with
'.github/workflows/'.",
+ )
+ break
+ if vote:
+ for p in _parse_artifact_paths(vote):
+ if not p.startswith(".github/workflows/"):
+ _form_append(
+ self.github_vote_workflow_path,
+ "GitHub workflow paths must start with
'.github/workflows/'.",
+ )
+ break
+ if finish:
+ for p in _parse_artifact_paths(finish):
+ if not p.startswith(".github/workflows/"):
+ _form_append(
+ self.github_finish_workflow_path,
+ "GitHub workflow paths must start with
'.github/workflows/'.",
+ )
+ break
return not self.errors
@@ -502,9 +520,15 @@ 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_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.github_compose_workflow_path = _parse_artifact_paths(
+ util.unwrap(policy_form.github_compose_workflow_path.data)
+ )
+ release_policy.github_vote_workflow_path = _parse_artifact_paths(
+ util.unwrap(policy_form.github_vote_workflow_path.data)
+ )
+ release_policy.github_finish_workflow_path = _parse_artifact_paths(
+ util.unwrap(policy_form.github_finish_workflow_path.data)
+ )
release_policy.strict_checking =
util.unwrap(policy_form.strict_checking.data)
# Vote section
@@ -549,9 +573,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_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
+ policy_form.github_compose_workflow_path.data =
"\n".join(project.policy_github_compose_workflow_path)
+ policy_form.github_vote_workflow_path.data =
"\n".join(project.policy_github_vote_workflow_path)
+ policy_form.github_finish_workflow_path.data =
"\n".join(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/migrations/versions/0027_2025.09.08_69e565eb.py
b/migrations/versions/0027_2025.09.08_69e565eb.py
new file mode 100644
index 0000000..5c7ca8c
--- /dev/null
+++ b/migrations/versions/0027_2025.09.08_69e565eb.py
@@ -0,0 +1,89 @@
+"""Use a list of strings for release policy workflow URLs
+
+Revision ID: 0027_2025.09.08_69e565eb
+Revises: 0026_2025.09.04_eb02c4d9
+Create Date: 2025-09-08 18:57:18.049164+00:00
+"""
+
+import json
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0027_2025.09.08_69e565eb"
+down_revision: str | None = "0026_2025.09.04_eb02c4d9"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ bind = op.get_bind()
+ for col in [
+ "github_compose_workflow_path",
+ "github_vote_workflow_path",
+ "github_finish_workflow_path",
+ ]:
+ rows = bind.execute(sa.text(f"SELECT id, {col} AS v FROM
releasepolicy")).mappings().all()
+ for row in rows:
+ v = row["v"]
+ if not v:
+ new_v = json.dumps([])
+ elif isinstance(v, str):
+ try:
+ parsed = json.loads(v)
+ if isinstance(parsed, list):
+ new_v = v
+ else:
+ new_v = json.dumps([v])
+ except Exception:
+ new_v = json.dumps([v])
+ else:
+ continue
+ bind.execute(
+ sa.text(f"UPDATE releasepolicy SET {col} = :v WHERE id = :id"),
+ {"v": new_v, "id": row["id"]},
+ )
+
+ with op.batch_alter_table("releasepolicy", schema=None) as batch_op:
+ batch_op.alter_column(
+ "github_compose_workflow_path", existing_type=sa.VARCHAR(),
type_=sa.JSON(), nullable=False
+ )
+ batch_op.alter_column("github_vote_workflow_path",
existing_type=sa.VARCHAR(), type_=sa.JSON(), nullable=False)
+ batch_op.alter_column(
+ "github_finish_workflow_path", existing_type=sa.VARCHAR(),
type_=sa.JSON(), nullable=False
+ )
+
+
+def downgrade() -> None:
+ bind = op.get_bind()
+ for col in [
+ "github_finish_workflow_path",
+ "github_vote_workflow_path",
+ "github_compose_workflow_path",
+ ]:
+ rows = bind.execute(sa.text(f"SELECT id, {col} AS v FROM
releasepolicy")).mappings().all()
+ for row in rows:
+ v = row["v"]
+ new_v = ""
+ if isinstance(v, str):
+ try:
+ parsed = json.loads(v)
+ if isinstance(parsed, list) and parsed:
+ new_v = str(parsed[0])
+ except Exception:
+ new_v = v
+ bind.execute(
+ sa.text(f"UPDATE releasepolicy SET {col} = :v WHERE id = :id"),
+ {"v": new_v, "id": row["id"]},
+ )
+
+ with op.batch_alter_table("releasepolicy", schema=None) as batch_op:
+ batch_op.alter_column(
+ "github_finish_workflow_path", existing_type=sa.JSON(),
type_=sa.VARCHAR(), nullable=False
+ )
+ batch_op.alter_column("github_vote_workflow_path",
existing_type=sa.JSON(), type_=sa.VARCHAR(), nullable=False)
+ batch_op.alter_column(
+ "github_compose_workflow_path", existing_type=sa.JSON(),
type_=sa.VARCHAR(), nullable=False
+ )
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]