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/05/20 19:08:19 UTC

(tooling-trusted-releases) branch sbp updated: Automatically re-run signature checks when KEYS change

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


The following commit(s) were added to refs/heads/sbp by this push:
     new ddb6f990 Automatically re-run signature checks when KEYS change
ddb6f990 is described below

commit ddb6f990a795066d60a917f76d9281c6f662b593
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Wed May 20 15:47:14 2026 +0100

    Automatically re-run signature checks when KEYS change
---
 atr/storage/writers/keys.py    | 31 +++++++++++++++++++++++++++++++
 atr/tasks/__init__.py          |  3 +++
 atr/tasks/checks/__init__.py   | 15 +++++++++++++++
 atr/tasks/checks/signature.py  |  2 +-
 tests/unit/test_keys_writer.py |  6 ++++++
 5 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index 7f834c37..88c4d415 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -43,6 +43,7 @@ import atr.paths as paths
 import atr.storage as storage
 import atr.storage.outcome as outcome
 import atr.storage.types as types
+import atr.tasks as tasks
 import atr.user as user
 import atr.util as util
 
@@ -226,6 +227,7 @@ class FoundationCommitter(GeneralPublic):
             )
             for committee_key in sorted(affected_committee_keys):
                 await self._sync_committee_keys_file(committee_key)
+            await self._recheck_committee_drafts(*affected_committee_keys)
             return outcome.Result(key)
         except Exception as e:
             return outcome.Error(e)
@@ -310,6 +312,29 @@ class FoundationCommitter(GeneralPublic):
     def _committee_keys_path(self, committee: sql.Committee) -> safe.StatePath:
         return paths.committee_downloads_dir(committee) / "KEYS"
 
+    async def _recheck_committee_drafts(self, *committee_keys: str) -> None:
+        # A KEYS change only invalidates signature checks, so we limit the re-queue to .asc files
+        affected = {committee_key for committee_key in committee_keys if committee_key}
+        if not affected:
+            return
+        drafts = await self.__data.release(
+            phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+            _committee=True,
+        ).all()
+        for draft in drafts:
+            committee = draft.project.committee
+            if (committee is None) or (committee.key not in affected):
+                continue
+            if not draft.latest_revision_number:
+                continue
+            await tasks.draft_checks(
+                self.__asf_uid,
+                draft.safe_project_key,
+                draft.safe_version_key,
+                draft.safe_latest_revision_number,
+                suffix_filter=[".asc"],
+            )
+
     async def _sync_committee_keys_file(self, committee_key: str) -> str | None:
         committee = await self.__data.committee(key=committee_key, _public_signing_keys=True).demand(
             storage.AccessError(f"Committee not found: {committee_key}", status=404)
@@ -439,6 +464,8 @@ class FoundationCommitter(GeneralPublic):
         for committee_key in sorted(affected):
             await self._sync_committee_keys_file(committee_key)
 
+        await self._recheck_committee_drafts(*affected)
+
         return affected
 
     def __block_model(self, key_block: str, ldap_data: cache.EmailUidLookup) -> types.Key:
@@ -640,6 +667,7 @@ class CommitteeParticipant(FoundationCommitter):
                 fingerprint=fingerprint,
                 committee_key=self.__committee_key,
             )
+            await self._recheck_committee_drafts(self.__committee_key)
         try:
             autogenerated_outcome = await self.autogenerate_keys_file()
         except Exception as e:
@@ -854,6 +882,8 @@ class CommitteeParticipant(FoundationCommitter):
                 inserted_fingerprints=sorted(key_inserts),
                 linked_fingerprints=sorted(link_inserts),
             )
+        if link_inserts:
+            await self._recheck_committee_drafts(self.__committee_key)
         return outcomes
 
     async def __ensure(
@@ -974,6 +1004,7 @@ class FoundationAdmin(CommitteeMember):
             )
             raise
 
+        await self._recheck_committee_drafts(self.__committee_key)
         return (num_unlinked, num_deleted)
 
 
