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