You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2025/04/08 14:23:01 UTC

(tooling-trusted-release) branch main updated: Use relative paths in tasks unless required to be absolute

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 17b9158  Use relative paths in tasks unless required to be absolute
17b9158 is described below

commit 17b915860afe9f47f1189591be3ab934e461e6a7
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue Apr 8 15:22:39 2025 +0100

    Use relative paths in tasks unless required to be absolute
---
 atr/db/__init__.py              |  6 +--
 atr/db/models.py                | 24 ++++++++++-
 atr/routes/draft.py             | 24 +++++------
 atr/tasks/__init__.py           | 69 +++++++++++-------------------
 atr/tasks/checks/__init__.py    | 86 ++++++++++++++++++++++---------------
 atr/tasks/checks/hashing.py     | 60 +++++++++++++-------------
 atr/tasks/checks/license.py     | 62 ++++++++++++---------------
 atr/tasks/checks/paths.py       | 39 +++++++++++------
 atr/tasks/checks/rat.py         | 22 ++++++----
 atr/tasks/checks/signature.py   | 37 ++++++++--------
 atr/tasks/checks/targz.py       | 40 ++++++++++++------
 atr/tasks/checks/zipformat.py   | 94 +++++++++++++++++++++++------------------
 atr/templates/draft-review.html |  2 +-
 atr/util.py                     | 20 ++++-----
 14 files changed, 321 insertions(+), 264 deletions(-)

diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index fb968af..4943d00 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -118,7 +118,7 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         id: Opt[int] = NotSet,
         release_name: Opt[str] = NotSet,
         checker: Opt[str] = NotSet,
-        path: Opt[str | None] = NotSet,
+        primary_rel_path: Opt[str | None] = NotSet,
         created: Opt[datetime.datetime] = NotSet,
         status: Opt[models.CheckResultStatus] = NotSet,
         message: Opt[str] = NotSet,
@@ -133,8 +133,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
             query = query.where(models.CheckResult.release_name == release_name)
         if is_defined(checker):
             query = query.where(models.CheckResult.checker == checker)
-        if is_defined(path):
-            query = query.where(models.CheckResult.path == path)
+        if is_defined(primary_rel_path):
+            query = query.where(models.CheckResult.primary_rel_path == primary_rel_path)
         if is_defined(created):
             query = query.where(models.CheckResult.created == created)
         if is_defined(status):
diff --git a/atr/db/models.py b/atr/db/models.py
index 110369e..31319fc 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -327,6 +327,8 @@ class Task(sqlmodel.SQLModel, table=True):
     error: str | None = None
     release_name: str | None = sqlmodel.Field(default=None, foreign_key="release.name")
     release: Optional["Release"] = sqlmodel.Relationship(back_populates="tasks")
