Skip to content

Commit e2ead16

Browse files
committed
Fix stackbrew generation when 8.X and 8.X.Y formats are mixed toghether
Update to recent RedisVersion with fixes in sorting and comparison Add mixed format tests for mixed use-case, e.g. when 8.4.0 is ga and 8.4-rc1 is rc.
1 parent 083c9af commit e2ead16

File tree

4 files changed

+169
-31
lines changed

4 files changed

+169
-31
lines changed

release-automation/src/stackbrew_generator/models.py

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Data models for stackbrew library generation."""
22

3+
import functools
34
import re
45
from enum import Enum
56
from typing import List, Optional
@@ -14,8 +15,12 @@ class DistroType(str, Enum):
1415
DEBIAN = "debian"
1516

1617

18+
@functools.total_ordering
1719
class RedisVersion(BaseModel):
18-
"""Represents a parsed Redis version."""
20+
"""Represents a parsed Redis version.
21+
22+
TODO: This class duplicates the code from redis-developer/redis-oss-release-automation
23+
"""
1924

2025
major: int = Field(..., ge=1, description="Major version number")
2126
minor: int = Field(..., ge=0, description="Minor version number")
@@ -63,20 +68,46 @@ def is_eol(self) -> bool:
6368
"""Check if this version is end-of-life."""
6469
return self.suffix.lower().endswith("-eol")
6570

71+
@property
72+
def is_rc(self) -> bool:
73+
"""Check if this version is a release candidate."""
74+
return self.suffix.lower().startswith("-rc")
75+
76+
@property
77+
def is_ga(self) -> bool:
78+
"""Check if this version is a general availability (GA) release."""
79+
return not self.is_milestone
80+
81+
@property
82+
def is_internal(self) -> bool:
83+
"""Check if this version is an internal release."""
84+
return bool(re.search(r"-int\d*$", self.suffix.lower()))
85+
6686
@property
6787
def mainline_version(self) -> str:
6888
"""Get the mainline version string (major.minor)."""
6989
return f"{self.major}.{self.minor}"
7090

7191
@property
72-
def sort_key(self) -> str:
73-
suffix_weight = 0
74-
if self.suffix.startswith("rc"):
75-
suffix_weight = 100
76-
elif self.suffix.startswith("m"):
77-
suffix_weight = 50
92+
def suffix_weight(self) -> str:
93+
# warning: using lexicographic order, letters doesn't have any meaning except for ordering
94+
suffix_weight = ""
95+
if self.is_ga:
96+
suffix_weight = "QQ"
97+
if self.is_rc:
98+
suffix_weight = "LL"
99+
elif self.suffix.startswith("-m"):
100+
suffix_weight = "II"
101+
102+
# internal versions are always lower than their GA/rc/m counterparts
103+
if self.is_internal:
104+
suffix_weight = suffix_weight[:1] + "E"
105+
106+
return suffix_weight
78107

79-
return f"{self.major}.{self.minor}.{self.patch or 0}.{suffix_weight}.{self.suffix}"
108+
@property
109+
def sort_key(self) -> str:
110+
return f"{self.major}.{self.minor}.{self.patch or 0}.{self.suffix_weight}{self.suffix}"
80111

81112
def __str__(self) -> str:
82113
"""String representation of the version."""
@@ -90,21 +121,17 @@ def __lt__(self, other: "RedisVersion") -> bool:
90121
if not isinstance(other, RedisVersion):
91122
return NotImplemented
92123

93-
# Compare major.minor.patch first
94-
self_tuple = (self.major, self.minor, self.patch or 0)
95-
other_tuple = (other.major, other.minor, other.patch or 0)
124+
return self.sort_key < other.sort_key
96125

97-
if self_tuple != other_tuple:
98-
return self_tuple < other_tuple
126+
def __eq__(self, other: object) -> bool:
127+
if not isinstance(other, RedisVersion):
128+
return NotImplemented
99129

100-
# If numeric parts are equal, compare suffixes
101-
# Empty suffix (GA) comes after suffixes (milestones)
102-
if not self.suffix and other.suffix:
103-
return False
104-
if self.suffix and not other.suffix:
105-
return True
130+
return self.sort_key == other.sort_key
106131

107-
return self.suffix < other.suffix
132+
def __hash__(self) -> int:
133+
"""Hash for use in sets and dicts."""
134+
return hash((self.major, self.minor, self.patch or 0, self.suffix))
108135

109136

110137
class Distribution(BaseModel):

release-automation/src/stackbrew_generator/version_filter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,13 @@ def filter_actual_versions(self, versions: List[Tuple[RedisVersion, str, str]])
105105
patch_versions = OrderedDict()
106106

107107
for version, commit, tag_ref in versions:
108-
patch_key = (version.major, version.minor, version.patch)
108+
patch_key = (version.major, version.minor, version.patch or 0)
109109
if patch_key not in patch_versions:
110110
patch_versions[patch_key] = (version, commit, tag_ref)
111111
elif patch_versions[patch_key][0].is_milestone and not version.is_milestone:
112112
# GA always takes precedence over milestone for the same major.minor.patch
113113
patch_versions[patch_key] = (version, commit, tag_ref)
114114

115-
print(patch_versions.values())
116115
filtered_versions = []
117116
mainlines_with_ga = set()
118117

release-automation/tests/test_models.py

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@ def test_parse_eol_version(self):
4949
assert version.suffix == "-eol"
5050
assert version.is_eol is True
5151

52+
def test_parse_rc_internal_version(self):
53+
"""Test parsing RC internal version."""
54+
version = RedisVersion.parse("8.2.1-rc2-int3")
55+
assert version.major == 8
56+
assert version.minor == 2
57+
assert version.patch == 1
58+
assert version.suffix == "-rc2-int3"
59+
assert version.is_rc is True
60+
assert version.is_internal is True
61+
assert len(version.sort_key) > 0
62+
63+
version = RedisVersion.parse("8.4-int")
64+
assert version.major == 8
65+
assert version.minor == 4
66+
assert version.patch == None
67+
assert version.suffix == "-int"
68+
assert version.is_internal is True
69+
assert len(version.sort_key) > 0
70+
5271
def test_parse_invalid_version(self):
5372
"""Test parsing invalid version strings."""
5473
with pytest.raises(ValueError):
@@ -82,22 +101,67 @@ def test_string_representation(self):
82101

83102
def test_version_comparison(self):
84103
"""Test version comparison for sorting."""
85-
v1 = RedisVersion.parse("8.2.1")
86-
v2 = RedisVersion.parse("8.2.2")
87-
v3 = RedisVersion.parse("8.2.1-m01")
88-
v4 = RedisVersion.parse("8.3.0")
104+
v8_2_1 = RedisVersion.parse("8.2.1")
105+
v8_2_2 = RedisVersion.parse("8.2.2")
106+
v8_2_1_m_01 = RedisVersion.parse("8.2.1-m01")
107+
v8_2_1_rc_01 = RedisVersion.parse("8.2.1-rc01")
108+
v8_2_1_rc_01_int_1 = RedisVersion.parse("8.2.1-rc01-int1")
109+
v8_3_0 = RedisVersion.parse("8.3.0")
110+
v8_3_0_rc_1 = RedisVersion.parse("8.3.0-rc1")
111+
v8_3_0_rc_1_int_1 = RedisVersion.parse("8.3.0-rc1-int1")
112+
v8_3_0_rc_1_int_2 = RedisVersion.parse("8.3.0-rc1-int2")
113+
v8_4 = RedisVersion.parse("8.4")
114+
v8_4_rc_1 = RedisVersion.parse("8.4-rc1")
115+
v8_6_int = RedisVersion.parse("8.6-int")
89116

90117
# Test numeric comparison
91-
assert v1 < v2
92-
assert v2 < v4
118+
assert v8_2_1 < v8_2_2
119+
assert v8_2_2 < v8_3_0
93120

94121
# Test milestone vs GA (GA comes after milestone)
95-
assert v3 < v1
122+
assert v8_2_1_m_01 < v8_2_1
123+
124+
assert v8_3_0_rc_1 < v8_3_0
125+
126+
assert v8_2_1_rc_01 > v8_2_1_m_01
127+
assert v8_2_1_rc_01_int_1 > v8_2_1_m_01
128+
assert v8_2_1_rc_01_int_1 < v8_2_1_rc_01
129+
130+
assert v8_3_0_rc_1_int_1 < v8_3_0_rc_1_int_2
131+
132+
assert v8_3_0_rc_1 > v8_3_0_rc_1_int_1
133+
assert v8_3_0_rc_1 > v8_3_0_rc_1_int_2
96134

97135
# Test sorting
98-
versions = [v4, v1, v3, v2]
136+
versions = [
137+
v8_3_0,
138+
v8_2_1,
139+
v8_2_1_m_01,
140+
v8_2_2,
141+
v8_3_0_rc_1,
142+
v8_3_0_rc_1_int_1,
143+
v8_3_0_rc_1_int_2,
144+
v8_6_int,
145+
v8_4,
146+
v8_4_rc_1,
147+
v8_2_1_rc_01,
148+
v8_2_1_rc_01_int_1,
149+
]
99150
sorted_versions = sorted(versions)
100-
assert sorted_versions == [v3, v1, v2, v4]
151+
assert sorted_versions == [
152+
v8_2_1_m_01,
153+
v8_2_1_rc_01_int_1,
154+
v8_2_1_rc_01,
155+
v8_2_1,
156+
v8_2_2,
157+
v8_3_0_rc_1_int_1,
158+
v8_3_0_rc_1_int_2,
159+
v8_3_0_rc_1,
160+
v8_3_0,
161+
v8_4_rc_1,
162+
v8_4,
163+
v8_6_int,
164+
]
101165

102166

103167
class TestDistribution:

release-automation/tests/test_version_filter.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,54 @@ def test_filter_actual_versions_milestone_only(self):
261261
expected_versions = ["8.2.1-m02", "8.1.0-m01"]
262262
assert version_strings == expected_versions
263263

264+
def test_filter_actual_versions_with_short_rc(self):
265+
"""Test actual version filtering with short (without patch) versions."""
266+
version_filter = VersionFilter(MockGitClient())
267+
268+
versions = create_version_tuples([
269+
"v8.4.0",
270+
"v8.4-rc1",
271+
"v8.2.3",
272+
])
273+
274+
result = version_filter.filter_actual_versions(versions)
275+
276+
version_strings = [str(v[0]) for v in result]
277+
expected_versions = ["8.4.0", "8.2.3"]
278+
assert version_strings == expected_versions
279+
280+
def test_filter_actual_versions_with_short_ga(self):
281+
"""Test actual version filtering with short (without patch) versions."""
282+
version_filter = VersionFilter(MockGitClient())
283+
284+
versions = create_version_tuples([
285+
"v8.4",
286+
"v8.4.0-rc1",
287+
"v8.2.3",
288+
])
289+
290+
result = version_filter.filter_actual_versions(versions)
291+
292+
version_strings = [str(v[0]) for v in result]
293+
expected_versions = ["8.4", "8.2.3"]
294+
assert version_strings == expected_versions
295+
296+
def test_filter_actual_versions_with_short_both_ga_and_rc(self):
297+
"""Test actual version filtering with short (without patch) versions."""
298+
version_filter = VersionFilter(MockGitClient())
299+
300+
versions = create_version_tuples([
301+
"v8.4",
302+
"v8.4-rc1",
303+
"v8.2.3",
304+
])
305+
306+
result = version_filter.filter_actual_versions(versions)
307+
308+
version_strings = [str(v[0]) for v in result]
309+
expected_versions = ["8.4", "8.2.3"]
310+
assert version_strings == expected_versions
311+
264312
def test_filter_actual_versions_empty(self):
265313
"""Test actual version filtering with empty input."""
266314
version_filter = VersionFilter(MockGitClient())

0 commit comments

Comments
 (0)