This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit 5b8a282d676e94f7623e38ceb1e833a78cfb9045
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Feb 10 17:30:59 2026 +0000

    Add an extra file of attestable JSON data mapping paths to hashes
---
 atr/attestable.py        | 59 ++++++++++++++++++++++++++++++++++++++++++++++++
 atr/models/attestable.py |  5 ++++
 atr/server.py            |  5 ++++
 3 files changed, 69 insertions(+)

diff --git a/atr/attestable.py b/atr/attestable.py
index d576236..f487d9e 100644
--- a/atr/attestable.py
+++ b/atr/attestable.py
@@ -39,6 +39,10 @@ def attestable_path(project_name: str, version_name: str, 
revision_number: str)
     return util.get_attestable_dir() / project_name / version_name / 
f"{revision_number}.json"
 
 
+def attestable_paths_path(project_name: str, version_name: str, 
revision_number: str) -> pathlib.Path:
+    return util.get_attestable_dir() / project_name / version_name / 
f"{revision_number}.paths.json"
+
+
 async def compute_file_hash(path: pathlib.Path) -> str:
     hasher = blake3.blake3()
     async with aiofiles.open(path, "rb") as f:
@@ -75,6 +79,58 @@ async def load(
         return None
 
 
+async def load_paths(
+    project_name: str,
+    version_name: str,
+    revision_number: str,
+) -> dict[str, str] | None:
+    file_path = attestable_paths_path(project_name, version_name, 
revision_number)
+    if await aiofiles.os.path.isfile(file_path):
+        try:
+            async with aiofiles.open(file_path, encoding="utf-8") as f:
+                data = json.loads(await f.read())
+            return models.AttestablePathsV1.model_validate(data).paths
+        except (json.JSONDecodeError, pydantic.ValidationError) as e:
+            # log.warning(f"Could not parse {file_path}, trying combined file: 
{e}")
+            log.warning(f"Could not parse {file_path}: {e}")
+    # combined = await load(project_name, version_name, revision_number)
+    # if combined is not None:
+    #     return combined.paths
+    return None
+
+
+def migrate_to_paths_files() -> int:
+    attestable_dir = util.get_attestable_dir()
+    if not attestable_dir.is_dir():
+        return 0
+    count = 0
+    for project_dir in sorted(attestable_dir.iterdir()):
+        if not project_dir.is_dir():
+            continue
+        for version_dir in sorted(project_dir.iterdir()):
+            if not version_dir.is_dir():
+                continue
+            for json_file in sorted(version_dir.glob("*.json")):
+                if "." in json_file.stem:
+                    continue
+                target = version_dir / f"{json_file.stem}.paths.json"
+                if target.exists():
+                    continue
+                try:
+                    with open(json_file, encoding="utf-8") as f:
+                        data = json.loads(f.read())
+                    validated = models.AttestableV1.model_validate(data)
+                    paths_result = 
models.AttestablePathsV1(paths=validated.paths)
+                    tmp = target.with_suffix(".tmp")
+                    with open(tmp, "w", encoding="utf-8") as f:
+                        f.write(paths_result.model_dump_json(indent=2))
+                    tmp.replace(target)
+                    count += 1
+                except (json.JSONDecodeError, pydantic.ValidationError):
+                    continue
+    return count
+
+
 async def write(
     release_directory: pathlib.Path,
     project_name: str,
@@ -89,6 +145,9 @@ async def write(
     result = await _generate(release_directory, revision_number, uploader_uid, 
previous)
     file_path = attestable_path(project_name, version_name, revision_number)
     await util.atomic_write_file(file_path, result.model_dump_json(indent=2))
+    paths_result = models.AttestablePathsV1(paths=result.paths)
+    paths_file_path = attestable_paths_path(project_name, version_name, 
revision_number)
+    await util.atomic_write_file(paths_file_path, 
paths_result.model_dump_json(indent=2))
 
 
 def _compute_hashes_with_attribution(
diff --git a/atr/models/attestable.py b/atr/models/attestable.py
index 3cff655..4bc574b 100644
--- a/atr/models/attestable.py
+++ b/atr/models/attestable.py
@@ -27,6 +27,11 @@ class HashEntry(schema.Strict):
     uploaders: list[Annotated[tuple[str, str], 
pydantic.BeforeValidator(tuple)]]
 
 
+class AttestablePathsV1(schema.Strict):
+    version: Literal[1] = 1
+    paths: dict[str, str] = schema.factory(dict)
+
+
 class AttestableV1(schema.Strict):
     version: Literal[1] = 1
     paths: dict[str, str] = schema.factory(dict)
diff --git a/atr/server.py b/atr/server.py
index 2abff42..c09357c 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -46,6 +46,7 @@ import quart_wtf
 import werkzeug.routing as routing
 
 import atr
+import atr.attestable as attestable
 import atr.blueprints as blueprints
 import atr.cache as cache
 import atr.config as config
@@ -260,6 +261,10 @@ def _app_setup_lifecycle(app: base.QuartApp, app_config: 
type[config.AppConfig])
 
         await asyncio.to_thread(_set_file_permissions_to_read_only)
 
+        migrated = await asyncio.to_thread(attestable.migrate_to_paths_files)
+        if migrated > 0:
+            log.info(f"Migrated {migrated} attestable files to paths format")
+
         await cache.admins_startup_load()
         admins_task = asyncio.create_task(cache.admins_refresh_loop())
         app.extensions["admins_task"] = admins_task


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

Reply via email to