+    # Identifier for the draft revision that this task targets, if any
+    draft_revision: str | None = sqlmodel.Field(default=None, index=True)
 
     # Create an index on status and added for efficient task claiming
     __table_args__ = (
@@ -410,6 +412,15 @@ def release_name(project_name: str, version_name: str) -> str:
     return f"{project_name}-{version_name}"
 
 
+def project_version(release_name: str) -> tuple[str, str]:
+    """Return the project and version for a given release name."""
+    try:
+        project_name, version_name = release_name.rsplit("-", 1)
+        return (project_name, version_name)
+    except ValueError:
+        raise ValueError(f"Invalid release name: {release_name}")
+
+
 @event.listens_for(Release, "before_insert")
 def check_release_name(_mapper: sqlalchemy.orm.Mapper, _connection: sqlalchemy.Connection, release: Release) -> None:
     if release.name == "":
@@ -434,12 +445,23 @@ class CheckResult(sqlmodel.SQLModel, table=True):
     release_name: str = sqlmodel.Field(foreign_key="release.name")
     release: Release = sqlmodel.Relationship(back_populates="check_results")
     checker: str
-    path: str | None = None
+    primary_rel_path: str | None = sqlmodel.Field(default=None, index=True)
     created: datetime.datetime
     status: CheckResultStatus
     message: str
     data: Any = sqlmodel.Field(sa_column=sqlalchemy.Column(sqlalchemy.JSON))
 
+    # Link to the draft revisions that these results are for
+    histories: list["CheckResultHistoryLink"] = sqlmodel.Relationship(back_populates="check_result")
+
+
+class CheckResultHistoryLink(sqlmodel.SQLModel, table=True):
+    # Composite primary key, automatically handled by SQLModel
+    check_result_id: int = sqlmodel.Field(foreign_key="checkresult.id", primary_key=True)
+    draft_revision: str = sqlmodel.Field(index=True, primary_key=True)
+
+    check_result: CheckResult = sqlmodel.Relationship(back_populates="histories")
+
 
 class TextValue(sqlmodel.SQLModel, table=True):
     # Composite primary key, automatically handled by SQLModel
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index 680d21d..14c120d 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -491,13 +491,13 @@ async def review(session: routes.CommitterSession, project_name: str, version_na
 
         # Get successes, warnings, and errors
         path_successes[path] = await data.check_result(
-            release_name=release.name, path=str(path), status=models.CheckResultStatus.SUCCESS
+            release_name=release.name, primary_rel_path=str(path), status=models.CheckResultStatus.SUCCESS
         ).all()
         path_warnings[path] = await data.check_result(
-            release_name=release.name, path=str(path), status=models.CheckResultStatus.WARNING
+            release_name=release.name, primary_rel_path=str(path), status=models.CheckResultStatus.WARNING
         ).all()
         path_errors[path] = await data.check_result(
-            release_name=release.name, path=str(path), status=models.CheckResultStatus.FAILURE
+            release_name=release.name, primary_rel_path=str(path), status=models.CheckResultStatus.FAILURE
         ).all()
 
     # # TODO: This is only accurate to a second
@@ -534,8 +534,8 @@ async def review(session: routes.CommitterSession, project_name: str, version_na
     )
 
 
-@routes.committer("/draft/review/<project_name>/<version_name>/<path:file_path>")
-async def review_path(session: routes.CommitterSession, project_name: str, version_name: str, file_path: str) -> str:
+@routes.committer("/draft/review/<project_name>/<version_name>/<path:rel_path>")
+async def review_path(session: routes.CommitterSession, project_name: str, version_name: str, rel_path: str) -> str:
     """Show the status of all checks for a specific file."""
     # Check that the user has access to the project
     if not any((p.name == project_name) for p in (await session.user_projects)):
@@ -547,17 +547,17 @@ async def review_path(session: routes.CommitterSession, project_name: str, versi
             base.ASFQuartException("Release does not exist", errorcode=404)
         )
 
-        full_path = str(util.get_release_candidate_draft_dir() / project_name / version_name / file_path)
+        abs_path = util.get_release_candidate_draft_dir() / project_name / version_name / rel_path
 
         # Check that the file exists
-        if not await aiofiles.os.path.exists(full_path):
+        if not await aiofiles.os.path.exists(abs_path):
             raise base.ASFQuartException("File does not exist", errorcode=404)
 
-        modified = int(await aiofiles.os.path.getmtime(full_path))
-        file_size = await aiofiles.os.path.getsize(full_path)
+        modified = int(await aiofiles.os.path.getmtime(abs_path))
+        file_size = await aiofiles.os.path.getsize(abs_path)
 
         # Get all check results for this file
-        query = data.check_result(release_name=release.name, path=file_path).order_by(
+        query = data.check_result(release_name=release.name, primary_rel_path=str(rel_path)).order_by(
             db.validate_instrumented_attribute(models.CheckResult.checker).asc(),
             db.validate_instrumented_attribute(models.CheckResult.created).desc(),
         )
@@ -573,7 +573,7 @@ async def review_path(session: routes.CommitterSession, project_name: str, versi
         check_results_list = list(latest_check_results.values())
 
     file_data = {
-        "filename": pathlib.Path(file_path).name,
+        "filename": pathlib.Path(rel_path).name,
         "bytes_size": file_size,
         "uploaded": datetime.datetime.fromtimestamp(modified, tz=datetime.UTC),
     }
@@ -582,7 +582,7 @@ async def review_path(session: routes.CommitterSession, project_name: str, versi
         "draft-review-path.html",
         project_name=project_name,
         version_name=version_name,
-        file_path=file_path,
+        rel_path=rel_path,
         package=file_data,
         release=release,
         check_results=check_results_list,
diff --git a/atr/tasks/__init__.py b/atr/tasks/__init__.py
index 6ac2436..34d56e0 100644
--- a/atr/tasks/__init__.py
+++ b/atr/tasks/__init__.py
@@ -36,11 +36,9 @@ import atr.util as util
 
 
 async def asc_checks(release: models.Release, signature_path: str) -> list[models.Task]:
+    """Create signature check task for a .asc file."""
     tasks = []
 
-    draft_dir = util.get_release_candidate_draft_dir() / release.project.name / release.version
-    full_signature_path = str(draft_dir / signature_path)
-
     if release.committee:
         tasks.append(
             models.Task(
@@ -49,7 +47,7 @@ async def asc_checks(release: models.Release, signature_path: str) -> list[model
                 task_args=signature.Check(
                     release_name=release.name,
                     committee_name=release.committee.name,
-                    abs_signature_path=full_signature_path,
+                    signature_rel_path=signature_path,
                 ).model_dump(),
                 release_name=release.name,
             ),
@@ -61,32 +59,31 @@ async def asc_checks(release: models.Release, signature_path: str) -> list[model
 async def draft_checks(project_name: str, release_version: str, caller_data: db.Session | None = None) -> int:
     """Core logic to analyse an rsync upload and queue checks."""
     base_path = util.get_release_candidate_draft_dir() / project_name / release_version
-    paths_recursive = await util.paths_recursive(base_path)
+    relative_paths = await util.paths_recursive(base_path)
 
     session_context = db.session() if (caller_data is None) else contextlib.nullcontext(caller_data)
     async with session_context as data:
         release = await data.release(name=models.release_name(project_name, release_version), _committee=True).demand(
             RuntimeError("Release not found")
         )
-        for path in paths_recursive:
-            for task_type, task_function in TASK_FUNCTIONS.items():
-                if path.name.endswith(task_type):
-                    for task in await task_function(release, str(path)):
+        for path in relative_paths:
+            path_str = str(path)
+            for suffix, task_function in TASK_FUNCTIONS.items():
+                if path.name.endswith(suffix):
+                    for task in await task_function(release, path_str):
                         data.add(task)
 
         path_check_task = models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.PATHS_CHECK,
-            task_args=paths.Check(
-                release_name=release.name,
-                base_release_dir=str(base_path),
-            ).model_dump(),
+            task_args=paths.Check(release_name=release.name).model_dump(),
+            release_name=release.name,
         )
         data.add(path_check_task)
         if caller_data is None:
             await data.commit()
 
-    return len(paths_recursive)
+    return len(relative_paths)
 
 
 def resolve(task_type: models.TaskType) -> Callable[..., Awaitable[str | None]]:  # noqa: C901
@@ -126,29 +123,16 @@ def resolve(task_type: models.TaskType) -> Callable[..., Awaitable[str | None]]:
 
 
 async def sha_checks(release: models.Release, hash_file: str) -> list[models.Task]:
+    """Create hash check task for a .sha256 or .sha512 file."""
     tasks = []
 
-    full_hash_file_path = str(
-        util.get_release_candidate_draft_dir() / release.project.name / release.version / hash_file
-    )
-    algorithm = "sha512"
-    if hash_file.endswith(".sha512"):
-        original_file = full_hash_file_path.removesuffix(".sha512")
-    elif hash_file.endswith(".sha256"):
-        original_file = full_hash_file_path.removesuffix(".sha256")
-        algorithm = "sha256"
-    else:
-        raise RuntimeError(f"Unsupported hash file: {hash_file}")
-
     tasks.append(
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.HASHING_CHECK,
-            task_args=hashing.Check(
+            task_args=checks.ReleaseAndRelPath(
                 release_name=release.name,
-                abs_path=original_file,
-                abs_hash_file=full_hash_file_path,
-                algorithm=algorithm,
+                primary_rel_path=hash_file,
             ).model_dump(),
             release_name=release.name,
         ),
@@ -158,39 +142,36 @@ async def sha_checks(release: models.Release, hash_file: str) -> list[models.Tas
 
 
 async def tar_gz_checks(release: models.Release, path: str) -> list[models.Task]:
-    # TODO: We should probably use an enum for task_type
-    full_path = str(util.get_release_candidate_draft_dir() / release.project.name / release.version / path)
-    # filename = os.path.basename(path)
-
+    """Create check tasks for a .tar.gz or .tgz file."""
     tasks = [
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.LICENSE_FILES,
-            task_args=license.Files(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=checks.ReleaseAndRelPath(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.LICENSE_HEADERS,
-            task_args=license.Headers(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=checks.ReleaseAndRelPath(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.RAT_CHECK,
-            task_args=rat.Check(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=rat.Check(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.TARGZ_INTEGRITY,
-            task_args=targz.Integrity(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=targz.Integrity(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.TARGZ_STRUCTURE,
-            task_args=checks.ReleaseAndAbsPath(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=checks.ReleaseAndRelPath(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
     ]
@@ -200,31 +181,29 @@ async def tar_gz_checks(release: models.Release, path: str) -> list[models.Task]
 
 async def zip_checks(release: models.Release, path: str) -> list[models.Task]:
     """Create check tasks for a .zip file."""
-    full_path = str(util.get_release_candidate_draft_dir() / release.project.name / release.version / path)
-
     tasks = [
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.ZIPFORMAT_INTEGRITY,
-            task_args=checks.ReleaseAndAbsPath(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=checks.ReleaseAndRelPath(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.ZIPFORMAT_LICENSE_FILES,
-            task_args=checks.ReleaseAndAbsPath(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=checks.ReleaseAndRelPath(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.ZIPFORMAT_LICENSE_HEADERS,
-            task_args=checks.ReleaseAndAbsPath(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=checks.ReleaseAndRelPath(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
         models.Task(
             status=models.TaskStatus.QUEUED,
             task_type=models.TaskType.ZIPFORMAT_STRUCTURE,
-            task_args=checks.ReleaseAndAbsPath(release_name=release.name, abs_path=full_path).model_dump(),
+            task_args=checks.ReleaseAndRelPath(release_name=release.name, primary_rel_path=path).model_dump(),
             release_name=release.name,
         ),
     ]
diff --git a/atr/tasks/checks/__init__.py b/atr/tasks/checks/__init__.py
index eff9973..5480427 100644
--- a/atr/tasks/checks/__init__.py
+++ b/atr/tasks/checks/__init__.py
@@ -18,7 +18,6 @@
 from __future__ import annotations
 
 import datetime
-import pathlib
 from functools import wraps
 from types import FunctionType
 from typing import TYPE_CHECKING, Any, TypeVar
@@ -27,55 +26,67 @@ import pydantic
 import sqlmodel
 
 if TYPE_CHECKING:
+    import pathlib
     from collections.abc import Awaitable, Callable
 
-import atr.config as config
 import atr.db as db
 import atr.db.models as models
+import atr.util as util
 
 
 class Check:
     def __init__(
-        self, checker: str | Callable[..., Any], release_name: str, path: str | None = None, afresh: bool = True
+        self,
+        checker: str | Callable[..., Any],
+        release_name: str,
+        primary_rel_path: str | None = None,
+        afresh: bool = True,
     ) -> None:
         if isinstance(checker, FunctionType):
             checker = function_key(checker)
         if not isinstance(checker, str):
             raise ValueError("Checker must be a string or a callable")
         self.checker = checker
+        project_name, version_name = models.project_version(release_name)
+        self.project_name = project_name
+        self.version_name = version_name
         self.release_name = release_name
-        self.path = path
+        self.primary_rel_path = primary_rel_path
         self.afresh = afresh
         self._constructed = False
 
     @classmethod
     async def create(
-        cls, checker: str | Callable[..., Any], release_name: str, path: str | None = None, afresh: bool = True
+        cls,
+        checker: str | Callable[..., Any],
+        release_name: str,
+        primary_rel_path: str | None = None,
+        afresh: bool = True,
     ) -> Check:
-        check = cls(checker, release_name, path, afresh)
+        check = cls(checker, release_name, primary_rel_path, afresh)
         if afresh is True:
             # Clear outer path whether it's specified or not
-            await check.clear(path)
+            await check.clear(primary_rel_path)
         check._constructed = True
         return check
 
     async def _add(
-        self, status: models.CheckResultStatus, message: str, data: Any, path: str | None = None
+        self, status: models.CheckResultStatus, message: str, data: Any, primary_rel_path: str | None = None
     ) -> models.CheckResult:
         if self._constructed is False:
             raise RuntimeError("Cannot add check result to a check that has not been constructed")
-        if path is not None:
-            if self.path is not None:
+        if primary_rel_path is not None:
+            if self.primary_rel_path is not None:
                 raise ValueError("Cannot specify path twice")
             if self.afresh is True:
                 # Clear inner path only if it's specified
-                await self.clear(path)
+                await self.clear(primary_rel_path)
 
         result = models.CheckResult(
             release_name=self.release_name,
             checker=self.checker,
-            path=path or self.path,
-            created=datetime.datetime.now(),
+            primary_rel_path=primary_rel_path or self.primary_rel_path,
+            created=datetime.datetime.now(datetime.UTC),
             status=status,
             message=message,
             data=data,
@@ -89,41 +100,50 @@ class Check:
             await session.commit()
         return result
 
-    async def clear(self, path: str | None = None) -> None:
+    async def abs_path(self, rel_path: str | None = None) -> pathlib.Path | None:
+        if rel_path is not None:
+            return util.get_release_candidate_draft_dir() / self.project_name / self.version_name / rel_path
+        if self.primary_rel_path is not None:
+            return (
+                util.get_release_candidate_draft_dir() / self.project_name / self.version_name / self.primary_rel_path
+            )
+        return util.get_release_candidate_draft_dir() / self.project_name / self.version_name
+
+    async def clear(self, primary_rel_path: str | None = None) -> None:
         async with db.session() as data:
             stmt = sqlmodel.delete(models.CheckResult).where(
                 db.validate_instrumented_attribute(models.CheckResult.release_name) == self.release_name,
                 db.validate_instrumented_attribute(models.CheckResult.checker) == self.checker,
-                db.validate_instrumented_attribute(models.CheckResult.path) == path,
+                db.validate_instrumented_attribute(models.CheckResult.primary_rel_path) == primary_rel_path,
             )
             await data.execute(stmt)
             await data.commit()
 
-    async def exception(self, message: str, data: Any, path: str | None = None) -> models.CheckResult:
-        return await self._add(models.CheckResultStatus.EXCEPTION, message, data, path=path)
+    async def exception(self, message: str, data: Any, primary_rel_path: str | None = None) -> models.CheckResult:
+        return await self._add(models.CheckResultStatus.EXCEPTION, message, data, primary_rel_path=primary_rel_path)
 
-    async def failure(self, message: str, data: Any, path: str | None = None) -> models.CheckResult:
-        return await self._add(models.CheckResultStatus.FAILURE, message, data, path=path)
+    async def failure(self, message: str, data: Any, primary_rel_path: str | None = None) -> models.CheckResult:
+        return await self._add(models.CheckResultStatus.FAILURE, message, data, primary_rel_path=primary_rel_path)
 
-    async def success(self, message: str, data: Any, path: str | None = None) -> models.CheckResult:
-        return await self._add(models.CheckResultStatus.SUCCESS, message, data, path=path)
+    async def success(self, message: str, data: Any, primary_rel_path: str | None = None) -> models.CheckResult:
+        return await self._add(models.CheckResultStatus.SUCCESS, message, data, primary_rel_path=primary_rel_path)
 
-    async def warning(self, message: str, data: Any, path: str | None = None) -> models.CheckResult:
-        return await self._add(models.CheckResultStatus.WARNING, message, data, path=path)
+    async def warning(self, message: str, data: Any, primary_rel_path: str | None = None) -> models.CheckResult:
+        return await self._add(models.CheckResultStatus.WARNING, message, data, primary_rel_path=primary_rel_path)
 
 
 def function_key(func: Callable[..., Any]) -> str:
     return func.__module__ + "." + func.__name__
 
 
-def rel_path(abs_path: str) -> str:
-    """Return the relative path for a given absolute path."""
-    conf = config.get()
-    phase_dir = pathlib.Path(conf.PHASE_STORAGE_DIR)
-    phase_sub_dir = pathlib.Path(abs_path).relative_to(phase_dir)
-    # Skip the first component, which is the phase name
-    # And the next two components, which are the project name and version name
-    return str(pathlib.Path(*phase_sub_dir.parts[3:]))
+# def rel_path(abs_path: str) -> str:
+#     """Return the relative path for a given absolute path."""
+#     conf = config.get()
+#     phase_dir = pathlib.Path(conf.PHASE_STORAGE_DIR)
+#     phase_sub_dir = pathlib.Path(abs_path).relative_to(phase_dir)
+#     # Skip the first component, which is the phase name
+#     # And the next two components, which are the project name and version name
+#     return str(pathlib.Path(*phase_sub_dir.parts[3:]))
 
 
 # def using(cls: type[pydantic.BaseModel]) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
@@ -155,6 +175,6 @@ def with_model(model_class: type[T]) -> Callable[[Callable[..., Awaitable[R]]],
     return decorator
 
 
-class ReleaseAndAbsPath(pydantic.BaseModel):
+class ReleaseAndRelPath(pydantic.BaseModel):
     release_name: str = pydantic.Field(..., description="Release name")
-    abs_path: str = pydantic.Field(..., description="Absolute path to the file to check")
+    primary_rel_path: str = pydantic.Field(..., description="Primary relative path to check")
diff --git a/atr/tasks/checks/hashing.py b/atr/tasks/checks/hashing.py
index 8921e1a..b02d048 100644
--- a/atr/tasks/checks/hashing.py
+++ b/atr/tasks/checks/hashing.py
@@ -21,63 +21,63 @@ import secrets
 from typing import Final
 
 import aiofiles
-import pydantic
 
 import atr.tasks.checks as checks
 
 _LOGGER: Final = logging.getLogger(__name__)
 
 
-class Check(pydantic.BaseModel):
-    """Parameters for file hash checking."""
-
-    release_name: str = pydantic.Field(..., description="Release name")
-    abs_path: str = pydantic.Field(..., description="Absolute path to the file to check")
-    abs_hash_file: str = pydantic.Field(..., description="Absolute path to the hash file")
-    algorithm: str = pydantic.Field(..., description="Hash algorithm to use")
-
-
-@checks.with_model(Check)
-async def check(args: Check) -> str | None:
+@checks.with_model(checks.ReleaseAndRelPath)
+async def check(args: checks.ReleaseAndRelPath) -> str | None:
     """Check the hash of a file."""
-    # This is probably the best idea for the rel_path
-    # But we might want to use the artifact instead
-    rel_path = checks.rel_path(args.abs_hash_file)
-    check_instance = await checks.Check.create(checker=check, release_name=args.release_name, path=rel_path)
+    check_obj = await checks.Check.create(
+        checker=check, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
 
-    _LOGGER.info(f"Checking hash ({args.algorithm}) for {args.abs_path} against {args.abs_hash_file} (rel: {rel_path})")
+    if not (hash_abs_path := await check_obj.abs_path()):
+        return None
 
-    if args.algorithm == "sha256":
-        hash_func = hashlib.sha256
-    elif args.algorithm == "sha512":
-        hash_func = hashlib.sha512
-    else:
-        await check_instance.failure(f"Unsupported hash algorithm: {args.algorithm}", {"algorithm": args.algorithm})
+    algorithm = hash_abs_path.suffix.lstrip(".")
+    if algorithm not in {"sha256", "sha512"}:
+        await check_obj.failure("Unsupported hash algorithm", {"algorithm": algorithm})
         return None
 
+    # Remove the hash file suffix to get the artifact path
+    # This replaces the last suffix, which is what we want
+    # >>> pathlib.Path("a/b/c.d.e.f.g").with_suffix(".x")
+    # PosixPath('a/b/c.d.e.f.x')
+    # >>> pathlib.Path("a/b/c.d.e.f.g").with_suffix("")
+    # PosixPath('a/b/c.d.e.f')
+    artifact_abs_path = hash_abs_path.with_suffix("")
+
+    _LOGGER.info(
+        f"Checking hash ({algorithm}) for {artifact_abs_path} against {hash_abs_path} (rel: {args.primary_rel_path})"
+    )
+
+    hash_func = hashlib.sha256 if algorithm == "sha256" else hashlib.sha512
     hash_obj = hash_func()
     try:
-        async with aiofiles.open(args.abs_path, mode="rb") as f:
+        async with aiofiles.open(artifact_abs_path, mode="rb") as f:
             while chunk := await f.read(4096):
                 hash_obj.update(chunk)
         computed_hash = hash_obj.hexdigest()
 
-        async with aiofiles.open(args.abs_hash_file) as f:
+        async with aiofiles.open(hash_abs_path) as f:
             expected_hash = await f.read()
         # May be in the format "HASH FILENAME\n"
         # TODO: Check the FILENAME part
         expected_hash = expected_hash.strip().split()[0]
 
         if secrets.compare_digest(computed_hash, expected_hash):
-            await check_instance.success(
-                f"Hash ({args.algorithm}) matches expected value",
+            await check_obj.success(
+                f"Hash ({algorithm}) matches expected value",
                 {"computed_hash": computed_hash, "expected_hash": expected_hash},
             )
         else:
-            await check_instance.failure(
-                f"Hash ({args.algorithm}) mismatch",
+            await check_obj.failure(
+                f"Hash ({algorithm}) mismatch",
                 {"computed_hash": computed_hash, "expected_hash": expected_hash},
             )
     except Exception as e:
-        await check_instance.exception("Unable to verify hash", {"error": str(e)})
+        await check_obj.failure("Unable to verify hash", {"error": str(e)})
     return None
diff --git a/atr/tasks/checks/license.py b/atr/tasks/checks/license.py
index 6871eaf..389b24f 100644
--- a/atr/tasks/checks/license.py
+++ b/atr/tasks/checks/license.py
@@ -23,8 +23,6 @@ import re
 import tarfile
 from typing import Any, Final
 
-import pydantic
-
 import atr.tasks.checks as checks
 import atr.tasks.checks.targz as targz
 
@@ -174,66 +172,62 @@ INCLUDED_PATTERNS: Final[list[str]] = [
 # Tasks
 
 
-class Files(pydantic.BaseModel):
-    """Parameters for LICENSE and NOTICE file checks."""
-
-    release_name: str = pydantic.Field(..., description="Release name")
-    abs_path: str = pydantic.Field(..., description="Absolute path to the .tar.gz file to check")
+@checks.with_model(checks.ReleaseAndRelPath)
+async def files(args: checks.ReleaseAndRelPath) -> str | None:
+    """Check that the LICENSE and NOTICE files exist and are valid."""
+    check = await checks.Check.create(
+        checker=files, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
 
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
 
-@checks.with_model(Files)
-async def files(args: Files) -> str | None:
-    """Check that the LICENSE and NOTICE files exist and are valid."""
-    rel_path = checks.rel_path(args.abs_path)
-    check_instance = await checks.Check.create(checker=files, release_name=args.release_name, path=rel_path)
-    _LOGGER.info(f"Checking license files for {args.abs_path} (rel: {rel_path})")
+    _LOGGER.info(f"Checking license files for {artifact_abs_path} (rel: {args.primary_rel_path})")
 
     try:
-        result_data = await asyncio.to_thread(_files_check_core_logic, args.abs_path)
+        result_data = await asyncio.to_thread(_files_check_core_logic, str(artifact_abs_path))
 
         if result_data.get("error"):
-            await check_instance.failure(result_data["error"], result_data)
+            await check.failure(result_data["error"], result_data)
         elif result_data["license_valid"] and result_data["notice_valid"]:
-            await check_instance.success("LICENSE and NOTICE files present and valid", result_data)
+            await check.success("LICENSE and NOTICE files present and valid", result_data)
         else:
             # TODO: Be more specific about the issues
-            await check_instance.failure("Issues found with LICENSE or NOTICE files", result_data)
+            await check.failure("Issues found with LICENSE or NOTICE files", result_data)
 
     except Exception as e:
-        await check_instance.failure("Error checking license files", {"error": str(e)})
+        await check.failure("Error checking license files", {"error": str(e)})
 
     return None
 
 
-class Headers(pydantic.BaseModel):
-    """Parameters for license header checks."""
-
-    release_name: str = pydantic.Field(..., description="Release name")
-    abs_path: str = pydantic.Field(..., description="Absolute path to the .tar.gz file to check")
+@checks.with_model(checks.ReleaseAndRelPath)
+async def headers(args: checks.ReleaseAndRelPath) -> str | None:
+    """Check that all source files have valid license headers."""
+    check = await checks.Check.create(
+        checker=headers, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
 
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
 
-@checks.with_model(Headers)
-async def headers(args: Headers) -> str | None:
-    """Check that all source files have valid license headers."""
-    rel_path = checks.rel_path(args.abs_path)
-    check_instance = await checks.Check.create(checker=headers, release_name=args.release_name, path=rel_path)
-    _LOGGER.info(f"Checking license headers for {args.abs_path} (rel: {rel_path})")
+    _LOGGER.info(f"Checking license headers for {artifact_abs_path} (rel: {args.primary_rel_path})")
 
     try:
-        result_data = await asyncio.to_thread(_headers_check_core_logic, args.abs_path)
+        result_data = await asyncio.to_thread(_headers_check_core_logic, str(artifact_abs_path))
 
         if result_data.get("error_message"):
             # Handle errors during the check process itself
-            await check_instance.failure(result_data["error_message"], result_data)
+            await check.failure(result_data["error_message"], result_data)
         elif not result_data["valid"]:
             # Handle validation failures
-            await check_instance.failure(result_data["message"], result_data)
+            await check.failure(result_data["message"], result_data)
         else:
             # Handle success
-            await check_instance.success(result_data["message"], result_data)
+            await check.success(result_data["message"], result_data)
 
     except Exception as e:
-        await check_instance.exception("Error checking license headers", {"error": str(e)})
+        await check.failure("Error checking license headers", {"error": str(e)})
 
     return None
 
diff --git a/atr/tasks/checks/paths.py b/atr/tasks/checks/paths.py
index f74f190..661c53b 100644
--- a/atr/tasks/checks/paths.py
+++ b/atr/tasks/checks/paths.py
@@ -34,7 +34,6 @@ class Check(pydantic.BaseModel):
     """Arguments for the path structure and naming convention check."""
 
     release_name: str = pydantic.Field(..., description="Name of the release being checked")
-    base_release_dir: str = pydantic.Field(..., description="Absolute path to the base directory of the release draft")
 
 
 async def _check_artifact_rules(
@@ -139,12 +138,12 @@ async def _check_path_process_single(
     # Must aggregate errors and aggregate warnings otherwise they will be removed by afresh=True
     # Alternatively we could call Check.clear() manually
     if errors:
-        await check_errors.failure("; ".join(errors), {"errors": errors}, path=relative_path_str)
+        await check_errors.failure("; ".join(errors), {"errors": errors}, primary_rel_path=relative_path_str)
     if warnings:
-        await check_warnings.warning("; ".join(warnings), {"warnings": warnings}, path=relative_path_str)
+        await check_warnings.warning("; ".join(warnings), {"warnings": warnings}, primary_rel_path=relative_path_str)
     if not (errors or warnings):
         await check_success.success(
-            "Path structure and naming conventions conform to policy", {}, path=relative_path_str
+            "Path structure and naming conventions conform to policy", {}, primary_rel_path=relative_path_str
         )
 
 
@@ -154,22 +153,36 @@ async def check(args: Check) -> None:
     # We refer to the following authoritative policies:
     # - Release Creation Process (RCP)
     # - Release Distribution Policy (RDP)
-    base_path = pathlib.Path(args.base_release_dir)
-
-    if not await aiofiles.os.path.isdir(base_path):
-        _LOGGER.error("Base release directory does not exist or is not a directory: %s", base_path)
-        return
 
     check_errors = await checks.Check.create(
-        checker=checks.function_key(check) + "_errors", release_name=args.release_name, path=None, afresh=True
+        checker=checks.function_key(check) + "_errors",
+        release_name=args.release_name,
+        primary_rel_path=None,
+        afresh=True,
     )
     check_warnings = await checks.Check.create(
-        checker=checks.function_key(check) + "_warnings", release_name=args.release_name, path=None, afresh=True
+        checker=checks.function_key(check) + "_warnings",
+        release_name=args.release_name,
+        primary_rel_path=None,
+        afresh=True,
     )
     check_success = await checks.Check.create(
-        checker=checks.function_key(check) + "_success", release_name=args.release_name, path=None, afresh=True
+        checker=checks.function_key(check) + "_success",
+        release_name=args.release_name,
+        primary_rel_path=None,
+        afresh=True,
     )
+
+    # As primary_rel_path is None, the base path is the release candidate draft directory
+    if not (base_path := await check_success.abs_path()):
+        return
+
+    if not await aiofiles.os.path.isdir(base_path):
+        _LOGGER.error("Base release directory does not exist or is not a directory: %s", base_path)
+        return
+
     relative_paths = await util.paths_recursive(base_path)
+    relative_paths_set = set(str(p) for p in relative_paths)
     for relative_path in relative_paths:
         # Delegate processing of each path to the helper function
         await _check_path_process_single(
@@ -178,7 +191,7 @@ async def check(args: Check) -> None:
             check_errors,
             check_warnings,
             check_success,
-            set(str(p) for p in relative_paths),
+            relative_paths_set,
         )
 
     return None
diff --git a/atr/tasks/checks/rat.py b/atr/tasks/checks/rat.py
index d272a3d..bcab4e8 100644
--- a/atr/tasks/checks/rat.py
+++ b/atr/tasks/checks/rat.py
@@ -48,7 +48,7 @@ class Check(pydantic.BaseModel):
     """Parameters for Apache RAT license checking."""
 
     release_name: str = pydantic.Field(..., description="Release name")
-    abs_path: str = pydantic.Field(..., description="Absolute path to the .tar.gz file to check")
+    primary_rel_path: str = pydantic.Field(..., description="Relative path to the archive file to check")
     rat_jar_path: str = pydantic.Field(
         default=_CONFIG.APACHE_RAT_JAR_PATH, description="Path to the Apache RAT JAR file"
     )
@@ -61,14 +61,18 @@ class Check(pydantic.BaseModel):
 @checks.with_model(Check)
 async def check(args: Check) -> str | None:
     """Use Apache RAT to check the licenses of the files in the artifact."""
-    rel_path = checks.rel_path(args.abs_path)
-    check_instance = await checks.Check.create(checker=check, release_name=args.release_name, path=rel_path)
-    _LOGGER.info(f"Checking RAT licenses for {args.abs_path} (rel: {rel_path})")
+    check_obj = await checks.Check.create(
+        checker=check, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
+    if not (artifact_abs_path := await check_obj.abs_path()):
+        return None
+
+    _LOGGER.info(f"Checking RAT licenses for {artifact_abs_path} (rel: {args.primary_rel_path})")
 
     try:
         result_data = await asyncio.to_thread(
             _check_core_logic,
-            artifact_path=args.abs_path,
+            artifact_path=str(artifact_abs_path),
             rat_jar_path=args.rat_jar_path,
             max_extract_size=args.max_extract_size,
             chunk_size=args.chunk_size,
@@ -76,17 +80,17 @@ async def check(args: Check) -> str | None:
 
         if result_data.get("error"):
             # Handle errors from within the core logic
-            await check_instance.failure(result_data["message"], result_data)
+            await check_obj.failure(result_data["message"], result_data)
         elif not result_data["valid"]:
             # Handle RAT validation failures
-            await check_instance.failure(result_data["message"], result_data)
+            await check_obj.failure(result_data["message"], result_data)
         else:
             # Handle success
-            await check_instance.success(result_data["message"], result_data)
+            await check_obj.success(result_data["message"], result_data)
 
     except Exception as e:
         # TODO: Or bubble for task failure?
-        await check_instance.exception("Error running Apache RAT check", {"error": str(e)})
+        await check_obj.failure("Error running Apache RAT check", {"error": str(e)})
 
     return None
 
diff --git a/atr/tasks/checks/signature.py b/atr/tasks/checks/signature.py
index 83d0624..8f822bc 100644
--- a/atr/tasks/checks/signature.py
+++ b/atr/tasks/checks/signature.py
@@ -20,7 +20,6 @@ import logging
 import tempfile
 from typing import Any, Final
 
-import aiofiles.os
 import gnupg
 import pydantic
 import sqlmodel
@@ -28,7 +27,6 @@ import sqlmodel
 import atr.db as db
 import atr.db.models as models
 import atr.tasks.checks as checks
-import atr.util as util
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -38,43 +36,44 @@ class Check(pydantic.BaseModel):
 
     release_name: str = pydantic.Field(..., description="Release name")
     committee_name: str = pydantic.Field(..., description="Name of the committee whose keys should be used")
-    abs_signature_path: str = pydantic.Field(..., description="Absolute path to the signature file (.asc)")
+    signature_rel_path: str = pydantic.Field(..., description="Relative path to the signature file (.asc)")
 
 
 @checks.with_model(Check)
 async def check(args: Check) -> str | None:
     """Check a signature file."""
-    rel_path = checks.rel_path(args.abs_signature_path)
-    check_instance = await checks.Check.create(checker=check, release_name=args.release_name, path=rel_path)
-
-    # Get the artifact path
-    artifact_path = args.abs_signature_path.removesuffix(".asc")
-    abs_artifact_path = str(util.get_release_candidate_draft_dir() / artifact_path)
-    if not (await aiofiles.os.path.exists(abs_artifact_path)):
-        await check_instance.failure(f"Artifact {abs_artifact_path} does not exist", {})
+    check_obj = await checks.Check.create(
+        checker=check, release_name=args.release_name, primary_rel_path=args.signature_rel_path
+    )
+
+    if not (signature_abs_path := await check_obj.abs_path()):
+        return None
+
+    artifact_rel_path = args.signature_rel_path.removesuffix(".asc")
+    if not (artifact_abs_path := await check_obj.abs_path(artifact_rel_path)):
         return None
 
     _LOGGER.info(
-        f"Checking signature {args.abs_signature_path} for {abs_artifact_path}"
-        f" using {args.committee_name} keys (rel: {rel_path})"
+        f"Checking signature {signature_abs_path} for {artifact_abs_path}"
+        f" using {args.committee_name} keys (rel: {args.signature_rel_path})"
     )
 
     try:
         result_data = await _check_core_logic(
             committee_name=args.committee_name,
-            artifact_path=abs_artifact_path,
-            signature_path=args.abs_signature_path,
+            artifact_path=str(artifact_abs_path),
+            signature_path=str(signature_abs_path),
         )
         if result_data.get("error"):
-            await check_instance.failure(result_data["error"], result_data)
+            await check_obj.failure(result_data["error"], result_data)
         elif result_data.get("verified"):
-            await check_instance.success("Signature verified successfully", result_data)
+            await check_obj.success("Signature verified successfully", result_data)
         else:
             # Shouldn't happen
-            await check_instance.exception("Signature verification failed for unknown reasons", result_data)
+            await check_obj.failure("Signature verification failed for unknown reasons", result_data)
 
     except Exception as e:
-        await check_instance.exception("Error during signature check execution", {"error": str(e)})
+        await check_obj.failure("Error during signature check execution", {"error": str(e)})
 
     return None
 
diff --git a/atr/tasks/checks/targz.py b/atr/tasks/checks/targz.py
index 2c152ca..7cfe019 100644
--- a/atr/tasks/checks/targz.py
+++ b/atr/tasks/checks/targz.py
@@ -17,7 +17,6 @@
 
 import asyncio
 import logging
-import os.path
 import tarfile
 from typing import Final
 
@@ -32,35 +31,48 @@ class Integrity(pydantic.BaseModel):
     """Parameters for archive integrity checking."""
 
     release_name: str = pydantic.Field(..., description="Release name")
-    abs_path: str = pydantic.Field(..., description="Absolute path to the .tar.gz file to check")
+    primary_rel_path: str = pydantic.Field(..., description="Relative path to the .tar.gz file to check")
     chunk_size: int = pydantic.Field(default=4096, description="Size of chunks to read when checking the file")
 
 
 @checks.with_model(Integrity)
 async def integrity(args: Integrity) -> str | None:
     """Check the integrity of a .tar.gz file."""
-    rel_path = checks.rel_path(args.abs_path)
-    _LOGGER.info(f"ABS, REL: {args.abs_path} {rel_path}")
-    check = await checks.Check.create(checker=integrity, release_name=args.release_name, path=rel_path)
+    check = await checks.Check.create(
+        checker=integrity, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
+
+    _LOGGER.info(f"Checking integrity for {artifact_abs_path} (rel: {args.primary_rel_path})")
+
     try:
-        size = await asyncio.to_thread(_integrity_core, args.abs_path, args.chunk_size)
+        size = await asyncio.to_thread(_integrity_core, str(artifact_abs_path), args.chunk_size)
         await check.success("Able to read all entries of the archive using tarfile", {"size": size})
     except Exception as e:
         await check.failure("Unable to read all entries of the archive using tarfile", {"error": str(e)})
     return None
 
 
-@checks.with_model(checks.ReleaseAndAbsPath)
-async def structure(args: checks.ReleaseAndAbsPath) -> str | None:
+@checks.with_model(checks.ReleaseAndRelPath)
+async def structure(args: checks.ReleaseAndRelPath) -> str | None:
     """Check the structure of a .tar.gz file."""
-    rel_path = checks.rel_path(args.abs_path)
-    check = await checks.Check.create(checker=structure, release_name=args.release_name, path=rel_path)
-    filename = os.path.basename(args.abs_path)
-    expected_root: Final[str] = os.path.splitext(os.path.splitext(filename)[0])[0]
-    _LOGGER.info(f"Checking structure for {args.abs_path} (expected root: {expected_root})")
+    check = await checks.Check.create(
+        checker=structure, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
+
+    filename = artifact_abs_path.name
+    expected_root: Final[str] = (
+        filename.removesuffix(".tar.gz") if filename.endswith(".tar.gz") else filename.removesuffix(".tgz")
+    )
+    _LOGGER.info(
+        f"Checking structure for {artifact_abs_path} (expected root: {expected_root}) (rel: {args.primary_rel_path})"
+    )
 
     try:
-        root = await asyncio.to_thread(root_directory, args.abs_path)
+        root = await asyncio.to_thread(root_directory, str(artifact_abs_path))
         if root == expected_root:
             await check.success(
                 "Archive contains exactly one root directory matching the expected name",
diff --git a/atr/tasks/checks/zipformat.py b/atr/tasks/checks/zipformat.py
index b47c9ee..dbaffc7 100644
--- a/atr/tasks/checks/zipformat.py
+++ b/atr/tasks/checks/zipformat.py
@@ -27,41 +27,47 @@ import atr.tasks.checks.license as license
 _LOGGER = logging.getLogger(__name__)
 
 
-@checks.with_model(checks.ReleaseAndAbsPath)
-async def integrity(args: checks.ReleaseAndAbsPath) -> str | None:
+@checks.with_model(checks.ReleaseAndRelPath)
+async def integrity(args: checks.ReleaseAndRelPath) -> str | None:
     """Check that the zip archive is not corrupted and can be opened."""
-    rel_path = checks.rel_path(args.abs_path)
-    check_instance = await checks.Check.create(checker=integrity, release_name=args.release_name, path=rel_path)
-    _LOGGER.info(f"Checking zip integrity for {args.abs_path} (rel: {rel_path})")
+    check = await checks.Check.create(
+        checker=integrity, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
+
+    _LOGGER.info(f"Checking zip integrity for {artifact_abs_path} (rel: {args.primary_rel_path})")
 
     try:
-        result_data = await asyncio.to_thread(_integrity_check_core_logic, args.abs_path)
+        result_data = await asyncio.to_thread(_integrity_check_core_logic, str(artifact_abs_path))
         if result_data.get("error"):
-            await check_instance.failure(result_data["error"], result_data)
+            await check.failure(result_data["error"], result_data)
         else:
-            await check_instance.success(
-                f"Zip archive integrity OK ({result_data['member_count']} members)", result_data
-            )
+            await check.success(f"Zip archive integrity OK ({result_data['member_count']} members)", result_data)
     except Exception as e:
-        await check_instance.exception("Error checking zip integrity", {"error": str(e)})
+        await check.failure("Error checking zip integrity", {"error": str(e)})
 
     return None
 
 
-@checks.with_model(checks.ReleaseAndAbsPath)
-async def license_files(args: checks.ReleaseAndAbsPath) -> str | None:
+@checks.with_model(checks.ReleaseAndRelPath)
+async def license_files(args: checks.ReleaseAndRelPath) -> str | None:
     """Check that the LICENSE and NOTICE files exist and are valid within the zip."""
-    rel_path = checks.rel_path(args.abs_path)
-    check_instance = await checks.Check.create(checker=license_files, release_name=args.release_name, path=rel_path)
-    _LOGGER.info(f"Checking zip license files for {args.abs_path} (rel: {rel_path})")
+    check = await checks.Check.create(
+        checker=license_files, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
+
+    _LOGGER.info(f"Checking zip license files for {artifact_abs_path} (rel: {args.primary_rel_path})")
 
     try:
-        result_data = await asyncio.to_thread(_license_files_check_core_logic_zip, args.abs_path)
+        result_data = await asyncio.to_thread(_license_files_check_core_logic_zip, str(artifact_abs_path))
 
         if result_data.get("error"):
-            await check_instance.failure(result_data["error"], result_data)
+            await check.failure(result_data["error"], result_data)
         elif result_data.get("license_valid") and result_data.get("notice_valid"):
-            await check_instance.success("LICENSE and NOTICE files present and valid in zip", result_data)
+            await check.success("LICENSE and NOTICE files present and valid in zip", result_data)
         else:
             issues = []
             if not result_data.get("license_found"):
@@ -73,56 +79,64 @@ async def license_files(args: checks.ReleaseAndAbsPath) -> str | None:
             elif not result_data.get("notice_valid"):
                 issues.append("NOTICE invalid or empty")
             issue_str = ", ".join(issues) if issues else "Issues found with LICENSE or NOTICE files"
-            await check_instance.failure(issue_str, result_data)
+            await check.failure(issue_str, result_data)
 
     except Exception as e:
-        await check_instance.exception("Error checking zip license files", {"error": str(e)})
+        await check.failure("Error checking zip license files", {"error": str(e)})
 
     return None
 
 
-@checks.with_model(checks.ReleaseAndAbsPath)
-async def license_headers(args: checks.ReleaseAndAbsPath) -> str | None:
+@checks.with_model(checks.ReleaseAndRelPath)
+async def license_headers(args: checks.ReleaseAndRelPath) -> str | None:
     """Check that all source files within the zip have valid license headers."""
-    rel_path = checks.rel_path(args.abs_path)
-    check_instance = await checks.Check.create(checker=license_headers, release_name=args.release_name, path=rel_path)
-    _LOGGER.info(f"Checking zip license headers for {args.abs_path} (rel: {rel_path})")
+    check = await checks.Check.create(
+        checker=license_headers, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
+
+    _LOGGER.info(f"Checking zip license headers for {artifact_abs_path} (rel: {args.primary_rel_path})")
 
     try:
-        result_data = await asyncio.to_thread(_license_headers_check_core_logic_zip, args.abs_path)
+        result_data = await asyncio.to_thread(_license_headers_check_core_logic_zip, str(artifact_abs_path))
 
         if result_data.get("error_message"):
-            await check_instance.failure(result_data["error_message"], result_data)
+            await check.failure(result_data["error_message"], result_data)
         elif not result_data.get("valid"):
             num_issues = len(result_data.get("files_without_headers", []))
             failure_msg = f"{num_issues} file(s) missing or having invalid license headers"
-            await check_instance.failure(failure_msg, result_data)
+            await check.failure(failure_msg, result_data)
         else:
-            await check_instance.success(
+            await check.success(
                 f"License headers OK ({result_data.get('files_checked', 0)} files checked)", result_data
             )
 
     except Exception as e:
-        await check_instance.exception("Error checking zip license headers", {"error": str(e)})
+        await check.failure("Error checking zip license headers", {"error": str(e)})
 
     return None
 
 
-@checks.with_model(checks.ReleaseAndAbsPath)
-async def structure(args: checks.ReleaseAndAbsPath) -> str | None:
+@checks.with_model(checks.ReleaseAndRelPath)
+async def structure(args: checks.ReleaseAndRelPath) -> str | None:
     """Check that the zip archive has a single root directory matching the artifact name."""
-    rel_path = checks.rel_path(args.abs_path)
-    check_instance = await checks.Check.create(checker=structure, release_name=args.release_name, path=rel_path)
-    _LOGGER.info(f"Checking zip structure for {args.abs_path} (rel: {rel_path})")
+    check = await checks.Check.create(
+        checker=structure, release_name=args.release_name, primary_rel_path=args.primary_rel_path
+    )
+    if not (artifact_abs_path := await check.abs_path()):
+        return None
+
+    _LOGGER.info(f"Checking zip structure for {artifact_abs_path} (rel: {args.primary_rel_path})")
 
     try:
-        result_data = await asyncio.to_thread(_structure_check_core_logic, args.abs_path)
+        result_data = await asyncio.to_thread(_structure_check_core_logic, str(artifact_abs_path))
         if result_data.get("error"):
-            await check_instance.failure(result_data["error"], result_data)
+            await check.failure(result_data["error"], result_data)
         else:
-            await check_instance.success(f"Zip structure OK (root: {result_data['root_dir']})", result_data)
+            await check.success(f"Zip structure OK (root: {result_data['root_dir']})", result_data)
     except Exception as e:
-        await check_instance.exception("Error checking zip structure", {"error": str(e)})
+        await check.failure("Error checking zip structure", {"error": str(e)})
 
     return None
 
diff --git a/atr/templates/draft-review.html b/atr/templates/draft-review.html
index 0945b39..2d5f2d0 100644
--- a/atr/templates/draft-review.html
+++ b/atr/templates/draft-review.html
@@ -96,7 +96,7 @@
                        class="btn btn-sm btn-outline-primary fs-6">Download</a>
                     <a href="{{ as_url(routes.draft.tools, project_name=project_name, version_name=version_name, file_path=path) }}"
                        class="btn btn-sm btn-outline-primary fs-6 small ms-2">Tools</a>
-                    <a href="{{ as_url(routes.draft.review_path, project_name=project_name, version_name=version_name, file_path=path) }}"
+                    <a href="{{ as_url(routes.draft.review_path, project_name=project_name, version_name=version_name, rel_path=path) }}"
                        class="btn btn-sm btn-outline-primary fs-6 small ms-2">Review file</a>
                     <button class="btn btn-sm btn-outline-danger ms-2"
                             data-bs-toggle="modal"
diff --git a/atr/util.py b/atr/util.py
index 9a87374..81541f0 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -101,16 +101,16 @@ class QuartFormTyped(quart_wtf.QuartForm):
         return form
 
 
-def abs_path_to_release_and_rel_path(abs_path: str) -> tuple[str, str]:
-    """Return the release name and relative path for a given path."""
-    conf = config.get()
-    phase_dir = pathlib.Path(conf.PHASE_STORAGE_DIR)
-    phase_sub_dir = pathlib.Path(abs_path).relative_to(phase_dir)
-    # Skip the first component, which is the phase name
-    # The next two components are the project name and version name
-    project_name = phase_sub_dir.parts[1]
-    version_name = phase_sub_dir.parts[2]
-    return models.release_name(project_name, version_name), str(pathlib.Path(*phase_sub_dir.parts[3:]))
+# def abs_path_to_release_and_rel_path(abs_path: str) -> tuple[str, str]:
+#     """Return the release name and relative path for a given path."""
+#     conf = config.get()
+#     phase_dir = pathlib.Path(conf.PHASE_STORAGE_DIR)
+#     phase_sub_dir = pathlib.Path(abs_path).relative_to(phase_dir)
+#     # Skip the first component, which is the phase name
+#     # The next two components are the project name and version name
+#     project_name = phase_sub_dir.parts[1]
+#     version_name = phase_sub_dir.parts[2]
+#     return models.release_name(project_name, version_name), str(pathlib.Path(*phase_sub_dir.parts[3:]))
 
 
 def as_url(func: Callable, **kwargs: Any) -> str:


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org