This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 5c75cc8d3221 Check Container Upgrade - Improve container version
upgrade filters to prevent bad proposals (#21491)
5c75cc8d3221 is described below
commit 5c75cc8d3221d5ad3f4dff5a658a5d26e3f34bd6
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Feb 16 11:57:22 2026 +0100
Check Container Upgrade - Improve container version upgrade filters to
prevent bad proposals (#21491)
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../check-container-versions.py | 193 +++++++++++++++++----
.../test/infra/kafka/services/container.properties | 3 +
.../microprofile/lra/services/container.properties | 1 +
.../infra/milvus/services/container.properties | 2 +
.../infra/mongodb/services/container.properties | 1 +
.../infra/postgres/services/container.properties | 1 +
.../test/infra/redis/services/container.properties | 1 +
7 files changed, 167 insertions(+), 35 deletions(-)
diff --git
a/.github/actions/check-container-upgrade/check-container-versions.py
b/.github/actions/check-container-upgrade/check-container-versions.py
index 13c13581c66b..9716313c92d7 100755
--- a/.github/actions/check-container-upgrade/check-container-versions.py
+++ b/.github/actions/check-container-upgrade/check-container-versions.py
@@ -57,12 +57,27 @@ Version Filtering:
mysql.container=mysql:8.0.35
mysql.container.version.exclude=alpine,slim,debian
+ Major version freeze:
+ Prevents major version jumps (e.g., 3.x → 4.x):
+ <property>.version.freeze.major=true
+
+ Example:
+ kafka3.container=mirror.gcr.io/apache/kafka:3.9.1
+ kafka3.container.version.freeze.major=true
+
+ Structural pattern matching:
+ Version tags are automatically filtered to match the same format as the
+ current version. For example, if the current version is '17.5-alpine',
+ only tags matching the pattern X.Y-alpine will be considered. This
prevents
+ non-version tags (branch names, base image tags) from being proposed.
+
Notes:
+ - Structural pattern matching is applied first (automatic, no config
needed)
- Filters are case-insensitive
- Include filter: version must contain at least ONE of the words
- Exclude filter: version must NOT contain ANY of the words
- Exclude filters are checked first, then include filters
- - If no filters specified, all versions are considered
+ - If no filters specified, all versions matching the structural
pattern are considered
Usage:
python3 check-container-versions.py [options]
@@ -96,6 +111,87 @@ import time
# Initialize colorama for cross-platform colored output
init(autoreset=True)
+
+def extract_numeric_segments(version_str: str) -> List[int]:
+ """Extract all numeric segments from a version string for comparison.
+
+ Examples:
+ '17.5-alpine' → [17, 5]
+ 'v2.5.11' → [2, 5, 11]
+ 'latest-kafka-3.9.1' → [3, 9, 1]
+ 'RELEASE.2025-09-07T16-13-09Z-cpuv1' → [2025, 9, 7, 16, 13, 9, 1]
+ """
+ return [int(n) for n in re.findall(r'\d+', version_str)]
+
+
+def compare_version_tuples(v1: str, v2: str) -> int:
+ """Compare two version strings by their numeric segments.
+
+ Returns positive if v1 > v2, negative if v1 < v2, 0 if equal.
+ This avoids the broken string comparison fallback (e.g., "v1.9.7" >
"v1.15.1").
+ """
+ nums1 = extract_numeric_segments(v1)
+ nums2 = extract_numeric_segments(v2)
+
+ if not nums1 or not nums2:
+ return 0 # Can't compare
+
+ # Compare component by component
+ for n1, n2 in zip(nums1, nums2):
+ if n1 != n2:
+ return n1 - n2
+
+ return len(nums1) - len(nums2)
+
+
+def is_same_major_version(v1: str, v2: str) -> bool:
+ """Check if two versions share the same major version (first numeric
segment).
+
+ Used with version.freeze.major to prevent major version jumps.
+ """
+ nums1 = extract_numeric_segments(v1)
+ nums2 = extract_numeric_segments(v2)
+
+ if not nums1 or not nums2:
+ return True # Can't determine, allow
+
+ return nums1[0] == nums2[0]
+
+
+def infer_version_pattern(current_version: str) -> Optional[re.Pattern]:
+ """Infer a structural regex pattern from the current version tag.
+
+ Replaces numeric segments with \\d+ patterns while keeping literal text
+ segments intact. This ensures candidate versions match the same format
+ as the current version.
+
+ Examples:
+ '17.5-alpine' → '^\\d+\\.\\d+-alpine$' (matches '18.0-alpine',
not 'alpine3.23')
+ 'v2.5.11' → '^v\\d+\\.\\d+\\.\\d+$' (matches 'v2.6.0', not
'2.5.12')
+ 'latest-kafka-3.9.1'→ '^latest\\-kafka\\-\\d+\\.\\d+\\.\\d+$'
+ '0.12.0-cpu' → '^\\d+\\.\\d+\\.\\d+\\-cpu$' (matches
'0.13.0-cpu', not 'latest-gpu')
+ '7.0.12-jammy' → '^\\d+\\.\\d+\\.\\d+\\-jammy$'
+ """
+ if not current_version:
+ return None
+
+ parts = re.split(r'(\d+)', current_version)
+ pattern_parts = []
+ for part in parts:
+ if part == '':
+ continue
+ if re.match(r'^\d+$', part):
+ pattern_parts.append(r'\d+')
+ else:
+ pattern_parts.append(re.escape(part))
+
+ pattern = '^' + ''.join(pattern_parts) + '$'
+ try:
+ return re.compile(pattern)
+ except re.error:
+ return None
+
+
@dataclass
class ContainerImage:
"""Represents a container image with its registry, name, and version."""
@@ -107,6 +203,7 @@ class ContainerImage:
file_path: str
version_include: List[str] = None # Whitelist: version must contain one
of these words
version_exclude: List[str] = None # Blacklist: version must not contain
any of these words
+ version_freeze_major: bool = False # Lock to same major version when True
def __post_init__(self):
"""Initialize default values for optional fields."""
@@ -114,6 +211,8 @@ class ContainerImage:
self.version_include = []
if self.version_exclude is None:
self.version_exclude = []
+ # Infer structural pattern from current version (not a dataclass
field, excluded from asdict)
+ self._version_pattern = infer_version_pattern(self.current_version)
@property
def full_name(self) -> str:
@@ -128,11 +227,23 @@ class ContainerImage:
"""Returns the complete image reference with version."""
return f"{self.full_name}:{self.current_version}"
- def is_version_allowed(self, version: str) -> bool:
- """Check if a version matches the whitelist/blacklist criteria."""
- version_lower = version.lower()
+ def is_version_allowed(self, version_tag: str) -> bool:
+ """Check if a version matches structural pattern and
whitelist/blacklist criteria.
+
+ Checks are applied in order:
+ 1. Structural pattern: version must match the same format as current
version
+ 2. Blacklist (exclude): version must NOT contain any excluded words
+ 3. Whitelist (include): version must contain at least one included word
+ 4. Major version freeze: version must share the same first numeric
segment
+ """
+ # Check structural pattern first - this prevents non-version tags
+ # like branch names, base image tags, etc.
+ if self._version_pattern and not
self._version_pattern.match(version_tag):
+ return False
+
+ version_lower = version_tag.lower()
- # Check blacklist first (exclusions)
+ # Check blacklist (exclusions)
if self.version_exclude:
for exclude_word in self.version_exclude:
if exclude_word.lower() in version_lower:
@@ -141,13 +252,17 @@ class ContainerImage:
# Check whitelist (inclusions)
if self.version_include:
# If whitelist is specified, version must contain at least one of
the words
- for include_word in self.version_include:
- if include_word.lower() in version_lower:
- return True
- # If whitelist exists but no match found, reject
- return False
+ matched = any(include_word.lower() in version_lower
+ for include_word in self.version_include)
+ if not matched:
+ return False
+
+ # Check major version freeze
+ if self.version_freeze_major:
+ if not is_same_major_version(self.current_version, version_tag):
+ return False
- # No whitelist specified or version passed all checks
+ # Version passed all checks
return True
@dataclass
@@ -455,6 +570,9 @@ class MicrosoftRegistryAPI(ContainerRegistryAPI):
class ContainerVersionChecker:
"""Main class for checking container versions."""
+ # Pre-release indicators for version tags
+ PRERELEASE_INDICATORS = ['alpha', 'beta', 'rc', 'dev', 'snapshot',
'preview', 'nightly', 'canary']
+
def __init__(self,
include_prereleases: bool = False,
registry_timeout: int = 30,
@@ -477,6 +595,11 @@ class ContainerVersionChecker:
'cr.weaviate.io': DockerV2RegistryAPI(registry_timeout, "Weaviate
Container Registry"),
}
+ def _is_prerelease(self, ver: str) -> bool:
+ """Check if a version tag appears to be a pre-release."""
+ lower = ver.lower()
+ return any(indicator in lower for indicator in
self.PRERELEASE_INDICATORS)
+
def parse_container_reference(self, container_ref: str) -> Tuple[str, str,
str, str]:
"""Parse container reference into registry, namespace, name, and
version."""
# Handle cases like:
@@ -642,8 +765,8 @@ class ContainerVersionChecker:
processed_keys = set()
for key, value in properties.items():
- # Skip if already processed or if it's a filter property
- if key in processed_keys or '.version.include' in key or
'.version.exclude' in key:
+ # Skip if already processed or if it's a version filter/config
property
+ if key in processed_keys or '.version.include' in key or
'.version.exclude' in key or '.version.freeze' in key:
continue
try:
@@ -663,6 +786,12 @@ class ContainerVersionChecker:
if exclude_key in properties:
version_exclude = [word.strip() for word in
properties[exclude_key].split(',') if word.strip()]
+ # Look for .version.freeze.major property
+ freeze_major_key = f"{key}.version.freeze.major"
+ version_freeze_major = False
+ if freeze_major_key in properties:
+ version_freeze_major =
properties[freeze_major_key].lower() in ['true', 'yes', '1']
+
image = ContainerImage(
registry=registry,
namespace=namespace,
@@ -671,7 +800,8 @@ class ContainerVersionChecker:
property_name=key,
file_path=file_path,
version_include=version_include,
- version_exclude=version_exclude
+ version_exclude=version_exclude,
+ version_freeze_major=version_freeze_major
)
images.append(image)
processed_keys.add(key)
@@ -682,6 +812,8 @@ class ContainerVersionChecker:
print(f" Include filter: {',
'.join(version_include)}")
if version_exclude:
print(f" Exclude filter: {',
'.join(version_exclude)}")
+ if version_freeze_major:
+ print(f" Major version freeze: enabled")
except ValueError as e:
if self.verbose:
@@ -752,32 +884,23 @@ class ContainerVersionChecker:
excluded_count = len(available_versions) -
len(filtered_versions)
print(f" Filtered out {excluded_count} versions based on
include/exclude rules")
- # Sort versions
+ # Sort versions using numeric segment comparison (avoids
packaging.version failures)
def version_sort_key(v):
- try:
- return version.parse(v)
- except version.InvalidVersion:
- # For non-semantic versions, sort lexicographically
- return v
-
- try:
- sorted_versions = sorted(filtered_versions,
key=version_sort_key, reverse=True)
- except:
- # If sorting fails, use lexicographic sort
- sorted_versions = sorted(filtered_versions, reverse=True)
-
- # Find newer versions
+ nums = extract_numeric_segments(v)
+ if nums:
+ # Pad to 20 segments for consistent tuple comparison
+ return tuple(nums + [0] * (20 - len(nums)))
+ return (0,) * 20
+
+ sorted_versions = sorted(filtered_versions, key=version_sort_key,
reverse=True)
+
+ # Find newer versions using numeric segment comparison
newer_versions = []
current_ver = image.current_version
for ver in sorted_versions:
- try:
- if version.parse(ver) > version.parse(current_ver):
- if self.include_prereleases or not
version.parse(ver).is_prerelease:
- newer_versions.append(ver)
- except version.InvalidVersion:
- # For non-semantic versions, do string comparison
- if ver > current_ver:
+ if compare_version_tuples(ver, current_ver) > 0:
+ if self.include_prereleases or not
self._is_prerelease(ver):
newer_versions.append(ver)
latest_version = sorted_versions[0] if sorted_versions else None
diff --git
a/test-infra/camel-test-infra-kafka/src/main/resources/org/apache/camel/test/infra/kafka/services/container.properties
b/test-infra/camel-test-infra-kafka/src/main/resources/org/apache/camel/test/infra/kafka/services/container.properties
index 51beeb8725e1..45d8de482a6c 100644
---
a/test-infra/camel-test-infra-kafka/src/main/resources/org/apache/camel/test/infra/kafka/services/container.properties
+++
b/test-infra/camel-test-infra-kafka/src/main/resources/org/apache/camel/test/infra/kafka/services/container.properties
@@ -22,4 +22,7 @@ strimzi.container.image.version.include=latest-kafka
strimzi.container.image.version.exclude=s390x,ppc64le,arm64,amd64
confluent.container.image.version.exclude=amd64,arm64,ppc64le,s390x,x86_64,latest,ubi
kafka3.container.version.exclude=rc,beta,alpha
+kafka3.container.version.freeze.major=true
redpanda.container.image.version.exclude=fips,amd64,arm64
+strimzi.container.image.version.freeze.major=true
+confluent.container.image.version.freeze.major=true
diff --git
a/test-infra/camel-test-infra-microprofile-lra/src/main/resources/org/apache/camel/test/infra/microprofile/lra/services/container.properties
b/test-infra/camel-test-infra-microprofile-lra/src/main/resources/org/apache/camel/test/infra/microprofile/lra/services/container.properties
index a141915d6268..11ce8375f761 100644
---
a/test-infra/camel-test-infra-microprofile-lra/src/main/resources/org/apache/camel/test/infra/microprofile/lra/services/container.properties
+++
b/test-infra/camel-test-infra-microprofile-lra/src/main/resources/org/apache/camel/test/infra/microprofile/lra/services/container.properties
@@ -16,4 +16,5 @@
## ---------------------------------------------------------------------------
microprofile.lra.container=quay.io/jbosstm/lra-coordinator:5.13.1.Final-2.16.6.Final
microprofile.lra.container.ppc64le=icr.io/ppc64le-oss/quarkus/lra-coordinator-quarkus-jvm:latest
+microprofile.lra.container.version.freeze.major=true
microprofile.lra.container.ppc64le.version.exclude=latest,amd64,arm64
diff --git
a/test-infra/camel-test-infra-milvus/src/main/resources/org/apache/camel/test/infra/milvus/services/container.properties
b/test-infra/camel-test-infra-milvus/src/main/resources/org/apache/camel/test/infra/milvus/services/container.properties
index 1130c63ad958..cf61ef74d490 100644
---
a/test-infra/camel-test-infra-milvus/src/main/resources/org/apache/camel/test/infra/milvus/services/container.properties
+++
b/test-infra/camel-test-infra-milvus/src/main/resources/org/apache/camel/test/infra/milvus/services/container.properties
@@ -16,3 +16,5 @@
## ---------------------------------------------------------------------------
milvus.container=mirror.gcr.io/milvusdb/milvus:v2.5.11
milvus.container.ppc64le=icr.io/ppc64le-oss/milvus-ppc64le:v2.4.11
+milvus.container.version.freeze.major=true
+milvus.container.ppc64le.version.freeze.major=true
diff --git
a/test-infra/camel-test-infra-mongodb/src/main/resources/org/apache/camel/test/infra/mongodb/services/container.properties
b/test-infra/camel-test-infra-mongodb/src/main/resources/org/apache/camel/test/infra/mongodb/services/container.properties
index 0890de44be4b..4c53b7b23c2b 100644
---
a/test-infra/camel-test-infra-mongodb/src/main/resources/org/apache/camel/test/infra/mongodb/services/container.properties
+++
b/test-infra/camel-test-infra-mongodb/src/main/resources/org/apache/camel/test/infra/mongodb/services/container.properties
@@ -18,3 +18,4 @@ mongodb.container=mirror.gcr.io/mongo:7.0.12-jammy
mongodb.container.ppc64le=icr.io/ppc64le-oss/mongodb-ppc64le:4.4.24
mongodb.container.version.include=jammy
mongodb.container.ppc64le.version.exclude=bv
+mongodb.container.ppc64le.version.freeze.major=true
diff --git
a/test-infra/camel-test-infra-postgres/src/main/resources/org/apache/camel/test/infra/postgres/services/container.properties
b/test-infra/camel-test-infra-postgres/src/main/resources/org/apache/camel/test/infra/postgres/services/container.properties
index c6e5d30799d4..b7f386950911 100644
---
a/test-infra/camel-test-infra-postgres/src/main/resources/org/apache/camel/test/infra/postgres/services/container.properties
+++
b/test-infra/camel-test-infra-postgres/src/main/resources/org/apache/camel/test/infra/postgres/services/container.properties
@@ -16,3 +16,4 @@
## ---------------------------------------------------------------------------
postgres.container=mirror.gcr.io/postgres:17.5-alpine
postgres.container.version.include=alpine
+postgres.container.version.freeze.major=true
diff --git
a/test-infra/camel-test-infra-redis/src/main/resources/org/apache/camel/test/infra/redis/services/container.properties
b/test-infra/camel-test-infra-redis/src/main/resources/org/apache/camel/test/infra/redis/services/container.properties
index b0453e5cb238..b13833d993e9 100644
---
a/test-infra/camel-test-infra-redis/src/main/resources/org/apache/camel/test/infra/redis/services/container.properties
+++
b/test-infra/camel-test-infra-redis/src/main/resources/org/apache/camel/test/infra/redis/services/container.properties
@@ -17,3 +17,4 @@
redis.container=mirror.gcr.io/redis:7.4.0-alpine
redis.container.version.include=alpine
redis.container.version.exclude=rc,beta
+redis.container.version.freeze.major=true