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/09/15 10:34:27 UTC

(tooling-trusted-releases) branch arm updated: #1559 - Don't double count ATR releases

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


The following commit(s) were added to refs/heads/arm by this push:
     new 43dd6b5e #1559 - Don't double count ATR releases
43dd6b5e is described below

commit 43dd6b5edcecc7b0b6c9cb4895c99e523f3213a3
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Tue Sep 15 11:34:00 2026 +0100

    #1559 - Don't double count ATR releases
---
 atr/svn/__init__.py                      | 38 +++++++++++++++++++-------------
 atr/svn/catalog.py                       | 16 +++++++++++++-
 tests/unit/test_dist_watcher_removals.py | 26 ++++++++++++++++++++++
 3 files changed, 64 insertions(+), 16 deletions(-)

diff --git a/atr/svn/__init__.py b/atr/svn/__init__.py
index ed7d2afa..8a5e48a9 100644
--- a/atr/svn/__init__.py
+++ b/atr/svn/__init__.py
@@ -357,6 +357,28 @@ async def publish_release(source_dir: pathlib.Path, target_url: str, username: s
     return revision
 
 
+async def committed_by_atr(url: str, revision: str) -> bool:
+    # A commit ATR made carries the asf:tool=atr revprop, set by commit() - the one signal that marks
+    # our own publishes, since ATR commits under the release manager's UID, not the tool account, so
+    # the author reads the same as a hand commit does
+    stdin_bytes = _authentication(url)
+    tool = await _run_svn_command(
+        "propget",
+        url,
+        "--revprop",
+        "-r",
+        revision,
+        "asf:tool",
+        "--username",
+        ASF_TOOL,
+        "--password-from-stdin",
+        "--non-interactive",
+        timeout_seconds=INFO_TIMEOUT_SECONDS,
+        stdin_bytes=stdin_bytes,
+    )
+    return tool.strip() == ASF_TOOL
+
+
 async def publish_revision_matches(info: SvnInfo, author: str, message: str) -> bool:
     stdin_bytes = _authentication(info.url)
     revision = info.last_changed_rev_number
@@ -390,21 +412,7 @@ async def publish_revision_matches(info: SvnInfo, author: str, message: str) ->
     created_paths = {(path.text or "").strip() for path in entry.findall("./paths/path") if path.get("action") == "A"}
     if target_path not in created_paths:
         return False
-    tool = await _run_svn_command(
-        "propget",
-        info.url,
-        "--revprop",
-        "-r",
-        str(revision),
-        "asf:tool",
-        "--username",
-        ASF_TOOL,
-        "--password-from-stdin",
-        "--non-interactive",
-        timeout_seconds=INFO_TIMEOUT_SECONDS,
-        stdin_bytes=stdin_bytes,
-    )
-    return tool.strip() == ASF_TOOL
+    return await committed_by_atr(info.url, str(revision))
 
 
 async def remove_files(
diff --git a/atr/svn/catalog.py b/atr/svn/catalog.py
index 93b088a5..af1c25da 100644
--- a/atr/svn/catalog.py
+++ b/atr/svn/catalog.py
@@ -75,7 +75,6 @@ class _StructuralChanges(NamedTuple):
 
 async def catalogue_commit(commit: dict) -> None:
     if str(commit.get("committer", "")) == svn.ASF_TOOL:
-        # Our own publishes are already in the database
         return
     changed = commit.get("changed", {})
     if not isinstance(changed, dict):
@@ -87,6 +86,8 @@ async def catalogue_commit(commit: dict) -> None:
     added = _collapse_airflow_providers(added)
     if not (added or removed or changes.removed_files):
         return
+    if await _committed_by_atr(commit.get("id")):
+        return
     date = _commit_date(commit)
     # Resolve against ATR's projects in a read session first, so decomposition and
     # project lookups stay out of the storage writer
@@ -172,6 +173,19 @@ def _commit_date(commit: dict) -> datetime.datetime:
         return datetime.datetime.now(datetime.UTC)
 
 
+async def _committed_by_atr(revision: object) -> bool:
+    # Our own publishes carry the asf:tool=atr revprop; a failed read catalogues rather than drops,
+    # since a duplicate release is recoverable and a missed one isn't
+    url = config.get().SVN_PUBLISH_URL
+    if (url is None) or (revision is None):
+        return False
+    try:
+        return await svn.committed_by_atr(url, str(revision))
+    except Exception:
+        log.exception(f"dist watcher could not read asf:tool for r{revision}; treating it as external")
+        return False
+
+
 def _companion(siblings: set[str], artifact: str, suffixes: tuple[str, ...]) -> str | None:
     for suffix in suffixes:
         if (artifact + suffix) in siblings:
diff --git a/tests/unit/test_dist_watcher_removals.py b/tests/unit/test_dist_watcher_removals.py
index be4aaf39..19f3e8e9 100644
--- a/tests/unit/test_dist_watcher_removals.py
+++ b/tests/unit/test_dist_watcher_removals.py
@@ -147,3 +147,29 @@ async def test_the_same_release_is_only_archived_once_across_several_deleted_sou
         removed_files=[("foo/1.2.0", "foo-1.2.0-src.tar.gz"), ("foo/1.2.0", "foo-1.2.0-src.zip")],
     )
     assert len(archives) == 1
+
+
+# ATR's own publishes carry the asf:tool=atr revprop, so the watcher skips them rather than
+# re-cataloguing a managed release the publish already recorded
+
+
+@pytest.mark.asyncio
+async def test_a_commit_with_no_revision_is_treated_as_external(monkeypatch) -> None:
+    # Nothing to look a revprop up against, so it can't be recognised as ours - catalogue it
+    monkeypatch.setattr(catalog.config, "get", lambda: SimpleNamespace(SVN_PUBLISH_URL="https://dist.example/release"))
+    assert await catalog._committed_by_atr(None) is False
+
+
+@pytest.mark.asyncio
+async def test_a_commit_carrying_the_atr_tool_revprop_is_recognised_as_ours(monkeypatch) -> None:
+    monkeypatch.setattr(catalog.config, "get", lambda: SimpleNamespace(SVN_PUBLISH_URL="https://dist.example/release"))
+    monkeypatch.setattr(catalog.svn, "committed_by_atr", mock.AsyncMock(return_value=True))
+    assert await catalog._committed_by_atr(87431) is True
+
+
+@pytest.mark.asyncio
+async def test_a_revprop_read_failure_catalogues_rather_than_drops(monkeypatch) -> None:
+    # A dropped release can't be recovered but a duplicate can, so a failed lookup falls to cataloguing
+    monkeypatch.setattr(catalog.config, "get", lambda: SimpleNamespace(SVN_PUBLISH_URL="https://dist.example/release"))
+    monkeypatch.setattr(catalog.svn, "committed_by_atr", mock.AsyncMock(side_effect=RuntimeError("svn unreachable")))
+    assert await catalog._committed_by_atr(87431) is False


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