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 2026/07/14 15:10:40 UTC

(tooling-trusted-releases) branch sbp updated (4c1f99d1 -> 2f2d1e78)

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

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


    from 4c1f99d1 Use the policy vote duration in the API by default
     add e00c80a5 #1363 - change email content for watched vs announced
     add 8fc2a43f #1400 - classify SBOMs as SBOMs
     add ef5a1496 #1370 - support import and export of PMC CSVs
     new 2f2d1e78 Allow SHA-512 files to be generated without signatures

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 atr/admin/__init__.py                           | 233 +++++++++++
 atr/admin/templates/catalog-committee.html      |   1 +
 atr/admin/templates/catalog-import-preview.html |  12 +
 atr/admin/templates/catalog-import.html         |  59 +++
 atr/analysis.py                                 |  20 +-
 atr/classify.py                                 |  58 ++-
 atr/construct.py                                |  11 +-
 atr/get/checks.py                               |   2 +
 atr/models/attestable.py                        |   1 +
 atr/post/draft.py                               |  10 +-
 atr/server.py                                   |   3 +
 atr/shared/catalogue_diff.py                    | 491 ++++++++++++++++++++++
 atr/shared/catalogue_import.py                  | 318 +++++++++++++++
 atr/shared/catalogue_rows.py                    | 138 +++++++
 atr/shared/repoint.py                           |   6 +-
 atr/storage/writers/announce.py                 |   2 +-
 atr/storage/writers/catalogue.py                | 299 ++++++++++++--
 atr/storage/writers/release.py                  |  60 ++-
 atr/storage/writers/revision.py                 |   2 -
 atr/svn/catalog.py                              |   3 +-
 atr/templates/check-selected-path-table.html    |   4 +-
 atr/templates/draft-tools.html                  |   8 +-
 atr/templates/includes/topnav.html              |   5 +
 tests/unit/test_admin_catalog.py                |  63 +--
 tests/unit/test_catalogue_diff.py               | 458 +++++++++++++++++++++
 tests/unit/test_catalogue_import_apply.py       | 514 ++++++++++++++++++++++++
 tests/unit/test_classify.py                     |  45 ++-
 tests/unit/test_release_notification.py         |  22 +-
 tests/unit/test_release_writer_hashgen.py       |  95 +++--
 29 files changed, 2761 insertions(+), 182 deletions(-)
 create mode 100644 atr/admin/templates/catalog-import-preview.html
 create mode 100644 atr/admin/templates/catalog-import.html
 create mode 100644 atr/shared/catalogue_diff.py
 create mode 100644 atr/shared/catalogue_import.py
 create mode 100644 atr/shared/catalogue_rows.py
 create mode 100644 tests/unit/test_catalogue_diff.py
 create mode 100644 tests/unit/test_catalogue_import_apply.py


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


(tooling-trusted-releases) 01/01: Allow SHA-512 files to be generated without signatures

Posted by sb...@apache.org.
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 2f2d1e786a4ddd3aabb6d7970a4fd1b5e54d35b4
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue Jul 14 16:10:14 2026 +0100

    Allow SHA-512 files to be generated without signatures
---
 atr/models/attestable.py                  |  1 +
 atr/storage/writers/release.py            | 58 +++++++------------
 atr/storage/writers/revision.py           |  2 -
 atr/templates/draft-tools.html            |  6 +-
 tests/unit/test_release_writer_hashgen.py | 95 ++++++++++++++++++-------------
 5 files changed, 82 insertions(+), 80 deletions(-)

diff --git a/atr/models/attestable.py b/atr/models/attestable.py
index b9483dbb..8ea29432 100644
--- a/atr/models/attestable.py
+++ b/atr/models/attestable.py
@@ -54,6 +54,7 @@ class HashEntryV2(schema.Strict):
 
 
 class GeneratorV2(enum.Enum):
+    SHA512_FROM_CONTENT = "SHA512_from_content"
     SHA512_FROM_SIGNATURE = "SHA512_from_signature"
 
 