diff --git a/atr/tasks/__init__.py b/atr/tasks/__init__.py
index 3b1b8974..b8ce956b 100644
--- a/atr/tasks/__init__.py
+++ b/atr/tasks/__init__.py
@@ -145,6 +145,7 @@ async def draft_checks(
     release_version: safe.VersionKey,
     revision_number: safe.RevisionNumber,
     caller_data: db.Session | None = None,
+    suffix_filter: list[str] | None = None,
 ) -> int:
     """Core logic to analyse a draft revision and queue checks."""
     # Construct path to the specific revision
@@ -172,6 +173,8 @@ async def draft_checks(
             (v for v in release_versions if util.version_sort_key(str(v.version)) < release_version_sortable), None
         )
         for path in relative_paths:
+            if suffix_filter and not str(path).endswith(tuple(suffix_filter)):
+                continue
             await _draft_file_checks(
                 asf_uid,
                 caller_data,
diff --git a/atr/tasks/checks/__init__.py b/atr/tasks/checks/__init__.py
index 5f09841e..19884288 100644
--- a/atr/tasks/checks/__init__.py
+++ b/atr/tasks/checks/__init__.py
@@ -472,6 +472,20 @@ async def _resolve_committee_key(release: sql.Release, rel_path: str | None = No
     return release.committee.key
 
 
+async def _resolve_committee_signing_keys(release: sql.Release, rel_path: str | None = None) -> list[str]:
+    if release.committee is None:
+        raise ValueError("Release has no committee")
+    via = sql.validate_instrumented_attribute
+    committee_key = release.committee.key
+    async with db.session() as data:
+        statement = sqlmodel.select(via(sql.KeyLink.key_fingerprint)).where(
+            via(sql.KeyLink.committee_key) == committee_key
+        )
+        result = await data.execute(statement)
+        fingerprints = result.scalars().all()
+    return sorted(fp for fp in fingerprints if fp)
+
+
 async def _resolve_github_tp_sha(release: sql.Release, rel_path: str | None = None) -> str:
     if not release.latest_revision_number:
         return ""
@@ -503,6 +517,7 @@ async def _resolve_unsuffixed_file_hash(release: sql.Release, rel_path: str | No
 _EXTRA_ARG_RESOLVERS: Final[dict[str, Callable[[sql.Release, str | None], Any]]] = {
     "all_files": _resolve_all_files,
     "committee_key": _resolve_committee_key,
+    "committee_signing_keys": _resolve_committee_signing_keys,
     "github_tp_sha": _resolve_github_tp_sha,
     "is_podling": _resolve_is_podling,
     "unsuffixed_file_hash": _resolve_unsuffixed_file_hash,
diff --git a/atr/tasks/checks/signature.py b/atr/tasks/checks/signature.py
index 9f9c8b24..bb955b39 100644
--- a/atr/tasks/checks/signature.py
+++ b/atr/tasks/checks/signature.py
@@ -31,7 +31,7 @@ import atr.util as util
 
 # Release policy fields which this check relies on - used for result caching
 INPUT_POLICY_KEYS: Final[list[str]] = []
-INPUT_EXTRA_ARGS: Final[list[str]] = ["committee_key", "unsuffixed_file_hash"]
+INPUT_EXTRA_ARGS: Final[list[str]] = ["committee_key", "committee_signing_keys", "unsuffixed_file_hash"]
 CHECK_VERSION: Final[str] = "3"
 
 
diff --git a/tests/unit/test_keys_writer.py b/tests/unit/test_keys_writer.py
index deead07e..69174716 100644
--- a/tests/unit/test_keys_writer.py
+++ b/tests/unit/test_keys_writer.py
@@ -63,6 +63,9 @@ class Query:
     async def get(self):
         return self._value
 
+    async def all(self):
+        return [self._value]
+
     async def demand(self, error: Exception):
         if self._value is None:
             raise error
@@ -85,6 +88,9 @@ class MockData:
     def committee(self, *, key: str, _public_signing_keys: bool = False):
         return Query(self._committees_after_commit[key])
 
+    def release(self, *_args, **_kwargs):
+        return Query(SimpleNamespace(project=mock.AsyncMock()))
+
 
 @pytest.mark.asyncio
 async def test_database_add_model_audits_inserted_key():


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