This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new aeb692f901f Skip date-shaped tags in upgrade_important_versions image
bumper (#66588)
aeb692f901f is described below
commit aeb692f901ffc3540185eeda75da3c4f634b538c
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri May 8 17:46:29 2026 +0200
Skip date-shaped tags in upgrade_important_versions image bumper (#66588)
The CI image bumper picked alpine's daily-edge tag 20260127 over the
3.23 release in apache/airflow#66580 -- and the same logic would
auto-pin any other Docker Hub image to its date-stamped daily build
under the right circumstances. Root cause:
packaging.version.Version("20260127") parses as a valid single-component
PEP 440 version and sorts higher than 3.23, so the bumper's "latest
version" selection picked the daily.
Pre-filter the tag list against a date-shaped regex (v?\d{8}(\.\d+)?)
before parsing as a Version. Belt-and-braces: also reject parsed Version
objects whose major component is implausibly large (>= 10000), catching
any date-shaped value the regex somehow misses. Add `edge` to the
existing floating-tag skiplist while we're here.
Tests cover the alpine real-tag mix (release + date + edge), busybox
style (mixed dotted releases + bare-major aliases), v-prefixed date
tags, and date-with-revision-suffix variants.
---
scripts/ci/prek/upgrade_important_versions.py | 33 ++++++--
.../ci/prek/test_upgrade_important_versions.py | 94 ++++++++++++++++++++++
2 files changed, 122 insertions(+), 5 deletions(-)
diff --git a/scripts/ci/prek/upgrade_important_versions.py
b/scripts/ci/prek/upgrade_important_versions.py
index 64c3151cb4f..4c1798017ff 100755
--- a/scripts/ci/prek/upgrade_important_versions.py
+++ b/scripts/ci/prek/upgrade_important_versions.py
@@ -300,6 +300,14 @@ def get_latest_lts_node_version() -> str:
return latest_version
+# Match date-shaped tags published by official images for daily / edge
+# builds, e.g. Alpine's `20260127` or `v20260127`. These parse as valid
+# PEP 440 versions and would otherwise sort above any normal release tag.
+# Pattern: optional leading `v`, then 8 digits (YYYYMMDD), optionally
+# followed by `.N` (revision suffix on the same date).
+_DATE_SHAPED_TAG_RE = re.compile(r"^v?\d{8}(\.\d+)?$")
+
+
def get_latest_image_version(image: str) -> str:
"""
Fetch the latest tag released for a DockerHub image.
@@ -340,17 +348,32 @@ def get_latest_image_version(image: str) -> str:
version_tags = []
for tag in tags:
tag_name = tag["name"]
- # Skip tags like 'latest', 'stable', '0', 'v0', etc.
- if tag_name in ["latest", "stable", "main", "master", "0", "v0"]:
+ # Skip well-known floating tags.
+ if tag_name in ["latest", "stable", "main", "master", "0", "v0",
"edge"]:
+ continue
+ # Skip date-shaped tags (`YYYYMMDD`, `YYYYMMDD.N`, `vYYYYMMDD`).
+ # Several official images publish daily / edge builds under
+ # date-stamped numeric tags (e.g. Alpine's `20260127`). PEP 440
+ # parses those as valid `Version("20260127")` and they sort higher
+ # than any normal release version like `3.23`, so the bumper would
+ # auto-pin the daily edge image instead of the latest release.
+ if _DATE_SHAPED_TAG_RE.match(tag_name):
continue
try:
- # Try to parse as version to filter out non-version tags
- # Remove leading 'v' if present
+ # Try to parse as version to filter out non-version tags.
+ # Remove leading 'v' if present.
version_str = tag_name.lstrip("v")
version_obj = Version(version_str)
+ # Belt-and-braces sanity check: any version whose major component
+ # is implausibly large (≥ 10000) is a date stamp the regex above
+ # missed, not a release. The largest legitimate image major
+ # version on Docker Hub today is in the low hundreds (e.g.
+ # `node:24`), so 10000 is a safe ceiling.
+ if version_obj.major >= 10000:
+ continue
version_tags.append((version_obj, tag_name))
except Exception:
- # Skip tags that don't parse as versions
+ # Skip tags that don't parse as versions.
continue
if not version_tags:
diff --git a/scripts/tests/ci/prek/test_upgrade_important_versions.py
b/scripts/tests/ci/prek/test_upgrade_important_versions.py
new file mode 100644
index 00000000000..af5e5ea7736
--- /dev/null
+++ b/scripts/tests/ci/prek/test_upgrade_important_versions.py
@@ -0,0 +1,94 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Unit tests for ``scripts/ci/prek/upgrade_important_versions.py``."""
+
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+
[email protected]
+def fake_dockerhub_response(monkeypatch):
+ """Patch `requests.get` used by `get_latest_image_version` to return a
fixed tag list."""
+
+ def _install(tag_names: list[str]) -> mock.MagicMock:
+ from ci.prek import upgrade_important_versions as uiv
+
+ response = mock.MagicMock()
+ response.raise_for_status = mock.MagicMock()
+ response.json = mock.MagicMock(return_value={"results": [{"name": n}
for n in tag_names]})
+ get = mock.MagicMock(return_value=response)
+ monkeypatch.setattr(uiv.requests, "get", get)
+ return get
+
+ return _install
+
+
[email protected](
+ ("tags", "expected"),
+ [
+ pytest.param(
+ ["20260127", "3.23.5", "3.23", "3.22.0", "3", "latest", "edge"],
+ "3.23.5",
+ id="alpine-real-tag-mix-rejects-date-and-edge",
+ ),
+ pytest.param(
+ ["1.37.0", "1.37", "1", "1.36.1", "musl", "stable"],
+ "1.37.0",
+ id="busybox-style-tags",
+ ),
+ pytest.param(
+ ["v20260127", "v3.23.5"],
+ "v3.23.5",
+ id="v-prefixed-tags-with-date",
+ ),
+ pytest.param(
+ ["20260127.1", "3.23.5"],
+ "3.23.5",
+ id="date-with-revision-suffix-still-rejected",
+ ),
+ ],
+)
+def
test_get_latest_image_version_rejects_date_shaped_tags(fake_dockerhub_response,
tags, expected):
+ """A date-shaped daily-build tag must not be picked over a release tag.
+
+ Regression for the alpine `20260127` bump in apache/airflow#66580 — the
+ bumper used `packaging.version.Version` to sort tags, but PEP 440 happily
+ parses `20260127` as a single-component version that sorts above `3.23`,
+ so the script auto-pinned the daily edge image instead of the latest
+ release. The fix filters date-shaped tags before the version sort.
+ """
+ fake_dockerhub_response(tags)
+ from ci.prek.upgrade_important_versions import get_latest_image_version
+
+ assert get_latest_image_version("alpine") == expected
+
+
+def test_date_shaped_tag_regex_matches_only_date_stamps():
+ """The pre-filter regex matches date-stamped tags, not legitimate
releases."""
+ from ci.prek.upgrade_important_versions import _DATE_SHAPED_TAG_RE
+
+ # Date-shaped tags — should match (and therefore be skipped).
+ for tag in ["20260127", "v20260127", "20260127.1", "v20260127.10"]:
+ assert _DATE_SHAPED_TAG_RE.match(tag) is not None, f"expected match
for {tag!r}"
+
+ # Release tags — must not match.
+ for tag in ["3.23", "3.23.5", "1.37.0", "v1.37.0", "1", "3", "latest",
"stable"]:
+ assert _DATE_SHAPED_TAG_RE.match(tag) is None, f"unexpected match for
{tag!r}"