@red-hat-developer-hub/e2e-test-utils 1.1.22 → 1.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,125 @@ import yaml from "js-yaml";
4
4
  import { glob } from "zx";
5
5
  import { deepMerge } from "./merge-yamls.js";
6
6
  const OCI_REGISTRY_PREFIX = "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays";
7
+ // ── Detection ─────────────────────────────────────────────────────────────────
8
+ /**
9
+ * Detects if we're running in a nightly/periodic job context.
10
+ * Controls the entire nightly vs PR routing in deployment:
11
+ * - Nightly: uses metadata OCI refs (latest published versions), skips metadata injection
12
+ * - PR/local: uses metadata + OCI URL replacement
13
+ *
14
+ * Returns true when:
15
+ * - JOB_NAME contains "periodic-" (OpenShift CI nightly/periodic jobs), OR
16
+ * - E2E_NIGHTLY_MODE is set (manual override for local testing)
17
+ */
18
+ export function isNightlyJob() {
19
+ // PR check takes precedence over nightly mode
20
+ if (process.env.GIT_PR_NUMBER) {
21
+ return false;
22
+ }
23
+ if (process.env.E2E_NIGHTLY_MODE === "true" ||
24
+ process.env.E2E_NIGHTLY_MODE === "1") {
25
+ console.log("[PluginMetadata] Nightly mode (E2E_NIGHTLY_MODE is set)");
26
+ return true;
27
+ }
28
+ const jobName = process.env.JOB_NAME || "";
29
+ if (jobName.includes("periodic-")) {
30
+ console.log("[PluginMetadata] Nightly mode (periodic job detected)");
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ // ── Utilities ─────────────────────────────────────────────────────────────────
36
+ /**
37
+ * Extracts the plugin name from a package path or OCI reference.
38
+ *
39
+ * Handles various formats:
40
+ * - Local path: ./dynamic-plugins/dist/backstage-community-plugin-tech-radar
41
+ * - OCI with alias: oci://quay.io/rhdh/plugin@sha256:...!backstage-community-plugin-tech-radar
42
+ * - OCI without alias: oci://quay.io/rhdh/backstage-community-plugin-tech-radar:tag
43
+ */
44
+ export function extractPluginName(packageRef) {
45
+ const ref = packageRef.includes("!") ? packageRef.split("!")[0] : packageRef;
46
+ const match = ref.match(/\/([^/:@]+)(?:[:@].*)?$/);
47
+ return match?.[1] || packageRef;
48
+ }
49
+ /**
50
+ * Derives the displayName from a packageName.
51
+ * @backstage-community/plugin-tech-radar → backstage-community-plugin-tech-radar
52
+ */
53
+ function toDisplayName(packageName) {
54
+ return packageName.replace(/^@/, "").replace(/\//g, "-");
55
+ }
56
+ // ── Metadata Loading ──────────────────────────────────────────────────────────
57
+ export const DEFAULT_METADATA_PATH = "../metadata";
58
+ export function getMetadataDirectory(metadataPath = DEFAULT_METADATA_PATH) {
59
+ const resolvedPath = path.resolve(metadataPath);
60
+ if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
61
+ console.log(`[PluginMetadata] Using metadata directory: ${resolvedPath}`);
62
+ return resolvedPath;
63
+ }
64
+ console.log(`[PluginMetadata] Metadata directory not found: ${resolvedPath}`);
65
+ return null;
66
+ }
67
+ export async function parseMetadataFile(filePath) {
68
+ const content = await fs.readFile(filePath, "utf8");
69
+ const parsed = yaml.load(content);
70
+ const packagePath = parsed?.spec?.dynamicArtifact;
71
+ const packageName = parsed?.spec?.packageName;
72
+ const pluginConfig = parsed?.spec?.appConfigExamples?.[0]?.content;
73
+ if (!packagePath) {
74
+ throw new Error(`[PluginMetadata] Missing required field spec.dynamicArtifact in ${filePath}`);
75
+ }
76
+ if (!packageName) {
77
+ throw new Error(`[PluginMetadata] Missing required field spec.packageName in ${filePath}`);
78
+ }
79
+ return {
80
+ packagePath,
81
+ pluginConfig: pluginConfig || {},
82
+ packageName,
83
+ sourceFile: filePath,
84
+ };
85
+ }
86
+ export async function parseAllMetadataFiles(metadataDir) {
87
+ const pattern = path.join(metadataDir, "*.yaml");
88
+ const files = await glob(pattern);
89
+ console.log(`[PluginMetadata] Found ${files.length} metadata files in ${metadataDir}`);
90
+ const metadataMap = new Map();
91
+ for (const file of files) {
92
+ const metadata = await parseMetadataFile(file);
93
+ const pluginName = extractPluginName(metadata.packagePath);
94
+ metadataMap.set(pluginName, metadata);
95
+ console.log(`[PluginMetadata] Mapped plugin: ${pluginName} <- ${metadata.packagePath}`);
96
+ }
97
+ console.log(`[PluginMetadata] Successfully parsed ${metadataMap.size} plugin metadata entries`);
98
+ return metadataMap;
99
+ }
100
+ /**
101
+ * Loads and validates metadata from the workspace metadata directory.
102
+ * @throws Error if metadata directory not found or no valid metadata files
103
+ */
104
+ async function loadMetadata(metadataPath) {
105
+ const metadataDir = getMetadataDirectory(metadataPath);
106
+ if (!metadataDir) {
107
+ throw new Error(`[PluginMetadata] Metadata directory not found at: ${path.resolve(metadataPath)}`);
108
+ }
109
+ const metadataMap = await parseAllMetadataFiles(metadataDir);
110
+ if (metadataMap.size === 0) {
111
+ throw new Error(`[PluginMetadata] No valid metadata files found in ${metadataDir}`);
112
+ }
113
+ return [metadataDir, metadataMap];
114
+ }
115
+ /**
116
+ * Tries to load metadata, returns empty map if not available.
117
+ * Used by processPluginsForDeployment where metadata is optional.
118
+ */
119
+ async function tryLoadMetadata(metadataPath) {
120
+ const metadataDir = getMetadataDirectory(metadataPath);
121
+ if (!metadataDir)
122
+ return new Map();
123
+ return await parseAllMetadataFiles(metadataDir);
124
+ }
125
+ // ── PR: Fetch OCI URLs ───────────────────────────────────────────────────────
7
126
  /**
8
127
  * Fetches plugin versions from source repo and builds OCI URL map.
9
128
  * Only called when GIT_PR_NUMBER is set.
@@ -26,13 +145,11 @@ async function getOCIUrlsForPR(workspacePath, prNumber) {
26
145
  if (!ref) {
27
146
  throw new Error(`[PluginMetadata] source.json is missing required 'repo-ref' field: ${sourceJsonPath}`);
28
147
  }
29
- // Parse owner/repo from URL
30
148
  const match = repo.match(/github\.com\/(.+?)(?:\.git)?$/);
31
149
  if (!match) {
32
150
  throw new Error(`[PluginMetadata] Failed to parse GitHub repo from source.json: ${repo}`);
33
151
  }
34
152
  const ownerRepo = match[1];
35
- // Parse plugins-list.yaml as YAML and extract keys (plugin paths)
36
153
  const pluginsListContent = await fs.readFile(pluginsListPath, "utf-8");
37
154
  const pluginsListData = yaml.load(pluginsListContent);
38
155
  if (!pluginsListData || typeof pluginsListData !== "object") {
@@ -61,67 +178,29 @@ async function getOCIUrlsForPR(workspacePath, prNumber) {
61
178
  ` URL: ${rawUrl}`);
62
179
  }
63
180
  const { name, version } = pkgJson;
64
- // @backstage-community/plugin-tech-radar -> backstage-community-plugin-tech-radar
65
- const displayName = name.replace(/^@/, "").replace(/\//g, "-");
181
+ const displayName = toDisplayName(name);
66
182
  // TODO(RHDHBUGS-2530): Remove !alias suffix once Konflux builds include
67
- // io.backstage.dynamic-packages annotation. The suffix is a workaround
68
- // because install-dynamic-plugins.py can't auto-detect plugin path without it.
183
+ // io.backstage.dynamic-packages annotation.
69
184
  const ociUrl = `${OCI_REGISTRY_PREFIX}/${displayName}:pr_${prNumber}__${version}!${displayName}`;
70
185
  ociUrls.set(displayName, ociUrl);
71
186
  console.log(`[PluginMetadata] ${displayName} -> ${ociUrl}`);
72
187
  }
73
188
  return ociUrls;
74
189
  }
190
+ // ── Core: Unified Plugin Processing ──────────────────────────────────────────
75
191
  /**
76
- * Checks if plugin metadata handling should be enabled.
77
- * This controls both auto-generation and injection of plugin metadata.
78
- *
79
- * Default: ENABLED (for local dev and PR builds)
80
- * Disabled when:
81
- * - RHDH_SKIP_PLUGIN_METADATA_INJECTION is set, OR
82
- * - JOB_NAME contains "periodic-" (nightly/periodic builds)
83
- */
84
- export function shouldInjectPluginMetadata() {
85
- // Explicit opt-out
86
- if (process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION) {
87
- console.log("[PluginMetadata] Metadata handling disabled (RHDH_SKIP_PLUGIN_METADATA_INJECTION is set)");
88
- return false;
89
- }
90
- // Periodic/nightly job
91
- const jobName = process.env.JOB_NAME || "";
92
- if (jobName.includes("periodic-")) {
93
- console.log("[PluginMetadata] Metadata handling disabled (periodic job detected)");
94
- return false;
95
- }
96
- return true;
97
- }
98
- /**
99
- * Extracts the plugin name from a package path or OCI reference.
192
+ * Resolves plugin package references to their target OCI URLs where applicable.
100
193
  *
101
- * Handles various formats:
102
- * - Local path: ./dynamic-plugins/dist/backstage-community-plugin-tech-radar
103
- * - OCI with integrity: oci://quay.io/rhdh/plugin@sha256:...!backstage-community-plugin-tech-radar
104
- * - OCI without integrity: oci://quay.io/rhdh/backstage-community-plugin-tech-radar:tag
105
- *
106
- * @param packageRef The package reference string
107
- * @returns The extracted plugin name
194
+ * Resolution priority for each plugin:
195
+ * 1. PR OCI URL — if GIT_PR_NUMBER set and a PR image was published for this plugin
196
+ * 2. Metadata OCI ref — uses dynamicArtifact from metadata (latest published version)
197
+ * 3. Unchanged — local paths, npm packages, or other formats kept as-is
108
198
  */
109
- export function extractPluginName(packageRef) {
110
- // Strip ! suffix if present (e.g., oci://...@sha256:...!alias)
111
- const ref = packageRef.includes("!") ? packageRef.split("!")[0] : packageRef;
112
- // Regex to extract plugin name from various formats:
113
- // Captures the last path segment (chars except / : @) before any :tag or @digest
114
- const match = ref.match(/\/([^/:@]+)(?:[:@].*)?$/);
115
- return match?.[1] || packageRef;
116
- }
117
199
  /**
118
200
  * Returns a stable merge key for a plugin entry so OCI and local path for the same
119
201
  * logical plugin match when merging dynamic-plugins configs. Strips a trailing
120
202
  * "-dynamic" so e.g. backstage-community-plugin-catalog-backend-module-keycloak-dynamic
121
203
  * and ...-keycloak (from OCI) map to the same key.
122
- *
123
- * @param entry Plugin entry with optional package reference
124
- * @returns Normalized key for merge deduplication, or empty string if package is missing
125
204
  */
126
205
  export function getNormalizedPluginMergeKey(entry) {
127
206
  const pkg = entry?.package;
@@ -130,133 +209,62 @@ export function getNormalizedPluginMergeKey(entry) {
130
209
  }
131
210
  return extractPluginName(pkg).replace(/-dynamic$/, "");
132
211
  }
133
- /**
134
- * Default metadata directory path relative to the e2e-tests directory.
135
- * Follows the same pattern as user config paths (e.g., tests/config/dynamic-plugins.yaml).
136
- */
137
- export const DEFAULT_METADATA_PATH = "../metadata";
138
- /**
139
- * Gets the metadata directory path.
140
- * Uses the provided path or falls back to DEFAULT_METADATA_PATH.
141
- *
142
- * @param metadataPath Optional custom path to metadata directory
143
- * @returns The metadata directory path, or null if it doesn't exist
144
- */
145
- export function getMetadataDirectory(metadataPath = DEFAULT_METADATA_PATH) {
146
- const resolvedPath = path.resolve(metadataPath);
147
- if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
148
- console.log(`[PluginMetadata] Using metadata directory: ${resolvedPath}`);
149
- return resolvedPath;
150
- }
151
- console.log(`[PluginMetadata] Metadata directory not found: ${resolvedPath}`);
152
- return null;
153
- }
154
- /**
155
- * Parses a single metadata YAML file and extracts plugin configuration.
156
- *
157
- * @param filePath Path to the metadata YAML file
158
- * @returns PluginMetadata if valid, null otherwise
159
- */
160
- export async function parseMetadataFile(filePath) {
161
- const content = await fs.readFile(filePath, "utf8");
162
- const parsed = yaml.load(content);
163
- const packagePath = parsed?.spec?.dynamicArtifact;
164
- const packageName = parsed?.spec?.packageName;
165
- const pluginConfig = parsed?.spec?.appConfigExamples?.[0]?.content;
166
- if (!packagePath) {
167
- throw new Error(`[PluginMetadata] Missing required field spec.dynamicArtifact in ${filePath}`);
168
- }
169
- if (!packageName) {
170
- throw new Error(`[PluginMetadata] Missing required field spec.packageName in ${filePath}`);
171
- }
172
- return {
173
- packagePath,
174
- pluginConfig: pluginConfig || {},
175
- packageName,
176
- sourceFile: filePath,
177
- };
178
- }
179
- /**
180
- * Parses all metadata files in a directory and builds a map of plugin name to config.
181
- * The plugin name is extracted from the dynamicArtifact path for flexible matching.
182
- *
183
- * @param metadataDir Path to the metadata directory
184
- * @returns Map of plugin name to plugin configuration
185
- */
186
- export async function parseAllMetadataFiles(metadataDir) {
187
- const pattern = path.join(metadataDir, "*.yaml");
188
- const files = await glob(pattern);
189
- console.log(`[PluginMetadata] Found ${files.length} metadata files in ${metadataDir}`);
190
- const metadataMap = new Map();
191
- for (const file of files) {
192
- const metadata = await parseMetadataFile(file);
193
- const pluginName = extractPluginName(metadata.packagePath);
194
- metadataMap.set(pluginName, metadata);
195
- console.log(`[PluginMetadata] Mapped plugin: ${pluginName} <- ${metadata.packagePath}`);
196
- }
197
- console.log(`[PluginMetadata] Successfully parsed ${metadataMap.size} plugin metadata entries`);
198
- return metadataMap;
199
- }
200
- /**
201
- * Replaces local package paths with OCI URLs for plugins that have matching metadata.
202
- * Only applies to plugins with metadata (workspace plugins). Plugins without metadata
203
- * (e.g., keycloak, bulk-import from auth defaults) keep their original paths.
204
- *
205
- * @param plugins The plugin entries to process
206
- * @param metadataMap Map of plugin names to plugin metadata
207
- * @param metadataPath Path to the metadata directory (used to resolve workspace root)
208
- * @returns Plugin entries with OCI URLs replaced where applicable
209
- */
210
- async function replaceWithOCIUrls(plugins, metadataMap, metadataPath) {
212
+ async function resolvePluginPackages(plugins, metadataMap, metadataPath) {
213
+ // Build PR OCI URLs if applicable
211
214
  const prNumber = process.env.GIT_PR_NUMBER;
212
- if (!prNumber) {
213
- return plugins;
215
+ let prOciUrls = null;
216
+ if (prNumber) {
217
+ console.log(`[PluginMetadata] PR build detected (PR #${prNumber}), fetching OCI URLs...`);
218
+ const workspacePath = path.resolve(metadataPath, "..");
219
+ prOciUrls = await getOCIUrlsForPR(workspacePath, prNumber);
214
220
  }
215
- console.log(`[PluginMetadata] PR build detected (PR #${prNumber}), fetching OCI URLs...`);
216
- const workspacePath = path.resolve(metadataPath, "..");
217
- const ociUrls = await getOCIUrlsForPR(workspacePath, prNumber);
218
221
  return plugins.map((plugin) => {
219
- const pluginName = extractPluginName(plugin.package);
222
+ const pkg = plugin.package;
223
+ const pluginName = extractPluginName(pkg);
220
224
  const metadata = metadataMap.get(pluginName);
221
- if (!metadata?.packageName)
222
- return plugin;
223
- const displayName = metadata.packageName
224
- .replace(/^@/, "")
225
- .replace(/\//g, "-");
226
- const ociUrl = ociUrls.get(displayName);
227
- if (!ociUrl)
225
+ // 1. With metadata: resolve to PR OCI URL or metadata's dynamicArtifact
226
+ if (metadata?.packageName) {
227
+ const displayName = toDisplayName(metadata.packageName);
228
+ // PR: use PR-specific OCI URL if this plugin is part of the PR build
229
+ if (prOciUrls) {
230
+ const prUrl = prOciUrls.get(displayName);
231
+ if (prUrl) {
232
+ console.log(`[PluginMetadata] PR: ${pkg} → ${prUrl}`);
233
+ return { ...plugin, package: prUrl };
234
+ }
235
+ }
236
+ // Use metadata's dynamicArtifact directly (latest published version).
237
+ // This is more accurate than {{inherit}} because metadata is updated daily
238
+ // while the DPDY in the catalog index may lag behind.
239
+ if (metadata.packagePath.startsWith("oci://")) {
240
+ console.log(`[PluginMetadata] ${pkg} → ${metadata.packagePath}`);
241
+ return { ...plugin, package: metadata.packagePath };
242
+ }
228
243
  return plugin;
229
- console.log(`[PluginMetadata] Replacing ${plugin.package} with ${ociUrl}`);
230
- return { ...plugin, package: ociUrl };
244
+ }
245
+ // 2. Local paths (./dynamic-plugins/dist/...) and other formats — keep as-is.
246
+ // Local paths reference plugins bundled in the RHDH container image and work
247
+ // without OCI resolution. When the catalog index moves all plugins to OCI refs,
248
+ // they'll be handled by step 1 or 2 above automatically.
249
+ return plugin;
231
250
  });
232
251
  }
233
252
  /**
234
253
  * Injects plugin configurations from metadata into a dynamic plugins config.
235
254
  * Metadata config serves as the base, user-provided pluginConfig overrides it.
236
- *
237
- * Matching is done by extracting the plugin name from both the package reference
238
- * and the metadata's dynamicArtifact, allowing flexible matching across different
239
- * package formats (local paths, OCI references, etc.).
240
- *
241
- * @param dynamicPluginsConfig The dynamic plugins configuration to augment
242
- * @param metadataMap Map of plugin names to plugin metadata
243
- * @returns The augmented configuration with injected pluginConfigs
244
255
  */
245
- export function injectMetadataConfig(dynamicPluginsConfig, metadataMap) {
256
+ function injectMetadataConfig(dynamicPluginsConfig, metadataMap) {
246
257
  if (!dynamicPluginsConfig.plugins) {
247
258
  return dynamicPluginsConfig;
248
259
  }
249
260
  const augmentedPlugins = dynamicPluginsConfig.plugins.map((plugin) => {
250
- // Extract plugin name from package reference for flexible matching
251
261
  const pluginName = extractPluginName(plugin.package);
252
262
  const metadata = metadataMap.get(pluginName);
253
263
  if (!metadata) {
254
- // No metadata found for this plugin, keep as-is
255
264
  console.log(`[PluginMetadata] No metadata found for: ${pluginName} (from ${plugin.package})`);
256
265
  return plugin;
257
266
  }
258
267
  console.log(`[PluginMetadata] Injecting config for: ${pluginName} (from ${plugin.package})`);
259
- // Merge: metadata config (base) + user config (override)
260
268
  const mergedPluginConfig = deepMerge(metadata.pluginConfig, plugin.pluginConfig || {});
261
269
  return {
262
270
  ...plugin,
@@ -268,6 +276,7 @@ export function injectMetadataConfig(dynamicPluginsConfig, metadataMap) {
268
276
  plugins: augmentedPlugins,
269
277
  };
270
278
  }
279
+ // ── Public API ────────────────────────────────────────────────────────────────
271
280
  /**
272
281
  * Generates dynamic-plugins configuration for wrapper plugins
273
282
  * that need to be disabled. Each plugin entry contains:
@@ -290,78 +299,59 @@ export function disablePluginWrappers(plugins) {
290
299
  return pluginConfig;
291
300
  }
292
301
  /**
293
- * Generates a complete dynamic-plugins configuration from metadata files.
294
- * Iterates through all metadata files and creates plugin entries with:
295
- * - package: the dynamicArtifact path
296
- * - disabled: false (enabled by default)
297
- * - pluginConfig: from appConfigExamples[0].content
302
+ * Auto-generates plugin entries from workspace metadata files.
303
+ * Creates raw entries with local paths and disabled: false.
304
+ * Does NOT include pluginConfig — that's handled by processPluginsForDeployment.
298
305
  *
299
- * @param metadataPath Optional custom path to metadata directory (default: ../metadata)
300
- * @returns Complete dynamic plugins configuration
301
- * @throws Error if metadata directory not found or no valid metadata files
306
+ * @param metadataPath Optional custom path to metadata directory
307
+ * @returns Plugin entries discovered from metadata
302
308
  */
303
- export async function generateDynamicPluginsConfigFromMetadata(metadataPath = DEFAULT_METADATA_PATH) {
304
- // Skip if metadata handling is disabled
305
- if (!shouldInjectPluginMetadata()) {
306
- console.log("[PluginMetadata] Returning empty config (metadata handling disabled)");
307
- return { plugins: [] };
308
- }
309
- console.log("[PluginMetadata] No dynamic-plugins config provided, generating from metadata...");
310
- // Get metadata directory
311
- const metadataDir = getMetadataDirectory(metadataPath);
312
- if (!metadataDir) {
313
- throw new Error(`[PluginMetadata] Cannot generate dynamic-plugins config: metadata directory not found at: ${path.resolve(metadataPath)}`);
314
- }
315
- // Parse all metadata files
316
- const metadataMap = await parseAllMetadataFiles(metadataDir);
317
- if (metadataMap.size === 0) {
318
- throw new Error(`[PluginMetadata] Cannot generate dynamic-plugins config: no valid metadata files found in ${metadataDir}`);
319
- }
320
- // Build plugin entries from metadata
321
- let plugins = [];
309
+ export async function generatePluginsFromMetadata(metadataPath = DEFAULT_METADATA_PATH) {
310
+ console.log("[PluginMetadata] Auto-generating plugin entries from metadata...");
311
+ const [, metadataMap] = await loadMetadata(metadataPath);
312
+ const plugins = [];
322
313
  for (const [pluginName, metadata] of metadataMap) {
323
314
  console.log(`[PluginMetadata] Adding plugin: ${pluginName} (${metadata.packagePath})`);
324
315
  plugins.push({
325
316
  package: metadata.packagePath,
326
317
  disabled: false,
327
- pluginConfig: metadata.pluginConfig,
328
318
  });
329
319
  }
330
- // Replace local paths with OCI URLs for PR builds
331
- plugins = await replaceWithOCIUrls(plugins, metadataMap, metadataPath);
332
- console.log(`[PluginMetadata] Generated dynamic-plugins config with ${plugins.length} plugins`);
320
+ console.log(`[PluginMetadata] Generated ${plugins.length} plugin entries from metadata`);
333
321
  return { plugins };
334
322
  }
335
323
  /**
336
- * Main function to load and inject plugin metadata for PR builds.
337
- * For non-PR builds (nightly), returns the config unchanged.
324
+ * Processes a dynamic plugins configuration for deployment.
325
+ * Single entry point for both PR and nightly flows.
338
326
  *
339
- * @param dynamicPluginsConfig The dynamic plugins configuration
340
- * @param metadataPath Optional custom path to metadata directory (default: ../metadata)
341
- * @returns Augmented configuration with metadata (for PR) or unchanged (for nightly)
342
- * @throws Error if PR build but no metadata directory found
327
+ * Operations (in order):
328
+ * 1. Inject appConfigExamples from metadata (PR mode only, unless RHDH_SKIP_PLUGIN_METADATA_INJECTION is set)
329
+ * 2. Resolve all packages to OCI references:
330
+ * - PR with GIT_PR_NUMBER: workspace plugins in PR build → pr_ tags, rest unchanged
331
+ * - PR without GIT_PR_NUMBER: OCI plugins with metadata → metadata refs, rest unchanged
332
+ * - Nightly: OCI plugins with metadata → metadata refs, rest unchanged
333
+ *
334
+ * @param config The merged dynamic plugins configuration
335
+ * @param metadataPath Optional custom path to metadata directory
336
+ * @returns Processed configuration ready for deployment
343
337
  */
344
- export async function loadAndInjectPluginMetadata(dynamicPluginsConfig, metadataPath = DEFAULT_METADATA_PATH) {
345
- // Skip metadata injection if disabled
346
- if (!shouldInjectPluginMetadata()) {
347
- return dynamicPluginsConfig;
348
- }
349
- console.log("[PluginMetadata] Loading plugin metadata...");
350
- // Get metadata directory
351
- const metadataDir = getMetadataDirectory(metadataPath);
352
- if (!metadataDir) {
353
- throw new Error(`[PluginMetadata] PR build requires metadata directory but not found at: ${path.resolve(metadataPath)}`);
354
- }
355
- // Parse all metadata files
356
- const metadataMap = await parseAllMetadataFiles(metadataDir);
357
- if (metadataMap.size === 0) {
358
- throw new Error(`[PluginMetadata] PR build requires plugin metadata but no valid metadata files found in ${metadataDir}`);
359
- }
360
- // Inject metadata configs into the dynamic plugins config
361
- const result = injectMetadataConfig(dynamicPluginsConfig, metadataMap);
362
- // Replace local paths with OCI URLs for PR builds
363
- if (result.plugins) {
364
- result.plugins = await replaceWithOCIUrls(result.plugins, metadataMap, metadataPath);
338
+ export async function processPluginsForDeployment(config, metadataPath = DEFAULT_METADATA_PATH) {
339
+ if (!config.plugins)
340
+ return config;
341
+ const metadataMap = await tryLoadMetadata(metadataPath);
342
+ let result = { ...config };
343
+ // Inject appConfigExamples from metadata (PR mode only)
344
+ if (!isNightlyJob() &&
345
+ process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION !== "true" &&
346
+ metadataMap.size > 0) {
347
+ console.log("[PluginMetadata] Injecting metadata configs...");
348
+ result = injectMetadataConfig(result, metadataMap);
365
349
  }
350
+ // Resolve all packages to OCI references
351
+ console.log("[PluginMetadata] Resolving plugin packages to OCI...");
352
+ result = {
353
+ ...result,
354
+ plugins: await resolvePluginPackages(result.plugins, metadataMap, metadataPath),
355
+ };
366
356
  return result;
367
357
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Static utility for resolving paths relative to a workspace's e2e-tests directory.
3
+ * Uses `test.info().project.testDir` to determine the workspace location —
4
+ * works correctly whether Playwright runs from the workspace or from the repo root.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { WorkspacePaths } from '@red-hat-developer-hub/e2e-test-utils/utils';
9
+ *
10
+ * // One-liner to resolve a config file path
11
+ * const configPath = WorkspacePaths.resolve("tests/config/rbac-configmap.yaml");
12
+ *
13
+ * // Access well-known directories
14
+ * WorkspacePaths.e2eRoot; // /abs/path/workspaces/acr/e2e-tests
15
+ * WorkspacePaths.workspaceRoot; // /abs/path/workspaces/acr
16
+ * WorkspacePaths.metadataDir; // /abs/path/workspaces/acr/metadata
17
+ * WorkspacePaths.configDir; // /abs/path/workspaces/acr/e2e-tests/tests/config
18
+ * ```
19
+ */
20
+ export declare class WorkspacePaths {
21
+ private constructor();
22
+ /** The workspace's e2e-tests directory, derived from the current test's project testDir. */
23
+ static get e2eRoot(): string;
24
+ /** Resolve a relative path from the e2e-tests directory. */
25
+ static resolve(relativePath: string): string;
26
+ /** The workspace root directory (parent of e2e-tests). */
27
+ static get workspaceRoot(): string;
28
+ /** The metadata directory. e.g., `workspaces/acr/metadata` */
29
+ static get metadataDir(): string;
30
+ /** The tests/config directory. e.g., `workspaces/acr/e2e-tests/tests/config` */
31
+ static get configDir(): string;
32
+ /** Default app-config path: `tests/config/app-config-rhdh.yaml` */
33
+ static get appConfig(): string;
34
+ /** Default secrets path: `tests/config/rhdh-secrets.yaml` */
35
+ static get secrets(): string;
36
+ /** Default dynamic plugins path: `tests/config/dynamic-plugins.yaml` */
37
+ static get dynamicPlugins(): string;
38
+ /** Default Helm value file path: `tests/config/value_file.yaml` */
39
+ static get valueFile(): string;
40
+ /** Default operator subscription path: `tests/config/subscription.yaml` */
41
+ static get subscription(): string;
42
+ }
43
+ //# sourceMappingURL=workspace-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-paths.d.ts","sourceRoot":"","sources":["../../src/utils/workspace-paths.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,cAAc;IACzB,OAAO;IAEP,4FAA4F;IAC5F,MAAM,KAAK,OAAO,IAAI,MAAM,CAE3B;IAED,4DAA4D;IAC5D,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAI5C,0DAA0D;IAC1D,MAAM,KAAK,aAAa,IAAI,MAAM,CAEjC;IAED,8DAA8D;IAC9D,MAAM,KAAK,WAAW,IAAI,MAAM,CAE/B;IAED,gFAAgF;IAChF,MAAM,KAAK,SAAS,IAAI,MAAM,CAE7B;IAID,mEAAmE;IACnE,MAAM,KAAK,SAAS,IAAI,MAAM,CAE7B;IAED,6DAA6D;IAC7D,MAAM,KAAK,OAAO,IAAI,MAAM,CAE3B;IAED,wEAAwE;IACxE,MAAM,KAAK,cAAc,IAAI,MAAM,CAElC;IAED,mEAAmE;IACnE,MAAM,KAAK,SAAS,IAAI,MAAM,CAE7B;IAED,2EAA2E;IAC3E,MAAM,KAAK,YAAY,IAAI,MAAM,CAEhC;CACF"}
@@ -0,0 +1,65 @@
1
+ import path from "path";
2
+ import { test } from "@playwright/test";
3
+ /**
4
+ * Static utility for resolving paths relative to a workspace's e2e-tests directory.
5
+ * Uses `test.info().project.testDir` to determine the workspace location —
6
+ * works correctly whether Playwright runs from the workspace or from the repo root.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { WorkspacePaths } from '@red-hat-developer-hub/e2e-test-utils/utils';
11
+ *
12
+ * // One-liner to resolve a config file path
13
+ * const configPath = WorkspacePaths.resolve("tests/config/rbac-configmap.yaml");
14
+ *
15
+ * // Access well-known directories
16
+ * WorkspacePaths.e2eRoot; // /abs/path/workspaces/acr/e2e-tests
17
+ * WorkspacePaths.workspaceRoot; // /abs/path/workspaces/acr
18
+ * WorkspacePaths.metadataDir; // /abs/path/workspaces/acr/metadata
19
+ * WorkspacePaths.configDir; // /abs/path/workspaces/acr/e2e-tests/tests/config
20
+ * ```
21
+ */
22
+ export class WorkspacePaths {
23
+ constructor() { } // Static-only class
24
+ /** The workspace's e2e-tests directory, derived from the current test's project testDir. */
25
+ static get e2eRoot() {
26
+ return path.resolve(test.info().project.testDir, "..");
27
+ }
28
+ /** Resolve a relative path from the e2e-tests directory. */
29
+ static resolve(relativePath) {
30
+ return path.resolve(this.e2eRoot, relativePath);
31
+ }
32
+ /** The workspace root directory (parent of e2e-tests). */
33
+ static get workspaceRoot() {
34
+ return path.resolve(this.e2eRoot, "..");
35
+ }
36
+ /** The metadata directory. e.g., `workspaces/acr/metadata` */
37
+ static get metadataDir() {
38
+ return path.resolve(this.e2eRoot, "../metadata");
39
+ }
40
+ /** The tests/config directory. e.g., `workspaces/acr/e2e-tests/tests/config` */
41
+ static get configDir() {
42
+ return path.resolve(this.e2eRoot, "tests/config");
43
+ }
44
+ // ── Default config file paths ────────────────────────────────────────────
45
+ /** Default app-config path: `tests/config/app-config-rhdh.yaml` */
46
+ static get appConfig() {
47
+ return path.resolve(this.e2eRoot, "tests/config/app-config-rhdh.yaml");
48
+ }
49
+ /** Default secrets path: `tests/config/rhdh-secrets.yaml` */
50
+ static get secrets() {
51
+ return path.resolve(this.e2eRoot, "tests/config/rhdh-secrets.yaml");
52
+ }
53
+ /** Default dynamic plugins path: `tests/config/dynamic-plugins.yaml` */
54
+ static get dynamicPlugins() {
55
+ return path.resolve(this.e2eRoot, "tests/config/dynamic-plugins.yaml");
56
+ }
57
+ /** Default Helm value file path: `tests/config/value_file.yaml` */
58
+ static get valueFile() {
59
+ return path.resolve(this.e2eRoot, "tests/config/value_file.yaml");
60
+ }
61
+ /** Default operator subscription path: `tests/config/subscription.yaml` */
62
+ static get subscription() {
63
+ return path.resolve(this.e2eRoot, "tests/config/subscription.yaml");
64
+ }
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/e2e-test-utils",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "description": "Test utilities for RHDH E2E tests",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {