Skip to content

Commit 825351f

Browse files
authored
Merge pull request #491 from redis/fix_stackbrew_generation
Fix stackbrew generation when 8.X and 8.X.Y formats are mixed toghether
2 parents de8bb5f + 96ac2d9 commit 825351f

File tree

8 files changed

+1896
-38
lines changed

8 files changed

+1896
-38
lines changed

.github/workflows/build_release_automation.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ jobs:
6262
--entrypoint /bin/bash \
6363
test-image:latest \
6464
-c "
65-
cd release-automation
65+
cd /release-automation
6666
set -e
6767
echo '=== Installing test dependencies ==='
68-
pip install pytest pytest-cov
68+
uv sync --frozen --all-extras
6969
echo '=== Running tests ==='
70-
pytest -v tests/
70+
uv run pytest
7171
"
7272
7373
- name: Log in to Container Registry

release-automation/docker/Dockerfile

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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_integration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_invalid_major_version(self):
2626
result = self.runner.invoke(app, ["generate-stackbrew-content", "0"])
2727
assert result.exit_code != 0
2828

29-
@patch('stackbrew_generator.git_operations.GitClient')
29+
@patch('stackbrew_generator.cli.GitClient')
3030
def test_no_tags_found(self, mock_git_client_class):
3131
"""Test handling when no tags are found."""
3232
# Mock git client to return no tags
@@ -36,7 +36,7 @@ def test_no_tags_found(self, mock_git_client_class):
3636

3737
result = self.runner.invoke(app, ["generate-stackbrew-content", "99"])
3838
assert result.exit_code == 1
39-
assert "No tags found" in result.stderr
39+
assert "No versions found for major version 99" in result.stderr
4040

4141
@patch('stackbrew_generator.version_filter.VersionFilter.get_actual_major_redis_versions')
4242
def test_no_versions_found(self, mock_get_versions):

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)