Skip to content

feat(extensions): add source metadata to entities#3711

Open
hopehadfield wants to merge 4 commits into
redhat-developer:mainfrom
hopehadfield:add-source-metadata
Open

feat(extensions): add source metadata to entities#3711
hopehadfield wants to merge 4 commits into
redhat-developer:mainfrom
hopehadfield:add-source-metadata

Conversation

@hopehadfield

Copy link
Copy Markdown
Member

Hey, I just made a Pull Request!

Resolves RHIDP-13665 / Epic RHIDP-13606

Problem

When RHDH is configured with multiple catalog index images (CATALOG_INDEX_IMAGE + EXTRA_CATALOG_INDEX_IMAGES), the install-dynamic-plugins init container extracts entities from each image into separate directories. However, once BaseEntityProvider ingests them into the Backstage catalog, all source information is lost with the current implementation.

Solution

Add an extensions.backstage.io/catalog-source annotation that is automatically derived from the on-disk directory layout and set on every entity emitted by BaseEntityProvider.

The derivation logic:

  • Entities from extra/<name>/catalog-entities/… get catalog-source: "<name>" (matching the name from EXTRA_CATALOG_INDEX_IMAGES)
  • All other entities get catalog-source: "primary"

Changes

File Change
extensions-common/src/annotations.ts Add CATALOG_SOURCE to ExtensionsAnnotation enum
catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts Add deriveCatalogSource() static method; update addProviderAnnotations() to set the annotation using the entity's file path
catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts 12 new tests: source derivation (7) + annotation integration (5)
docs/catalog/plugins.md Document the new annotation
catalog-backend-module-extensions/README.md Document multi-source catalog layout and source derivation

How to test

Unit tests:

cd workspaces/extensions/plugins/catalog-backend-module-extensions
yarn backstage-cli package test --watchAll=false

Local dev (manual):

  1. Create a multi-source directory:
    test-extensions/
      catalog-entities/plugins/primary-plugin.yaml
      extra/community/catalog-entities/plugins/community-plugin.yaml
    
  2. Set extensions.directory in app-config.yaml to point at it
  3. Start the backend and inspect the relevant plugin entities

Design decisions

  • "primary" as default. Vendor-neutral. The UI/config layer can map it to a display label as desired.
  • Freeform string values. Not an enforced enum — operators choose source names via EXTRA_CATALOG_INDEX_IMAGES (community=quay.io/...).
  • Applies to all entity kinds. Implemented in BaseEntityProvider, so Plugins, Packages, and Collections all get the annotation.
  • No change to duplicate handling. Existing first-wins dedup policy is unchanged; source annotations don't affect it.

✔️ Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes
  • Screenshots attached (for UI changes)

Signed-off-by: Hope Hadfield <hhadfiel@redhat.com>
Signed-off-by: Hope Hadfield <hhadfiel@redhat.com>
Signed-off-by: Hope Hadfield <hhadfiel@redhat.com>
@rhdh-gh-app

rhdh-gh-app Bot commented Jul 8, 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/backstage-plugin-catalog-backend-module-extensions workspaces/extensions/plugins/catalog-backend-module-extensions minor v0.18.0
@red-hat-developer-hub/backstage-plugin-extensions-common workspaces/extensions/plugins/extensions-common minor v0.18.0

@hopehadfield hopehadfield marked this pull request as draft July 8, 2026 16:03
@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Jul 8, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Derive and attach catalog-source metadata to extension catalog entities

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Automatically annotate emitted catalog entities with a derived catalog source identifier.
• Infer source from extra//catalog-entities paths, defaulting to primary.
• Add unit tests and docs for multi-source directory layout and annotation behavior.
Diagram

