Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")

Expand Down
6 changes: 3 additions & 3 deletions src/github_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -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,
}

Expand Down
24 changes: 19 additions & 5 deletions src/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]":
Expand Down Expand Up @@ -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)
96 changes: 92 additions & 4 deletions tests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -31,15 +41,17 @@ 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

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 == []

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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":
Expand Down
39 changes: 38 additions & 1 deletion tests/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand Down Expand Up @@ -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":
Expand Down
Loading