Skip to content

feat(install-dynamic-plugins): add ref:// plugin reference resolution - #4110

Merged
nickboldt merged 1 commit into
redhat-developer:mainfrom
jonkoops:ref-plugin-resolution
Jul 30, 2026
Merged

feat(install-dynamic-plugins): add ref:// plugin reference resolution#4110
nickboldt merged 1 commit into
redhat-developer:mainfrom
jonkoops:ref-plugin-resolution

Conversation

@jonkoops

@jonkoops jonkoops commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Adds ref:// plugin reference resolution to the init container's config merger, matching the behaviour of the operator.

Plugin configurations can now use ref://plugin-name to reference another plugin by its extracted name, instead of repeating full OCI or HTTP package URLs. The resolver scans already-merged plugins, extracts their names from the package URL (OCI, HTTP, or local path), and replaces the ref with the matched package string.

NPM package references (e.g. @backstage/plugin-catalog) are not supported as ref:// targets, as ref:// is primarily designed to simplify references to OCI-distributed plugins in the catalog where full image URLs with digests would otherwise need to be repeated.

Ref: RHIDP-15875

@jonkoops
jonkoops requested review from a team as code owners July 30, 2026 16:42
@rhdh-gh-app

rhdh-gh-app Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/cli-module-install-dynamic-plugins workspaces/install-dynamic-plugins/packages/install-dynamic-plugins minor v0.3.0

@jonkoops
jonkoops requested a review from gazarenkov July 30, 2026 16:43
@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

Add ref:// plugin reference resolution to install-dynamic-plugins merger

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Support ref://plugin-name package references by resolving against already-merged plugins
• Extract plugin names from OCI/HTTP/local package strings to match refs consistently
• Add unit coverage for ref resolution and plugin-name parsing edge cases
Diagram

