Skip to content

Commit 276fc27

Browse files
authored
Add support for nested versions (#2)
1 parent 6a8a803 commit 276fc27

File tree

11 files changed

+596
-24
lines changed

11 files changed

+596
-24
lines changed

main.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,19 @@ def main():
5555
)
5656
continue
5757

58-
latest_version = sorted(package.versions, key=lambda v: v.version)[-1].version
59-
60-
if not rhdh_plugin_needs_update(latest_version, plugin.current_version):
58+
# sort by version, considering dual versions
59+
latest_package_version = sorted(
60+
package.versions, key=lambda v: (v.version, v.second_version or "")
61+
)[-1]
62+
latest_version = latest_package_version.version
63+
latest_second_version = latest_package_version.second_version
64+
65+
if not rhdh_plugin_needs_update(
66+
latest_version,
67+
plugin.current_version,
68+
latest_second_version,
69+
plugin.current_second_version,
70+
):
6171
logger.info(
6272
f"plugin {plugin.plugin_name} is up-to-date "
6373
f"(version: {plugin.current_version})"
@@ -72,7 +82,11 @@ def main():
7282
if UPDATE_PR_STRATEGY == GithubPullRequestStrategy.JOINT:
7383
logger.debug("caching plugin update for joint PR...")
7484
plugin_updates.append(
75-
RHDHPluginUpdate(rhdh_plugin=plugin, new_version=latest_version)
85+
RHDHPluginUpdate(
86+
rhdh_plugin=plugin,
87+
new_version=latest_version,
88+
new_second_version=latest_second_version,
89+
)
7690
)
7791
continue
7892

@@ -84,7 +98,7 @@ def main():
8498

8599
try:
86100
updated_yaml = rhdh_config_updater.update_rhdh_plugin(
87-
plugin, latest_version
101+
plugin, latest_version, latest_second_version
88102
)
89103

90104
pr_url = gh_api_client.create_pull_request(

src/github_api_client.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from github import Auth, Github
66
from github.ContentFile import ContentFile
77
from github.Repository import Repository
8-
from packaging.version import Version
98
from requests import Response
109

1110
from src.constants import GITHUB_REF, UPDATE_PR_STRATEGY, logger
@@ -16,7 +15,7 @@
1615
RHDHPluginPackageVersion,
1716
RHDHPluginUpdaterConfig,
1817
)
19-
from src.utils import match_tag_prefix
18+
from src.utils import match_tag_prefix, parse_dual_version
2019

2120

2221
class GithubAPIClient:
@@ -111,12 +110,17 @@ def _convert_to_rhdh_plugin_package(
111110
if not isinstance(created_at, str):
112111
continue
113112

113+
# remove prefix and parse potential dual version
114+
version_string = tag.replace(matched_prefix, "")
115+
version, second_version = parse_dual_version(version_string)
116+
114117
logger.debug(f"found version {tag} for package {package_name}")
115118
versions.append(
116119
RHDHPluginPackageVersion(
117120
name=str(v.get("name", "")),
118-
version=Version(tag.replace(matched_prefix, "")),
121+
version=version,
119122
created_at=created_at,
123+
second_version=second_version,
120124
)
121125
)
122126

src/loader.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
from src.exceptions import InvalidRHDHPluginPackageDefinitionException
1212
from src.types import RHDHPlugin, RHDHPluginUpdaterConfig
13-
from src.utils import get_plugins_list_from_dict, match_tag_prefix
13+
from src.utils import get_plugins_list_from_dict, match_tag_prefix, parse_dual_version
1414

1515

1616
class RHDHPluginsConfigLoader:
@@ -37,7 +37,9 @@ def _fetch_plugins_by_location(
3737

3838
return plugins_list if isinstance(plugins_list, list) else []
3939

40-
def _parse_package_string(self, package: "str") -> "dict[str, str | Version]":
40+
def _parse_package_string(
41+
self, package: "str"
42+
) -> "dict[str, str | Version | None]":
4143
"""
4244
parses the OCI package string to extract plugin info.
4345
@@ -89,13 +91,16 @@ def _parse_package_string(self, package: "str") -> "dict[str, str | Version]":
8991
f"Tag {raw_version} not valid for package {package}"
9092
)
9193

92-
version = Version(version=raw_version.replace(matched_prefix, ""))
94+
# remove prefix and parse potential dual version
95+
version_string = raw_version.replace(matched_prefix, "")
96+
version, second_version = parse_dual_version(version_string)
9397

9498
package_name = f"rhdh-plugin-export-overlays/{name}"
9599

96100
return {
97101
"package_name": package_name,
98102
"version": version,
103+
"second_version": second_version,
99104
"plugin_name": plugin_name,
100105
}
101106

@@ -134,6 +139,7 @@ def _convert_rhdhplugin_list(
134139
current_version=parsed["version"], # type: ignore
135140
plugin_name=str(parsed["plugin_name"]),
136141
disabled=disabled,
142+
current_second_version=parsed.get("second_version"), # type: ignore
137143
)
138144
)
139145
return rhdh_plugins

src/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class RHDHPluginPackageVersion:
1515
name: "str"
1616
version: "Version"
1717
created_at: "str"
18+
second_version: "Version | None" = None
1819

1920

2021
@dataclass
@@ -37,6 +38,7 @@ class RHDHPlugin:
3738
current_version: "Version"
3839
plugin_name: "str"
3940
disabled: "bool"
41+
current_second_version: "Version | None" = None
4042

4143

4244
@dataclass
@@ -47,6 +49,7 @@ class RHDHPluginUpdate:
4749

4850
rhdh_plugin: "RHDHPlugin"
4951
new_version: "Version"
52+
new_second_version: "Version | None" = None
5053

5154

5255
class RHDHPluginUpdaterConfig:

src/updater.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,27 @@ def __init__(
2323
self.config_path = config_path
2424
self.config_location = config_location
2525

26+
def _build_version_string(
27+
self, version: "Version", second_version: "Version | None" = None
28+
) -> "str":
29+
"""
30+
builds a version string, including second version if present
31+
"""
32+
if second_version:
33+
return f"{version}__{second_version}"
34+
return str(version)
35+
2636
def _find_current_tag_prefix(self, content: "str", plugin: "RHDHPlugin") -> "str":
2737
"""
2838
finds which tag prefix is currently being used for the plugin
2939
by searching the content for the plugin with any of the configured prefixes
3040
"""
41+
version_string = self._build_version_string(
42+
plugin.current_version, plugin.current_second_version
43+
)
44+
3145
for prefix in RHDHPluginUpdaterConfig.GH_PACKAGE_TAG_PREFIX:
32-
test_tag = f"{prefix}{plugin.current_version}"
46+
test_tag = f"{prefix}{version_string}"
3347
pattern = re.compile(
3448
rf"package:\s+(?:oci://)?[^\s]*{re.escape(plugin.plugin_name)}[^\s]*:{re.escape(test_tag)}![^\s]*",
3549
re.MULTILINE,
@@ -44,20 +58,26 @@ def _update_plugin_version_in_content(
4458
content: "str",
4559
plugin: "RHDHPlugin",
4660
new_version: "Version",
61+
new_second_version: "Version | None" = None,
4762
) -> "str":
4863
"""
4964
update a single plugin version in the YAML content using string replacement.
5065
This preserves the original YAML formatting.
5166
"""
67+
new_version_string = self._build_version_string(new_version, new_second_version)
5268
logger.debug(
53-
f"updating config for plugin {plugin.plugin_name} to version {new_version}"
69+
f"updating config for plugin {plugin.plugin_name} "
70+
f"to version {new_version_string}"
5471
)
5572

5673
# Find which tag prefix is currently used for this plugin
5774
current_prefix = self._find_current_tag_prefix(content, plugin)
5875

59-
old_tag = f"{current_prefix}{plugin.current_version}"
60-
new_tag = f"{current_prefix}{new_version}"
76+
old_version_string = self._build_version_string(
77+
plugin.current_version, plugin.current_second_version
78+
)
79+
old_tag = f"{current_prefix}{old_version_string}"
80+
new_tag = f"{current_prefix}{new_version_string}"
6181

6282
# pattern to find the specific plugin's package line with the old version
6383
pattern = re.compile(
@@ -71,12 +91,12 @@ def _update_plugin_version_in_content(
7191
if updated_content != content:
7292
logger.debug(
7393
f"updated config for {plugin.plugin_name} from "
74-
f"{plugin.current_version} to {new_version}"
94+
f"{old_version_string} to {new_version_string}"
7595
)
7696
else:
7797
logger.warning(
7898
f"no match found for plugin {plugin.plugin_name} with version "
79-
f"{plugin.current_version}"
99+
f"{old_version_string}"
80100
)
81101

82102
return updated_content
@@ -85,6 +105,7 @@ def update_rhdh_plugin(
85105
self,
86106
rhdh_plugin: "RHDHPlugin",
87107
new_version: "Version",
108+
new_second_version: "Version | None" = None,
88109
) -> "str":
89110
"""
90111
updates a single plugin and return the updated YAML content.
@@ -93,7 +114,7 @@ def update_rhdh_plugin(
93114
content = f.read()
94115

95116
updated_content = self._update_plugin_version_in_content(
96-
content, rhdh_plugin, new_version
117+
content, rhdh_plugin, new_version, new_second_version
97118
)
98119

99120
return updated_content
@@ -110,7 +131,10 @@ def bulk_update_rhdh_plugins(
110131

111132
for update in updates:
112133
content = self._update_plugin_version_in_content(
113-
content, update.rhdh_plugin, update.new_version
134+
content,
135+
update.rhdh_plugin,
136+
update.new_version,
137+
update.new_second_version,
114138
)
115139

116140
return content

src/utils.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,69 @@ def get_plugins_list_from_dict(
2424
return [] if not isinstance(current, list) else current
2525

2626

27+
def parse_dual_version(version_string: "str") -> "tuple[Version, Version | None]":
28+
"""
29+
parses a version string that may contain dual versions separated by '__'.
30+
"""
31+
if "__" in version_string:
32+
parts = version_string.split("__", 1)
33+
if len(parts) == 2 and parts[1]:
34+
return (Version(parts[0]), Version(parts[1]))
35+
36+
# single version or invalid dual version
37+
return (Version(version_string.split("__")[0]), None)
38+
39+
40+
def compare_versions(
41+
version1: "Version",
42+
version2: "Version",
43+
secondary1: "Version | None" = None,
44+
secondary2: "Version | None" = None,
45+
) -> "int":
46+
"""
47+
compares two versions with optional secondary versions
48+
"""
49+
# compare primary versions
50+
if version1 < version2:
51+
return -1
52+
if version1 > version2:
53+
return 1
54+
55+
# primary versions are equal, compare secondary versions
56+
if secondary1 is None and secondary2 is None:
57+
return 0
58+
59+
if secondary1 is None:
60+
return -1 # version2 has secondary, so it's greater
61+
62+
if secondary2 is None:
63+
return 1 # version1 has secondary, so it's greater
64+
65+
# both have secondary versions
66+
if secondary1 < secondary2:
67+
return -1
68+
if secondary1 > secondary2:
69+
return 1
70+
71+
return 0
72+
73+
2774
def rhdh_plugin_needs_update(
28-
latest_version: "Version", current_version: "Version"
75+
latest_version: "Version",
76+
current_version: "Version",
77+
latest_secondary: "Version | None" = None,
78+
current_secondary: "Version | None" = None,
2979
) -> "bool":
3080
"""
31-
checks if the latest version is greater than the current version
81+
checks if the latest version is greater than the current version.
82+
supports dual versions with optional secondary version components.
3283
"""
33-
return latest_version > current_version
84+
return (
85+
compare_versions(
86+
latest_version, current_version, latest_secondary, current_secondary
87+
)
88+
> 0
89+
)
3490

3591

3692
def match_tag_prefix(tag: "str") -> "str | None":

tests/conftest.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,41 @@ def sample_plugin_with_previous_prefix() -> "RHDHPlugin":
252252
plugin_name="another-plugin",
253253
disabled=False,
254254
)
255+
256+
257+
@pytest.fixture
258+
def sample_yaml_content_with_dual_versions() -> "str":
259+
"""
260+
creates a sample YAML content with plugins using dual versions.
261+
"""
262+
return """global:
263+
dynamic:
264+
plugins:
265+
- disabled: false
266+
package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/dual-version-plugin:next__1.42.5__0.1.0!dual-version-plugin
267+
- disabled: false
268+
package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-mcp-actions-backend:next__0.1.2!backstage-plugin-mcp-actions-backend
269+
"""
270+
271+
272+
@pytest.fixture
273+
def temp_yaml_file_with_dual_versions() -> "Any":
274+
"""
275+
creates a temporary YAML file with dual version plugins for testing.
276+
"""
277+
content = """global:
278+
dynamic:
279+
plugins:
280+
- disabled: false
281+
package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/dual-version-plugin:next__1.42.5__0.1.0!dual-version-plugin
282+
- disabled: false
283+
package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-mcp-actions-backend:next__0.1.2!backstage-plugin-mcp-actions-backend
284+
"""
285+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
286+
f.write(content)
287+
temp_path = f.name
288+
289+
yield temp_path
290+
291+
# clean temp files
292+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)