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/06/26 16:53:16 UTC

(tooling-trusted-releases) 03/03: #478 - Add is_archived for releases to allow null dates where we don't have one

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 570d12f85d5f92c9011448f5795bd332802b510d
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Fri Jun 26 17:52:44 2026 +0100

    #478 - Add is_archived for releases to allow null dates where we don't have one
---
 atr/db/interaction.py                           |  2 +-
 atr/models/sql.py                               | 10 ++++++---
 atr/shared/catalog.py                           |  2 +-
 atr/storage/writers/announce.py                 |  4 ++--
 atr/storage/writers/release.py                  |  4 ++--
 atr/svn/catalog.py                              |  2 +-
 atr/svn/dist.py                                 | 18 ++++++----------
 atr/templates/releases-finished.html            |  8 +++----
 migrations/versions/0098_2026.06.26_9a948e08.py | 28 +++++++++++++++++++++++++
 tests/unit/test_decompose.py                    | 15 +++++++++++++
 10 files changed, 67 insertions(+), 26 deletions(-)

diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index bd5241ba..8ae27272 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -540,7 +540,7 @@ async def prior_release_for_archive(
         .where(
             sql.Release.project_key == project.key,
             sql.Release.phase == sql.ReleasePhase.RELEASE,
-            via(sql.Release.archived).is_(None),
+            via(sql.Release.is_archived).is_(False),
         )
         .where(via(sql.Release.version) != version)
     )
diff --git a/atr/models/sql.py b/atr/models/sql.py
index e41b5bd4..b950abae 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -1291,14 +1291,18 @@ class Release(sqlmodel.SQLModel, table=True):
         sa_column=sqlalchemy.Column(UTCDateTime),
         **example(datetime.datetime(2025, 6, 1, 1, 2, 3, tzinfo=datetime.UTC)),
     )
-    # Mirrors the latest archive LifecycleEvent for this release. Null for
-    # releases that haven't been archived. Sourced by whatever archival flow
-    # #507 eventually settles on.
+    # Mirrors the latest archive LifecycleEvent for this release - the date it was
+    # archived. Null when there's no such event, which includes a release that's
+    # archived but undated (a catalogued historical release whose date we don't have);
+    # is_archived carries the status. Sourced by whatever archival flow #507 settles on.
     archived: datetime.datetime | None = sqlmodel.Field(
         default=None,
         sa_column=sqlalchemy.Column(UTCDateTime),
         **example(datetime.datetime(2026, 1, 15, 1, 2, 3, tzinfo=datetime.UTC)),
     )