diff --git a/atr/storage/writers/release.py b/atr/storage/writers/release.py
index 84329abc..249721a1 100644
--- a/atr/storage/writers/release.py
+++ b/atr/storage/writers/release.py
@@ -246,9 +246,9 @@ async def _signature_provenance_metadata_for(
     version_key: safe.VersionKey,
     parent_revision: sql.Revision | None,
     signature_rel_path: pathlib.Path,
-) -> dict[str, str]:
+) -> dict[str, str] | None:
     if parent_revision is None:
-        raise datatypes.FailedError("SHA512 generation requires a parent revision with a verified signature.")
+        return None
     release_key = sql.release_key(str(project_key), str(version_key))
     parent_revision_number = parent_revision.safe_number
     parent_number = str(parent_revision_number)
@@ -263,13 +263,10 @@ async def _signature_provenance_metadata_for(
     )
     if not check_results:
         log.info(
-            "SHA512 generation is waiting for signature verification"
+            "SHA512 generation proceeding without signature provenance"
             f" release={release_key} revision={parent_number} path={signature_rel_str}"
         )
-        raise datatypes.FailedError(
-            "Signature verification has not completed yet for"
-            f" {signature_rel_path.name}. Please wait for checks to finish and try again."
-        )
+        return None
     latest = check_results[0]
     latest_message = latest.message.strip() if isinstance(getattr(latest, "message", None), str) else None
     log.info(
@@ -279,14 +276,7 @@ async def _signature_provenance_metadata_for(
         f" status={latest.status.value} message={latest_message!r}"
     )
     if latest.status != sql.CheckResultStatus.NOTE:
-        if latest_message:
-            raise datatypes.FailedError(
-                f"Signature verification for {signature_rel_path.name} failed: {latest_message}"
-            )
-        raise datatypes.FailedError(
-            "Signature verification for"
-            f" {signature_rel_path.name} is {latest.status.value}; cannot generate the SHA512 yet."
-        )
+        return None
     payload = latest.data if isinstance(latest.data, dict) else {}
     metadata = {"signature_path": signature_rel_str}
     for key in ("fingerprint", "key_id", "timestamp", "username"):
@@ -674,35 +664,31 @@ class CommitteeParticipant(FoundationCommitter):
             if await aiofiles.os.path.exists(hash_path_in_new_revision):
                 raise storage.AccessError("SHA512 file already exists", status=409)
 
-            if not await aiofiles.os.path.exists(signature_path_in_new_revision):
-                raise datatypes.FailedError(
-                    "SHA512 generation requires a detached OpenPGP signature at"
-                    f" {signature_rel_path!s}. Please upload the signature first."
+            signature_metadata = None
+            if await aiofiles.os.path.exists(signature_path_in_new_revision):
+                signature_metadata = await _signature_provenance_metadata_for(
+                    project_key=project_key,
+                    version_key=version_key,
+                    parent_revision=old_rev,
+                    signature_rel_path=signature_rel_path,
                 )
 
-            signature_metadata = await _signature_provenance_metadata_for(
-                project_key=project_key,
-                version_key=version_key,
-                parent_revision=old_rev,
-                signature_rel_path=signature_rel_path,
-            )
-
             hash_value, source_content_hash = await hashes.compute_sha512_and_content_hash(path_in_new_revision.path)
 
             async with aiofiles.open(hash_path_in_new_revision, "w") as f:
                 await f.write(f"{hash_value}  {rel_path.name}\n")
 
             generated_rel = safe.RelPath(str(rel_path.parent / hash_path_rel_name))
-            provenance = attestable.ProvenanceV2(
-                generator=attestable.GeneratorV2.SHA512_FROM_SIGNATURE,
-                metadata={
-                    "initiated_by": self.__asf_uid,
-                    "signature_path": signature_metadata["signature_path"],
-                    "source_content_hashes": {str(rel_path): source_content_hash},
-                    "source_paths": [str(rel_path)],
-                    **{key: value for key, value in signature_metadata.items() if (key != "signature_path")},
-                },
-            )
+            generator = attestable.GeneratorV2.SHA512_FROM_CONTENT
+            metadata: dict[str, Any] = {
+                "initiated_by": self.__asf_uid,
+                "source_content_hashes": {str(rel_path): source_content_hash},
+                "source_paths": [str(rel_path)],
+            }
+            if signature_metadata is not None:
+                generator = attestable.GeneratorV2.SHA512_FROM_SIGNATURE
+                metadata.update(signature_metadata)
+            provenance = attestable.ProvenanceV2(generator=generator, metadata=metadata)
             return {generated_rel: provenance}
 
         await self.__write_as.revision.create_revision_with_quarantine(
diff --git a/atr/storage/writers/revision.py b/atr/storage/writers/revision.py
index ca8f8a7a..6102f5d0 100644
--- a/atr/storage/writers/revision.py
+++ b/atr/storage/writers/revision.py
@@ -334,8 +334,6 @@ async def _verify_provenance_sources_unchanged(
     # This function checks input file inodes to ensure that they match
     # If they don't match, we raise an error
     for generated_rel, provenance_entry in path_provenance.items():
-        if provenance_entry.generator != atr.models.attestable.GeneratorV2.SHA512_FROM_SIGNATURE:
-            continue
         dependency_paths = [path for path in provenance_entry.metadata.get("source_paths", []) if isinstance(path, str)]
         if (signature_path := provenance_entry.metadata.get("signature_path")) is not None:
             if isinstance(signature_path, str) and (signature_path not in dependency_paths):
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index d2f434e2..0b2155a9 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -24,9 +24,9 @@
   <h2>Tools</h2>
   <h3>Generate hash files</h3>
   <p>
-    Generate an SHA512 hash file for this file. ATR requires a detached OpenPGP
-    signature (<code>.asc</code>) alongside the file, and the signature check must
-    have completed successfully, before the SHA512 can be generated.
+    Generate an SHA512 hash file for this file. If a detached OpenPGP signature
+    (<code>.asc</code>) is present alongside the file and its check has completed
+    successfully, the signature is recorded in the provenance of the SHA512 file.
   </p>
   <div class="d-flex gap-2 mb-4">
     {{ sha512_form|safe }}
diff --git a/tests/unit/test_release_writer_hashgen.py b/tests/unit/test_release_writer_hashgen.py
index 5a6ddfb4..84908aa7 100644
--- a/tests/unit/test_release_writer_hashgen.py
+++ b/tests/unit/test_release_writer_hashgen.py
@@ -25,7 +25,6 @@ import pytest
 import atr.models.attestable as attestable
 import atr.models.safe as safe
 import atr.models.sql as sql
-import atr.storage.datatypes as datatypes
 import atr.storage.writers.release as release
 
 
@@ -129,11 +128,12 @@ async def test_generate_hash_file_creates_hash_file_and_returns_provenance(tmp_p
 
 
 @pytest.mark.asyncio
-async def test_generate_hash_file_requires_signature_file(tmp_path: pathlib.Path):
+async def test_generate_hash_file_without_signature_file(tmp_path: pathlib.Path):
     rel_path = pathlib.Path("artifact.tar.gz")
     (tmp_path / rel_path).write_bytes(b"payload")
     state_path = safe.StatePath(tmp_path)
     old_revision = SimpleNamespace(safe_number=safe.RevisionNumber("00001"))
+    captured: dict[str, object] = {}
 
     async def create_revision_with_quarantine(
         project_key: safe.ProjectKey,
@@ -146,23 +146,40 @@ async def test_generate_hash_file_requires_signature_file(tmp_path: pathlib.Path
     ) -> None:
         if modify is None:
             raise RuntimeError("Expected modify callback")
-        await modify(state_path, old_revision)
+        captured["path_provenance"] = await modify(state_path, old_revision)
 
     participant = _make_participant(create_revision_with_quarantine)
 
-    with mock.patch.object(
-        release,
-        "_signature_provenance_metadata_for",
-        new=mock.AsyncMock(),
-    ) as signature_metadata_mock:
-        with pytest.raises(datatypes.FailedError, match=r"requires a detached OpenPGP signature"):
-            await participant.generate_hash_file(
-                safe.ProjectKey("proj"),
-                safe.VersionKey("1.0"),
-                rel_path,
-            )
+    with (
+        mock.patch.object(
+            release,
+            "_signature_provenance_metadata_for",
+            new=mock.AsyncMock(),
+        ) as signature_metadata_mock,
+        mock.patch.object(
+            release.hashes,
+            "compute_sha512_and_content_hash",
+            new=mock.AsyncMock(return_value=("sha512hex", "blake3:source")),
+        ),
+    ):
+        await participant.generate_hash_file(
+            safe.ProjectKey("proj"),
+            safe.VersionKey("1.0"),
+            rel_path,
+        )
 
     assert signature_metadata_mock.await_count == 0
+    assert (tmp_path / "artifact.tar.gz.sha512").read_text() == "sha512hex  artifact.tar.gz\n"
+    assert captured["path_provenance"] == {
+        safe.RelPath("artifact.tar.gz.sha512"): attestable.ProvenanceV2(
+            generator=attestable.GeneratorV2.SHA512_FROM_CONTENT,
+            metadata={
+                "initiated_by": "alice",
+                "source_content_hashes": {"artifact.tar.gz": "blake3:source"},
+                "source_paths": ["artifact.tar.gz"],
+            },
+        )
+    }
 
 
 @pytest.mark.asyncio
@@ -203,7 +220,7 @@ async def test_signature_provenance_metadata_for_filters_unknown_fields():
 
 
 @pytest.mark.asyncio
-async def test_signature_provenance_metadata_for_requires_completed_check():
+async def test_signature_provenance_metadata_for_without_completed_check():
     parent_revision = SimpleNamespace(safe_number=safe.RevisionNumber("00005"))
     session = CheckResultSession([])
 
@@ -211,30 +228,30 @@ async def test_signature_provenance_metadata_for_requires_completed_check():
         mock.patch.object(release.db, "session", return_value=session),
         mock.patch.object(release.log, "info") as log_info_mock,
     ):
-        with pytest.raises(datatypes.FailedError, match=r"has not completed yet"):
-            await release._signature_provenance_metadata_for(
-                project_key=safe.ProjectKey("proj"),
-                version_key=safe.VersionKey("1.0"),
-                parent_revision=parent_revision,
-                signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
-            )
-    assert log_info_mock.call_count == 1
-    assert "waiting for signature verification" in log_info_mock.call_args.args[0]
-
-
-@pytest.mark.asyncio
-async def test_signature_provenance_metadata_for_requires_parent_revision():
-    with pytest.raises(datatypes.FailedError, match=r"requires a parent revision"):
-        await release._signature_provenance_metadata_for(
+        metadata = await release._signature_provenance_metadata_for(
             project_key=safe.ProjectKey("proj"),
             version_key=safe.VersionKey("1.0"),
-            parent_revision=None,
+            parent_revision=parent_revision,
             signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
         )
+    assert metadata is None
+    assert log_info_mock.call_count == 1
+    assert "proceeding without signature provenance" in log_info_mock.call_args.args[0]
+
+
+@pytest.mark.asyncio
+async def test_signature_provenance_metadata_for_without_parent_revision():
+    metadata = await release._signature_provenance_metadata_for(
+        project_key=safe.ProjectKey("proj"),
+        version_key=safe.VersionKey("1.0"),
+        parent_revision=None,
+        signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
+    )
+    assert metadata is None
 
 
 @pytest.mark.asyncio
-async def test_signature_provenance_metadata_for_requires_successful_check():
+async def test_signature_provenance_metadata_for_without_successful_check():
     parent_revision = SimpleNamespace(safe_number=safe.RevisionNumber("00005"))
     result = SimpleNamespace(
         message="No valid signature found",
@@ -248,13 +265,13 @@ async def test_signature_provenance_metadata_for_requires_successful_check():
         mock.patch.object(release.db, "session", return_value=session),
         mock.patch.object(release.log, "info") as log_info_mock,
     ):
-        with pytest.raises(datatypes.FailedError, match=r"failed: No valid signature found"):
-            await release._signature_provenance_metadata_for(
-                project_key=safe.ProjectKey("proj"),
-                version_key=safe.VersionKey("1.0"),
-                parent_revision=parent_revision,
-                signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
-            )
+        metadata = await release._signature_provenance_metadata_for(
+            project_key=safe.ProjectKey("proj"),
+            version_key=safe.VersionKey("1.0"),
+            parent_revision=parent_revision,
+            signature_rel_path=pathlib.Path("artifact.tar.gz.asc"),
+        )
+    assert metadata is None
     assert log_info_mock.call_count == 1
     assert "status=concern" in log_info_mock.call_args.args[0]
 


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