diff --git a/docs/api/utils/plugin-metadata.md b/docs/api/utils/plugin-metadata.md index 59714b2..0cc1b2b 100644 --- a/docs/api/utils/plugin-metadata.md +++ b/docs/api/utils/plugin-metadata.md @@ -13,7 +13,7 @@ Plugin metadata handling is fully automatic during `rhdh.deploy()`. The function Extracts the plugin name from a package path or OCI reference. ```typescript -function extractPluginName(packageRef: string): string +function extractPluginName(packageRef: string): string; ``` **Parameters:** @@ -25,18 +25,20 @@ function extractPluginName(packageRef: string): string **Supported Formats:** -| Format | Example | Extracted Name | -|--------|---------|----------------| -| Wrapper path | `./dynamic-plugins/dist/my-plugin` | `my-plugin` | -| OCI with tag | `oci://quay.io/rhdh/my-plugin:1.0.0` | `my-plugin` | -| OCI with digest | `oci://quay.io/rhdh/my-plugin@sha256:abc...` | `my-plugin` | -| OCI with alias | `oci://quay.io/rhdh/my-plugin@sha256:abc!alias` | `my-plugin` | -| GHCR | `ghcr.io/org/repo/my-plugin:tag` | `my-plugin` | +| Format | Example | Extracted Name | +| --------------- | ----------------------------------------------- | -------------- | +| Wrapper path | `./dynamic-plugins/dist/my-plugin` | `my-plugin` | +| OCI with tag | `oci://quay.io/rhdh/my-plugin:1.0.0` | `my-plugin` | +| OCI with digest | `oci://quay.io/rhdh/my-plugin@sha256:abc...` | `my-plugin` | +| OCI with alias | `oci://quay.io/rhdh/my-plugin@sha256:abc!alias` | `my-plugin` | +| GHCR | `ghcr.io/org/repo/my-plugin:tag` | `my-plugin` | **Example:** ```typescript -const name = extractPluginName("oci://quay.io/rhdh/backstage-community-plugin-tech-radar:1.0.0"); +const name = extractPluginName( + "oci://quay.io/rhdh/backstage-community-plugin-tech-radar:1.0.0", +); // Returns: "backstage-community-plugin-tech-radar" ``` @@ -47,7 +49,7 @@ const name = extractPluginName("oci://quay.io/rhdh/backstage-community-plugin-te Returns a stable merge key for a plugin entry so that OCI and local path for the same logical plugin match when merging dynamic-plugins configs. Strips a trailing `-dynamic` suffix so that e.g. `backstage-community-plugin-catalog-backend-module-keycloak-dynamic` (local) and `backstage-community-plugin-catalog-backend-module-keycloak` (from OCI) map to the same key. ```typescript -function getNormalizedPluginMergeKey(entry: { package?: string }): string +function getNormalizedPluginMergeKey(entry: { package?: string }): string; ``` **Parameters:** @@ -62,12 +64,14 @@ function getNormalizedPluginMergeKey(entry: { package?: string }): string ```typescript // OCI and local path for the same plugin yield the same key getNormalizedPluginMergeKey({ - package: "oci://ghcr.io/org/repo/backstage-community-plugin-catalog-backend-module-keycloak:tag!alias", + package: + "oci://ghcr.io/org/repo/backstage-community-plugin-catalog-backend-module-keycloak:tag!alias", }); // Returns: "backstage-community-plugin-catalog-backend-module-keycloak" getNormalizedPluginMergeKey({ - package: "./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic", + package: + "./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic", }); // Returns: "backstage-community-plugin-catalog-backend-module-keycloak" ``` @@ -79,12 +83,13 @@ getNormalizedPluginMergeKey({ Determines whether the current execution is a nightly/periodic job. Controls whether metadata config injection is enabled and which OCI resolution strategy is used. ```typescript -function isNightlyJob(): boolean +function isNightlyJob(): boolean; ``` **Returns:** `true` if running in nightly mode, `false` for PR/local mode. **Priority order:** + 1. If `GIT_PR_NUMBER` is set → returns `false` (PR mode takes precedence) 2. If `E2E_NIGHTLY_MODE` is `"true"` or `"1"` → returns `true` 3. If `JOB_NAME` contains `periodic-` → returns `true` @@ -97,7 +102,7 @@ function isNightlyJob(): boolean Gets the metadata directory path. ```typescript -function getMetadataDirectory(metadataPath?: string): string | null +function getMetadataDirectory(metadataPath?: string): string | null; ``` **Parameters:** @@ -115,8 +120,8 @@ Parses all metadata files in a directory and builds a map of plugin name to conf ```typescript async function parseAllMetadataFiles( - metadataDir: string -): Promise> + metadataDir: string, +): Promise>; ``` **Parameters:** @@ -134,8 +139,8 @@ Auto-generates plugin entries from workspace metadata files when no user-provide ```typescript async function generatePluginsFromMetadata( - metadataPath?: string -): Promise + metadataPath?: string, +): Promise; ``` **Parameters:** @@ -154,8 +159,9 @@ Unified entry point for both PR and nightly plugin resolution flows. Called auto ```typescript async function processPluginsForDeployment( config: DynamicPluginsConfig, - metadataPath?: string -): Promise + metadataPath?: string, + dpdyPackages?: Set, +): Promise; ``` **Parameters:** @@ -163,13 +169,15 @@ 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()`): Plugins in `default.packages.yaml` whose metadata `spec.dynamicArtifact` is an OCI ref use `{{inherit}}` with configurable registry (default `registry.access.redhat.com/rhdh`, overridable via `NIGHTLY_DPDY_OCI_REGISTRY` or `NIGHTLY_DPDY_OCI_REGISTRY_MAP`) — RHDH resolves both the OCI tag and default config from its built-in DPDY. Plugins NOT in `default.packages.yaml` with OCI metadata use full metadata refs with config injection. +- Respects `RHDH_SKIP_PLUGIN_METADATA_INJECTION` to skip config injection (local only, ignored in CI) --- @@ -178,7 +186,7 @@ async function processPluginsForDeployment( Creates a dynamic plugins config that disables wrapper plugins. Used during PR builds when wrapper plugins would conflict with PR-built OCI images. ```typescript -function disablePluginWrappers(plugins: string[]): DynamicPluginsConfig +function disablePluginWrappers(plugins: string[]): DynamicPluginsConfig; ``` **Parameters:** diff --git a/docs/changelog.md b/docs/changelog.md index b0f241a..8f60725 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,15 @@ All notable changes to this project will be documented in this file. -## [1.1.39] - Current +## [1.1.40] - Current + +### Changed + +- **Nightly `{{inherit}}` resolution**: In nightly mode, plugins listed in [`default.packages.yaml`](https://github.com/redhat-developer/rhdh/blob/main/default.packages.yaml) whose metadata `spec.dynamicArtifact` is an OCI ref now resolve to `{{inherit}}` tags instead of pinned OCI refs. The `{{inherit}}` tag tells RHDH to resolve both the OCI tag (version) and default config from its built-in DPDY (`dynamic-plugins.default.yaml` in the catalog index image), so no config injection is needed from our side. This tests against the exact versions and configuration shipped in the RC. Plugins NOT in `default.packages.yaml` with OCI metadata continue using full metadata refs with config injection (they aren't in RHDH's built-in defaults). The DPDY list is fetched at runtime from the `rhdh` repo using `RELEASE_BRANCH_NAME` (required in CI, defaults to `main` locally). The `{{inherit}}` registry defaults to `registry.access.redhat.com/rhdh` and can be overridden with `NIGHTLY_DPDY_OCI_REGISTRY` (blanket) or `NIGHTLY_DPDY_OCI_REGISTRY_MAP` (per-plugin JSON: `{"registry": ["pkg1", "pkg2"]}`). This decouples the `{{inherit}}` registry from metadata's `spec.dynamicArtifact`, avoiding mismatches when metadata points to `ghcr.io` but the DPDY uses `registry.access.redhat.com`. +- **`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.39] ### Changed diff --git a/docs/guide/configuration/config-files.md b/docs/guide/configuration/config-files.md index ca5c61f..4d65d99 100644 --- a/docs/guide/configuration/config-files.md +++ b/docs/guide/configuration/config-files.md @@ -212,18 +212,24 @@ Your `pluginConfig` in `dynamic-plugins.yaml` overrides values from metadata. ### When Injection is Enabled 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 plugins NOT in `default.packages.yaml` with OCI metadata get injection; plugins in `default.packages.yaml` use `{{inherit}}` — RHDH resolves both the OCI tag and default config from its built-in DPDY ::: warning When injection is enabled, deployment will fail if: + - The `metadata/` directory doesn't exist - No valid metadata files are found in the directory -::: + ::: ### OCI URL Replacement for PR Builds @@ -244,6 +250,7 @@ This allows E2E tests to run against the actual OCI images built for the PR. If you want to reproduce OCI URL replacement locally, create the required files at the workspace root: **source.json** + ```json { "repo": "https://github.com/redhat-developer/rhdh-plugin-export-overlays", @@ -252,6 +259,7 @@ If you want to reproduce OCI URL replacement locally, create the required files ``` **plugins-list.yaml** + ```yaml plugins/tech-radar: plugins/my-plugin: @@ -274,12 +282,12 @@ See [Plugin Metadata - OCI URL Generation](/guide/utilities/plugin-metadata#oci- The package automatically matches plugins across different reference formats: -| Format | Example | -|--------|---------| -| Wrapper path | `./dynamic-plugins/dist/my-plugin` | -| OCI with tag | `oci://quay.io/rhdh/my-plugin:1.0.0` | +| Format | Example | +| --------------- | -------------------------------------------- | +| Wrapper path | `./dynamic-plugins/dist/my-plugin` | +| OCI with tag | `oci://quay.io/rhdh/my-plugin:1.0.0` | | OCI with digest | `oci://quay.io/rhdh/my-plugin@sha256:abc...` | -| GHCR | `ghcr.io/org/repo/my-plugin:tag` | +| GHCR | `ghcr.io/org/repo/my-plugin:tag` | All formats extract the plugin name (`my-plugin`) for matching against metadata. @@ -287,13 +295,14 @@ All formats extract the plugin name (`my-plugin`) for matching against metadata. Use these syntaxes in YAML files: -| Syntax | Description | -|--------|-------------| -| `$VAR` | Simple substitution | -| `${VAR}` | Braced substitution | -| `${VAR:-default}` | Default if unset | +| Syntax | Description | +| ----------------- | ------------------- | +| `$VAR` | Simple substitution | +| `${VAR}` | Braced substitution | +| `${VAR:-default}` | Default if unset | Example: + ```yaml backend: baseUrl: https://backstage-${NAMESPACE}.${K8S_CLUSTER_ROUTER_BASE} diff --git a/docs/guide/configuration/environment-variables.md b/docs/guide/configuration/environment-variables.md index 7079ef0..d49026c 100644 --- a/docs/guide/configuration/environment-variables.md +++ b/docs/guide/configuration/environment-variables.md @@ -6,47 +6,52 @@ Complete reference of all environment variables used by the package. These are optional but commonly set to control deployment behavior: -| Variable | Description | Example | Default | -|----------|-------------|---------|---------| -| `RHDH_VERSION` | RHDH version to deploy | `"1.5"` | `"next"` | -| `INSTALLATION_METHOD` | Deployment method | `"helm"` or `"operator"` | `"helm"` | +| Variable | Description | Example | Default | +| --------------------- | ---------------------- | ------------------------ | -------- | +| `RHDH_VERSION` | RHDH version to deploy | `"1.5"` | `"next"` | +| `INSTALLATION_METHOD` | Deployment method | `"helm"` or `"operator"` | `"helm"` | ## Auto-Generated Variables These are set automatically during deployment: -| Variable | Description | Set By | -|----------|-------------|--------| -| `K8S_CLUSTER_ROUTER_BASE` | OpenShift ingress domain | Global setup | -| `RHDH_BASE_URL` | Full RHDH URL | RHDHDeployment | +| Variable | Description | Set By | +| ------------------------- | ------------------------ | -------------- | +| `K8S_CLUSTER_ROUTER_BASE` | OpenShift ingress domain | Global setup | +| `RHDH_BASE_URL` | Full RHDH URL | RHDHDeployment | ## Playwright Variables -| Variable | Description | Default | -|----------|-------------|---------| +| Variable | Description | Default | +| -------------------- | ------------------------------------------------- | ------- | | `PLAYWRIGHT_WORKERS` | Number of parallel workers (e.g., `"4"`, `"50%"`) | `"50%"` | -| `PLAYWRIGHT_RETRIES` | Number of test retries on failure | `0` | +| `PLAYWRIGHT_RETRIES` | Number of test retries on failure | `0` | ## Optional Variables -| Variable | Description | Default | -|----------|-------------|---------| -| `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 | - | +| Variable | Description | Default | +| ------------------------------------- | ------------------------------------------------------------- | -------------------------- | +| `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 (local only, ignored in CI) | - | ## Plugin Metadata Variables -These control automatic plugin configuration injection from metadata files: +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 | -| `JOB_MODE` | CI-only: `nightly` or `pr-check` (set by step registry) | Informational | +> **DPDY** refers to `dynamic-plugins.default.yaml` in the catalog index image shipped with RHDH. The list of DPDY packages is defined in [`default.packages.yaml`](https://github.com/redhat-developer/rhdh/blob/main/default.packages.yaml). + +| 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 | Plugins in `default.packages.yaml` with OCI metadata use `{{inherit}}` (RHDH resolves both OCI tag and config from DPDY); other OCI plugins use full metadata refs with 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 | +| `NIGHTLY_DPDY_OCI_REGISTRY` | OCI registry for `{{inherit}}` refs | Overrides default `registry.access.redhat.com/rhdh` for all plugins using `{{inherit}}` in nightly mode | +| `NIGHTLY_DPDY_OCI_REGISTRY_MAP` | JSON: `{"registry": ["pkg1", "pkg2"]}` | Per-plugin registry override for `{{inherit}}` refs (takes precedence over `NIGHTLY_DPDY_OCI_REGISTRY`) | +| `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 @@ -66,15 +71,15 @@ See [Plugin Metadata](/guide/utilities/plugin-metadata#oci-url-generation-for-pr Required when using `auth: "keycloak"`: -| Variable | Description | -|----------|-------------| -| `KEYCLOAK_BASE_URL` | Keycloak instance URL | -| `KEYCLOAK_REALM` | Realm name | -| `KEYCLOAK_CLIENT_ID` | OIDC client ID | -| `KEYCLOAK_CLIENT_SECRET` | OIDC client secret | -| `KEYCLOAK_METADATA_URL` | OIDC discovery URL | -| `KEYCLOAK_LOGIN_REALM` | Login realm name | -| `KEYCLOAK_USER_NAME` | Default test username | +| Variable | Description | +| ------------------------ | --------------------- | +| `KEYCLOAK_BASE_URL` | Keycloak instance URL | +| `KEYCLOAK_REALM` | Realm name | +| `KEYCLOAK_CLIENT_ID` | OIDC client ID | +| `KEYCLOAK_CLIENT_SECRET` | OIDC client secret | +| `KEYCLOAK_METADATA_URL` | OIDC discovery URL | +| `KEYCLOAK_LOGIN_REALM` | Login realm name | +| `KEYCLOAK_USER_NAME` | Default test username | | `KEYCLOAK_USER_PASSWORD` | Default test password | These are automatically set by `KeycloakHelper.configureForRHDH()`. @@ -83,12 +88,12 @@ These are automatically set by `KeycloakHelper.configureForRHDH()`. For GitHub integration: -| Variable | Description | Required | -|----------|-------------|----------| +| Variable | Description | Required | +| ------------------------- | ---------------------------- | ------------ | | `VAULT_GITHUB_USER_TOKEN` | GitHub personal access token | For API/auth | -| `VAULT_GH_USER_NAME` | GitHub username | For login | -| `VAULT_GH_USER_PASSWORD` | GitHub password | For login | -| `VAULT_GH_2FA_SECRET` | 2FA secret for OTP | For login | +| `VAULT_GH_USER_NAME` | GitHub username | For login | +| `VAULT_GH_USER_PASSWORD` | GitHub password | For login | +| `VAULT_GH_2FA_SECRET` | 2FA secret for OTP | For login | ## Custom Variables @@ -145,7 +150,7 @@ Set programmatically: test.beforeAll(async ({ rhdh }) => { process.env.MY_CUSTOM_URL = await rhdh.k8sClient.getRouteLocation( rhdh.deploymentConfig.namespace, - "my-service" + "my-service", ); await rhdh.deploy(); diff --git a/docs/guide/utilities/plugin-metadata.md b/docs/guide/utilities/plugin-metadata.md index ea3d81e..8fadd89 100644 --- a/docs/guide/utilities/plugin-metadata.md +++ b/docs/guide/utilities/plugin-metadata.md @@ -18,22 +18,30 @@ workspaces// ``` During deployment, the package reads these metadata files and: + - **Auto-generates** a complete config if `dynamic-plugins.yaml` doesn't exist - **Injects** metadata into existing plugins if `dynamic-plugins.yaml` exists ## When Metadata Handling is Enabled 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-`): + +- Plugins listed in `default.packages.yaml` whose metadata `spec.dynamicArtifact` is an OCI ref use `{{inherit}}` tags — RHDH provides both the OCI tag and default config from its built-in DPDY +- Plugins NOT in `default.packages.yaml` whose metadata is an OCI ref use full metadata refs — with config injection (these plugins aren't in RHDH's built-in defaults) +- Wrapper plugins (local paths) keep their metadata paths — no config injection ::: info Priority The `isNightlyJob()` function checks in this order: + 1. If `GIT_PR_NUMBER` is set → PR mode (returns `false`, metadata injection enabled) 2. If `E2E_NIGHTLY_MODE` is `"true"` or `"1"` → nightly mode (returns `true`) 3. If `JOB_NAME` contains `periodic-` → nightly mode (returns `true`) @@ -59,15 +67,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 plugins NOT in `default.packages.yaml` whose metadata is OCI (plugins in `default.packages.yaml` get both config and OCI tag 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 + in `default.packages.yaml` + OCI metadata | `{{inherit}}` tag (RHDH resolves both OCI tag and default config from its 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): @@ -158,10 +168,10 @@ oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/{plugin-name}:pr_{PR_ For OCI URL generation, your workspace must have these files (generated by CI): -| File | Purpose | -|------|---------| -| `source.json` | Contains `repo` (GitHub URL) and `repo-ref` (commit SHA) | -| `plugins-list.yaml` | Lists plugin paths (e.g., `plugins/tech-radar:`) | +| File | Purpose | +| ------------------- | -------------------------------------------------------- | +| `source.json` | Contains `repo` (GitHub URL) and `repo-ref` (commit SHA) | +| `plugins-list.yaml` | Lists plugin paths (e.g., `plugins/tech-radar:`) | ::: warning For PR builds, OCI URL generation is strict - deployment will fail if required files are missing or version fetching fails. This ensures PR builds don't silently fall back to local paths. @@ -171,40 +181,54 @@ For PR builds, OCI URL generation is strict - deployment will fail if required f 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 | -| **Wrapper disabling** | Yes (`disableWrappers`) | No | No | -| **Cross-workspace plugins** | Kept as-is | Kept as-is | Kept as-is | +| | **PR Check** | **Nightly** | **Local Dev** | +| ------------------------------------------- | ------------------------ | ------------------------------------------------------------- | ----------------- | +| **Trigger** | `GIT_PR_NUMBER` set | `E2E_NIGHTLY_MODE=true` | No env vars | +| **Config injection** | Yes — all plugins | Only plugins NOT in `default.packages.yaml` with OCI metadata | Yes — all plugins | +| **`default.packages.yaml` OCI plugins** | PR tags or metadata refs | `{{inherit}}` tag (RHDH provides both OCI tag and config) | Metadata refs | +| **Non-`default.packages.yaml` OCI plugins** | 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 | + +### DPDY vs Non-DPDY in Nightly -### Why Metadata Refs (Not `{{inherit}}`) +DPDY refers to `dynamic-plugins.default.yaml` in the catalog index image shipped with RHDH. The list of DPDY packages is defined in [`default.packages.yaml`](https://github.com/redhat-developer/rhdh/blob/main/default.packages.yaml) (both `enabled` and `disabled` sections). In nightly mode: -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. +- **In `default.packages.yaml` + OCI metadata**: Use `{{inherit}}` tag — RHDH resolves both the OCI tag (version) and default config from its built-in DPDY. This tests the exact versions and configuration shipped in the RC. No config injection from our side. The registry defaults to `registry.access.redhat.com/rhdh` and can be overridden with `NIGHTLY_DPDY_OCI_REGISTRY` (blanket) or `NIGHTLY_DPDY_OCI_REGISTRY_MAP` (per-plugin JSON, takes precedence). +- **NOT in `default.packages.yaml` + OCI metadata**: Use full metadata refs from `spec.dynamicArtifact`. Config injection enabled — these plugins aren't in RHDH's built-in defaults, so they need `appConfigExamples` from metadata. +- **Wrapper plugins** (local paths in `spec.dynamicArtifact`): Always use the metadata wrapper path regardless of `default.packages.yaml` status. No config injection. + +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 2: Resolve packages to OCI (both modes) - → Per plugin: PR OCI URL > metadata OCI ref > passthrough +Step 1: Inject metadata configs + → PR/local: deepMerge(metadata.appConfigExamples, user.pluginConfig) for all plugins + → Nightly: only plugins NOT in default.packages.yaml with OCI metadata get injection + (plugins in default.packages.yaml get both config and OCI tag via {{inherit}}) + → Skipped locally when RHDH_SKIP_PLUGIN_METADATA_INJECTION="true" (ignored in CI) + +Step 2: Resolve packages (both modes) + → Per plugin: PR OCI URL > DPDY {{inherit}} > metadata OCI ref > wrapper path > passthrough + → {{inherit}} registry: NIGHTLY_DPDY_OCI_REGISTRY_MAP > NIGHTLY_DPDY_OCI_REGISTRY > registry.access.redhat.com/rhdh ``` ## Environment Variables -| 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 | -| `JOB_MODE` | CI-only: `nightly` or `pr-check` (set by step registry) | +| Variable | Effect | +| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `GIT_PR_NUMBER` | Enables OCI URL generation for PR builds | +| `E2E_NIGHTLY_MODE` | When `true`, activates nightly mode (`default.packages.yaml` OCI plugins → `{{inherit}}`, others → full metadata refs) | +| `RELEASE_BRANCH_NAME` | Branch for fetching `default.packages.yaml` (required in CI, defaults to `main` locally) | +| `NIGHTLY_DPDY_OCI_REGISTRY` | Blanket override for `{{inherit}}` registry (default: `registry.access.redhat.com/rhdh`) | +| `NIGHTLY_DPDY_OCI_REGISTRY_MAP` | Per-plugin registry override as JSON: `{"registry": ["pkg1", "pkg2"]}` (takes precedence over blanket) | +| `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..9e8ba13 100644 --- a/docs/overlay/reference/environment-variables.md +++ b/docs/overlay/reference/environment-variables.md @@ -7,7 +7,7 @@ For using @red-hat-developer-hub/e2e-test-utils in external projects, see the [G This page documents all environment variables used in overlay E2E tests. -## Vault Secrets (VAULT_*) +## Vault Secrets (VAULT\_\*) In OpenShift CI, secrets are managed through [HashiCorp Vault](https://vault.ci.openshift.org) and automatically exported as environment variables. @@ -19,11 +19,11 @@ For complete Vault setup instructions including paths, annotations, and access r Set `VAULT=1` or `VAULT=true` to automatically fetch secrets from Vault during global setup. This replaces the need to manually copy secrets into `.env` files. -| Variable | Description | Default | -|----------|-------------|---------| -| `VAULT` | Enable automatic Vault secret loading | - | -| `VAULT_ADDR` | Vault server URL | `https://vault.ci.openshift.org` | -| `VAULT_BASE_PATH` | Base path in Vault KV store | `selfservice/rhdh-plugin-export-overlays` | +| Variable | Description | Default | +| ----------------- | ------------------------------------- | ----------------------------------------- | +| `VAULT` | Enable automatic Vault secret loading | - | +| `VAULT_ADDR` | Vault server URL | `https://vault.ci.openshift.org` | +| `VAULT_BASE_PATH` | Base path in Vault KV store | `selfservice/rhdh-plugin-export-overlays` | ```bash VAULT=1 yarn test @@ -36,26 +36,27 @@ See [Running Locally - Secrets from Vault](/overlay/tutorials/running-locally#se ### RHDH Configuration -| Variable | Description | Default | Required | -|----------|-------------|---------|----------| -| `RHDH_VERSION` | RHDH version to deploy (e.g., "1.5", "next") | `next` | No | -| `INSTALLATION_METHOD` | Deployment method: `helm` or `operator` | `helm` | No | -| `CHART_URL` | Custom Helm chart URL | `oci://quay.io/rhdh/chart` | No | +| Variable | Description | Default | Required | +| --------------------- | -------------------------------------------- | -------------------------- | -------- | +| `RHDH_VERSION` | RHDH version to deploy (e.g., "1.5", "next") | `next` | No | +| `INSTALLATION_METHOD` | Deployment method: `helm` or `operator` | `helm` | No | +| `CHART_URL` | Custom Helm chart URL | `oci://quay.io/rhdh/chart` | No | ### Cluster Configuration -| Variable | Description | Default | Required | -|----------|-------------|---------|----------| -| `K8S_CLUSTER_ROUTER_BASE` | Cluster router base domain | Auto-detected | No | +| Variable | Description | Default | Required | +| ------------------------- | -------------------------- | ------------- | -------- | +| `K8S_CLUSTER_ROUTER_BASE` | Cluster router base domain | Auto-detected | No | ### Authentication -| Variable | Description | Default | Required | -|----------|-------------|---------|----------| -| `SKIP_KEYCLOAK_DEPLOYMENT` | Skip Keycloak deployment entirely (useful for guest auth) | `false` | No | +| Variable | Description | Default | Required | +| -------------------------- | --------------------------------------------------------- | ------- | -------- | +| `SKIP_KEYCLOAK_DEPLOYMENT` | Skip Keycloak deployment entirely (useful for guest auth) | `false` | No | ::: tip Keycloak Deployment Behavior By default (`SKIP_KEYCLOAK_DEPLOYMENT=false`): + - If Keycloak already exists in the cluster, it uses the existing instance - If Keycloak doesn't exist, it deploys a new one @@ -64,41 +65,52 @@ Set `SKIP_KEYCLOAK_DEPLOYMENT=true` when using guest authentication and you don' ### CI/CD -| Variable | Description | Default | Required | -|----------|-------------|---------|----------| -| `CI` | Set automatically in CI environments | `false` | No | -| `JOB_MODE` | Set by CI step registry: `nightly` or `pr-check` | - | No | +| Variable | Description | Default | Required | +| ---------- | ------------------------------------------------ | ------- | -------- | +| `CI` | Set automatically in CI environments | `false` | No | +| `JOB_MODE` | Set by CI step registry: `nightly` or `pr-check` | - | No | ## Plugin Metadata Variables -These control automatic plugin configuration generation from metadata files: +These control automatic plugin configuration generation from metadata files. + +> **DPDY** refers to `dynamic-plugins.default.yaml` in the catalog index image shipped with RHDH. The list of DPDY packages is defined in [`default.packages.yaml`](https://github.com/redhat-developer/rhdh/blob/main/default.packages.yaml). -| 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 | +| 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 | Plugins in `default.packages.yaml` with OCI metadata use `{{inherit}}` (RHDH resolves both OCI tag and config from DPDY); other OCI plugins use full metadata refs with 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 | +| `NIGHTLY_DPDY_OCI_REGISTRY` | OCI registry for `{{inherit}}` refs | Overrides default `registry.access.redhat.com/rhdh` for all plugins using `{{inherit}}` in nightly mode | +| `NIGHTLY_DPDY_OCI_REGISTRY_MAP` | JSON: `{"registry": ["pkg1", "pkg2"]}` | Per-plugin registry override (takes precedence over blanket) | +| `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 -| Scenario | Variables to Set | -|----------|------------------| -| 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` | +| Scenario | Variables to Set | +| --------------------------- | -------------------------------------------------------------------------------- | +| 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 (local only) | Set `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true` (ignored in CI) | ### Metadata Handling Behavior **Enabled by default** for: + - 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-`): + +- Plugins in `default.packages.yaml` with OCI metadata: no injection (use `{{inherit}}` tag — RHDH resolves both the OCI tag and default config from its built-in DPDY) +- Plugins NOT in `default.packages.yaml` with OCI metadata: injection enabled (full metadata refs, config from `appConfigExamples`) +- 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. @@ -124,6 +136,7 @@ yarn test ``` Example transformation: + ```yaml # Without GIT_PR_NUMBER - package: ./dynamic-plugins/dist/backstage-community-plugin-tech-radar @@ -138,11 +151,11 @@ See [Configuration Files - PR Builds](/overlay/test-structure/configuration-file These are used by `run-e2e.sh` (the [unified test runner](/overlay/reference/run-e2e)): -| Variable | Description | Default | -|----------|-------------|---------| -| `E2E_TEST_UTILS_PATH` | Absolute path to a local `e2e-test-utils` build | - | +| Variable | Description | Default | +| ------------------------ | ------------------------------------------------------- | ----------------------------------- | +| `E2E_TEST_UTILS_PATH` | Absolute path to a local `e2e-test-utils` build | - | | `E2E_TEST_UTILS_VERSION` | Pin `@red-hat-developer-hub/e2e-test-utils` npm version | `latest` (nightly), empty otherwise | -| `PLAYWRIGHT_VERSION` | Pin `@playwright/test` version | `1.59.1` | +| `PLAYWRIGHT_VERSION` | Pin `@playwright/test` version | `1.59.1` | ::: tip Version Pinning `E2E_TEST_UTILS_PATH` takes precedence over `E2E_TEST_UTILS_VERSION`. If neither is set, the version in each workspace's `package.json` is used. @@ -192,9 +205,9 @@ VAULT_API_KEY: api-key-value ## Using Variables -| Where you need it | How to access | -|-------------------|---------------| -| Test code (`*.spec.ts`) | `process.env.VAULT_*` directly | +| Where you need it | How to access | +| ------------------------------------------------------------- | -------------------------------------------------------- | +| Test code (`*.spec.ts`) | `process.env.VAULT_*` directly | | RHDH configs (`app-config-rhdh.yaml`, `dynamic-plugins.yaml`) | Add to `rhdh-secrets.yaml` first, then use `${VAR_NAME}` | For detailed examples, see [Configuration Files - rhdh-secrets.yaml](/overlay/test-structure/configuration-files#rhdh-secrets-yaml-optional). diff --git a/docs/overlay/reference/plugin-metadata-resolution.md b/docs/overlay/reference/plugin-metadata-resolution.md index 38ddaa2..cbb1d91 100644 --- a/docs/overlay/reference/plugin-metadata-resolution.md +++ b/docs/overlay/reference/plugin-metadata-resolution.md @@ -6,15 +6,17 @@ This page explains how plugin packages are resolved in overlay E2E tests. It is The test framework resolves plugin package references before deploying RHDH. This page explains how the resolution works in each mode, what metadata controls, and the common scenarios you'll encounter. +> **DPDY** refers to `dynamic-plugins.default.yaml` in the catalog index image shipped with RHDH. The list of DPDY packages is defined in [`default.packages.yaml`](https://github.com/redhat-developer/rhdh/blob/main/default.packages.yaml). + ## Modes The system detects the mode from environment variables: -| Mode | Detection | Use case | -|------|-----------|----------| -| **PR check** | `GIT_PR_NUMBER` is set | CI PR validation | -| **Nightly** | `E2E_NIGHTLY_MODE=true` or `JOB_NAME` contains `periodic-` | Daily CI regression | -| **Local dev** | Neither of the above | Development | +| Mode | Detection | Use case | +| ------------- | ---------------------------------------------------------- | ------------------- | +| **PR check** | `GIT_PR_NUMBER` is set | CI PR validation | +| **Nightly** | `E2E_NIGHTLY_MODE=true` or `JOB_NAME` contains `periodic-` | Daily CI regression | +| **Local dev** | Neither of the above | Development | `GIT_PR_NUMBER` always wins — if both it and `E2E_NIGHTLY_MODE` are set, the system runs in PR mode. @@ -26,9 +28,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 plugins NOT in `default.packages.yaml` whose metadata `spec.dynamicArtifact` is an OCI ref get injection. Plugins in `default.packages.yaml` with OCI metadata use `{{inherit}}`, which tells RHDH to resolve both the OCI tag (version) and default config from its built-in DPDY. Wrapper plugins get no injection. +- Disabled locally when `RHDH_SKIP_PLUGIN_METADATA_INJECTION=true` (ignored in CI) #### Example: Deep Merge Behavior (PR / Local mode) @@ -59,7 +61,7 @@ plugins: backstage-community.plugin-argocd: mountPoints: - mountPoint: entity.page.cd/cards - importName: CustomArgoContent # overrides ArgoContent + importName: CustomArgoContent # overrides ArgoContent ``` ```yaml @@ -72,19 +74,21 @@ plugins: backstage-community.plugin-argocd: mountPoints: - mountPoint: entity.page.cd/cards - importName: CustomArgoContent # from user (wins) + importName: CustomArgoContent # from user (wins) entityTabs: - path: /cd - title: CD # from metadata (preserved) + title: CD # from metadata (preserved) ``` -| Scenario | Result | -|----------|--------| -| Metadata has config, user has none | Metadata config injected as `pluginConfig` | -| 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 | +| Scenario | Result | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Metadata has config, user has none | Metadata config injected as `pluginConfig` | +| 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 — in `default.packages.yaml` + OCI metadata** | **Skipped** — plugin uses `{{inherit}}`, so RHDH resolves both the OCI tag and default config from its built-in DPDY | +| **Nightly — NOT in `default.packages.yaml` + OCI metadata** | **Injected** — metadata `appConfigExamples` merged as base, user `pluginConfig` overrides (these plugins aren't in RHDH's built-in defaults) | +| **Nightly — wrapper** | **Skipped** — user `pluginConfig` preserved as-is | ### Step 2: Package Resolution @@ -101,58 +105,72 @@ 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 default.packages.yaml AND metadata spec.dynamicArtifact is OCI? + Yes → use {{inherit}} tag: oci://{registry}/plugin:{{inherit}} + RHDH resolves both the OCI tag (version) and default config from its built-in DPDY. + Registry: NIGHTLY_DPDY_OCI_REGISTRY_MAP > NIGHTLY_DPDY_OCI_REGISTRY > default registry.access.redhat.com/rhdh + 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 plugins in `default.packages.yaml` with OCI metadata in nightly mode — these use `{{inherit}}` so RHDH resolves both the OCI tag and config from its built-in DPDY, testing the exact versions and configuration shipped in the 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 listed in [`default.packages.yaml`](https://github.com/redhat-developer/rhdh/blob/main/default.packages.yaml) (both `enabled` and `disabled` sections) AND whether its metadata `spec.dynamicArtifact` is an OCI ref. The list is fetched at runtime from the `rhdh` repo using `RELEASE_BRANCH_NAME`. ### PR Check Mode (`GIT_PR_NUMBER` set) -| # | 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:pr_1845__3.33.3!alias` | Yes (metadata base + user override) | -| 2 | Workspace plugin (wrapper) | `./dynamic-plugins/dist/plugin-tech-radar` | `./dynamic-plugins/dist/plugin-tech-radar` | `oci://ghcr.io/.../plugin-tech-radar:pr_1845__1.13.0!alias` | Yes | -| 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` | `oci://ghcr.io/.../plugin-github-org:pr_1845__0.3.20!alias` | Yes | -| 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:pr_1845__3.33.3!alias` | Yes | -| 5 | Cross-workspace (local path, no metadata) | — | `./dynamic-plugins/dist/plugin-kubernetes-backend-dynamic` | unchanged | No (no metadata) | -| 6 | Cross-workspace (OCI, no metadata) | — | `oci://ghcr.io/.../plugin-dynamic-home-page:bs_1.45.3__1.10.3!alias` | unchanged | No | -| 7 | npm package (no metadata) | — | `@rhdh/plugin-global-header-test@0.0.2` | unchanged | No | -| 8 | Different registry (quay.io) | `oci://quay.io/rhdh/plugin-events@sha256:abc` | `oci://ghcr.io/.../plugin-events:old_tag` | `oci://ghcr.io/.../plugin-events:pr_1845__0.4.6!alias` | Yes | -| 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://ghcr.io/.../plugin-orch:pr_1845__1.0.0!alias` | Yes | +| # | 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:pr_1845__3.33.3!alias` | Yes (metadata base + user override) | +| 2 | Workspace plugin (wrapper) | `./dynamic-plugins/dist/plugin-tech-radar` | `./dynamic-plugins/dist/plugin-tech-radar` | `oci://ghcr.io/.../plugin-tech-radar:pr_1845__1.13.0!alias` | Yes | +| 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` | `oci://ghcr.io/.../plugin-github-org:pr_1845__0.3.20!alias` | Yes | +| 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:pr_1845__3.33.3!alias` | Yes | +| 5 | Cross-workspace (local path, no metadata) | — | `./dynamic-plugins/dist/plugin-kubernetes-backend-dynamic` | unchanged | No (no metadata) | +| 6 | Cross-workspace (OCI, no metadata) | — | `oci://ghcr.io/.../plugin-dynamic-home-page:bs_1.45.3__1.10.3!alias` | unchanged | No | +| 7 | npm package (no metadata) | — | `@rhdh/plugin-global-header-test@0.0.2` | unchanged | No | +| 8 | Different registry (quay.io) | `oci://quay.io/rhdh/plugin-events@sha256:abc` | `oci://ghcr.io/.../plugin-events:old_tag` | `oci://ghcr.io/.../plugin-events:pr_1845__0.4.6!alias` | Yes | +| 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://ghcr.io/.../plugin-orch:pr_1845__1.0.0!alias` | Yes | ### 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 | In `default.packages.yaml`, OCI metadata | Yes | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` | `oci://ghcr.io/.../plugin-tekton:old_tag!alias` | `oci://registry.access.redhat.com/rhdh/plugin-tekton:{{inherit}}` (default RHEC registry) | **Skipped** — RHDH resolves both OCI tag and config from DPDY | +| 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 | NOT in `default.packages.yaml`, OCI metadata | 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** — not in RHDH's built-in defaults, needs config from metadata | +| 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 | In `default.packages.yaml`, OCI metadata (wrapper in config) | Yes | `oci://ghcr.io/.../plugin-tekton:bs_1.49.4__3.33.3!alias` | `./dynamic-plugins/dist/plugin-tekton` | `oci://registry.access.redhat.com/rhdh/plugin-tekton:{{inherit}}` | **Skipped** — RHDH resolves both OCI tag and config from DPDY | +| 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 | In `default.packages.yaml` (metadata on 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}}` (default RHEC) | **Skipped** — RHDH resolves both OCI tag and config from DPDY | +| 11 | In `default.packages.yaml` (metadata on ghcr.io) | Yes | `oci://ghcr.io/.../plugin-orch:bs_1.49.4__5.7.10!alias` | `oci://ghcr.io/.../plugin-orch:old` | `oci://registry.access.redhat.com/rhdh/plugin-orch:{{inherit}}` (default RHEC, not metadata) | **Skipped** — RHDH resolves both OCI tag and config from DPDY | +| 12 | NOT in `default.packages.yaml` (quay.io metadata) | 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** — not in RHDH's built-in defaults | +| 13 | NOT in `default.packages.yaml` (RHEC metadata) | 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** — not in RHDH's built-in defaults | ### Key Takeaways -| Rule | Explanation | -|------|-------------| -| **Metadata always wins** | When metadata exists, `spec.dynamicArtifact` determines the package — the user config's `package` field is overwritten | -| **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` | -| **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 | +| Rule | Explanation | +| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Metadata always wins** | When metadata exists, `spec.dynamicArtifact` determines the package — the user config's `package` field is overwritten | +| **`default.packages.yaml` + OCI → `{{inherit}}`** | In nightly, plugins in `default.packages.yaml` with OCI metadata use `{{inherit}}` — RHDH resolves both the OCI tag (version) and default config from its built-in DPDY. No config injection from our side | +| **Not in `default.packages.yaml` + OCI → full ref + injection** | In nightly, plugins NOT in `default.packages.yaml` with OCI metadata use full metadata refs and get `appConfigExamples` injected — they aren't in RHDH's built-in defaults, so they need config from metadata | +| **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` | +| **`{{inherit}}` registry is configurable** | Default: `registry.access.redhat.com/rhdh`. Override with `NIGHTLY_DPDY_OCI_REGISTRY` (blanket) or `NIGHTLY_DPDY_OCI_REGISTRY_MAP` (per-plugin JSON). 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 +197,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 plugins **NOT in `default.packages.yaml`** whose metadata `spec.dynamicArtifact` is an OCI ref. Plugins in `default.packages.yaml` with OCI metadata use `{{inherit}}`, which tells RHDH to resolve both the OCI tag and default config from its built-in DPDY — so no config injection is needed from our side. Wrapper plugins also get no injection. If your test relies on specific config for a `default.packages.yaml` 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..ce79458 100644 --- a/docs/overlay/tutorials/ci-pipeline.md +++ b/docs/overlay/tutorials/ci-pipeline.md @@ -214,8 +214,11 @@ 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) | +| `NIGHTLY_DPDY_OCI_REGISTRY` | Blanket override for `{{inherit}}` registry (default: `registry.access.redhat.com/rhdh`) | +| `NIGHTLY_DPDY_OCI_REGISTRY_MAP` | Per-plugin registry override as JSON (takes precedence over blanket) | +| `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 0fccf5f..d692723 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@red-hat-developer-hub/e2e-test-utils", - "version": "1.1.39", + "version": "1.1.40", "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..20bcfe7 100644 --- a/src/utils/plugin-metadata.ts +++ b/src/utils/plugin-metadata.ts @@ -75,6 +75,98 @@ export function isNightlyJob(): boolean { return false; } +// ── Default Packages (DPDY) ────────────────────────────────────────────────── + +const DEFAULT_PACKAGES_BASE_URL = + "https://raw.githubusercontent.com/redhat-developer/rhdh/refs/heads"; + +const DEFAULT_DPDY_OCI_REGISTRY = "registry.access.redhat.com/rhdh"; + +interface DefaultPackagesYaml { + packages?: { + enabled?: Array<{ package: string }>; + disabled?: Array<{ package: string }>; + }; +} + +/** + * Fetches the list of packages from default.packages.yaml (source for the + * dynamic-plugins.default.yaml — DPDY — in the catalog index image shipped + * with RHDH). 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; +} + +/** + * Resolves the OCI registry for a plugin's {{inherit}} ref in nightly mode. + * + * Resolution priority: + * 1. NIGHTLY_DPDY_OCI_REGISTRY_MAP — JSON object mapping registry → array of package names + * 2. NIGHTLY_DPDY_OCI_REGISTRY — blanket override for all plugins using {{inherit}} + * 3. Default: registry.access.redhat.com/rhdh + */ +export function getDpdyRegistry(packageName: string): string { + const map = process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP; + if (map) { + const parsed = JSON.parse(map) as Record; + for (const [registry, packages] of Object.entries(parsed)) { + if (packages.includes(packageName)) return registry; + } + } + + if (process.env.NIGHTLY_DPDY_OCI_REGISTRY) { + return process.env.NIGHTLY_DPDY_OCI_REGISTRY; + } + + return DEFAULT_DPDY_OCI_REGISTRY; +} + // ── Utilities ───────────────────────────────────────────────────────────────── /** @@ -355,6 +447,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 +465,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 +478,21 @@ 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: if the plugin is in default.packages.yaml and its metadata + // spec.dynamicArtifact is an OCI ref, use {{inherit}} — RHDH resolves + // both the OCI tag and default config from its built-in DPDY. + // Registry: getDpdyRegistry() (env var overrides > default RHEC). + if ( + dpdyPackages?.has(metadata.packageName) && + metadata.packagePath.startsWith("oci://") + ) { + const registry = getDpdyRegistry(metadata.packageName); + const inheritRef = `oci://${registry}/${displayName}:{{inherit}}`; + console.log(`[PluginMetadata] DPDY inherit: ${pkg} → ${inheritRef}`); + return { ...plugin, package: inheritRef }; + } + + // OCI: use metadata's dynamicArtifact directly (not in default.packages.yaml, or not nightly). if (metadata.packagePath.startsWith("oci://")) { console.log(`[PluginMetadata] ${pkg} → ${metadata.packagePath}`); return { ...plugin, package: metadata.packagePath }; @@ -401,10 +506,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 +615,89 @@ 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 inject config for plugins NOT in default.packages.yaml whose + // metadata is OCI. DPDY plugins get both config and OCI tag 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 plugins NOT in default.packages.yaml with OCI metadata + * (plugins in default.packages.yaml get both config and OCI tag via {{inherit}}) + * 2. Resolve all packages: + * - PR with GIT_PR_NUMBER: workspace plugins → pr_ OCI tags + * - Nightly DPDY + OCI: {{inherit}} tag with configurable registry + * (NIGHTLY_DPDY_OCI_REGISTRY_MAP > NIGHTLY_DPDY_OCI_REGISTRY > registry.access.redhat.com/rhdh) + * - Nightly (not in default.packages.yaml) / 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..cf6cc82 100644 --- a/src/utils/tests/plugin-metadata.nightly.test.ts +++ b/src/utils/tests/plugin-metadata.nightly.test.ts @@ -1,12 +1,14 @@ /** * 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"; import { isNightlyJob, processPluginsForDeployment, + getDpdyRegistry, type DynamicPluginsConfig, } from "../plugin-metadata.js"; import { withCleanEnv, createMetadataFixture } from "./helpers.js"; @@ -107,7 +109,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 +133,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 +177,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 +193,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 +214,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 +255,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 +292,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 +308,646 @@ 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 with default RHEC registry", 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://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton:{{inherit}}", + "DPDY OCI plugin must resolve to {{inherit}} with default RHEC registry", + ); + } 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.strictEqual( + result.plugins![0].package, + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-topology:{{inherit}}", + "{{inherit}} ref must use default RHEC registry with no alias suffix", + ); + assert.ok( + !result.plugins![0].package.includes("!"), + "{{inherit}} ref must NOT include !alias suffix", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("{{inherit}} uses default RHEC even when metadata has ghcr.io (PR #2449 scenario)", async () => { + const metadataDir = await createMetadataFixture([ + { + name: "red-hat-developer-hub-backstage-plugin-orchestrator", + packageName: "@red-hat-developer-hub/backstage-plugin-orchestrator", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:bs_1.49.4__5.7.10!red-hat-developer-hub-backstage-plugin-orchestrator", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:old", + 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 default RHEC registry regardless of metadata's ghcr.io", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY overrides default registry for all plugins", async () => { + process.env.NIGHTLY_DPDY_OCI_REGISTRY = + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays"; + + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tekton", + packageName: "@backstage-community/plugin-tekton", + dynamicArtifact: + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc", + disabled: false, + }, + ], + }; + + const result = await processPluginsForDeployment( + config, + metadataDir, + new Set(["@backstage-community/plugin-tekton"]), + ); + + assert.strictEqual( + result.plugins![0].package, + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:{{inherit}}", + "NIGHTLY_DPDY_OCI_REGISTRY must override default RHEC registry", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY_MAP overrides registry for specific plugins", async () => { + process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({ + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [ + "@backstage-community/plugin-tekton", + ], + }); + + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tekton", + packageName: "@backstage-community/plugin-tekton", + dynamicArtifact: + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc", + }, + { + name: "red-hat-developer-hub-backstage-plugin-orchestrator", + packageName: "@red-hat-developer-hub/backstage-plugin-orchestrator", + dynamicArtifact: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:bs_1.49.4__5.7.10!red-hat-developer-hub-backstage-plugin-orchestrator", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc", + disabled: false, + }, + { + package: + "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:old", + disabled: false, + }, + ], + }; + + const dpdyPackages = new Set([ + "@backstage-community/plugin-tekton", + "@red-hat-developer-hub/backstage-plugin-orchestrator", + ]); + 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}}", + "tekton must use ghcr.io from NIGHTLY_DPDY_OCI_REGISTRY_MAP", + ); + assert.strictEqual( + result.plugins![1].package, + "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", + "orchestrator must use default RHEC (not in map)", + ); + } finally { + await fs.remove(metadataDir); + } + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY_MAP takes precedence over NIGHTLY_DPDY_OCI_REGISTRY", async () => { + process.env.NIGHTLY_DPDY_OCI_REGISTRY = "quay.io/rhdh"; + process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({ + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [ + "@backstage-community/plugin-tekton", + ], + }); + + const metadataDir = await createMetadataFixture([ + { + name: "backstage-community-plugin-tekton", + packageName: "@backstage-community/plugin-tekton", + dynamicArtifact: + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc", + }, + { + 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:def", + }, + ]); + + try { + const config: DynamicPluginsConfig = { + plugins: [ + { + package: + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc", + disabled: false, + }, + { + package: + "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator@sha256:def", + disabled: false, + }, + ], + }; + + const dpdyPackages = new Set([ + "@backstage-community/plugin-tekton", + "@red-hat-developer-hub/backstage-plugin-orchestrator", + ]); + 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}}", + "tekton must use ghcr.io from MAP (takes precedence over blanket)", + ); + assert.strictEqual( + result.plugins![1].package, + "oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", + "orchestrator must use quay.io from NIGHTLY_DPDY_OCI_REGISTRY (blanket fallback)", + ); + } 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("mixed scenario: DPDY OCI → RHEC 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}} with default RHEC, no config injection + assert.strictEqual( + result.plugins![0].package, + "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton:{{inherit}}", + "DPDY plugin must use {{inherit}} with default RHEC registry", + ); + 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); + } + }); +}); + +// ── getDpdyRegistry unit tests ────────────────────────────────────────────── + +describe("getDpdyRegistry", () => { + const env = withCleanEnv(); + beforeEach(() => env.save()); + afterEach(() => env.restore()); + + it("returns default RHEC registry when no env vars set", () => { + delete process.env.NIGHTLY_DPDY_OCI_REGISTRY; + delete process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP; + + assert.strictEqual( + getDpdyRegistry("@backstage-community/plugin-tekton"), + "registry.access.redhat.com/rhdh", + ); + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY overrides default for all plugins", () => { + process.env.NIGHTLY_DPDY_OCI_REGISTRY = + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays"; + delete process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP; + + assert.strictEqual( + getDpdyRegistry("@backstage-community/plugin-tekton"), + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays", + ); + assert.strictEqual( + getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays", + ); + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY_MAP returns mapped registry for listed plugin", () => { + delete process.env.NIGHTLY_DPDY_OCI_REGISTRY; + process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({ + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [ + "@backstage-community/plugin-tekton", + "@backstage-community/plugin-argocd", + ], + }); + + assert.strictEqual( + getDpdyRegistry("@backstage-community/plugin-tekton"), + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays", + ); + assert.strictEqual( + getDpdyRegistry("@backstage-community/plugin-argocd"), + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays", + ); + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY_MAP falls back to default for unlisted plugin", () => { + delete process.env.NIGHTLY_DPDY_OCI_REGISTRY; + process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({ + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [ + "@backstage-community/plugin-tekton", + ], + }); + + assert.strictEqual( + getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), + "registry.access.redhat.com/rhdh", + "unlisted plugin must fall back to default RHEC", + ); + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY_MAP takes precedence over NIGHTLY_DPDY_OCI_REGISTRY", () => { + process.env.NIGHTLY_DPDY_OCI_REGISTRY = "quay.io/rhdh"; + process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({ + "ghcr.io/custom": ["@backstage-community/plugin-tekton"], + }); + + assert.strictEqual( + getDpdyRegistry("@backstage-community/plugin-tekton"), + "ghcr.io/custom", + "MAP must take precedence over blanket", + ); + assert.strictEqual( + getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), + "quay.io/rhdh", + "unlisted plugin must fall back to blanket NIGHTLY_DPDY_OCI_REGISTRY", + ); + }); + + it("supports multiple registries in NIGHTLY_DPDY_OCI_REGISTRY_MAP", () => { + delete process.env.NIGHTLY_DPDY_OCI_REGISTRY; + process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({ + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [ + "@backstage-community/plugin-tekton", + ], + "quay.io/rhdh": ["@red-hat-developer-hub/backstage-plugin-orchestrator"], + }); + + assert.strictEqual( + getDpdyRegistry("@backstage-community/plugin-tekton"), + "ghcr.io/redhat-developer/rhdh-plugin-export-overlays", + ); + assert.strictEqual( + getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), + "quay.io/rhdh", + ); + assert.strictEqual( + getDpdyRegistry("@backstage-community/plugin-argocd"), + "registry.access.redhat.com/rhdh", + "unlisted plugin falls back to default", + ); + }); +}); diff --git a/src/utils/tests/plugin-metadata.pr.test.ts b/src/utils/tests/plugin-metadata.pr.test.ts index e939ce8..5869f28 100644 --- a/src/utils/tests/plugin-metadata.pr.test.ts +++ b/src/utils/tests/plugin-metadata.pr.test.ts @@ -5,7 +5,6 @@ import { describe, it, beforeEach, afterEach } from "node:test"; import assert from "node:assert"; import fs from "fs-extra"; import { - isNightlyJob, processPluginsForDeployment, type DynamicPluginsConfig, } from "../plugin-metadata.js"; @@ -231,6 +230,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 +268,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"; @@ -439,19 +477,92 @@ describe("processPluginsForDeployment — PR mode", () => { }); }); - // ── PR vs nightly precedence ──────────────────────────────────────────── + // ── {{inherit}} must NOT apply in PR/local mode ───────────────────────── - describe("PR vs nightly precedence", () => { - it("isNightlyJob returns false when both GIT_PR_NUMBER and E2E_NIGHTLY_MODE are set", () => { + describe("{{inherit}} does not apply in PR/local mode", () => { + it("DPDY OCI plugin uses metadata ref (not {{inherit}}) when GIT_PR_NUMBER is set", async () => { process.env.GIT_PR_NUMBER = "42"; process.env.E2E_NIGHTLY_MODE = "true"; - assert.strictEqual( - isNightlyJob(), - false, - "GIT_PR_NUMBER must make isNightlyJob return false", - ); + + const { wsDir, metadataDir } = await createWorkspaceFixture([ + { + 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 result = await processPluginsForDeployment(config, metadataDir); + + assert.ok( + !result.plugins![0].package.includes("{{inherit}}"), + "{{inherit}} must NOT be used when GIT_PR_NUMBER is set (PR takes precedence)", + ); + } finally { + await fs.remove(wsDir); + } + }); + + it("NIGHTLY_DPDY_OCI_REGISTRY env vars are ignored in PR/local mode", async () => { + delete process.env.GIT_PR_NUMBER; + delete process.env.E2E_NIGHTLY_MODE; + process.env.NIGHTLY_DPDY_OCI_REGISTRY = "quay.io/rhdh"; + + 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 result = await processPluginsForDeployment(config, metadataDir); + + assert.ok( + !result.plugins![0].package.includes("quay.io"), + "NIGHTLY_DPDY_OCI_REGISTRY must be ignored in local/PR mode", + ); + assert.ok( + !result.plugins![0].package.includes("{{inherit}}"), + "{{inherit}} must NOT be used in local/PR mode", + ); + assert.ok( + result.plugins![0].package.includes("bs_1.49.4__3.33.3"), + "must use metadata dynamicArtifact in local/PR mode", + ); + } finally { + await fs.remove(metadataDir); + } }); + }); + // ── PR vs nightly precedence ──────────────────────────────────────────── + + describe("PR vs nightly precedence", () => { it("injects metadata config when GIT_PR_NUMBER is set (PR mode despite nightly env)", async () => { process.env.GIT_PR_NUMBER = "42"; process.env.E2E_NIGHTLY_MODE = "true";