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 61fdcb7  Use project and version metadata in tasks rather than release 
name
61fdcb7 is described below

commit 61fdcb73b42ce8053f57010bd8f299a20ff9aed9
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jun 11 16:40:07 2025 +0100

    Use project and version metadata in tasks rather than release name
---
 atr/blueprints/admin/admin.py                   |  2 +-
 atr/db/__init__.py                              | 15 ++++------
 atr/db/interaction.py                           |  4 +--
 atr/db/models.py                                |  9 ++----
 atr/routes/announce.py                          |  3 +-
 atr/routes/draft.py                             | 10 +++----
 atr/routes/resolve.py                           | 34 +++++++++++++----------
 atr/routes/vote.py                              |  7 +++--
 atr/routes/voting.py                            |  3 +-
 atr/tasks/__init__.py                           |  3 +-
 atr/tasks/checks/__init__.py                    | 18 ++++++------
 atr/tasks/checks/paths.py                       |  9 ++++--
 atr/worker.py                                   | 12 +++++---
 migrations/versions/0007_2025.06.11_4887c85c.py | 37 +++++++++++++++++++++++++
 playwright/test.py                              |  8 ++++--
 15 files changed, 110 insertions(+), 64 deletions(-)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 7abb42e..8bfbc40 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -558,7 +558,7 @@ async def _delete_release_data(release_name: str) -> None:
         # Delete from the database
         _LOGGER.info("Deleting database records for release: %s", release_name)
         # Cascade should handle this, but we delete manually anyway
-        tasks_to_delete = await data.task(release_name=release_name).all()
+        tasks_to_delete = await data.task(project_name=release.project.name, 
version_name=release.version).all()
         for task in tasks_to_delete:
             await data.delete(task)
         _LOGGER.debug("Deleted %d tasks for %s", len(tasks_to_delete), 
release_name)
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index a50fcf2..c1cfd31 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -394,8 +394,6 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
             query = 
query.options(select_in_load(models.Release.release_policy))
         if _committee:
             query = 
query.options(select_in_load_nested(models.Release.project, 
models.Project.committee))
-        if _tasks:
-            query = query.options(select_in_load(models.Release.tasks))
         if _revisions:
             query = query.options(select_in_load(models.Release.revisions))
 
@@ -505,8 +503,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         completed: Opt[datetime.datetime | None] = NOT_SET,
         result: Opt[Any | None] = NOT_SET,
         error: Opt[str | None] = NOT_SET,
-        release_name: Opt[str | None] = NOT_SET,
-        _release: bool = False,
+        project_name: Opt[str | None] = NOT_SET,
+        version_name: Opt[str | None] = NOT_SET,
     ) -> Query[models.Task]:
         query = sqlmodel.select(models.Task)
 
@@ -530,11 +528,10 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
             query = query.where(models.Task.result == result)
         if is_defined(error):
             query = query.where(models.Task.error == error)
