You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/05/20 14:48:50 UTC

(tooling-trusted-releases) 01/01: Automatically re-run signature checks when KEYS change

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

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

commit ce4e9acbc668037fd7cf4c4e2d5ae72a91f73d56
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 +-
 4 files changed, 50 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"
 
 


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