graph TD
  A["dynamic-plugins.yaml"] --> B["mergePlugin"] --> C{"package type"}
  C -->|"ref://"| D["resolveRefPlugin"] --> E[("PluginMap")] --> B
  C -->|"oci://"| F["mergeOciPlugin"] --> E
  C -->|"other"| G["mergeNpmPlugin"] --> E
  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _dec{"Decision"} ~~~ _store[("Store")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Build a name→package index during merge
  • ➕ Avoids O(n) scan of PluginMap for each ref:// resolution
  • ➕ Can detect duplicate plugin names deterministically and fail fast
  • ➖ Requires maintaining additional state alongside PluginMap
  • ➖ Slightly more complex control flow for a feature likely used sparingly
2. Resolve by explicit key instead of extracted name (e.g., refkey://...)
  • ➕ Eliminates ambiguous matches when multiple plugins share the same extracted name
  • ➕ Avoids relying on name parsing heuristics for HTTP/local formats
  • ➖ Less ergonomic: users must know/encode the normalized key format
  • ➖ Harder to use when keys include plugin paths and OCI normalization details
3. Extend ref:// targets to include NPM packages
  • ➕ More uniform UX across OCI and NPM sources
  • ➖ Ambiguity between scoped/unscoped NPM names and other formats
  • ➖ Would require a different name extraction strategy (or explicit opt-in) to avoid surprising matches

Recommendation: Current approach (resolve ref:// by extracting a human-readable name from already-merged package strings) is a good fit for the stated goal of simplifying OCI-heavy configs. If ref:// usage grows or repos commonly contain duplicate plugin basenames, consider adding duplicate-detection (or switching to a precomputed index) so ambiguous matches fail explicitly rather than selecting the first match.

Files changed (5) +322 / -2

Enhancement (3) +99 / -1
merger.tsResolve ref:// packages before protocol-specific merging +22/-1

Resolve ref:// packages before protocol-specific merging

• Adds resolveRefPlugin() and integrates it into mergePlugin() so ref://plugin-name is replaced with a previously-merged concrete package string. Resolution matches by extracted plugin name across already-merged PluginMap entries and throws clear errors for empty/unknown refs.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.ts

protocols.tsIntroduce ref:// protocol helpers +5/-0

Introduce ref:// protocol helpers

• Adds REF_PROTO and isRefUrl() to recognize ref:// package references as a first-class protocol alongside oci:// and others.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/protocols.ts

plugin-name.tsAdd package-string → plugin-name extraction utility +72/-0

Add package-string → plugin-name extraction utility

• Introduces extractPluginName() to derive a stable plugin name from OCI URLs, HTTP(S) tarball URLs, and local ./ paths. OCI parsing strips !path, digest, and tag; HTTP parsing strips archive extensions and version suffixes; unknown formats return null.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.ts

Tests (2) +223 / -1
merger.test.tsAdd ref:// merge and resolver unit tests +78/-1

Add ref:// merge and resolver unit tests

• Adds coverage for ref:// resolution within mergePlugin() and direct resolveRefPlugin() behavior across OCI/HTTP/local targets, including error cases for empty refs and unknown plugin names.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.test.ts

plugin-name.test.tsAdd extractPluginName parsing matrix tests +145/-0

Add extractPluginName parsing matrix tests

• Adds a comprehensive table-driven test suite covering OCI variants (tag/digest/ports/!paths/{{inherit}}), HTTP(S) variants (extensions, versions, pre-releases, query strings), local paths, and unsupported formats (e.g., NPM packages).

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.test.ts

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.98%. Comparing base (153fa28) to head (2404755).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4110      +/-   ##
==========================================
+ Coverage   57.94%   57.98%   +0.03%     
==========================================
  Files        2399     2400       +1     
  Lines       96151    96184      +33     
  Branches    26829    26841      +12     
==========================================
+ Hits        55713    55769      +56     
+ Misses      38938    38915      -23     
  Partials     1500     1500              
Flag Coverage Δ *Carryforward flag
adoption-insights 84.54% <ø> (ø) Carriedforward from 153fa28
ai-integrations 69.06% <ø> (ø) Carriedforward from 153fa28
app-defaults 69.79% <ø> (ø) Carriedforward from 153fa28
augment 46.67% <ø> (ø) Carriedforward from 153fa28
boost 76.77% <ø> (ø) Carriedforward from 153fa28
bulk-import 72.55% <ø> (ø) Carriedforward from 153fa28
cost-management 13.55% <ø> (ø) Carriedforward from 153fa28
dcm 60.72% <ø> (ø) Carriedforward from 153fa28
extensions 56.48% <ø> (ø) Carriedforward from 153fa28
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 153fa28
global-header 62.17% <ø> (ø) Carriedforward from 153fa28
homepage 47.46% <ø> (ø) Carriedforward from 153fa28
install-dynamic-plugins 59.95% <100.00%> (+2.80%) ⬆️
intelligent-assistant 74.53% <ø> (ø) Carriedforward from 153fa28
konflux 91.98% <ø> (ø) Carriedforward from 153fa28
lightspeed 69.02% <ø> (ø) Carriedforward from 153fa28
mcp-integrations 83.40% <ø> (ø) Carriedforward from 153fa28
orchestrator 66.85% <ø> (ø) Carriedforward from 153fa28
quickstart 65.04% <ø> (ø) Carriedforward from 153fa28
sandbox 79.56% <ø> (ø) Carriedforward from 153fa28
scorecard 85.34% <ø> (ø) Carriedforward from 153fa28
theme 88.52% <ø> (ø) Carriedforward from 153fa28
translations 5.12% <ø> (ø) Carriedforward from 153fa28
x2a 79.20% <ø> (ø) Carriedforward from 153fa28

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 153fa28...2404755. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rhdh-qodo-merge

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 11 rules
✅ Cross-repo context
  Explored: repo: redhat-developer/rhdh (sha: 5fe91c8b)
  Explored: repo: redhat-developer/rhdh-local (sha: 2ae9e8c8)
  Explored: repo: redhat-developer/rhdh-operator (sha: 3df5f74e)
  Not relevant to this PR: redhat-developer/rhdh-chart

Grey Divider


Remediation recommended

1. HTTP name parsing bug 🐞 Bug ≡ Correctness
Description
httpName() strips everything after any -<digit> suffix, so HTTP packages whose *name* contains a
hyphen-digit segment (not a version) will be mis-parsed (e.g. plugin-3scale-backend.tgz becomes
plugin). This can make ref://... fail to resolve or resolve to an unintended plugin for
HTTP-distributed plugins.
Code

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.ts[R64-71]

+function httpName(pkg: string): string | null {
+  if (!URL.canParse(pkg)) return null;
+
+  const name = basename(new URL(pkg).pathname)
+    .replace(/\.(tar\.gz|tgz)$/, '')
+    .replace(/(.*)-\d.*$/, '$1');
+
+  return name || null;
Relevance

●●● Strong

Team often accepts parsing/normalization correctness fixes with tests (e.g., accepted normalization
bugfix in PR 3534).

PR-#3534
PR-#3554

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The HTTP name extraction unconditionally applies /(.*)-\d.*$/, which matches any trailing segment
starting with -<digit> and is not constrained to a version format; this name is then used by
resolveRefPlugin() for matching. Tests explicitly indicate versionless .tgz URLs are supported
and also that plugin names may contain digits, making the combination realistic.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.ts[64-71]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.test.ts[97-106]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.test.ts[118-123]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.ts[129-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`extractPluginName()`'s HTTP parsing uses an overly broad regex (`/(.*)-\d.*$/`) to remove a "version suffix". This incorrectly strips valid plugin names that contain hyphen-digit segments when there is no explicit version suffix, breaking `ref://` resolution.

### Issue Context
`resolveRefPlugin()` depends on `extractPluginName()` to match `ref://<name>` against already-merged plugin packages.

### Fix Focus Areas
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.ts[64-71]

### Suggested fix
Replace the broad `-(digit...)` stripping with a semver-like suffix check, e.g.:
- strip only if the final hyphen-separated segment matches something like `^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$` (and possibly allow `+build` if desired)
- otherwise, keep the full basename (minus `.tgz`/`.tar.gz`).

Add a regression test for a digit-containing, versionless filename like `https://example.com/plugins/plugin-3scale-backend.tgz` to ensure it returns `plugin-3scale-backend`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Ambiguous ref resolution 🐞 Bug ≡ Correctness
Description
resolveRefPlugin() returns the first package whose extracted name matches the ref name, without
detecting multiple matches. If multiple already-merged plugins share the same extracted name (e.g.,
different .tgz versions), ref:// becomes order-dependent and can silently select the wrong
package.
Code

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.ts[R129-140]

+export function resolveRefPlugin(pkg: string, allPlugins: PluginMap): string {
+  const refName = pkg.slice(REF_PROTO.length);
+  if (!refName) {
+    throw new InstallException(
+      `Invalid ref:// reference: empty plugin name in ref://`,
+    );
+  }
+  for (const entry of Object.values(allPlugins)) {
+    if (extractPluginName(entry.package) === refName) {
+      return entry.package;
+    }
+  }
Relevance

●● Moderate

No direct precedent for ref:// ambiguity; team sometimes hardens duplicate/conflict handling (PRs
2897, 3539).

PR-#2897
PR-#3539

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation returns on the first match during iteration. Separately, tarballs and local paths
use the full URL/path as the plugin key, allowing multiple entries with the same extracted name to
exist simultaneously; since HTTP name extraction strips the version portion, multiple distinct
.tgz URLs can map to the same refName.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.ts[129-140]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/npm-key.ts[34-37]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.ts[25-29]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-name.ts[64-71]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`resolveRefPlugin()` stops at the first name match in `allPlugins`, which makes `ref://` resolution depend on insertion order when multiple plugins share the same extracted name.

### Issue Context
This is especially plausible for HTTP tarballs and local paths because their plugin-map keys preserve the full URL/path (so multiple versions/locations can coexist) while `extractPluginName()` strips HTTP versions.

### Fix Focus Areas
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.ts[129-144]

### Suggested fix
Collect all matching candidates instead of returning the first:
- If 0 matches: keep current "unknown" error.
- If 1 match: return it.
- If >1 matches: throw an `InstallException` describing the ambiguity and listing the matching `package` strings (and optionally their `last_modified_level`), and instruct the user to use an explicit package URL instead of `ref://`.

Add a unit test with two matching packages (e.g. `.../foo-1.0.0.tgz` and `.../foo-2.0.0.tgz`) and assert that `resolveRefPlugin('ref://foo', ...)` throws an ambiguity error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

@rhdh-qodo-merge rhdh-qodo-merge Bot added enhancement New feature or request Tests labels Jul 30, 2026

@gashcrumb gashcrumb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Do you have other stuff to add before creating a new release of this package; or if not does this need a changeset?

Allow plugin configurations to reference other plugins by name using
ref://plugin-name instead of repeating full OCI or HTTP package URLs.
The resolver matches the ref name against extracted plugin names from
already-merged plugins and replaces the ref with the resolved package URL.

Ref: RHIDP-15875
Signed-off-by: Jon Koops <jonkoops@gmail.com>
@jonkoops
jonkoops force-pushed the ref-plugin-resolution branch from 4e2ec90 to 2404755 Compare July 30, 2026 16:58
@sonarqubecloud

Copy link
Copy Markdown

@davidfestal davidfestal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether it is expected to allow smooth transition from the wrappers local references to oci artifacts ?
If that's the case, how do we manage the -dynamic suffix at the end of the wrapper folder name ?

@jonkoops

Copy link
Copy Markdown
Contributor Author

I'm wondering whether it is expected to allow smooth transition from the wrappers local references to oci artifacts ? If that's the case, how do we manage the -dynamic suffix at the end of the wrapper folder name ?

There will need to be documentation for customers to migrate over; this will be a breaking change for 2.1. Either way, it is not a concern of this PR, but a general issue that we need to handle as part of the wrapper removal.

The same goes for removing support for local paths and HTTP URL base ref:// plugin references. We should consider their removal holistically and not leave gaps between implementations in the interim.

@nickboldt
nickboldt merged commit 924cefe into redhat-developer:main Jul 30, 2026
106 checks passed
@jonkoops
jonkoops deleted the ref-plugin-resolution branch August 3, 2026 08:05
JslYoon pushed a commit to JslYoon/rhdh-plugins that referenced this pull request Aug 3, 2026
…redhat-developer#4110)

Allow plugin configurations to reference other plugins by name using
ref://plugin-name instead of repeating full OCI or HTTP package URLs.
The resolver matches the ref name against extracted plugin names from
already-merged plugins and replaces the ref with the resolved package URL.

Ref: RHIDP-15875

Signed-off-by: Jon Koops <jonkoops@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants