From 99d1f33e992a70f60ad26ee38218e4e4f7ccfcb4 Mon Sep 17 00:00:00 2001 From: Subhash Khileri Date: Tue, 5 May 2026 16:58:17 +0530 Subject: [PATCH] feat: use {{inherit}} for DPDY plugins and inject config for non-DPDY OCI in nightly mode In nightly mode, plugins listed in RHDH's default.packages.yaml (DPDY) now use the {{inherit}} tag so RHDH resolves the exact RC version from its built-in dynamic-plugins.default.yaml. Non-DPDY OCI plugins receive full metadata config injection (appConfigExamples) since they are not covered by RHDH defaults. Key changes: - Add fetchDefaultPackages() to fetch DPDY package list from rhdh repo - Resolve DPDY OCI plugins to {{inherit}} refs, preserving the registry from metadata so the runtime key matches the DPDY entry - Inject appConfigExamples only for non-DPDY OCI plugins in nightly mode - Skip config injection for DPDY and wrapper plugins in nightly mode - Parallelize metadata loading and DPDY fetch with Promise.all - Add comprehensive nightly mode tests (72 tests total) - Update docs: resolution reference, changelog, environment variables, config files, troubleshooting, CI pipeline guide RHIDP-13402 --- docs/api/utils/plugin-metadata.md | 8 +- docs/changelog.md | 6 + docs/guide/configuration/config-files.md | 8 +- .../configuration/environment-variables.md | 9 +- docs/guide/utilities/plugin-metadata.md | 55 +- .../reference/environment-variables.md | 20 +- .../reference/plugin-metadata-resolution.md | 61 ++- docs/overlay/reference/troubleshooting.md | 2 +- docs/overlay/tutorials/ci-pipeline.md | 5 +- package.json | 2 +- src/utils/plugin-metadata.ts | 159 +++++- .../tests/plugin-metadata.fixtures.test.ts | 12 +- .../tests/plugin-metadata.nightly.test.ts | 479 +++++++++++++++++- src/utils/tests/plugin-metadata.pr.test.ts | 39 ++ 14 files changed, 765 insertions(+), 100 deletions(-) diff --git a/docs/api/utils/plugin-metadata.md b/docs/api/utils/plugin-metadata.md index 59714b2..a13a32b 100644 --- a/docs/api/utils/plugin-metadata.md +++ b/docs/api/utils/plugin-metadata.md @@ -154,7 +154,8 @@ Unified entry point for both PR and nightly plugin resolution flows. Called auto ```typescript async function processPluginsForDeployment( config: DynamicPluginsConfig, - metadataPath?: string + metadataPath?: string, + dpdyPackages?: Set ): Promise ``` @@ -163,13 +164,14 @@ async function processPluginsForDeployment( |-----------|------|---------|-------------| | `config` | [`DynamicPluginsConfig`](#dynamicpluginsconfig) | - | The plugins config to process | | `metadataPath` | `string` | `"../metadata"` | Path to metadata directory | +| `dpdyPackages` | `Set` | - | Pre-loaded DPDY package set (for testing; fetched automatically if omitted in nightly) | **Returns:** Processed configuration with resolved OCI references. **Behavior:** - **PR mode** (`!isNightlyJob()`): Injects `appConfigExamples` from metadata as base config, then resolves packages to OCI URLs (PR-specific if `GIT_PR_NUMBER` set, metadata refs otherwise) -- **Nightly mode** (`isNightlyJob()`): Resolves packages to OCI refs from metadata only (no config injection) -- Respects `RHDH_SKIP_PLUGIN_METADATA_INJECTION` to skip config injection +- **Nightly mode** (`isNightlyJob()`): DPDY plugins use `{{inherit}}` (no config injection); non-DPDY OCI plugins use full metadata refs with config injection +- Respects `RHDH_SKIP_PLUGIN_METADATA_INJECTION` to skip config injection (local only, ignored in CI) --- diff --git a/docs/changelog.md b/docs/changelog.md index c315905..dd7a0c8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -18,6 +18,12 @@ All notable changes to this project will be documented in this file. - **Plugin Metadata Resolution documentation**: New overlay reference page (`docs/overlay/reference/plugin-metadata-resolution.md`) with scenario tables showing how every plugin type resolves in PR check and nightly modes, config injection behavior with deep merge examples, and cross-workspace plugin handling. +### Changed + +- **Nightly `{{inherit}}` resolution for DPDY plugins**: In nightly mode, plugins that exist in the RHDH catalog index (`default.packages.yaml`) now resolve to `{{inherit}}` tags instead of pinned OCI refs. This tests against the exact plugin versions shipped in the RC. Non-DPDY OCI plugins continue using full metadata refs with config injection. The DPDY list is fetched at runtime from the `rhdh` repo, using `RELEASE_BRANCH_NAME` to select the branch (required in CI, defaults to `main` locally). The `{{inherit}}` ref preserves the registry from metadata (`registry.access.redhat.com`, `quay.io`, etc.) so the runtime key matches the DPDY entry. +- **`RHDH_SKIP_PLUGIN_METADATA_INJECTION` is local-only**: This env var is now ignored in CI (`CI=true`). It was intended for local development opt-out only — in CI, metadata injection should always run to ensure consistent test behavior. +- **`RELEASE_BRANCH_NAME` required in CI for nightly**: When running nightly mode in CI, `RELEASE_BRANCH_NAME` must be set (exported by the OpenShift CI step registry). Locally it defaults to `main`. + ## [1.1.35] ### Fixed diff --git a/docs/guide/configuration/config-files.md b/docs/guide/configuration/config-files.md index ca5c61f..62be087 100644 --- a/docs/guide/configuration/config-files.md +++ b/docs/guide/configuration/config-files.md @@ -215,9 +215,11 @@ Plugin metadata injection is **enabled by default** for: - Local development - PR builds in CI -Injection is **disabled** when: -- [`RHDH_SKIP_PLUGIN_METADATA_INJECTION`](/guide/configuration/environment-variables#plugin-metadata-variables) environment variable is set -- `JOB_NAME` contains `periodic-` (nightly/periodic CI builds) +Injection is **disabled locally** when: +- [`RHDH_SKIP_PLUGIN_METADATA_INJECTION`](/guide/configuration/environment-variables#plugin-metadata-variables) is set to `true` (ignored in CI) + +In **nightly mode** (`E2E_NIGHTLY_MODE=true` or `JOB_NAME` contains `periodic-`): +- Only non-DPDY OCI plugins get injection; DPDY plugins use `{{inherit}}` with RHDH defaults ::: warning When injection is enabled, deployment will fail if: diff --git a/docs/guide/configuration/environment-variables.md b/docs/guide/configuration/environment-variables.md index 7079ef0..f5849ae 100644 --- a/docs/guide/configuration/environment-variables.md +++ b/docs/guide/configuration/environment-variables.md @@ -34,7 +34,7 @@ These are set automatically during deployment: | `CI` | Enables auto-cleanup | - | | `CHART_URL` | Custom Helm chart URL | `oci://quay.io/rhdh/chart` | | `SKIP_KEYCLOAK_DEPLOYMENT` | Skip Keycloak auto-deploy | `false` | -| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | Disable plugin metadata injection | - | +| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | Disable plugin metadata injection (local only, ignored in CI) | - | ## Plugin Metadata Variables @@ -43,9 +43,10 @@ These control automatic plugin configuration injection from metadata files: | Variable | Description | Effect | |----------|-------------|--------| | `GIT_PR_NUMBER` | PR number (set by OpenShift CI) | Enables OCI URL generation for PR builds | -| `E2E_NIGHTLY_MODE` | When `"true"`, activates nightly mode | Uses released OCI refs, skips metadata injection | -| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | When `"true"`, disables metadata injection | Opt-out | -| `JOB_NAME` | CI job name (set by OpenShift CI/Prow) | If contains `periodic-`, injection is disabled | +| `E2E_NIGHTLY_MODE` | When `"true"`, activates nightly mode | DPDY plugins use `{{inherit}}`, non-DPDY OCI plugins get full refs + config injection | +| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | When `"true"`, disables metadata injection | Local-only opt-out (ignored when `CI=true`) | +| `RELEASE_BRANCH_NAME` | Release branch (set by OpenShift CI step registry) | Used to fetch `default.packages.yaml` for DPDY resolution in nightly mode. Required in CI, defaults to `main` locally | +| `JOB_NAME` | CI job name (set by OpenShift CI/Prow) | If contains `periodic-`, nightly mode is activated | | `JOB_MODE` | CI-only: `nightly` or `pr-check` (set by step registry) | Informational | ### OCI URL Generation diff --git a/docs/guide/utilities/plugin-metadata.md b/docs/guide/utilities/plugin-metadata.md index ea3d81e..6c01f4c 100644 --- a/docs/guide/utilities/plugin-metadata.md +++ b/docs/guide/utilities/plugin-metadata.md @@ -27,10 +27,13 @@ Metadata handling is **enabled by default** for: - Local development - PR builds in CI -Metadata handling is **disabled** when: -- `RHDH_SKIP_PLUGIN_METADATA_INJECTION` is set to `true` -- `E2E_NIGHTLY_MODE` is set to `true` -- `JOB_NAME` contains `periodic-` (nightly builds) +Metadata injection is **disabled** when: +- `RHDH_SKIP_PLUGIN_METADATA_INJECTION` is set to `true` (local only — ignored in CI) + +In **nightly mode** (`E2E_NIGHTLY_MODE=true` or `JOB_NAME` contains `periodic-`): +- DPDY plugins (in `default.packages.yaml`) use `{{inherit}}` tags — no config injection +- Non-DPDY OCI plugins use full metadata refs — with config injection +- Wrapper plugins keep their local paths — no config injection ::: info Priority The `isNightlyJob()` function checks in this order: @@ -59,15 +62,17 @@ test.beforeAll(async ({ rhdh }) => { - If `dynamic-plugins.yaml` **exists**: merged with package defaults + auth config - If `dynamic-plugins.yaml` **doesn't exist**: auto-generated from all `metadata/*.yaml` files, then merged with defaults/auth (deduplicated by normalized plugin name — OCI wins over local `-dynamic` paths) -2. **Metadata injection** (PR/local mode only, skipped in nightly): - - `appConfigExamples` from metadata merged as base config - - User-provided `pluginConfig` overrides metadata values +2. **Metadata injection**: + - **PR/local**: `appConfigExamples` from metadata merged as base config; user `pluginConfig` overrides + - **Nightly**: only for non-DPDY OCI plugins (DPDY plugins get config via `{{inherit}}`) -3. **Package resolution** (both modes) — per plugin, in priority order: +3. **Package resolution** — per plugin, in priority order: | Condition | Result | |-----------|--------| | Plugin in workspace build + `GIT_PR_NUMBER` set | PR OCI URL: `pr_{number}__{version}` | + | Nightly + DPDY + OCI | `{{inherit}}` tag (RHDH resolves version from built-in DPDY) | | Plugin has metadata with OCI `dynamicArtifact` | Metadata's OCI ref (preserves original registry) | + | Plugin has metadata with wrapper path | Wrapper path from metadata | | No metadata match (cross-workspace plugins, npm packages) | Kept as-is | 4. **Wrapper disabling** (PR builds only, when `GIT_PR_NUMBER` set): @@ -174,26 +179,35 @@ The system operates in three modes based on environment variables: | | **PR Check** | **Nightly** | **Local Dev** | |---|---|---|---| | **Trigger** | `GIT_PR_NUMBER` set | `E2E_NIGHTLY_MODE=true` | No env vars | -| **Config injection** | Yes — `appConfigExamples` merged | Skipped | Yes | -| **OCI resolution** | PR tags (`pr_{n}__{v}`) for workspace plugins, metadata refs for others | Metadata refs for all | Metadata refs for all | +| **Config injection** | Yes — all plugins | Selective — non-DPDY OCI only | Yes — all plugins | +| **DPDY OCI resolution** | PR tags or metadata refs | `{{inherit}}` tag | Metadata refs | +| **Non-DPDY OCI resolution** | PR tags or metadata refs | Metadata refs + config injection | Metadata refs | +| **Wrapper plugins** | Metadata path | Metadata path | Metadata path | | **Wrapper disabling** | Yes (`disableWrappers`) | No | No | | **Cross-workspace plugins** | Kept as-is | Kept as-is | Kept as-is | -### Why Metadata Refs (Not `{{inherit}}`) +### DPDY vs Non-DPDY in Nightly + +DPDY (dynamic-plugins.default.yaml) plugins are those shipped in the RHDH catalog index image, listed in [`default.packages.yaml`](https://github.com/redhat-developer/rhdh/blob/main/default.packages.yaml) (both `enabled` and `disabled` sections). In nightly mode: + +- **DPDY + OCI**: Use `{{inherit}}` tag — RHDH resolves the version from its built-in DPDY. This tests the exact versions shipped in the RC. No config injection (RHDH provides defaults). +- **Non-DPDY + OCI**: Use full metadata refs from `spec.dynamicArtifact`. Config injection enabled (these plugins aren't in the RHDH defaults). +- **Wrapper plugins**: Always use the metadata wrapper path regardless of DPDY status. No config injection. -Metadata files are the most accurate source for latest published plugin versions. The daily `update-plugins-repo-refs` workflow keeps them current. By contrast, many OCI plugins (~49) are not in the catalog index (DPDY), and some that are have older versions. Using metadata ensures nightly tests run against the latest published artifacts. +The DPDY list is fetched at runtime from the `rhdh` repo using `RELEASE_BRANCH_NAME` (required in CI, defaults to `main` locally). ### processPluginsForDeployment This is the unified entry point for both PR and nightly plugin resolution flows. It is called automatically during `deploy()`. ``` -Step 1: Inject metadata configs (PR/local mode only) - → deepMerge(metadata.appConfigExamples, user.pluginConfig) - → Skipped when: isNightlyJob() OR RHDH_SKIP_PLUGIN_METADATA_INJECTION="true" +Step 1: Inject metadata configs + → PR/local: deepMerge(metadata.appConfigExamples, user.pluginConfig) for all plugins + → Nightly: only non-DPDY OCI plugins get injection + → Skipped locally when RHDH_SKIP_PLUGIN_METADATA_INJECTION="true" (ignored in CI) -Step 2: Resolve packages to OCI (both modes) - → Per plugin: PR OCI URL > metadata OCI ref > passthrough +Step 2: Resolve packages (both modes) + → Per plugin: PR OCI URL > DPDY {{inherit}} > metadata OCI ref > wrapper path > passthrough ``` ## Environment Variables @@ -201,9 +215,10 @@ Step 2: Resolve packages to OCI (both modes) | Variable | Effect | |----------|--------| | `GIT_PR_NUMBER` | Enables OCI URL generation for PR builds | -| `E2E_NIGHTLY_MODE` | When `true`, activates nightly mode (uses released OCI refs) | -| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | Disables all metadata handling | -| `JOB_NAME` | If contains `periodic-`, disables metadata handling | +| `E2E_NIGHTLY_MODE` | When `true`, activates nightly mode (DPDY → `{{inherit}}`, non-DPDY → full refs) | +| `RELEASE_BRANCH_NAME` | Branch for fetching `default.packages.yaml` (required in CI, defaults to `main` locally) | +| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | Disables metadata injection (local only, ignored in CI) | +| `JOB_NAME` | If contains `periodic-`, activates nightly mode | | `JOB_MODE` | CI-only: `nightly` or `pr-check` (set by step registry) | See [Environment Variables](/guide/configuration/environment-variables#plugin-metadata-variables) for details. diff --git a/docs/overlay/reference/environment-variables.md b/docs/overlay/reference/environment-variables.md index aac9ef6..e439e84 100644 --- a/docs/overlay/reference/environment-variables.md +++ b/docs/overlay/reference/environment-variables.md @@ -76,9 +76,10 @@ These control automatic plugin configuration generation from metadata files: | Variable | Description | Effect | |----------|-------------|--------| | `GIT_PR_NUMBER` | PR number | Enables OCI URL generation using that PR's built images | -| `E2E_NIGHTLY_MODE` | When `true`, activates nightly mode | Uses released OCI refs from metadata, skips config injection | -| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | When `true`, disables metadata injection | Opt-out for all metadata handling | -| `JOB_NAME` | CI job name (set by OpenShift CI/Prow) | If contains `periodic-`, injection is disabled | +| `E2E_NIGHTLY_MODE` | When `true`, activates nightly mode | DPDY plugins use `{{inherit}}`, non-DPDY OCI plugins get full refs + config injection | +| `RELEASE_BRANCH_NAME` | Release branch (set by OpenShift CI) | Used to fetch `default.packages.yaml` for DPDY resolution. Required in CI, defaults to `main` locally | +| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | When `true`, disables metadata injection | Local-only opt-out (ignored when `CI=true`) | +| `JOB_NAME` | CI job name (set by OpenShift CI/Prow) | If contains `periodic-`, nightly mode is activated | ### When to Use These Variables @@ -87,7 +88,7 @@ These control automatic plugin configuration generation from metadata files: | PR builds in CI | `GIT_PR_NUMBER` is set automatically | | Test PR builds locally | Set `GIT_PR_NUMBER` manually to use PR's OCI images | | Nightly/periodic builds | `E2E_NIGHTLY_MODE=true` or `JOB_NAME` contains `periodic-` (auto-detected in CI) | -| Manual opt-out | Set `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true` | +| Manual opt-out (local only) | Set `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true` (ignored in CI) | ### Metadata Handling Behavior @@ -95,10 +96,13 @@ These control automatic plugin configuration generation from metadata files: - Local development - PR builds in CI -**Disabled automatically** when: -- `RHDH_SKIP_PLUGIN_METADATA_INJECTION` is set to `true` -- `E2E_NIGHTLY_MODE` is set to `true` -- `JOB_NAME` contains `periodic-` (nightly builds) +**Disabled locally** when: +- `RHDH_SKIP_PLUGIN_METADATA_INJECTION` is set to `true` (ignored in CI) + +**Selective in nightly mode** (`E2E_NIGHTLY_MODE=true` or `JOB_NAME` contains `periodic-`): +- DPDY OCI plugins: no injection (use `{{inherit}}` tag, RHDH provides config) +- Non-DPDY OCI plugins: injection enabled (full metadata refs) +- Wrapper plugins: no injection ::: info Priority When `GIT_PR_NUMBER` is set, PR mode always takes precedence over nightly mode. This prevents broken combinations of PR images with nightly configuration. diff --git a/docs/overlay/reference/plugin-metadata-resolution.md b/docs/overlay/reference/plugin-metadata-resolution.md index 38ddaa2..1d7d2a1 100644 --- a/docs/overlay/reference/plugin-metadata-resolution.md +++ b/docs/overlay/reference/plugin-metadata-resolution.md @@ -26,9 +26,9 @@ Every plugin entry in `dynamic-plugins.yaml` goes through two steps: Merge `appConfigExamples` from metadata into `pluginConfig`. -- **PR / Local**: metadata config is the base, user `pluginConfig` overrides it (deep merge) -- **Nightly**: skipped entirely — user `pluginConfig` is preserved as-is -- Disabled when `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true` +- **PR / Local**: metadata config is the base, user `pluginConfig` overrides it (deep merge) for all plugins +- **Nightly**: selective — only non-DPDY OCI plugins get injection; DPDY plugins get config from RHDH via `{{inherit}}`; wrapper plugins get no injection +- Disabled locally when `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true` (ignored in CI) #### Example: Deep Merge Behavior (PR / Local mode) @@ -84,7 +84,9 @@ plugins: | Metadata has config, user has partial override | Deep merge — user keys win, metadata fills the rest | | Metadata has config, user overrides same key | User value wins | | No `appConfigExamples` in metadata | No `pluginConfig` injected | -| **Nightly mode** | **Skipped** — user `pluginConfig` preserved exactly as-is, metadata config NOT merged | +| **Nightly — DPDY OCI** | **Skipped** — plugin uses `{{inherit}}`, RHDH provides config defaults | +| **Nightly — non-DPDY OCI** | **Injected** — metadata `appConfigExamples` merged as base, user `pluginConfig` overrides | +| **Nightly — wrapper** | **Skipped** — user `pluginConfig` preserved as-is | ### Step 2: Package Resolution @@ -101,15 +103,22 @@ For each plugin, the resolver checks in order: Yes → replace with PR OCI URL: oci://ghcr.io/.../plugin:pr_{number}__{version} No ↓ -3. Use metadata's dynamicArtifact as-is +3. Is nightly mode AND plugin is in DPDY AND metadata is OCI? + Yes → use {{inherit}} tag: oci://{registry}/plugin:{{inherit}} + (registry preserved from metadata's dynamicArtifact) + No ↓ + +4. Use metadata's dynamicArtifact as-is (OCI ref → OCI ref, wrapper path → wrapper path) ``` -Metadata is always the source of truth for the package reference. Whatever `spec.dynamicArtifact` says — OCI ref or wrapper path — that's what the plugin resolves to. +Metadata is the source of truth for the package reference, except for DPDY plugins in nightly mode which use `{{inherit}}` to test the exact versions shipped in the RHDH RC. ## Resolution Scenarios -The tables below show what happens to each plugin type in PR check and nightly modes. Local dev behaves the same as nightly for package resolution, and the same as PR check for config injection. +The tables below show what happens to each plugin type in PR check and nightly modes. Local dev behaves the same as PR check (metadata refs + full config injection). + +In nightly mode, resolution depends on whether the plugin's npm package name is in the DPDY (`default.packages.yaml` — both `enabled` and `disabled` sections). The DPDY list is fetched at runtime from the `rhdh` repo using `RELEASE_BRANCH_NAME`. ### PR Check Mode (`GIT_PR_NUMBER` set) @@ -127,32 +136,38 @@ The tables below show what happens to each plugin type in PR check and nightly m ### Nightly Mode (`E2E_NIGHTLY_MODE=true`, no `GIT_PR_NUMBER`) -| # | Scenario | Metadata `dynamicArtifact` | User config `package` | Resolved `package` | Config injection | -|---|----------|---------------------------|----------------------|---------------------|-----------------| -| 1 | Workspace plugin (OCI) | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` | `oci://ghcr.io/.../plugin-tekton:old_tag!alias` | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` (from metadata) | **Skipped** | -| 2 | Workspace plugin (wrapper) | `./dynamic-plugins/dist/plugin-tech-radar` | `./dynamic-plugins/dist/plugin-tech-radar` | `./dynamic-plugins/dist/plugin-tech-radar` (from metadata) | **Skipped** | -| 3 | Workspace plugin (wrapper, stale OCI in config) | `./dynamic-plugins/dist/plugin-github-org-dynamic` | `oci://ghcr.io/.../plugin-github-org:bs_1.45.3__0.3.16` | `./dynamic-plugins/dist/plugin-github-org-dynamic` (from metadata) | **Skipped** | -| 4 | Workspace plugin (OCI, wrapper in config) | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` | `./dynamic-plugins/dist/plugin-tekton` | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` (from metadata) | **Skipped** | -| 5 | Cross-workspace (local path, no metadata) | — | `./dynamic-plugins/dist/plugin-kubernetes-backend-dynamic` | unchanged | **Skipped** | -| 6 | Cross-workspace (OCI, no metadata) | — | `oci://ghcr.io/.../plugin-dynamic-home-page:bs_1.45.3__1.10.3!alias` | unchanged | **Skipped** | -| 7 | npm package (no metadata) | — | `@rhdh/plugin-global-header-test@0.0.2` | unchanged | **Skipped** | -| 8 | Different registry (quay.io) | `oci://quay.io/rhdh/plugin-events@sha256:abc` | `oci://ghcr.io/.../plugin-events:old_tag` | `oci://quay.io/rhdh/plugin-events@sha256:abc` (from metadata, different registry) | **Skipped** | -| 9 | Different registry (registry.access.redhat.com) | `oci://registry.access.redhat.com/rhdh/plugin-orch@sha256:f40d` | `oci://ghcr.io/.../plugin-orch:some_tag` | `oci://registry.access.redhat.com/rhdh/plugin-orch@sha256:f40d` (from metadata) | **Skipped** | +| # | Scenario | In DPDY? | Metadata `dynamicArtifact` | User config `package` | Resolved `package` | Config injection | +|---|----------|----------|---------------------------|----------------------|---------------------|-----------------| +| 1 | DPDY OCI plugin | Yes | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` | `oci://ghcr.io/.../plugin-tekton:old_tag!alias` | `oci://ghcr.io/.../plugin-tekton:{{inherit}}` | **Skipped** (RHDH provides defaults) | +| 2 | DPDY wrapper plugin | Yes | `./dynamic-plugins/dist/plugin-tech-radar` | `./dynamic-plugins/dist/plugin-tech-radar` | `./dynamic-plugins/dist/plugin-tech-radar` (from metadata) | **Skipped** | +| 3 | DPDY wrapper (stale OCI in config) | Yes | `./dynamic-plugins/dist/plugin-github-org-dynamic` | `oci://ghcr.io/.../plugin-github-org:bs_1.45.3__0.3.16` | `./dynamic-plugins/dist/plugin-github-org-dynamic` (from metadata) | **Skipped** | +| 4 | Non-DPDY OCI plugin | No | `oci://ghcr.io/.../plugin-scorecard:bs_1.49.4__1.0.0!alias` | `oci://ghcr.io/.../plugin-scorecard:old_tag` | `oci://ghcr.io/.../plugin-scorecard:bs_1.49.4__1.0.0!alias` (from metadata) | **Yes** (non-DPDY needs config) | +| 5 | Non-DPDY wrapper plugin | No | `./dynamic-plugins/dist/plugin-custom` | `./dynamic-plugins/dist/plugin-custom` | `./dynamic-plugins/dist/plugin-custom` (from metadata) | **Skipped** | +| 6 | DPDY OCI (wrapper in config) | Yes | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` | `./dynamic-plugins/dist/plugin-tekton` | `oci://ghcr.io/.../plugin-tekton:{{inherit}}` | **Skipped** | +| 7 | Cross-workspace (local path, no metadata) | — | — | `./dynamic-plugins/dist/plugin-kubernetes-backend-dynamic` | unchanged | **Skipped** | +| 8 | Cross-workspace (OCI, no metadata) | — | — | `oci://ghcr.io/.../plugin-dynamic-home-page:bs_1.45.3__1.10.3!alias` | unchanged | **Skipped** | +| 9 | npm package (no metadata) | — | — | `@rhdh/plugin-global-header-test@0.0.2` | unchanged | **Skipped** | +| 10 | DPDY different registry (RHEC) | Yes | `oci://registry.access.redhat.com/rhdh/plugin-orch@sha256:f40d` | `oci://ghcr.io/.../plugin-orch:some_tag` | `oci://registry.access.redhat.com/rhdh/plugin-orch:{{inherit}}` | **Skipped** | +| 11 | DPDY different registry (quay.io) | Yes | `oci://quay.io/rhdh/plugin-cost@sha256:abc` | `oci://quay.io/rhdh/plugin-cost@sha256:abc` | `oci://quay.io/rhdh/plugin-cost:{{inherit}}` | **Skipped** | +| 12 | Non-DPDY different registry (quay.io) | No | `oci://quay.io/rhdh/plugin-events@sha256:abc` | `oci://ghcr.io/.../plugin-events:old_tag` | `oci://quay.io/rhdh/plugin-events@sha256:abc` (from metadata) | **Yes** | +| 13 | Non-DPDY different registry (RHEC) | No | `oci://registry.access.redhat.com/rhdh/plugin-custom@sha256:f40d` | `oci://ghcr.io/.../plugin-custom:some_tag` | `oci://registry.access.redhat.com/rhdh/plugin-custom@sha256:f40d` (from metadata) | **Yes** | ### Key Takeaways | Rule | Explanation | |------|-------------| | **Metadata always wins** | When metadata exists, `spec.dynamicArtifact` determines the package — the user config's `package` field is overwritten | +| **DPDY OCI → `{{inherit}}`** | In nightly, DPDY plugins with OCI metadata use `{{inherit}}` to test the exact RC versions. No config injection — RHDH provides defaults | +| **Non-DPDY OCI → full ref + injection** | In nightly, non-DPDY OCI plugins use metadata refs and get `appConfigExamples` injected (they're not in RHDH defaults) | +| **Wrappers never get `{{inherit}}`** | Wrapper plugins always use the metadata path, regardless of DPDY status | | **No metadata = passthrough** | Cross-workspace plugins, npm packages, and anything without a metadata match passes through unchanged | | **PR mode overrides everything** | Even if metadata says wrapper, PR mode builds an OCI URL from `source.json` + `plugins-list.yaml` | -| **Nightly skips config injection** | User `pluginConfig` is preserved as-is; metadata `appConfigExamples` is NOT merged in | -| **Registry comes from metadata** | In nightly/local, the exact registry from metadata is used (quay.io, registry.access.redhat.com, etc.). In PR mode, all PR images come from `ghcr.io` | +| **Registry comes from metadata** | In nightly/local, the exact registry from metadata is used — including `{{inherit}}` refs (rows 10-11). The runtime matches by registry prefix, so `{{inherit}}` must use the same registry as the DPDY entry. In PR mode, all PR images come from `ghcr.io` | | **Row 3 is a common pitfall** | If your config has a stale OCI ref but metadata says wrapper, the resolver uses the wrapper path from metadata. Keep your `dynamic-plugins.yaml` in sync, or better yet, don't create one — let it auto-generate from metadata | ### Cross-Workspace Plugins -The resolver only looks at `metadata/` in the **current workspace**. It does not search other workspaces. If your test needs a plugin from another workspace (rows 5-6 above), there's no metadata match, so the package reference passes through unchanged in all modes. +The resolver only looks at `metadata/` in the **current workspace**. It does not search other workspaces. If your test needs a plugin from another workspace (PR rows 5-6, nightly rows 7-8), there's no metadata match, so the package reference passes through unchanged in all modes. When using an OCI ref for a cross-workspace plugin, you often need to also **disable the local wrapper** for that plugin (included in `dynamic-plugins.default.yaml`), otherwise both versions load and conflict: @@ -179,9 +194,9 @@ This is the recommended approach — most workspaces don't need a `dynamic-plugi ## Common Pitfalls -### Config injection is skipped in nightly +### Config injection in nightly is selective -In nightly mode, `appConfigExamples` from metadata are NOT injected. If your test relies on config from metadata, you must provide it explicitly in `app-config-rhdh.yaml` or inline in `pluginConfig`. +In nightly mode, config injection only happens for **non-DPDY OCI plugins**. DPDY plugins get their config from RHDH's built-in defaults via `{{inherit}}`, and wrapper plugins get no injection. If your test relies on config for a DPDY plugin, provide it explicitly in `app-config-rhdh.yaml` or inline in `pluginConfig`. ### PR mode requires /publish first diff --git a/docs/overlay/reference/troubleshooting.md b/docs/overlay/reference/troubleshooting.md index 781676c..325bb57 100644 --- a/docs/overlay/reference/troubleshooting.md +++ b/docs/overlay/reference/troubleshooting.md @@ -222,7 +222,7 @@ oc login --token= --server= **"metadata directory not found" or "no valid metadata files"** - Ensure `workspaces//metadata/` exists and contains valid Package CRD YAML files. -- If you intentionally want to skip metadata injection, set `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true`. +- If you intentionally want to skip metadata injection locally, set `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true` (this is ignored in CI). ### "Tests pass locally but fail in CI" diff --git a/docs/overlay/tutorials/ci-pipeline.md b/docs/overlay/tutorials/ci-pipeline.md index cde78e0..1d4ed62 100644 --- a/docs/overlay/tutorials/ci-pipeline.md +++ b/docs/overlay/tutorials/ci-pipeline.md @@ -214,8 +214,9 @@ The following environment variables are available during CI execution: | Variable | Effect | |----------|--------| | `GIT_PR_NUMBER` | Enables OCI URL generation for PR builds | -| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | Disables all metadata handling | -| `JOB_NAME` | If contains `periodic-`, disables metadata handling | +| `RELEASE_BRANCH_NAME` | Branch for `default.packages.yaml` fetch (required in CI for nightly) | +| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | Disables metadata injection (local only, ignored in CI) | +| `JOB_NAME` | If contains `periodic-`, activates nightly mode | See [Environment Variables Reference](/overlay/reference/environment-variables#plugin-metadata-variables) for details. diff --git a/package.json b/package.json index 5cbe6f9..4676aff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@red-hat-developer-hub/e2e-test-utils", - "version": "1.1.37", + "version": "1.2.0", "description": "Test utilities for RHDH E2E tests", "license": "Apache-2.0", "repository": { diff --git a/src/utils/plugin-metadata.ts b/src/utils/plugin-metadata.ts index 0f9c43f..5e72bcf 100644 --- a/src/utils/plugin-metadata.ts +++ b/src/utils/plugin-metadata.ts @@ -75,6 +75,71 @@ export function isNightlyJob(): boolean { return false; } +// ── Default Packages (DPDY) ────────────────────────────────────────────────── + +const DEFAULT_PACKAGES_BASE_URL = + "https://raw.githubusercontent.com/redhat-developer/rhdh/refs/heads"; + +interface DefaultPackagesYaml { + packages?: { + enabled?: Array<{ package: string }>; + disabled?: Array<{ package: string }>; + }; +} + +/** + * Fetches the list of packages shipped in the RHDH catalog index image (DPDY). + * Used in nightly mode to determine which plugins support {{inherit}} tag + * resolution vs which need full OCI refs from metadata. + * + * Branch is determined by RELEASE_BRANCH_NAME (set by OpenShift CI), + * defaulting to "main" for local development. + */ +export async function fetchDefaultPackages(): Promise> { + const branch = process.env.RELEASE_BRANCH_NAME; + if (!branch) { + if (process.env.CI) { + throw new Error( + "[PluginMetadata] RELEASE_BRANCH_NAME is required in CI to fetch default.packages.yaml", + ); + } + console.log( + "[PluginMetadata] RELEASE_BRANCH_NAME not set — defaulting to 'main' (local dev)", + ); + } + const resolvedBranch = branch || "main"; + const url = `${DEFAULT_PACKAGES_BASE_URL}/${resolvedBranch}/default.packages.yaml`; + + console.log( + `[PluginMetadata] Fetching default packages from ${url} (branch: ${resolvedBranch})...`, + ); + + const response = await fetch(url); + if (!response.ok) { + throw new Error( + `[PluginMetadata] Failed to fetch default.packages.yaml: ${response.status} ${response.statusText}\n` + + ` URL: ${url}\n` + + ` Branch: ${resolvedBranch} (from RELEASE_BRANCH_NAME)`, + ); + } + + const content = await response.text(); + const parsed = yaml.load(content) as DefaultPackagesYaml; + + const packages = new Set(); + for (const list of [parsed?.packages?.enabled, parsed?.packages?.disabled]) { + for (const entry of list || []) { + if (entry.package) packages.add(entry.package); + } + } + + console.log( + `[PluginMetadata] Found ${packages.size} packages in default.packages.yaml (branch: ${resolvedBranch})`, + ); + + return packages; +} + // ── Utilities ───────────────────────────────────────────────────────────────── /** @@ -355,6 +420,7 @@ async function resolvePluginPackages( plugins: PluginEntry[], metadataMap: Map, metadataPath: string, + dpdyPackages: Set | null = null, ): Promise { // Build PR OCI URLs if applicable const prNumber = process.env.GIT_PR_NUMBER; @@ -372,7 +438,7 @@ async function resolvePluginPackages( const pluginName = extractPluginName(pkg); const metadata = metadataMap.get(pluginName); - // 1. With metadata: resolve to PR OCI URL or metadata's dynamicArtifact + // 1. With metadata: resolve to PR OCI URL, {{inherit}}, or metadata's dynamicArtifact if (metadata?.packageName) { const displayName = toDisplayName(metadata.packageName); @@ -385,9 +451,22 @@ async function resolvePluginPackages( } } - // Use metadata's dynamicArtifact directly (latest published version). - // This is more accurate than {{inherit}} because metadata is updated daily - // while the DPDY in the catalog index may lag behind. + // Nightly DPDY + OCI: use {{inherit}} tag so RHDH resolves the version + // from its built-in dynamic-plugins.default.yaml (RC testing). + // Registry is preserved from metadata so the runtime key matches the DPDY entry. + if ( + dpdyPackages?.has(metadata.packageName) && + metadata.packagePath.startsWith("oci://") + ) { + const noAlias = metadata.packagePath.split("!")[0]; + const name = extractPluginName(noAlias); + const idx = noAlias.lastIndexOf(name); + const inheritRef = `${noAlias.substring(0, idx + name.length)}:{{inherit}}`; + console.log(`[PluginMetadata] DPDY inherit: ${pkg} → ${inheritRef}`); + return { ...plugin, package: inheritRef }; + } + + // OCI: use metadata's dynamicArtifact directly (non-DPDY or non-nightly). if (metadata.packagePath.startsWith("oci://")) { console.log(`[PluginMetadata] ${pkg} → ${metadata.packagePath}`); return { ...plugin, package: metadata.packagePath }; @@ -401,10 +480,7 @@ async function resolvePluginPackages( return { ...plugin, package: metadata.packagePath }; } - // 2. Local paths (./dynamic-plugins/dist/...) and other formats — keep as-is. - // Local paths reference plugins bundled in the RHDH container image and work - // without OCI resolution. When the catalog index moves all plugins to OCI refs, - // they'll be handled by step 1 or 2 above automatically. + // 2. No metadata — keep as-is (cross-workspace, npm packages, etc.) return plugin; }); } @@ -513,49 +589,86 @@ export async function generatePluginsFromMetadata( return { plugins }; } +function selectMetadataForInjection( + metadataMap: Map, + nightly: boolean, + dpdyPackages: Set | null, +): Map | null { + if ( + !process.env.CI && + process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION === "true" + ) + return null; + if (metadataMap.size === 0) return null; + + if (!nightly) return metadataMap; + if (!dpdyPackages) return null; + + // Nightly: only non-DPDY OCI plugins (DPDY plugins get config via {{inherit}}) + return new Map( + [...metadataMap].filter( + ([, m]) => + !dpdyPackages.has(m.packageName) && m.packagePath.startsWith("oci://"), + ), + ); +} + /** * Processes a dynamic plugins configuration for deployment. * Single entry point for both PR and nightly flows. * * Operations (in order): - * 1. Inject appConfigExamples from metadata (PR mode only, unless RHDH_SKIP_PLUGIN_METADATA_INJECTION is set) - * 2. Resolve all packages to OCI references: - * - PR with GIT_PR_NUMBER: workspace plugins in PR build → pr_ tags, rest unchanged - * - PR without GIT_PR_NUMBER: OCI plugins with metadata → metadata refs, rest unchanged - * - Nightly: OCI plugins with metadata → metadata refs, rest unchanged + * 1. Inject appConfigExamples from metadata: + * - PR/local: all plugins with metadata (unless RHDH_SKIP_PLUGIN_METADATA_INJECTION) + * - Nightly: only non-DPDY OCI plugins (DPDY plugins inherit config from RHDH) + * 2. Resolve all packages: + * - PR with GIT_PR_NUMBER: workspace plugins → pr_ OCI tags + * - Nightly DPDY + OCI: {{inherit}} tag (RC testing against shipped version) + * - Nightly non-DPDY / local: metadata's dynamicArtifact as-is * * @param config The merged dynamic plugins configuration * @param metadataPath Optional custom path to metadata directory + * @param dpdyPackages Optional pre-loaded DPDY package set (for testing; fetched automatically if omitted in nightly) * @returns Processed configuration ready for deployment */ export async function processPluginsForDeployment( config: DynamicPluginsConfig, metadataPath: string = DEFAULT_METADATA_PATH, + dpdyPackages?: Set, ): Promise { if (!config.plugins) return config; - const metadataMap = await tryLoadMetadata(metadataPath); + const nightly = isNightlyJob(); + + const [metadataMap, resolvedDpdyPackages] = await Promise.all([ + tryLoadMetadata(metadataPath), + nightly ? (dpdyPackages ?? fetchDefaultPackages()) : Promise.resolve(null), + ]); let result = { ...config }; - // Inject appConfigExamples from metadata (PR mode only) - if ( - !isNightlyJob() && - process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION !== "true" && - metadataMap.size > 0 - ) { - console.log("[PluginMetadata] Injecting metadata configs..."); - result = injectMetadataConfig(result, metadataMap); + // Inject appConfigExamples from metadata + const metadataToInject = selectMetadataForInjection( + metadataMap, + nightly, + resolvedDpdyPackages, + ); + if (metadataToInject && metadataToInject.size > 0) { + console.log( + `[PluginMetadata] Injecting metadata configs for ${metadataToInject.size} plugin(s)...`, + ); + result = injectMetadataConfig(result, metadataToInject); } // Resolve all packages to OCI references - console.log("[PluginMetadata] Resolving plugin packages to OCI..."); + console.log("[PluginMetadata] Resolving plugin packages..."); result = { ...result, plugins: await resolvePluginPackages( result.plugins!, metadataMap, metadataPath, + resolvedDpdyPackages, ), }; diff --git a/src/utils/tests/plugin-metadata.fixtures.test.ts b/src/utils/tests/plugin-metadata.fixtures.test.ts index 6a8492b..cc7c540 100644 --- a/src/utils/tests/plugin-metadata.fixtures.test.ts +++ b/src/utils/tests/plugin-metadata.fixtures.test.ts @@ -254,7 +254,12 @@ describe("processPluginsForDeployment — workspace fixtures", () => { ], }; - const result = await processPluginsForDeployment(config, metadataDir); + // Empty DPDY set — these plugins are not in default.packages.yaml + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(), + ); const plugins = result.plugins!; assert.ok( @@ -269,7 +274,7 @@ describe("processPluginsForDeployment — workspace fixtures", () => { assert.deepStrictEqual( plugins[0].pluginConfig, { events: { http: { topics: ["github"] } } }, - "nightly must preserve user pluginConfig without metadata injection", + "nightly must preserve user pluginConfig for non-DPDY OCI plugin", ); assert.strictEqual( @@ -346,11 +351,12 @@ describe("processPluginsForDeployment — workspace fixtures", () => { "local path must stay unchanged in PR mode", ); - // Nightly mode + // Nightly mode (empty DPDY set to avoid network fetch in unit tests) process.env.E2E_NIGHTLY_MODE = "true"; const nightlyResult = await processPluginsForDeployment( { ...config, plugins: config.plugins!.map((p) => ({ ...p })) }, metadataDir, + new Set(), ); assert.strictEqual( diff --git a/src/utils/tests/plugin-metadata.nightly.test.ts b/src/utils/tests/plugin-metadata.nightly.test.ts index 693a9a2..9c4c263 100644 --- a/src/utils/tests/plugin-metadata.nightly.test.ts +++ b/src/utils/tests/plugin-metadata.nightly.test.ts @@ -1,6 +1,7 @@ /** * Nightly mode tests — isNightlyJob detection and nightly plugin resolution. */ +/* eslint-disable @typescript-eslint/naming-convention -- test fixtures use real plugin config keys with dots/dashes */ import { describe, it, beforeEach, afterEach } from "node:test"; import assert from "node:assert"; import fs from "fs-extra"; @@ -107,7 +108,7 @@ describe("processPluginsForDeployment — nightly mode", () => { }); afterEach(() => env.restore()); - it("skips metadata injection in nightly mode", async () => { + it("skips metadata injection for wrapper plugins in nightly mode", async () => { const metadataDir = await createMetadataFixture([ { name: "backstage-community-plugin-tech-radar", @@ -131,12 +132,16 @@ describe("processPluginsForDeployment — nightly mode", () => { ], }; - const result = await processPluginsForDeployment(config, metadataDir); + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-tech-radar"]), + ); assert.strictEqual( result.plugins![0].pluginConfig, undefined, - "nightly mode must NOT inject metadata pluginConfig", + "nightly mode must NOT inject metadata pluginConfig for wrapper plugins", ); } finally { await fs.remove(metadataDir); @@ -171,7 +176,11 @@ describe("processPluginsForDeployment — nightly mode", () => { ], }; - const result = await processPluginsForDeployment(config, metadataDir); + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(), + ); assert.deepStrictEqual( result.plugins![0].pluginConfig, @@ -183,7 +192,7 @@ describe("processPluginsForDeployment — nightly mode", () => { } }); - it("resolves OCI plugin to metadata dynamicArtifact in nightly", async () => { + it("resolves non-DPDY OCI plugin to metadata dynamicArtifact in nightly", async () => { const metadataDir = await createMetadataFixture([ { name: "backstage-community-plugin-tekton", @@ -204,11 +213,16 @@ describe("processPluginsForDeployment — nightly mode", () => { ], }; - const result = await processPluginsForDeployment(config, metadataDir); + // Empty DPDY set — plugin is NOT in default.packages.yaml + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(), + ); assert.ok( result.plugins![0].package.includes("bs_1.45.3__3.33.3"), - "nightly must resolve to metadata dynamicArtifact (latest published version)", + "non-DPDY OCI plugin must resolve to metadata dynamicArtifact", ); } finally { await fs.remove(metadataDir); @@ -240,7 +254,11 @@ describe("processPluginsForDeployment — nightly mode", () => { ], }; - const result = await processPluginsForDeployment(config, metadataDir); + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(), + ); assert.strictEqual( result.plugins![0].package, @@ -273,7 +291,11 @@ describe("processPluginsForDeployment — nightly mode", () => { ], }; - const result = await processPluginsForDeployment(config, metadataDir); + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(), + ); assert.strictEqual( result.plugins![0].package, @@ -285,3 +307,442 @@ describe("processPluginsForDeployment — nightly mode", () => { } }); }); + +// ── {{inherit}} resolution (DPDY plugins) ────────────────────────────────── + +describe("processPluginsForDeployment — nightly {{inherit}}", () => { + const env = withCleanEnv(); + beforeEach(() => { + env.save(); + delete process.env.GIT_PR_NUMBER; + process.env.E2E_NIGHTLY_MODE = "true"; + }); + afterEach(() => env.restore()); + + it("resolves DPDY OCI plugin to {{inherit}} tag", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tekton", + packageName: "@backstage-community/plugin-tekton", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old_tag", + disabled: false, + }, + ], + }; + + const dpdyPackages = new Set(["@backstage-community/plugin-tekton"]); + const result = await processPluginsForDeployment( + config, + metadataDir, + dpdyPackages, + ); + + assert.strictEqual( + result.plugins![0].package, + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:{{inherit}}", + "DPDY OCI plugin must resolve to {{inherit}} tag", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("{{inherit}} ref has no !alias suffix", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-topology", + packageName: "@backstage-community/plugin-topology", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-topology:bs_1.49.4__1.2.0!backstage-community-plugin-topology", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-topology:old", + disabled: false, + }, + ], + }; + + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-topology"]), + ); + + assert.ok( + !result.plugins![0].package.includes("!"), + "{{inherit}} ref must NOT include !alias suffix", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("{{inherit}} preserves registry.access.redhat.com registry from metadata", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "red-hat-developer-hub-backstage-plugin-orchestrator", + packageName: "@red-hat-developer-hub/backstage-plugin-orchestrator", + dynamicArtifact: + "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator@sha256:062a536d", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator@sha256:062a536d", + disabled: false, + }, + ], + }; + + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@red-hat-developer-hub/backstage-plugin-orchestrator"]), + ); + + assert.strictEqual( + result.plugins![0].package, + "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", + "{{inherit}} must use registry from metadata, not hardcoded ghcr.io", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("{{inherit}} preserves quay.io registry from metadata", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-cost-management", + packageName: "@backstage-community/plugin-cost-management", + dynamicArtifact: + "oci://quay.io/redhat-resource-optimization/backstage-community-plugin-cost-management@sha256:abc123", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://quay.io/redhat-resource-optimization/backstage-community-plugin-cost-management@sha256:abc123", + disabled: false, + }, + ], + }; + + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-cost-management"]), + ); + + assert.strictEqual( + result.plugins![0].package, + "oci://quay.io/redhat-resource-optimization/backstage-community-plugin-cost-management:{{inherit}}", + "{{inherit}} must use registry from metadata, not hardcoded ghcr.io", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("DPDY wrapper plugin keeps wrapper path (no {{inherit}})", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tech-radar", + packageName: "@backstage-community/plugin-tech-radar", + dynamicArtifact: + "./dynamic-plugins/dist/backstage-community-plugin-tech-radar", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "./dynamic-plugins/dist/backstage-community-plugin-tech-radar", + disabled: false, + }, + ], + }; + + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-tech-radar"]), + ); + + assert.strictEqual( + result.plugins![0].package, + "./dynamic-plugins/dist/backstage-community-plugin-tech-radar", + "DPDY wrapper plugin must keep wrapper path, not use {{inherit}}", + ); + assert.ok( + !result.plugins![0].package.includes("inherit"), + "wrapper plugin must not contain {{inherit}}", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("non-DPDY OCI plugin uses full metadata ref (not {{inherit}})", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "red-hat-developer-hub-backstage-plugin-scorecard", + packageName: "@red-hat-developer-hub/backstage-plugin-scorecard", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:bs_1.49.4__1.0.0!red-hat-developer-hub-backstage-plugin-scorecard", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:old", + disabled: false, + }, + ], + }; + + // Scorecard is NOT in the DPDY + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-tekton"]), + ); + + assert.ok( + result.plugins![0].package.includes("bs_1.49.4__1.0.0"), + "non-DPDY OCI plugin must use full metadata ref", + ); + assert.ok( + !result.plugins![0].package.includes("inherit"), + "non-DPDY OCI plugin must NOT use {{inherit}}", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("skips config injection for DPDY OCI plugins", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tekton", + packageName: "@backstage-community/plugin-tekton", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton", + appConfigExamples: { + dynamicPlugins: { + frontend: { + "backstage-community.plugin-tekton": { enabled: true }, + }, + }, + }, + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old", + disabled: false, + }, + ], + }; + + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-tekton"]), + ); + + assert.strictEqual( + result.plugins![0].pluginConfig, + undefined, + "DPDY plugin must NOT get metadata config injected — RHDH provides it via {{inherit}}", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("injects config for non-DPDY OCI plugins", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "red-hat-developer-hub-backstage-plugin-scorecard", + packageName: "@red-hat-developer-hub/backstage-plugin-scorecard", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:bs_1.49.4__1.0.0!red-hat-developer-hub-backstage-plugin-scorecard", + appConfigExamples: { + scorecard: { apiUrl: "http://scorecard.example.com" }, + }, + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:old", + disabled: false, + }, + ], + }; + + // Scorecard NOT in DPDY + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(), + ); + + assert.deepStrictEqual( + result.plugins![0].pluginConfig, + { scorecard: { apiUrl: "http://scorecard.example.com" } }, + "non-DPDY OCI plugin must get metadata config injected in nightly", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("does not inject config for non-DPDY wrapper plugins", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "backstage-plugin-catalog-backend-module-github-org", + packageName: "@backstage/plugin-catalog-backend-module-github-org", + dynamicArtifact: + "./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-org-dynamic", + appConfigExamples: { + catalog: { providers: { github: { org: "test" } } }, + }, + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-org-dynamic", + disabled: false, + }, + ], + }; + + // NOT in DPDY, but it's a wrapper — no injection + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(), + ); + + assert.strictEqual( + result.plugins![0].pluginConfig, + undefined, + "non-DPDY wrapper plugin must NOT get metadata config injected", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("mixed scenario: DPDY OCI → inherit, non-DPDY OCI → full ref + config", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tekton", + packageName: "@backstage-community/plugin-tekton", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton", + appConfigExamples: { + tekton: { enabled: true }, + }, + }, + { + name: "red-hat-developer-hub-backstage-plugin-scorecard", + packageName: "@red-hat-developer-hub/backstage-plugin-scorecard", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:bs_1.49.4__1.0.0!red-hat-developer-hub-backstage-plugin-scorecard", + appConfigExamples: { + scorecard: { apiUrl: "http://scorecard.example.com" }, + }, + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old", + disabled: false, + }, + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:old", + disabled: false, + }, + ], + }; + + // Only tekton is in DPDY + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-tekton"]), + ); + + // Tekton: DPDY → {{inherit}}, no config injection + assert.ok( + result.plugins![0].package.includes("{{inherit}}"), + "DPDY plugin must use {{inherit}}", + ); + assert.strictEqual( + result.plugins![0].pluginConfig, + undefined, + "DPDY plugin must not have config injected", + ); + + // Scorecard: non-DPDY → full OCI ref, config injected + assert.ok( + result.plugins![1].package.includes("bs_1.49.4__1.0.0"), + "non-DPDY plugin must use full metadata ref", + ); + assert.deepStrictEqual( + result.plugins![1].pluginConfig, + { scorecard: { apiUrl: "http://scorecard.example.com" } }, + "non-DPDY OCI plugin must have config injected", + ); + } finally { + await fs.remove(metadataDir); + } + }); +}); diff --git a/src/utils/tests/plugin-metadata.pr.test.ts b/src/utils/tests/plugin-metadata.pr.test.ts index e939ce8..af863c2 100644 --- a/src/utils/tests/plugin-metadata.pr.test.ts +++ b/src/utils/tests/plugin-metadata.pr.test.ts @@ -231,6 +231,7 @@ describe("processPluginsForDeployment — PR mode", () => { }); it("skips injection when RHDH_SKIP_PLUGIN_METADATA_INJECTION is 'true'", async () => { + delete process.env.CI; process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION = "true"; const metadataDir = await createMetadataFixture([ @@ -268,6 +269,44 @@ describe("processPluginsForDeployment — PR mode", () => { } }); + it("ignores RHDH_SKIP_PLUGIN_METADATA_INJECTION in CI", async () => { + process.env.CI = "true"; + process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION = "true"; + + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tech-radar", + packageName: "@backstage-community/plugin-tech-radar", + dynamicArtifact: + "./dynamic-plugins/dist/backstage-community-plugin-tech-radar", + appConfigExamples: { + techRadar: { url: "http://default.example.com" }, + }, + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "./dynamic-plugins/dist/backstage-community-plugin-tech-radar", + disabled: false, + }, + ], + }; + + const result = await processPluginsForDeployment(config, metadataDir); + + assert.ok( + result.plugins![0].pluginConfig, + "pluginConfig must be injected in CI even when RHDH_SKIP_PLUGIN_METADATA_INJECTION=true", + ); + } finally { + await fs.remove(metadataDir); + } + }); + it("does not skip injection when RHDH_SKIP_PLUGIN_METADATA_INJECTION is 'false'", async () => { process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION = "false";