graph TD
  FS["Extensions dir layout"] --> Derive["deriveCatalogSource()"] --> Provider["BaseEntityProvider"] --> Out["Entities w/ catalog-source"] --> Catalog["Backstage Catalog"]
  Enum["ExtensionsAnnotation.CATALOG_SOURCE"] --> Provider
  Provider --> Docs["Docs/README"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split providers per source directory
  • ➕ No path parsing/regex; source is explicit per provider instance
  • ➕ Could allow per-source scheduling/logging and collision policies later
  • ➖ More wiring/config complexity (provider factory, multiple registrations)
  • ➖ Harder to keep behavior consistent across all entity kinds
2. Reuse existing location/origin annotations instead of adding a new one
  • ➕ No new annotation key to standardize across packages
  • ➕ Source could be inferred from the full file path already available in some contexts
  • ➖ UI/consumers must parse paths (brittle and not standardized)
  • ➖ Current provider sets location/origin to provider name, not the entity file path, so consumers still lack source context

Recommendation: Proceed with the current approach: a dedicated extensions.backstage.io/catalog-source annotation derived in BaseEntityProvider. It centralizes the behavior for all entity kinds, avoids consumer-side path parsing, and keeps the multi-source mechanism aligned with the install-dynamic-plugins directory layout without introducing additional provider instances.

Files changed (8) +254 / -3

Enhancement (2) +30 / -3
BaseEntityProvider.tsDerive catalog source from file path and annotate emitted entities +29/-3

Derive catalog source from file path and annotate emitted entities

• Adds a default source constant and a 'deriveCatalogSource' helper that detects 'extra/<name>/' in file paths (with Windows path normalization). Updates provider annotation injection to include 'extensions.backstage.io/catalog-source' for every emitted entity using the entity's originating file path.

workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts

annotations.tsAdd shared ExtensionsAnnotation key for catalog-source +1/-0

Add shared ExtensionsAnnotation key for catalog-source

• Extends 'ExtensionsAnnotation' with 'CATALOG_SOURCE = 'extensions.backstage.io/catalog-source'' so providers and consumers share the same annotation key constant.

workspaces/extensions/plugins/extensions-common/src/annotations.ts

Tests (1) +172 / -0
BaseEntityProvider.test.tsAdd unit coverage for source derivation and annotation injection +172/-0

Add unit coverage for source derivation and annotation injection

• Adds focused tests for 'deriveCatalogSource' across primary/extra layouts, nested paths, and Windows path separators. Adds integration tests ensuring 'getEntities' sets the 'catalog-source' annotation correctly and preserves it under the existing first-wins collision policy.

workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts

Documentation (4) +46 / -0
plugins.mdDocument the new catalog-source annotation semantics +15/-0

Document the new catalog-source annotation semantics

• Adds a dedicated documentation section explaining how 'extensions.backstage.io/catalog-source' is automatically derived from the extracted directory layout (primary vs extra sources) and should not be set manually.

workspaces/extensions/docs/catalog/plugins.md

README.mdExplain multi-source catalog directory layout and source derivation +27/-0

Explain multi-source catalog directory layout and source derivation

• Documents how extra catalog index images are extracted under 'extra/<name>/...' and how the provider assigns 'catalog-source' values to entities based on their origin path.

workspaces/extensions/plugins/catalog-backend-module-extensions/README.md

report.api.mdUpdate API report for new BaseEntityProvider source-derivation API +2/-0

Update API report for new BaseEntityProvider source-derivation API

• Updates the generated API report to include 'DEFAULT_CATALOG_SOURCE' and the 'deriveCatalogSource(filePath)' static method on 'BaseEntityProvider'.

workspaces/extensions/plugins/catalog-backend-module-extensions/report.api.md

report.api.mdUpdate extensions-common API report for new annotation enum value +2/-0

Update extensions-common API report for new annotation enum value

• Updates the generated API report to reflect the new 'ExtensionsAnnotation.CATALOG_SOURCE' enum member.

workspaces/extensions/plugins/extensions-common/report.api.md

Other (1) +6 / -0
khaki-points-mate.mdAdd changeset for minor version bumps due to new annotation surface +6/-0

Add changeset for minor version bumps due to new annotation surface

• Introduces a changeset bumping the catalog-backend-module-extensions and extensions-common packages to reflect the new catalog source annotation capability.

workspaces/extensions/.changeset/khaki-points-mate.md

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Jul 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials

Grey Divider


Remediation recommended

1. Unanchored extra path matching 🐞 Bug ≡ Correctness
Description
BaseEntityProvider.deriveCatalogSource() matches any "/extra/<name>/" segment anywhere in the full
file path, so entities can be mis-labeled when the configured extensions directory (or another
parent segment) happens to contain "extra". This results in incorrect
extensions.backstage.io/catalog-source values, undermining source attribution for multi-image
catalogs.
Code

workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[R81-85]

+  static deriveCatalogSource(filePath: string): string {
+    // Normalise to forward slashes so the regex works on all platforms.
+    const normalised = filePath.replace(/\\/g, '/');
+    const match = normalised.match(/\/extra\/([^/]+)\//);
+    return match ? match[1] : BaseEntityProvider.DEFAULT_CATALOG_SOURCE;
Relevance

⭐⭐⭐ High

Team accepted anchoring/normalizing path-matching regexes to avoid false matches (e.g.,
start-anchored regex in PR #2724).

PR-#2724

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The derivation uses a non-anchored regex over the full path (/\/extra\/([^/]+)\//). File paths are
produced by glob in readYamlFiles and passed through getEntities into addProviderAnnotations;
meanwhile getExtensionsDirectory can return an arbitrary configured directory, so absolute paths can
legitimately contain an unrelated /extra/ segment.

workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[81-86]
workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[187-206]
workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[231-242]
workspaces/extensions/plugins/catalog-backend-module-extensions/src/utils/file-utils.ts[56-67]

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

## Issue description
`deriveCatalogSource()` searches the entire path string for `/extra/<name>/`. Because `readYamlFiles()` returns file paths from glob (commonly absolute), unrelated directories containing an `extra` segment (e.g. a custom `extensions.directory` like `/mnt/extra/extensions`) can be incorrectly treated as an extra catalog source.

## Issue Context
The intended derivation is based on the on-disk layout *under the extensions root* (`extra/<name>/catalog-entities/...` vs everything else).

## Fix Focus Areas
- Compute a root-relative path before derivation and only derive from that:
 - workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[231-242]
- Tighten matching to only treat `extra/<name>/...` when it’s the first path segment (after normalization), e.g. `^extra/([^/]+)/`:
 - workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[81-86]
- Update/add unit tests to cover a configured extensions directory path that contains an `extra` segment (and ensure correct fallback to `primary`):
 - workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts[141-196]

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


Grey Divider

Previous review results

Review updated until commit 800af80

Results up to commit b2d19bc


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Unanchored extra path matching 🐞 Bug ≡ Correctness
Description
BaseEntityProvider.deriveCatalogSource() matches any "/extra/<name>/" segment anywhere in the full
file path, so entities can be mis-labeled when the configured extensions directory (or another
parent segment) happens to contain "extra". This results in incorrect
extensions.backstage.io/catalog-source values, undermining source attribution for multi-image
catalogs.
Code

workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[R81-85]

+  static deriveCatalogSource(filePath: string): string {
+    // Normalise to forward slashes so the regex works on all platforms.
+    const normalised = filePath.replace(/\\/g, '/');
+    const match = normalised.match(/\/extra\/([^/]+)\//);
+    return match ? match[1] : BaseEntityProvider.DEFAULT_CATALOG_SOURCE;
Relevance

⭐⭐⭐ High

Team accepted anchoring/normalizing path-matching regexes to avoid false matches (e.g.,
start-anchored regex in PR #2724).

PR-#2724

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The derivation uses a non-anchored regex over the full path (/\/extra\/([^/]+)\//). File paths are
produced by glob in readYamlFiles and passed through getEntities into addProviderAnnotations;
meanwhile getExtensionsDirectory can return an arbitrary configured directory, so absolute paths can
legitimately contain an unrelated /extra/ segment.

workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[81-86]
workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[187-206]
workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[231-242]
workspaces/extensions/plugins/catalog-backend-module-extensions/src/utils/file-utils.ts[56-67]

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

## Issue description
`deriveCatalogSource()` searches the entire path string for `/extra/<name>/`. Because `readYamlFiles()` returns file paths from glob (commonly absolute), unrelated directories containing an `extra` segment (e.g. a custom `extensions.directory` like `/mnt/extra/extensions`) can be incorrectly treated as an extra catalog source.

## Issue Context
The intended derivation is based on the on-disk layout *under the extensions root* (`extra/<name>/catalog-entities/...` vs everything else).

## Fix Focus Areas
- Compute a root-relative path before derivation and only derive from that:
 - workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[231-242]
- Tighten matching to only treat `extra/<name>/...` when it’s the first path segment (after normalization), e.g. `^extra/([^/]+)/`:
 - workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts[81-86]
- Update/add unit tests to cover a configured extensions directory path that contains an `extra` segment (and ensure correct fallback to `primary`):
 - workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts[141-196]

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


Qodo Logo

@rhdh-qodo-merge rhdh-qodo-merge Bot added documentation Improvements or additions to documentation enhancement New feature or request Tests labels Jul 8, 2026
Signed-off-by: Hope Hadfield <hhadfiel@redhat.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

@hopehadfield hopehadfield marked this pull request as ready for review July 8, 2026 17:29
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 54.55%. Comparing base (26407b3) to head (800af80).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3711   +/-   ##
=======================================
  Coverage   54.55%   54.55%           
=======================================
  Files        2349     2349           
  Lines       89735    89741    +6     
  Branches    25135    25136    +1     
=======================================
+ Hits        48953    48959    +6     
  Misses      40482    40482           
  Partials      300      300           
Flag Coverage Δ *Carryforward flag
adoption-insights 83.70% <ø> (ø) Carriedforward from 26407b3
ai-integrations 68.35% <ø> (ø) Carriedforward from 26407b3
app-defaults 69.79% <ø> (ø) Carriedforward from 26407b3
augment 46.39% <ø> (ø) Carriedforward from 26407b3
boost 74.68% <ø> (ø) Carriedforward from 26407b3
bulk-import 72.46% <ø> (ø) Carriedforward from 26407b3
cost-management 14.10% <ø> (ø) Carriedforward from 26407b3
dcm 61.81% <ø> (ø) Carriedforward from 26407b3
extensions 61.60% <100.00%> (+0.06%) ⬆️
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 26407b3
global-header 59.71% <ø> (ø) Carriedforward from 26407b3
homepage 50.23% <ø> (ø) Carriedforward from 26407b3
install-dynamic-plugins 56.77% <ø> (ø) Carriedforward from 26407b3
konflux 91.49% <ø> (ø) Carriedforward from 26407b3
lightspeed 68.81% <ø> (ø) Carriedforward from 26407b3
mcp-integrations 85.46% <ø> (ø) Carriedforward from 26407b3
orchestrator 42.97% <ø> (ø) Carriedforward from 26407b3
quickstart 65.63% <ø> (ø) Carriedforward from 26407b3
sandbox 79.56% <ø> (ø) Carriedforward from 26407b3
scorecard 82.67% <ø> (ø) Carriedforward from 26407b3
theme 61.26% <ø> (ø) Carriedforward from 26407b3
translations 7.25% <ø> (ø) Carriedforward from 26407b3
x2a 78.68% <ø> (ø) Carriedforward from 26407b3

*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 26407b3...800af80. 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 was updated up to the latest commit 800af80

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request Tests workspace/extensions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant