From 1c3222c7de83d6bb812a42c759a6bea71bc77be5 Mon Sep 17 00:00:00 2001 From: Jordan Dubrick Date: Thu, 1 Oct 2026 12:26:34 -0400 Subject: [PATCH] feat: extend plugin replacer to handle new rhdh chart Signed-off-by: Jordan Dubrick --- README.md | 14 +++--- action.yml | 4 +- src/constants.py | 12 ++++- src/github_api_client.py | 6 +-- src/loader.py | 24 +++++++--- tests/test_loader.py | 96 ++++++++++++++++++++++++++++++++++++++-- tests/test_updater.py | 39 +++++++++++++++- 7 files changed, 173 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 26a4555..62baff9 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Specify custom tag prefixes to filter plugin versions: | Input | Description | Required | Default | | -------------------- | ---------------------------------------------------------------------------------------- | -------- | ------------------------ | | `config-path` | Path to the dynamic plugins config YAML file (e.g., `charts/rhdh/values.yaml`) | Yes | - | -| `config-location` | Location of the dynamic plugins config inside the YAML file | No | `global.dynamic.plugins` | +| `config-location` | Location of the dynamic plugins config inside the YAML file | No | `redhat-developer-hub.dynamicPlugins.plugins` | | `github-token` | GitHub token for API access and PR creation | Yes | - | | `update-pr-strategy` | PR creation strategy: `separate` or `joint` | No | `separate` | | `pr-creation-limit` | Maximum number of PRs to create (0 for unlimited, only applies with `separate` strategy) | No | `0` | @@ -155,18 +155,22 @@ Specify custom tag prefixes to filter plugin versions: ## Supported Plugin Format -The action works with RHDH dynamic plugins defined in the following format: +The action reads both dynamic and intelligent assistant plugins from a parent chart's values file: ```yaml -global: - dynamic: +redhat-developer-hub: + dynamicPlugins: plugins: - disabled: false package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-name:next__1.0.0 + intelligentAssistant: + plugins: - disabled: false - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/another-plugin:next__2.0.0 + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/lightspeed-plugin:next__2.0.0 ``` +If a new list is missing, the action falls back to its legacy location: `global.dynamic.plugins` for dynamic plugins and `global.lightspeed.plugins` for intelligent assistant plugins. The two fallbacks are independent. An empty new list takes precedence over its legacy list. Matching package tags are updated wherever they occur in the file. The `config-location` input overrides the primary dynamic plugins location. + > **Note:** The legacy format with `!plugin-name` suffix (e.g., `...:next__1.0.0!backstage-plugin-name`) is also supported for backward compatibility. ### Tag Prefixes diff --git a/action.yml b/action.yml index 4029276..0407d68 100644 --- a/action.yml +++ b/action.yml @@ -9,9 +9,9 @@ inputs: description: 'Path to the dynamic plugins config YAML file (e.g., charts/rhdh/values.yaml)' required: true config-location: - description: 'Location of the dynamic plugins config inside the YAML file (e.g., global.dynamic.plugins)' + description: 'Location of the dynamic plugins config inside the YAML file (e.g., redhat-developer-hub.dynamicPlugins.plugins)' required: false - default: 'global.dynamic.plugins' + default: 'redhat-developer-hub.dynamicPlugins.plugins' github-token: description: 'GitHub token for API access and PR creation' required: true diff --git a/src/constants.py b/src/constants.py index 5d38fda..f7241e2 100644 --- a/src/constants.py +++ b/src/constants.py @@ -10,18 +10,26 @@ # DYNAMIC_PLUGINS_CONFIG_YAML_LOCATION: is the location of the dynamic # plugins config inside the yaml file. DYNAMIC_PLUGINS_CONFIG_YAML_LOCATION = os.getenv( - "DYNAMIC_PLUGINS_CONFIG_YAML_LOCATION", "global.dynamic.plugins" + "DYNAMIC_PLUGINS_CONFIG_YAML_LOCATION", + "redhat-developer-hub.dynamicPlugins.plugins", ) # DYNAMIC_PLUGINS_CONFIG_YAML_EXTRA_LOCATIONS: list of # additional locations to check for plugins in the yaml file. _extra_locations_str = os.getenv( - "DYNAMIC_PLUGINS_CONFIG_YAML_EXTRA_LOCATIONS", "global.lightspeed.plugins" + "DYNAMIC_PLUGINS_CONFIG_YAML_EXTRA_LOCATIONS", + "redhat-developer-hub.intelligentAssistant.plugins", ) DYNAMIC_PLUGINS_CONFIG_YAML_EXTRA_LOCATIONS = [ loc.strip() for loc in _extra_locations_str.split(",") if loc.strip() ] +# Use the legacy location only when the corresponding chart values list is absent. +DYNAMIC_PLUGINS_CONFIG_YAML_FALLBACK_LOCATIONS = { + "redhat-developer-hub.dynamicPlugins.plugins": "global.dynamic.plugins", + "redhat-developer-hub.intelligentAssistant.plugins": "global.lightspeed.plugins", +} + # GITHUB_TOKEN: is the GitHub token to use for authentication GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "") diff --git a/src/github_api_client.py b/src/github_api_client.py index 3bc9f2d..0e9b262 100644 --- a/src/github_api_client.py +++ b/src/github_api_client.py @@ -39,8 +39,8 @@ def __init__(self, token: "str", per_page=100) -> "None": ) def _fetch_next( - self, response: "Response", url: "str", params: "dict[str, int]" - ) -> "tuple[str, dict[str, int]]": + self, response: "Response", url: "str", params: "dict[str, str | int]" + ) -> "tuple[str, dict[str, str | int]]": """ extracts the next URL from the response links if available """ @@ -62,7 +62,7 @@ def _paginate( ::raises:: requests.HTTPError If the API request fails """ items: "list[dict[str, Any]]" = [] - params = { + params: "dict[str, str | int]" = { "per_page": self.per_page, } diff --git a/src/loader.py b/src/loader.py index 3cbc340..6e586b4 100644 --- a/src/loader.py +++ b/src/loader.py @@ -5,6 +5,7 @@ from src.constants import ( DYNAMIC_PLUGINS_CONFIG_YAML_EXTRA_LOCATIONS, + DYNAMIC_PLUGINS_CONFIG_YAML_FALLBACK_LOCATIONS, DYNAMIC_PLUGINS_CONFIG_YAML_FILE_PATH, DYNAMIC_PLUGINS_CONFIG_YAML_LOCATION, logger, @@ -47,6 +48,19 @@ def _fetch_plugins_by_location( return plugins_list if isinstance(plugins_list, list) else [] + def _resolve_plugins_location( + self, data: "dict[str, str | int | bool]", location: "str" + ) -> "str": + current = data + for key in location.split("."): + if not isinstance(current, dict) or key not in current: + return DYNAMIC_PLUGINS_CONFIG_YAML_FALLBACK_LOCATIONS.get( + location, location + ) + current = current[key] + + return location + def _parse_package_string( self, package: "str" ) -> "dict[str, str | Version | None]": @@ -167,11 +181,11 @@ def load_rhdh_plugins(self) -> "list[RHDHPlugin]": with open(self.config_path, "r") as f: data = yaml.safe_load(f) - all_plugins_list = self._fetch_plugins_by_location(data, strict=False) - for extra_location in self.extra_config_locations: + all_plugins_list = [] + for location in [self.config_location, *self.extra_config_locations]: + resolved_location = self._resolve_plugins_location(data, location) all_plugins_list.extend( - self._fetch_plugins_by_location(data, extra_location, strict=False) + self._fetch_plugins_by_location(data, resolved_location, strict=False) ) - rhdh_plugins = self._convert_rhdhplugin_list(all_plugins_list) - return rhdh_plugins + return self._convert_rhdhplugin_list(all_plugins_list) diff --git a/tests/test_loader.py b/tests/test_loader.py index 175fdac..6cbd63d 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,6 +10,16 @@ from src.loader import RHDHPluginsConfigLoader +def _plugin(name: "str", version: "str") -> "dict[str, str | bool]": + return { + "disabled": False, + "package": ( + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/" + f"{name}:next__{version}" + ), + } + + class TestRHDHPluginsConfigLoader: """ handles all tests for RHDHPluginsConfigLoader class. @@ -18,7 +28,7 @@ class TestRHDHPluginsConfigLoader: def test_init_with_defaults(self) -> "None": loader = RHDHPluginsConfigLoader() assert loader.config_path == "dynamic-plugins.yaml" - assert loader.config_location == "global.dynamic.plugins" + assert loader.config_location == "redhat-developer-hub.dynamicPlugins.plugins" def test_init_with_custom_values(self) -> "None": loader = RHDHPluginsConfigLoader( @@ -31,7 +41,9 @@ def test_fetch_plugins_by_location( self, sample_config_data: "dict[str, Any]" ) -> "None": loader = RHDHPluginsConfigLoader() - plugins = loader._fetch_plugins_by_location(sample_config_data) + plugins = loader._fetch_plugins_by_location( + sample_config_data, "global.dynamic.plugins" + ) assert isinstance(plugins, list) assert len(plugins) == 4 @@ -39,7 +51,7 @@ def test_fetch_plugins_by_location( def test_fetch_plugins_by_location_empty_when_not_list(self) -> "None": loader = RHDHPluginsConfigLoader() data = {"global": {"dynamic": {"plugins": "not_a_list"}}} - plugins = loader._fetch_plugins_by_location(data) + plugins = loader._fetch_plugins_by_location(data, "global.dynamic.plugins") assert plugins == [] @@ -361,7 +373,9 @@ def test_convert_rhdhplugin_list_without_exclamation(self) -> "None": def test_init_with_default_extra_locations(self) -> "None": loader = RHDHPluginsConfigLoader() - assert loader.extra_config_locations == ["global.lightspeed.plugins"] + assert loader.extra_config_locations == [ + "redhat-developer-hub.intelligentAssistant.plugins" + ] def test_init_with_custom_extra_locations(self) -> "None": loader = RHDHPluginsConfigLoader( @@ -404,6 +418,80 @@ def test_load_rhdh_plugins_from_both_locations( assert "backstage-plugin-mcp-actions-backend" in plugin_names assert "lightspeed-plugin" in plugin_names + @pytest.mark.parametrize( + ("dynamic_versions", "assistant_versions", "expected"), + [ + ( + ["2.0.0"], + ["2.0.0"], + {"dynamic": Version("2.0.0"), "assistant": Version("2.0.0")}, + ), + ( + ["2.0.0"], + None, + {"dynamic": Version("2.0.0"), "assistant": Version("1.0.0")}, + ), + ( + None, + ["2.0.0"], + {"dynamic": Version("1.0.0"), "assistant": Version("2.0.0")}, + ), + ([], [], {}), + ], + ) + def test_new_locations_precede_independent_legacy_fallbacks( + self, + tmp_path: "Path", + dynamic_versions: "list[str] | None", + assistant_versions: "list[str] | None", + expected: "dict[str, Version]", + ) -> "None": + values = { + "global": { + "dynamic": {"plugins": [_plugin("dynamic", "1.0.0")]}, + "lightspeed": {"plugins": [_plugin("assistant", "1.0.0")]}, + } + } + hub = {} + if dynamic_versions is not None: + hub["dynamicPlugins"] = { + "plugins": [_plugin("dynamic", v) for v in dynamic_versions] + } + if assistant_versions is not None: + hub["intelligentAssistant"] = { + "plugins": [_plugin("assistant", v) for v in assistant_versions] + } + values["redhat-developer-hub"] = hub + config_path = tmp_path / "values.yaml" + config_path.write_text(yaml.safe_dump(values)) + + plugins = RHDHPluginsConfigLoader(config_path=config_path).load_rhdh_plugins() + + assert {p.plugin_name: p.current_version for p in plugins} == expected + assert len(plugins) == len(expected) + + def test_load_rhdh_plugins_custom_primary_location( + self, tmp_path: "Path" + ) -> "None": + config_path = tmp_path / "values.yaml" + config_path.write_text( + yaml.safe_dump( + { + "custom": {"plugins": [_plugin("custom", "2.0.0")]}, + "global": { + "dynamic": {"plugins": [_plugin("legacy", "1.0.0")]}, + "lightspeed": {"plugins": [_plugin("lightspeed", "1.0.0")]}, + }, + } + ) + ) + + plugins = RHDHPluginsConfigLoader( + config_path=config_path, config_location="custom.plugins" + ).load_rhdh_plugins() + + assert {p.plugin_name for p in plugins} == {"custom", "lightspeed"} + def test_load_rhdh_plugins_from_lightspeed_only( self, temp_yaml_file_lightspeed_only: "str" ) -> "None": diff --git a/tests/test_updater.py b/tests/test_updater.py index 6fc48b5..0e23b3c 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -4,6 +4,7 @@ import pytest from packaging.version import Version +from src.loader import RHDHPluginsConfigLoader from src.types import RHDHPlugin, RHDHPluginUpdate from src.updater import RHDHPluginConfigUpdater @@ -16,7 +17,7 @@ class TestRHDHPluginConfigUpdater: def test_init_with_defaults(self) -> "None": updater = RHDHPluginConfigUpdater() assert updater.config_path == "dynamic-plugins.yaml" - assert updater.config_location == "global.dynamic.plugins" + assert updater.config_location == "redhat-developer-hub.dynamicPlugins.plugins" def test_init_with_custom_values(self) -> "None": updater = RHDHPluginConfigUpdater( @@ -95,6 +96,42 @@ def test_update_plugin_version_updates_correct_plugin_only( # check that other plugin was not affected assert "mcp-actions-backend:next__0.1.2" in updated_content + @pytest.mark.parametrize( + ("old_section", "new_section"), + [ + ("dynamic", "dynamicPlugins"), + ("lightspeed", "intelligentAssistant"), + ], + ) + def test_update_matching_package_in_new_and_legacy_locations( + self, tmp_path: "Any", old_section: "str", new_section: "str" + ) -> "None": + package = ( + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/" + "shared-plugin:next__1.0.0" + ) + content = f"""global: + {old_section}: + plugins: + - package: {package} +redhat-developer-hub: + {new_section}: + plugins: + - package: {package} +""" + config_path = tmp_path / "values.yaml" + config_path.write_text(content) + + plugins = RHDHPluginsConfigLoader(config_path=config_path).load_rhdh_plugins() + assert len(plugins) == 1 + + updated = RHDHPluginConfigUpdater(config_path=config_path).update_rhdh_plugin( + plugins[0], Version("1.0.1") + ) + + assert updated.count("shared-plugin:next__1.0.1") == 2 + assert "shared-plugin:next__1.0.0" not in updated + def test_update_rhdh_plugin( self, temp_yaml_file: "Any", sample_plugin: "RHDHPlugin" ) -> "None":