-        if is_defined(release_name):
-            query = query.where(models.Task.release_name == release_name)
-
-        if _release:
-            query = query.options(select_in_load(models.Task.release))
+        if is_defined(project_name):
+            query = query.where(models.Task.project_name == project_name)
+        if is_defined(version_name):
+            query = query.where(models.Task.version_name == version_name)
 
         return Query(self, query)
 
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 9365cf9..df02a12 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -266,13 +266,13 @@ async def path_info(release: models.Release, paths: 
list[pathlib.Path]) -> PathI
 
 
 async def tasks_ongoing(project_name: str, version_name: str, revision_number: 
str) -> int:
-    release_name = models.release_name(project_name, version_name)
     async with db.session() as data:
         query = (
             sqlmodel.select(sqlalchemy.func.count())
             .select_from(models.Task)
             .where(
-                models.Task.release_name == release_name,
+                models.Task.project_name == project_name,
+                models.Task.version_name == version_name,
                 models.Task.revision_number == revision_number,
                 models.validate_instrumented_attribute(models.Task.status).in_(
                     [models.TaskStatus.QUEUED, models.TaskStatus.ACTIVE]
diff --git a/atr/db/models.py b/atr/db/models.py
index 3336b62..6aec0f2 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -533,8 +533,8 @@ class Task(sqlmodel.SQLModel, table=True):
 
     # Used for check tasks
     # We don't put these in task_args because we want to query them efficiently
-    release_name: str | None = sqlmodel.Field(default=None, 
foreign_key="release.name")
-    release: Optional["Release"] = 
sqlmodel.Relationship(back_populates="tasks")
+    project_name: str | None = sqlmodel.Field(default=None, 
foreign_key="project.name")
+    version_name: str | None = sqlmodel.Field(default=None, index=True)
     revision_number: str | None = sqlmodel.Field(default=None, index=True)
     primary_rel_path: str | None = sqlmodel.Field(default=None, index=True)
 
@@ -612,11 +612,6 @@ class Release(sqlmodel.SQLModel, table=True):
         },
     )
 
-    # One-to-many: A release can have multiple tasks
-    tasks: list["Task"] = sqlmodel.Relationship(
-        back_populates="release", sa_relationship_kwargs={"cascade": "all, 
delete-orphan"}
-    )
-
     # One-to-many: A release can have multiple check results
     check_results: list["CheckResult"] = 
sqlmodel.Relationship(back_populates="release")
 
diff --git a/atr/routes/announce.py b/atr/routes/announce.py
index 3a41b61..4974787 100644
--- a/atr/routes/announce.py
+++ b/atr/routes/announce.py
@@ -176,7 +176,8 @@ async def selected_post(
                     body=body,
                     in_reply_to=None,
                 ).model_dump(),
-                release_name=release.name,
+                project_name=project_name,
+                version_name=version_name,
             )
             data.add(task)
 
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index 1a2ebcf..c7d8f10 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -310,8 +310,6 @@ async def sbomgen(
         # Create and queue the task, using paths within the new revision
         async with db.session() as data:
             # We still need release.name for the task metadata
-            release = await session.release(project_name, version_name, 
data=data)
-
             sbom_task = models.Task(
                 task_type=models.TaskType.SBOM_GENERATE_CYCLONEDX,
                 task_args=sbom.GenerateCycloneDX(
@@ -320,7 +318,8 @@ async def sbomgen(
                 ).model_dump(),
                 added=datetime.datetime.now(datetime.UTC),
                 status=models.TaskStatus.QUEUED,
-                release_name=release.name,
+                project_name=project_name,
+                version_name=version_name,
                 revision_number=creating.new.number,
             )
             data.add(sbom_task)
@@ -354,8 +353,6 @@ async def svnload(session: routes.CommitterSession, 
project_name: str, version_n
     await session.check_access(project_name)
 
     form = await upload.SvnImportForm.create_form()
-    release = await session.release(project_name, version_name, 
with_project=False)
-
     if not await form.validate_on_submit():
         for _field, errors in form.errors.items():
             for error in errors:
@@ -381,7 +378,8 @@ async def svnload(session: routes.CommitterSession, 
project_name: str, version_n
                 task_args=task_args,
                 added=datetime.datetime.now(datetime.UTC),
                 status=models.TaskStatus.QUEUED,
-                release_name=release.name,
+                project_name=project_name,
+                version_name=version_name,
             )
             data.add(svn_import_task)
             await data.commit()
diff --git a/atr/routes/resolve.py b/atr/routes/resolve.py
index dd8c6ca..598a914 100644
--- a/atr/routes/resolve.py
+++ b/atr/routes/resolve.py
@@ -19,6 +19,7 @@ import json
 import os
 
 import quart
+import sqlmodel
 import werkzeug.wrappers.response as response
 import wtforms
 
@@ -52,20 +53,22 @@ class ResolveForm(util.QuartFormTyped):
     submit = wtforms.SubmitField("Resolve vote")
 
 
-def release_latest_vote_task(release: models.Release) -> models.Task | None:
-    # Find the most recent VOTE_INITIATE task for this release
-    # TODO: Make this a proper query
-    for task in sorted(release.tasks, key=lambda t: t.added, reverse=True):
-        if task.task_type != models.TaskType.VOTE_INITIATE:
-            continue
-        # if task.status != models.TaskStatus.COMPLETED:
-        #     continue
-        if (task.status == models.TaskStatus.QUEUED) or (task.status == 
models.TaskStatus.ACTIVE):
-            continue
-        if task.result is None:
-            continue
+async def release_latest_vote_task(release: models.Release) -> models.Task | 
None:
+    """Find the most recent VOTE_INITIATE task for this release."""
+    via = models.validate_instrumented_attribute
+    async with db.session() as data:
+        query = (
+            sqlmodel.select(models.Task)
+            .where(models.Task.project_name == release.project_name)
+            .where(models.Task.version_name == release.version)
+            .where(models.Task.task_type == models.TaskType.VOTE_INITIATE)
+            .where(via(models.Task.status).notin_([models.TaskStatus.QUEUED, 
models.TaskStatus.ACTIVE]))
+            .where(via(models.Task.result).is_not(None))
+            .order_by(via(models.Task.added).desc())
+            .limit(1)
+        )
+        task = (await data.execute(query)).scalar_one_or_none()
         return task
-    return None
 
 
 @routes.committer("/resolve/<project_name>/<version_name>", methods=["POST"], 
measure_performance=False)
@@ -167,7 +170,7 @@ async def _send_resolution(
     body: str,
 ) -> str | None:
     # Get the email thread
-    latest_vote_task = release_latest_vote_task(release)
+    latest_vote_task = await release_latest_vote_task(release)
     if latest_vote_task is None:
         return "No vote task found, unable to send resolution message."
     vote_thread_mid = task_mid_get(latest_vote_task)
@@ -194,7 +197,8 @@ async def _send_resolution(
             body=body,
             in_reply_to=in_reply_to,
         ).model_dump(),
-        release_name=release.name,
+        project_name=release.project.name,
+        version_name=release.version,
     )
     async with db.session() as data:
         data.add(task)
diff --git a/atr/routes/vote.py b/atr/routes/vote.py
index a893bf9..26ee96b 100644
--- a/atr/routes/vote.py
+++ b/atr/routes/vote.py
@@ -53,7 +53,7 @@ async def selected(session: routes.CommitterSession, 
project_name: str, version_
     release = await session.release(
         project_name, version_name, with_committee=True, with_tasks=True, 
phase=models.ReleasePhase.RELEASE_CANDIDATE
     )
-    latest_vote_task = resolve.release_latest_vote_task(release)
+    latest_vote_task = await resolve.release_latest_vote_task(release)
     archive_url = None
     task_mid = None
     if latest_vote_task is not None:
@@ -115,7 +115,7 @@ async def _send_vote(
     comment: str,
 ) -> tuple[str, str]:
     # Get the email thread
-    latest_vote_task = resolve.release_latest_vote_task(release)
+    latest_vote_task = await resolve.release_latest_vote_task(release)
     if latest_vote_task is None:
         return "", "No vote task found."
     vote_thread_mid = resolve.task_mid_get(latest_vote_task)
@@ -147,7 +147,8 @@ async def _send_vote(
             body=body_text,
             in_reply_to=in_reply_to,
         ).model_dump(),
-        release_name=release.name,
+        project_name=release.project.name,
+        version_name=release.version,
     )
     async with db.session() as data:
         data.add(task)
diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index 18d1035..cf3563c 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -170,7 +170,8 @@ async def selected_revision(
                     subject=subject_data,
                     body=body_data,
                 ).model_dump(),
-                release_name=release.name,
+                project_name=project_name,
+                version_name=version,
             )
             data.add(task)
             await data.commit()
diff --git a/atr/tasks/__init__.py b/atr/tasks/__init__.py
index d7202ae..cb2f41e 100644
--- a/atr/tasks/__init__.py
+++ b/atr/tasks/__init__.py
@@ -124,7 +124,8 @@ def queued(
         status=models.TaskStatus.QUEUED,
         task_type=task_type,
         task_args=extra_args or {},
-        release_name=release.name,
+        project_name=release.project.name,
+        version_name=release.version,
         revision_number=revision_number,
         primary_rel_path=primary_rel_path,
     )
diff --git a/atr/tasks/checks/__init__.py b/atr/tasks/checks/__init__.py
index 4366b6b..22b8237 100644
--- a/atr/tasks/checks/__init__.py
+++ b/atr/tasks/checks/__init__.py
@@ -40,7 +40,8 @@ import atr.util as util
 @dataclasses.dataclass
 class FunctionArguments:
     recorder: Callable[[], Awaitable[Recorder]]
-    release_name: str
+    project_name: str
+    version_name: str
     revision_number: str
     primary_rel_path: str | None
     extra_args: dict[str, Any]
@@ -59,35 +60,36 @@ class Recorder:
     def __init__(
         self,
         checker: str | Callable[..., Any],
-        release_name: str,
+        project_name: str,
+        version_name: str,
         revision_number: str,
         primary_rel_path: str | None = None,
         member_rel_path: str | None = None,
         afresh: bool = True,
     ) -> None:
         self.checker = function_key(checker) if callable(checker) else checker
-        self.release_name = release_name
+        self.release_name = models.release_name(project_name, version_name)
         self.revision_number = revision_number
         self.primary_rel_path = primary_rel_path
         self.member_rel_path = member_rel_path
         self.afresh = afresh
         self.constructed = False
 
-        # project_name, version_name = models.project_version(release_name)
-        # self.project_name = project_name
-        # self.version_name = version_name
+        self.project_name = project_name
+        self.version_name = version_name
 
     @classmethod
     async def create(
         cls,
         checker: str | Callable[..., Any],
-        release_name: str,
+        project_name: str,
+        version_name: str,
         revision_number: str,
         primary_rel_path: str | None = None,
         member_rel_path: str | None = None,
         afresh: bool = True,
     ) -> Recorder:
-        recorder = cls(checker, release_name, revision_number, 
primary_rel_path, member_rel_path, afresh)
+        recorder = cls(checker, project_name, version_name, revision_number, 
primary_rel_path, member_rel_path, afresh)
         if afresh is True:
             # Clear outer path whether it's specified or not
             await recorder.clear(primary_rel_path=primary_rel_path, 
member_rel_path=member_rel_path)
diff --git a/atr/tasks/checks/paths.py b/atr/tasks/checks/paths.py
index 5afff99..b06074e 100644
--- a/atr/tasks/checks/paths.py
+++ b/atr/tasks/checks/paths.py
@@ -37,21 +37,24 @@ async def check(args: checks.FunctionArguments) -> None:
 
     recorder_errors = await checks.Recorder.create(
         checker=checks.function_key(check) + "_errors",
-        release_name=args.release_name,
+        project_name=args.project_name,
+        version_name=args.version_name,
         revision_number=args.revision_number,
         primary_rel_path=None,
         afresh=True,
     )
     recorder_warnings = await checks.Recorder.create(
         checker=checks.function_key(check) + "_warnings",
-        release_name=args.release_name,
+        project_name=args.project_name,
+        version_name=args.version_name,
         revision_number=args.revision_number,
         primary_rel_path=None,
         afresh=True,
     )
     recorder_success = await checks.Recorder.create(
         checker=checks.function_key(check) + "_success",
-        release_name=args.release_name,
+        project_name=args.project_name,
+        version_name=args.version_name,
         revision_number=args.revision_number,
         primary_rel_path=None,
         afresh=True,
diff --git a/atr/worker.py b/atr/worker.py
index 41b5aa1..7333ba9 100644
--- a/atr/worker.py
+++ b/atr/worker.py
@@ -199,8 +199,10 @@ async def _task_process(task_id: int, task_type: str, 
task_args: list[str] | dic
                 )
 
             # Validate required fields from the Task object itself
-            if task_obj.release_name is None:
-                raise ValueError(f"Task {task_id} is missing required 
release_name")
+            if task_obj.project_name is None:
+                raise ValueError(f"Task {task_id} is missing required 
project_name")
+            if task_obj.version_name is None:
+                raise ValueError(f"Task {task_id} is missing required 
version_name")
             if task_obj.revision_number is None:
                 raise ValueError(f"Task {task_id} is missing required 
revision_number")
 
@@ -213,14 +215,16 @@ async def _task_process(task_id: int, task_type: str, 
task_args: list[str] | dic
             async def recorder_factory() -> checks.Recorder:
                 return await checks.Recorder.create(
                     checker=handler,
-                    release_name=task_obj.release_name or "",
+                    project_name=task_obj.project_name or "",
+                    version_name=task_obj.version_name or "",
                     revision_number=task_obj.revision_number or "",
                     primary_rel_path=task_obj.primary_rel_path,
                 )
 
             function_arguments = checks.FunctionArguments(
                 recorder=recorder_factory,
-                release_name=task_obj.release_name,
+                project_name=task_obj.project_name or "",
+                version_name=task_obj.version_name or "",
                 revision_number=task_obj.revision_number,
                 primary_rel_path=task_obj.primary_rel_path,
                 extra_args=task_args,
diff --git a/migrations/versions/0007_2025.06.11_4887c85c.py 
b/migrations/versions/0007_2025.06.11_4887c85c.py
new file mode 100644
index 0000000..eeeb573
--- /dev/null
+++ b/migrations/versions/0007_2025.06.11_4887c85c.py
@@ -0,0 +1,37 @@
+"""Use project and version in tasks
+
+Revision ID: 0007_2025.06.11_4887c85c
+Revises: 0006_2025.05.30_9672a901
+Create Date: 2025-06-11 15:27:25.527805+00:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0007_2025.06.11_4887c85c"
+down_revision: str | None = "0006_2025.05.30_9672a901"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    with op.batch_alter_table("task", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("project_name", sa.String(), 
nullable=True))
+        batch_op.add_column(sa.Column("version_name", sa.String(), 
nullable=True))
+        batch_op.create_index(batch_op.f("ix_task_version_name"), 
["version_name"], unique=False)
+        batch_op.drop_constraint(batch_op.f("fk_task_release_name_release"), 
type_="foreignkey")
+        
batch_op.create_foreign_key(batch_op.f("fk_task_project_name_project"), 
"project", ["project_name"], ["name"])
+        batch_op.drop_column("release_name")
+
+
+def downgrade() -> None:
+    with op.batch_alter_table("task", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("release_name", sa.VARCHAR(), 
nullable=True))
+        batch_op.drop_constraint(batch_op.f("fk_task_project_name_project"), 
type_="foreignkey")
+        
batch_op.create_foreign_key(batch_op.f("fk_task_release_name_release"), 
"release", ["release_name"], ["name"])
+        batch_op.drop_index(batch_op.f("ix_task_version_name"))
+        batch_op.drop_column("version_name")
+        batch_op.drop_column("project_name")
diff --git a/playwright/test.py b/playwright/test.py
index b6d72d2..920d472 100644
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -862,14 +862,16 @@ def test_projects_03_add_project(page: sync_api.Page, 
credentials: Credentials)
     base_project_label = "tooling"
     project_name = "Apache Tooling Test Example"
     project_label = "tooling-test-example"
-    derived_project_input_value = "Test Example"
 
     logging.info("Navigating to the add derived project page")
     go_to_path(page, f"/project/add/{base_project_label}")
     logging.info("Add a new project page loaded")
 
-    logging.info(f"Filling derived project name 
'{derived_project_input_value}'")
-    
page.locator('input[name="derived_project_name"]').fill(derived_project_input_value)
+    logging.info(f"Filling display name '{project_name}'")
+    page.locator('input[name="display_name"]').fill(project_name)
+
+    logging.info(f"Filling label '{project_label}'")
+    page.locator('input[name="label"]').fill(project_label)
 
     logging.info("Submitting the add derived project form")
     submit_button_locator = page.locator('input[type="submit"][value="Add 
project"]')


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

Reply via email to