+    # The archived status. The archived date above can be null while this is true,
+    # for a catalogued historical release we know is archived but can't date
+    is_archived: bool = sqlmodel.Field(default=False)
     activity_at: datetime.datetime = sqlmodel.Field(
         default_factory=lambda: datetime.datetime.now(datetime.UTC),
         sa_column=sqlalchemy.Column(UTCDateTime, nullable=False),
diff --git a/atr/shared/catalog.py b/atr/shared/catalog.py
index aa0fccb1..203cda68 100644
--- a/atr/shared/catalog.py
+++ b/atr/shared/catalog.py
@@ -143,7 +143,7 @@ def _lifecycle_badge(cycle: sql.ProjectCycle, now: datetime.datetime) -> str:
 
 def _status(release: sql.Release | None) -> Literal["released", "archived"]:
     # Treat a missing release as archived, so it is never shown as downloadable.
-    if (release is not None) and (release.archived is None):
+    if (release is not None) and not release.is_archived:
         return "released"
     return "archived"
 
diff --git a/atr/storage/writers/announce.py b/atr/storage/writers/announce.py
index 80076f7d..5432414c 100644
--- a/atr/storage/writers/announce.py
+++ b/atr/storage/writers/announce.py
@@ -381,8 +381,8 @@ class ReleaseManager(CommitteeParticipant):
         update_stmt = (
             sqlmodel.update(sql.Release)
             .where(via(sql.Release.key) == release_row.key)
-            .where(via(sql.Release.archived).is_(None))
-            .values(archived=archive_date)
+            .where(via(sql.Release.is_archived).is_(False))
+            .values(archived=archive_date, is_archived=True)
         )
         update_result = await self.__data.execute_query(update_stmt)
         if getattr(update_result, "rowcount", 0) != 1:
diff --git a/atr/storage/writers/release.py b/atr/storage/writers/release.py
index 273134f9..5dba83e9 100644
--- a/atr/storage/writers/release.py
+++ b/atr/storage/writers/release.py
@@ -108,8 +108,8 @@ async def _archive_release(
     update_stmt = (
         sqlmodel.update(sql.Release)
         .where(via(sql.Release.key) == release.key)
-        .where(via(sql.Release.archived).is_(None))
-        .values(archived=archive_date)
+        .where(via(sql.Release.is_archived).is_(False))
+        .values(archived=archive_date, is_archived=True)
     )
     update_result = await data.execute_query(update_stmt)
     if getattr(update_result, "rowcount", 0) != 1:
diff --git a/atr/svn/catalog.py b/atr/svn/catalog.py
index 8078d337..211946aa 100644
--- a/atr/svn/catalog.py
+++ b/atr/svn/catalog.py
@@ -194,7 +194,7 @@ async def _resolve_archive(
     if project is None:
         return None
     release_record = await data.release(key=f"{project.key}-{version}").get()
-    if (release_record is None) or (release_record.archived is not None):
+    if (release_record is None) or release_record.is_archived:
         return None
     return _safe_keys(project.key, version)
 
diff --git a/atr/svn/dist.py b/atr/svn/dist.py
index 548aac08..73e6816a 100644
--- a/atr/svn/dist.py
+++ b/atr/svn/dist.py
@@ -116,17 +116,17 @@ def version_from_filename(filename: str) -> str | None:
 def _choose_version(
     dir_version: str | None, dir_source: Literal["dir", "combined", "unknown"], filename: str | None
 ) -> tuple[str | None, Literal["dir", "combined", "filename", "unknown"]]:
-    # The dir and the filename can each carry a version. The filename usually wins (a 4.5/ series
-    # dir vs the 4.5.13 file), but when its leading version is a distractor (kafka_2.13-4.3.1.tgz,
-    # the 2.13 Scala build) and the dir pins a version the filename confirms, the dir wins - unless
-    # the filename merely refines the dir version (a 1.0.0-incubating tail)
+    # The dir and the filename can each carry a version. An explicit version dir is the release
+    # boundary, so a file under it whose own version doesn't refine the dir is a sub-component -
+    # an opendal language binding at its own version, or the kafka_2.13-4.3.1 Scala build - and
+    # stays on the dir's release. The filename wins only when it refines the dir (a 4.5 series dir
+    # the 4.5.13 file pins, a 1.0.0-incubating tail) or there's no dir version to anchor to
     if dir_version is not None:
         dir_version = _normalise_version(dir_version)
     name_version = version_from_filename(filename) if filename else None
     if name_version is None:
         return dir_version, dir_source
-    dir_confirmed = (dir_version is not None) and (filename is not None) and _version_in_filename(dir_version, filename)
-    if dir_confirmed and (dir_version is not None) and not name_version.startswith(dir_version):
+    if (dir_version is not None) and not name_version.startswith(dir_version):
         return dir_version, dir_source
     return name_version, "filename"
 
@@ -232,9 +232,3 @@ def _subproject_and_dir_version(
         if deeper is not None:
             return first, deeper[1], "combined"
     return first, None, "unknown"
-
-
-def _version_in_filename(version: str, filename: str) -> bool:
-    # The version appears in the filename as its own token, not as the prefix of a longer
-    # version (4.5 inside 4.5.13) nor tacked onto another number
-    return re.search(rf"(?<![\d.]){re.escape(version)}(?!\.?\d)", filename) is not None
diff --git a/atr/templates/releases-finished.html b/atr/templates/releases-finished.html
index 8c3b0f4e..b1232f25 100644
--- a/atr/templates/releases-finished.html
+++ b/atr/templates/releases-finished.html
@@ -16,19 +16,19 @@
     <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
       {% for release in releases %}
         <div class="col">
-          <div class="card h-100 shadow-sm{% if release.archived %} border-secondary atr-archived-release{% endif %}">
+          <div class="card h-100 shadow-sm{% if release.is_archived %} border-secondary atr-archived-release{% endif %}">
             <div class="card-body d-flex flex-column">
               <div class="d-flex align-items-center gap-2 mb-1">
                 <strong class="card-title fs-5 mb-0">{{ release.version }}</strong>
-                {% if release.archived %}
+                {% if release.is_archived %}
                   <span class="badge bg-secondary" title="This release has been archived">
                     <i class="bi bi-archive me-1"></i>Archived
                   </span>
                 {% endif %}
               </div>
               <p class="card-text text-muted mb-1">Released on {{ format_datetime(release.released or release.created) }}</p>
-              <p class="card-text text-muted small mb-2{% if not release.archived %} invisible{% endif %}">
-                {% if release.archived %}Archived on {{ format_datetime(release.archived) }}{% else %}&nbsp;{% endif %}
+              <p class="card-text text-muted small mb-2{% if not release.is_archived %} invisible{% endif %}">
+                {% if release.archived %}Archived on {{ format_datetime(release.archived) }}{% elif release.is_archived %}Archived{% else %}&nbsp;{% endif %}
               </p>
               <div class="mt-auto">
                 <a href="{{ as_url(get.download.all_selected, project_key=release.project.key, version_key=release.version) }}"
diff --git a/migrations/versions/0098_2026.06.26_9a948e08.py b/migrations/versions/0098_2026.06.26_9a948e08.py
new file mode 100644
index 00000000..4066f80b
--- /dev/null
+++ b/migrations/versions/0098_2026.06.26_9a948e08.py
@@ -0,0 +1,28 @@
+"""Add is_archived to release
+
+Revision ID: 0098_2026.06.26_9a948e08
+Revises: 0097_2026.06.24_6741a40a
+Create Date: 2026-06-26 15:33:58.125528+00:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0098_2026.06.26_9a948e08"
+down_revision: str | None = "0097_2026.06.24_6741a40a"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    with op.batch_alter_table("release", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("is_archived", sa.Boolean(), nullable=False, server_default=sa.false()))
+    op.execute(sa.text("UPDATE release SET is_archived = 1 WHERE archived IS NOT NULL"))
+
+
+def downgrade() -> None:
+    with op.batch_alter_table("release", schema=None) as batch_op:
+        batch_op.drop_column("is_archived")
diff --git a/tests/unit/test_decompose.py b/tests/unit/test_decompose.py
index 9958dec1..ff14dfc8 100644
--- a/tests/unit/test_decompose.py
+++ b/tests/unit/test_decompose.py
@@ -116,6 +116,21 @@ _CASES: list[tuple[str, tuple[str, ...], str | None, str | None, str | None, cla
     # has no marker, so it falls to source, but both still attach to kafka 4.3.1
     ("kafka", ("4.3.1",), "kafka_2.13-4.3.1.tgz", None, "4.3.1", _SOURCE),
     ("kafka", ("4.3.1",), "kafka-4.3.1-src.tgz", None, "4.3.1", _SOURCE),
+    # A versioned sub-component inside a release's version dir is an artifact of that release, so
+    # the dir version holds and the component's own version is ignored - otherwise each binding
+    # forks a spurious release (opendal ships a dozen language bindings at independent versions)
+    ("opendal", ("0.45.1",), "apache-opendal-bindings-go-0.0.0-src.tar.gz", None, "0.45.1", _SOURCE),
+    # A component that happens to share the release version still lands on it
+    ("opendal", ("0.45.1",), "apache-opendal-core-0.45.1-src.tar.gz", None, "0.45.1", _SOURCE),
+    # A sub-component versioned higher than its release (an integration at 1.0.2 under 0.51.0) still
+    # collapses onto the release, so it can't mint a spurious higher release
+    ("opendal", ("0.51.0",), "apache-opendal-integrations-compat-1.0.2-src.tar.gz", None, "0.51.0", _SOURCE),
+    # A build variant whose version refines the dir (a hadoop1 build of the 0.98.13 release) keeps
+    # the filename version, since it's the same release built differently
+    ("hbase", ("0.98.13",), "hbase-0.98.13-hadoop1-bin.tar.gz", None, "0.98.13", _BINARY),
+    # A sub-component published in its own name-version dir, not inside a release dir, stays a
+    # distinct release
+    ("hbase", ("hbase-connectors-1.0.0",), "hbase-connectors-1.0.0-src.tar.gz", "hbase-connectors", "1.0.0", _SOURCE),
     # A series dir is still less specific than the filename, which pins the point release
     ("example", ("4.5",), "example-4.5.13.tar.gz", None, "4.5.13", _SOURCE),
     # A leading source/ dir is a build bucket, so a flat TLP release reads from the filename


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