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]