@red-hat-developer-hub/e2e-test-utils 1.1.10

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.
Files changed (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +6 -0
  3. package/dist/deployment/keycloak/config/keycloak-values.yaml +96 -0
  4. package/dist/deployment/keycloak/constants.d.ts +29 -0
  5. package/dist/deployment/keycloak/constants.d.ts.map +1 -0
  6. package/dist/deployment/keycloak/constants.js +75 -0
  7. package/dist/deployment/keycloak/deployment.d.ts +89 -0
  8. package/dist/deployment/keycloak/deployment.d.ts.map +1 -0
  9. package/dist/deployment/keycloak/deployment.js +435 -0
  10. package/dist/deployment/keycloak/index.d.ts +2 -0
  11. package/dist/deployment/keycloak/index.d.ts.map +1 -0
  12. package/dist/deployment/keycloak/index.js +1 -0
  13. package/dist/deployment/keycloak/types.d.ts +59 -0
  14. package/dist/deployment/keycloak/types.d.ts.map +1 -0
  15. package/dist/deployment/keycloak/types.js +1 -0
  16. package/dist/deployment/rhdh/config/auth/guest/app-config.yaml +5 -0
  17. package/dist/deployment/rhdh/config/auth/keycloak/app-config.yaml +19 -0
  18. package/dist/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml +3 -0
  19. package/dist/deployment/rhdh/config/auth/keycloak/secrets.yaml +12 -0
  20. package/dist/deployment/rhdh/config/common/app-config-rhdh.yaml +6 -0
  21. package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +3 -0
  22. package/dist/deployment/rhdh/config/common/rhdh-secrets.yaml +7 -0
  23. package/dist/deployment/rhdh/config/helm/value_file.yaml +7 -0
  24. package/dist/deployment/rhdh/config/operator/subscription.yaml +21 -0
  25. package/dist/deployment/rhdh/constants.d.ts +19 -0
  26. package/dist/deployment/rhdh/constants.d.ts.map +1 -0
  27. package/dist/deployment/rhdh/constants.js +27 -0
  28. package/dist/deployment/rhdh/deployment.d.ts +40 -0
  29. package/dist/deployment/rhdh/deployment.d.ts.map +1 -0
  30. package/dist/deployment/rhdh/deployment.js +303 -0
  31. package/dist/deployment/rhdh/index.d.ts +2 -0
  32. package/dist/deployment/rhdh/index.d.ts.map +1 -0
  33. package/dist/deployment/rhdh/index.js +1 -0
  34. package/dist/deployment/rhdh/types.d.ts +31 -0
  35. package/dist/deployment/rhdh/types.d.ts.map +1 -0
  36. package/dist/deployment/rhdh/types.js +1 -0
  37. package/dist/eslint/base.config.d.ts +10 -0
  38. package/dist/eslint/base.config.d.ts.map +1 -0
  39. package/dist/eslint/base.config.js +213 -0
  40. package/dist/playwright/base-config.d.ts +14 -0
  41. package/dist/playwright/base-config.d.ts.map +1 -0
  42. package/dist/playwright/base-config.js +47 -0
  43. package/dist/playwright/fixtures/test.d.ts +13 -0
  44. package/dist/playwright/fixtures/test.d.ts.map +1 -0
  45. package/dist/playwright/fixtures/test.js +48 -0
  46. package/dist/playwright/global-setup.d.ts +6 -0
  47. package/dist/playwright/global-setup.d.ts.map +1 -0
  48. package/dist/playwright/global-setup.js +67 -0
  49. package/dist/playwright/helpers/accessibility.d.ts +13 -0
  50. package/dist/playwright/helpers/accessibility.d.ts.map +1 -0
  51. package/dist/playwright/helpers/accessibility.js +24 -0
  52. package/dist/playwright/helpers/api-endpoints.d.ts +11 -0
  53. package/dist/playwright/helpers/api-endpoints.d.ts.map +1 -0
  54. package/dist/playwright/helpers/api-endpoints.js +15 -0
  55. package/dist/playwright/helpers/api-helper.d.ts +77 -0
  56. package/dist/playwright/helpers/api-helper.d.ts.map +1 -0
  57. package/dist/playwright/helpers/api-helper.js +285 -0
  58. package/dist/playwright/helpers/common.d.ts +31 -0
  59. package/dist/playwright/helpers/common.d.ts.map +1 -0
  60. package/dist/playwright/helpers/common.js +342 -0
  61. package/dist/playwright/helpers/index.d.ts +5 -0
  62. package/dist/playwright/helpers/index.d.ts.map +1 -0
  63. package/dist/playwright/helpers/index.js +4 -0
  64. package/dist/playwright/helpers/navbar.d.ts +2 -0
  65. package/dist/playwright/helpers/navbar.d.ts.map +1 -0
  66. package/dist/playwright/helpers/navbar.js +1 -0
  67. package/dist/playwright/helpers/ui-helper.d.ts +106 -0
  68. package/dist/playwright/helpers/ui-helper.d.ts.map +1 -0
  69. package/dist/playwright/helpers/ui-helper.js +439 -0
  70. package/dist/playwright/page-objects/global-obj.d.ts +25 -0
  71. package/dist/playwright/page-objects/global-obj.d.ts.map +1 -0
  72. package/dist/playwright/page-objects/global-obj.js +24 -0
  73. package/dist/playwright/page-objects/page-obj.d.ts +41 -0
  74. package/dist/playwright/page-objects/page-obj.d.ts.map +1 -0
  75. package/dist/playwright/page-objects/page-obj.js +40 -0
  76. package/dist/playwright/pages/catalog-import.d.ts +31 -0
  77. package/dist/playwright/pages/catalog-import.d.ts.map +1 -0
  78. package/dist/playwright/pages/catalog-import.js +65 -0
  79. package/dist/playwright/pages/catalog.d.ts +14 -0
  80. package/dist/playwright/pages/catalog.d.ts.map +1 -0
  81. package/dist/playwright/pages/catalog.js +37 -0
  82. package/dist/playwright/pages/extensions.d.ts +38 -0
  83. package/dist/playwright/pages/extensions.d.ts.map +1 -0
  84. package/dist/playwright/pages/extensions.js +110 -0
  85. package/dist/playwright/pages/home-page.d.ts +10 -0
  86. package/dist/playwright/pages/home-page.d.ts.map +1 -0
  87. package/dist/playwright/pages/home-page.js +46 -0
  88. package/dist/playwright/pages/index.d.ts +6 -0
  89. package/dist/playwright/pages/index.d.ts.map +1 -0
  90. package/dist/playwright/pages/index.js +5 -0
  91. package/dist/playwright/pages/notifications.d.ts +24 -0
  92. package/dist/playwright/pages/notifications.d.ts.map +1 -0
  93. package/dist/playwright/pages/notifications.js +112 -0
  94. package/dist/utils/bash.d.ts +3 -0
  95. package/dist/utils/bash.d.ts.map +1 -0
  96. package/dist/utils/bash.js +4 -0
  97. package/dist/utils/common.d.ts +3 -0
  98. package/dist/utils/common.d.ts.map +1 -0
  99. package/dist/utils/common.js +8 -0
  100. package/dist/utils/index.d.ts +5 -0
  101. package/dist/utils/index.d.ts.map +1 -0
  102. package/dist/utils/index.js +4 -0
  103. package/dist/utils/kubernetes-client.d.ts +97 -0
  104. package/dist/utils/kubernetes-client.d.ts.map +1 -0
  105. package/dist/utils/kubernetes-client.js +558 -0
  106. package/dist/utils/merge-yamls.d.ts +50 -0
  107. package/dist/utils/merge-yamls.d.ts.map +1 -0
  108. package/dist/utils/merge-yamls.js +96 -0
  109. package/dist/utils/plugin-metadata.d.ts +116 -0
  110. package/dist/utils/plugin-metadata.d.ts.map +1 -0
  111. package/dist/utils/plugin-metadata.js +330 -0
  112. package/package.json +108 -0
  113. package/tsconfig.base.json +16 -0
@@ -0,0 +1,96 @@
1
+ import fs from "fs-extra";
2
+ import yaml from "js-yaml";
3
+ import mergeWith from "lodash.mergewith";
4
+ /**
5
+ * Merges two arrays of objects by a specific key.
6
+ * Objects with matching keys are deeply merged, new objects are appended.
7
+ */
8
+ function mergeArraysByKey(target, source, key, mergeOptions) {
9
+ const result = [...target];
10
+ for (const srcItem of source) {
11
+ if (typeof srcItem === "object" && srcItem !== null && key in srcItem) {
12
+ const srcKeyValue = srcItem[key];
13
+ const existingIndex = result.findIndex((item) => typeof item === "object" &&
14
+ item !== null &&
15
+ item[key] === srcKeyValue);
16
+ if (existingIndex !== -1) {
17
+ // Merge existing object with source object
18
+ result[existingIndex] = deepMerge(result[existingIndex], srcItem, mergeOptions);
19
+ }
20
+ else {
21
+ // Append new object
22
+ result.push(srcItem);
23
+ }
24
+ }
25
+ else {
26
+ // Non-object or missing key, append as-is
27
+ result.push(srcItem);
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+ /**
33
+ * Deeply merges two YAML-compatible objects.
34
+ * Array handling is controlled by the arrayMergeStrategy option.
35
+ */
36
+ export function deepMerge(target, source, options = {}) {
37
+ const strategy = options.arrayMergeStrategy ?? "replace";
38
+ return mergeWith({ ...target }, source, (objValue, srcValue) => {
39
+ if (Array.isArray(objValue) && Array.isArray(srcValue)) {
40
+ if (strategy === "replace") {
41
+ return srcValue;
42
+ }
43
+ else if (strategy === "concat") {
44
+ return [...objValue, ...srcValue];
45
+ }
46
+ else if (typeof strategy === "object" && "byKey" in strategy) {
47
+ return mergeArraysByKey(objValue, srcValue, strategy.byKey, options);
48
+ }
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * Merge multiple YAML files into one object.
54
+ *
55
+ * @param paths List of YAML file paths (base first, overlays last)
56
+ * @param options Optional merge options (e.g., arrayMergeStrategy)
57
+ * @returns Merged YAML object
58
+ */
59
+ export async function mergeYamlFiles(paths, options = {}) {
60
+ let merged = {};
61
+ for (const path of paths) {
62
+ const content = await fs.readFile(path, "utf8");
63
+ const parsed = (yaml.load(content) || {});
64
+ merged = deepMerge(merged, parsed, options);
65
+ }
66
+ return merged;
67
+ }
68
+ /**
69
+ * Merge multiple YAML files if they exist.
70
+ *
71
+ * @param paths List of YAML file paths
72
+ * @param options Optional merge options (e.g., arrayMergeStrategy)
73
+ * @returns Merged YAML object
74
+ */
75
+ export async function mergeYamlFilesIfExists(paths, options = {}) {
76
+ return await mergeYamlFiles(paths.filter((path) => {
77
+ const exists = fs.existsSync(path);
78
+ if (!exists)
79
+ console.log(`YAML file ${path} does not exist`);
80
+ return exists;
81
+ }), options);
82
+ }
83
+ /**
84
+ * Merge multiple YAML files and write the result to an output file.
85
+ *
86
+ * @param inputPaths List of input YAML files
87
+ * @param outputPath Output YAML file path
88
+ * @param dumpOptions Optional dump formatting
89
+ * @param mergeOptions Optional merge options (e.g., arrayMergeStrategy)
90
+ */
91
+ export async function mergeYamlFilesToFile(inputPaths, outputPath, dumpOptions = { lineWidth: -1 }, mergeOptions = {}) {
92
+ const merged = await mergeYamlFiles(inputPaths, mergeOptions);
93
+ const yamlString = yaml.dump(merged, dumpOptions);
94
+ await fs.outputFile(outputPath, yamlString);
95
+ console.log(`Merged ${inputPaths.length} YAML files into ${outputPath}`);
96
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Represents parsed plugin metadata from a Package CRD file.
3
+ */
4
+ export interface PluginMetadata {
5
+ /** The dynamic artifact path (e.g., ./dynamic-plugins/dist/plugin-name) */
6
+ packagePath: string;
7
+ /** The plugin configuration from appConfigExamples[0].content */
8
+ pluginConfig: Record<string, unknown>;
9
+ /** The package name (e.g., @backstage-community/plugin-tech-radar) */
10
+ packageName: string;
11
+ /** Source metadata file path */
12
+ sourceFile: string;
13
+ }
14
+ /**
15
+ * Checks if plugin metadata handling should be enabled.
16
+ * This controls both auto-generation and injection of plugin metadata.
17
+ *
18
+ * Default: ENABLED (for local dev and PR builds)
19
+ * Disabled when:
20
+ * - RHDH_SKIP_PLUGIN_METADATA_INJECTION is set, OR
21
+ * - JOB_NAME contains "periodic-" (nightly/periodic builds)
22
+ */
23
+ export declare function shouldInjectPluginMetadata(): boolean;
24
+ /**
25
+ * Extracts the plugin name from a package path or OCI reference.
26
+ *
27
+ * Handles various formats:
28
+ * - Local path: ./dynamic-plugins/dist/backstage-community-plugin-tech-radar
29
+ * - OCI with integrity: oci://quay.io/rhdh/plugin@sha256:...!backstage-community-plugin-tech-radar
30
+ * - OCI without integrity: oci://quay.io/rhdh/backstage-community-plugin-tech-radar:tag
31
+ *
32
+ * @param packageRef The package reference string
33
+ * @returns The extracted plugin name
34
+ */
35
+ export declare function extractPluginName(packageRef: string): string;
36
+ /**
37
+ * Default metadata directory path relative to the e2e-tests directory.
38
+ * Follows the same pattern as user config paths (e.g., tests/config/dynamic-plugins.yaml).
39
+ */
40
+ export declare const DEFAULT_METADATA_PATH = "../metadata";
41
+ /**
42
+ * Gets the metadata directory path.
43
+ * Uses the provided path or falls back to DEFAULT_METADATA_PATH.
44
+ *
45
+ * @param metadataPath Optional custom path to metadata directory
46
+ * @returns The metadata directory path, or null if it doesn't exist
47
+ */
48
+ export declare function getMetadataDirectory(metadataPath?: string): string | null;
49
+ /**
50
+ * Parses a single metadata YAML file and extracts plugin configuration.
51
+ *
52
+ * @param filePath Path to the metadata YAML file
53
+ * @returns PluginMetadata if valid, null otherwise
54
+ */
55
+ export declare function parseMetadataFile(filePath: string): Promise<PluginMetadata>;
56
+ /**
57
+ * Parses all metadata files in a directory and builds a map of plugin name to config.
58
+ * The plugin name is extracted from the dynamicArtifact path for flexible matching.
59
+ *
60
+ * @param metadataDir Path to the metadata directory
61
+ * @returns Map of plugin name to plugin configuration
62
+ */
63
+ export declare function parseAllMetadataFiles(metadataDir: string): Promise<Map<string, PluginMetadata>>;
64
+ /**
65
+ * Plugin entry in dynamic-plugins.yaml
66
+ */
67
+ export interface PluginEntry {
68
+ package: string;
69
+ disabled?: boolean;
70
+ pluginConfig?: Record<string, unknown>;
71
+ [key: string]: unknown;
72
+ }
73
+ /**
74
+ * Dynamic plugins configuration structure
75
+ */
76
+ export interface DynamicPluginsConfig {
77
+ plugins?: PluginEntry[];
78
+ includes?: string[];
79
+ [key: string]: unknown;
80
+ }
81
+ /**
82
+ * Injects plugin configurations from metadata into a dynamic plugins config.
83
+ * Metadata config serves as the base, user-provided pluginConfig overrides it.
84
+ *
85
+ * Matching is done by extracting the plugin name from both the package reference
86
+ * and the metadata's dynamicArtifact, allowing flexible matching across different
87
+ * package formats (local paths, OCI references, etc.).
88
+ *
89
+ * @param dynamicPluginsConfig The dynamic plugins configuration to augment
90
+ * @param metadataMap Map of plugin names to plugin metadata
91
+ * @returns The augmented configuration with injected pluginConfigs
92
+ */
93
+ export declare function injectMetadataConfig(dynamicPluginsConfig: DynamicPluginsConfig, metadataMap: Map<string, PluginMetadata>): DynamicPluginsConfig;
94
+ /**
95
+ * Generates a complete dynamic-plugins configuration from metadata files.
96
+ * Iterates through all metadata files and creates plugin entries with:
97
+ * - package: the dynamicArtifact path
98
+ * - disabled: false (enabled by default)
99
+ * - pluginConfig: from appConfigExamples[0].content
100
+ *
101
+ * @param metadataPath Optional custom path to metadata directory (default: ../metadata)
102
+ * @returns Complete dynamic plugins configuration
103
+ * @throws Error if metadata directory not found or no valid metadata files
104
+ */
105
+ export declare function generateDynamicPluginsConfigFromMetadata(metadataPath?: string): Promise<DynamicPluginsConfig>;
106
+ /**
107
+ * Main function to load and inject plugin metadata for PR builds.
108
+ * For non-PR builds (nightly), returns the config unchanged.
109
+ *
110
+ * @param dynamicPluginsConfig The dynamic plugins configuration
111
+ * @param metadataPath Optional custom path to metadata directory (default: ../metadata)
112
+ * @returns Augmented configuration with metadata (for PR) or unchanged (for nightly)
113
+ * @throws Error if PR build but no metadata directory found
114
+ */
115
+ export declare function loadAndInjectPluginMetadata(dynamicPluginsConfig: DynamicPluginsConfig, metadataPath?: string): Promise<DynamicPluginsConfig>;
116
+ //# sourceMappingURL=plugin-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.ts"],"names":[],"mappings":"AA8HA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;CACpB;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAmBpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,GAAE,MAA8B,GAC3C,MAAM,GAAG,IAAI,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CA0BzB;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwBtC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA4CD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,oBAAoB,EAAE,oBAAoB,EAC1C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GACvC,oBAAoB,CAsCtB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wCAAwC,CAC5D,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAsD/B;AAED;;;;;;;;GAQG;AACH,wBAAsB,2BAA2B,CAC/C,oBAAoB,EAAE,oBAAoB,EAC1C,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAuC/B"}
@@ -0,0 +1,330 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import yaml from "js-yaml";
4
+ import { glob } from "zx";
5
+ import { deepMerge } from "./merge-yamls.js";
6
+ const OCI_REGISTRY_PREFIX = "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays";
7
+ /**
8
+ * Fetches plugin versions from source repo and builds OCI URL map.
9
+ * Only called when GIT_PR_NUMBER is set.
10
+ */
11
+ async function getOCIUrlsForPR(workspacePath, prNumber) {
12
+ const ociUrls = new Map();
13
+ const sourceJsonPath = path.join(workspacePath, "source.json");
14
+ const pluginsListPath = path.join(workspacePath, "plugins-list.yaml");
15
+ if (!fs.existsSync(sourceJsonPath)) {
16
+ throw new Error(`[PluginMetadata] PR build requires source.json but not found at: ${sourceJsonPath}`);
17
+ }
18
+ if (!fs.existsSync(pluginsListPath)) {
19
+ throw new Error(`[PluginMetadata] PR build requires plugins-list.yaml but not found at: ${pluginsListPath}`);
20
+ }
21
+ const sourceJson = JSON.parse(await fs.readFile(sourceJsonPath, "utf-8"));
22
+ const { repo, "repo-ref": ref, "repo-flat": repoFlat } = sourceJson;
23
+ if (!repo) {
24
+ throw new Error(`[PluginMetadata] source.json is missing required 'repo' field: ${sourceJsonPath}`);
25
+ }
26
+ if (!ref) {
27
+ throw new Error(`[PluginMetadata] source.json is missing required 'repo-ref' field: ${sourceJsonPath}`);
28
+ }
29
+ // Parse owner/repo from URL
30
+ const match = repo.match(/github\.com\/(.+?)(?:\.git)?$/);
31
+ if (!match) {
32
+ throw new Error(`[PluginMetadata] Failed to parse GitHub repo from source.json: ${repo}`);
33
+ }
34
+ const ownerRepo = match[1];
35
+ // Parse plugins-list.yaml as YAML and extract keys (plugin paths)
36
+ const pluginsListContent = await fs.readFile(pluginsListPath, "utf-8");
37
+ const pluginsListData = yaml.load(pluginsListContent);
38
+ if (!pluginsListData || typeof pluginsListData !== "object") {
39
+ throw new Error(`[PluginMetadata] plugins-list.yaml is empty or invalid: ${pluginsListPath}`);
40
+ }
41
+ const pluginPaths = Object.keys(pluginsListData);
42
+ const workspaceName = path.basename(workspacePath);
43
+ console.log(`[PluginMetadata] Fetching versions for ${pluginPaths.length} plugins from source...`);
44
+ for (const pluginPath of pluginPaths) {
45
+ const pkgJsonPath = repoFlat
46
+ ? `${pluginPath}/package.json`
47
+ : `workspaces/${workspaceName}/${pluginPath}/package.json`;
48
+ const rawUrl = `https://raw.githubusercontent.com/${ownerRepo}/${ref}/${pkgJsonPath}`;
49
+ const res = await fetch(rawUrl);
50
+ if (!res.ok) {
51
+ throw new Error(`[PluginMetadata] Failed to fetch package.json for ${pluginPath}: ${res.status} ${res.statusText}\n` +
52
+ ` URL: ${rawUrl}`);
53
+ }
54
+ const pkgJson = (await res.json());
55
+ if (!pkgJson.name) {
56
+ throw new Error(`[PluginMetadata] package.json is missing 'name' field for ${pluginPath}\n` +
57
+ ` URL: ${rawUrl}`);
58
+ }
59
+ if (!pkgJson.version) {
60
+ throw new Error(`[PluginMetadata] package.json is missing 'version' field for ${pluginPath}\n` +
61
+ ` URL: ${rawUrl}`);
62
+ }
63
+ const { name, version } = pkgJson;
64
+ // @backstage-community/plugin-tech-radar -> backstage-community-plugin-tech-radar
65
+ const displayName = name.replace(/^@/, "").replace(/\//g, "-");
66
+ // 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.
69
+ const ociUrl = `${OCI_REGISTRY_PREFIX}/${displayName}:pr_${prNumber}__${version}!${displayName}`;
70
+ ociUrls.set(displayName, ociUrl);
71
+ console.log(`[PluginMetadata] ${displayName} -> ${ociUrl}`);
72
+ }
73
+ return ociUrls;
74
+ }
75
+ /**
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.
100
+ *
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
108
+ */
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
+ /**
118
+ * Default metadata directory path relative to the e2e-tests directory.
119
+ * Follows the same pattern as user config paths (e.g., tests/config/dynamic-plugins.yaml).
120
+ */
121
+ export const DEFAULT_METADATA_PATH = "../metadata";
122
+ /**
123
+ * Gets the metadata directory path.
124
+ * Uses the provided path or falls back to DEFAULT_METADATA_PATH.
125
+ *
126
+ * @param metadataPath Optional custom path to metadata directory
127
+ * @returns The metadata directory path, or null if it doesn't exist
128
+ */
129
+ export function getMetadataDirectory(metadataPath = DEFAULT_METADATA_PATH) {
130
+ const resolvedPath = path.resolve(metadataPath);
131
+ if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
132
+ console.log(`[PluginMetadata] Using metadata directory: ${resolvedPath}`);
133
+ return resolvedPath;
134
+ }
135
+ console.log(`[PluginMetadata] Metadata directory not found: ${resolvedPath}`);
136
+ return null;
137
+ }
138
+ /**
139
+ * Parses a single metadata YAML file and extracts plugin configuration.
140
+ *
141
+ * @param filePath Path to the metadata YAML file
142
+ * @returns PluginMetadata if valid, null otherwise
143
+ */
144
+ export async function parseMetadataFile(filePath) {
145
+ const content = await fs.readFile(filePath, "utf8");
146
+ const parsed = yaml.load(content);
147
+ const packagePath = parsed?.spec?.dynamicArtifact;
148
+ const packageName = parsed?.spec?.packageName;
149
+ const pluginConfig = parsed?.spec?.appConfigExamples?.[0]?.content;
150
+ if (!packagePath) {
151
+ throw new Error(`[PluginMetadata] Missing required field spec.dynamicArtifact in ${filePath}`);
152
+ }
153
+ if (!packageName) {
154
+ throw new Error(`[PluginMetadata] Missing required field spec.packageName in ${filePath}`);
155
+ }
156
+ return {
157
+ packagePath,
158
+ pluginConfig: pluginConfig || {},
159
+ packageName,
160
+ sourceFile: filePath,
161
+ };
162
+ }
163
+ /**
164
+ * Parses all metadata files in a directory and builds a map of plugin name to config.
165
+ * The plugin name is extracted from the dynamicArtifact path for flexible matching.
166
+ *
167
+ * @param metadataDir Path to the metadata directory
168
+ * @returns Map of plugin name to plugin configuration
169
+ */
170
+ export async function parseAllMetadataFiles(metadataDir) {
171
+ const pattern = path.join(metadataDir, "*.yaml");
172
+ const files = await glob(pattern);
173
+ console.log(`[PluginMetadata] Found ${files.length} metadata files in ${metadataDir}`);
174
+ const metadataMap = new Map();
175
+ for (const file of files) {
176
+ const metadata = await parseMetadataFile(file);
177
+ const pluginName = extractPluginName(metadata.packagePath);
178
+ metadataMap.set(pluginName, metadata);
179
+ console.log(`[PluginMetadata] Mapped plugin: ${pluginName} <- ${metadata.packagePath}`);
180
+ }
181
+ console.log(`[PluginMetadata] Successfully parsed ${metadataMap.size} plugin metadata entries`);
182
+ return metadataMap;
183
+ }
184
+ /**
185
+ * Replaces local package paths with OCI URLs for plugins that have matching metadata.
186
+ * Only applies to plugins with metadata (workspace plugins). Plugins without metadata
187
+ * (e.g., keycloak, bulk-import from auth defaults) keep their original paths.
188
+ *
189
+ * @param plugins The plugin entries to process
190
+ * @param metadataMap Map of plugin names to plugin metadata
191
+ * @param metadataPath Path to the metadata directory (used to resolve workspace root)
192
+ * @returns Plugin entries with OCI URLs replaced where applicable
193
+ */
194
+ async function replaceWithOCIUrls(plugins, metadataMap, metadataPath) {
195
+ const prNumber = process.env.GIT_PR_NUMBER;
196
+ if (!prNumber) {
197
+ return plugins;
198
+ }
199
+ console.log(`[PluginMetadata] PR build detected (PR #${prNumber}), fetching OCI URLs...`);
200
+ const workspacePath = path.resolve(metadataPath, "..");
201
+ const ociUrls = await getOCIUrlsForPR(workspacePath, prNumber);
202
+ return plugins.map((plugin) => {
203
+ const pluginName = extractPluginName(plugin.package);
204
+ const metadata = metadataMap.get(pluginName);
205
+ if (!metadata?.packageName)
206
+ return plugin;
207
+ const displayName = metadata.packageName
208
+ .replace(/^@/, "")
209
+ .replace(/\//g, "-");
210
+ const ociUrl = ociUrls.get(displayName);
211
+ if (!ociUrl)
212
+ return plugin;
213
+ console.log(`[PluginMetadata] Replacing ${plugin.package} with ${ociUrl}`);
214
+ return { ...plugin, package: ociUrl };
215
+ });
216
+ }
217
+ /**
218
+ * Injects plugin configurations from metadata into a dynamic plugins config.
219
+ * Metadata config serves as the base, user-provided pluginConfig overrides it.
220
+ *
221
+ * Matching is done by extracting the plugin name from both the package reference
222
+ * and the metadata's dynamicArtifact, allowing flexible matching across different
223
+ * package formats (local paths, OCI references, etc.).
224
+ *
225
+ * @param dynamicPluginsConfig The dynamic plugins configuration to augment
226
+ * @param metadataMap Map of plugin names to plugin metadata
227
+ * @returns The augmented configuration with injected pluginConfigs
228
+ */
229
+ export function injectMetadataConfig(dynamicPluginsConfig, metadataMap) {
230
+ if (!dynamicPluginsConfig.plugins) {
231
+ return dynamicPluginsConfig;
232
+ }
233
+ const augmentedPlugins = dynamicPluginsConfig.plugins.map((plugin) => {
234
+ // Extract plugin name from package reference for flexible matching
235
+ const pluginName = extractPluginName(plugin.package);
236
+ const metadata = metadataMap.get(pluginName);
237
+ if (!metadata) {
238
+ // No metadata found for this plugin, keep as-is
239
+ console.log(`[PluginMetadata] No metadata found for: ${pluginName} (from ${plugin.package})`);
240
+ return plugin;
241
+ }
242
+ console.log(`[PluginMetadata] Injecting config for: ${pluginName} (from ${plugin.package})`);
243
+ // Merge: metadata config (base) + user config (override)
244
+ const mergedPluginConfig = deepMerge(metadata.pluginConfig, plugin.pluginConfig || {});
245
+ return {
246
+ ...plugin,
247
+ pluginConfig: mergedPluginConfig,
248
+ };
249
+ });
250
+ return {
251
+ ...dynamicPluginsConfig,
252
+ plugins: augmentedPlugins,
253
+ };
254
+ }
255
+ /**
256
+ * Generates a complete dynamic-plugins configuration from metadata files.
257
+ * Iterates through all metadata files and creates plugin entries with:
258
+ * - package: the dynamicArtifact path
259
+ * - disabled: false (enabled by default)
260
+ * - pluginConfig: from appConfigExamples[0].content
261
+ *
262
+ * @param metadataPath Optional custom path to metadata directory (default: ../metadata)
263
+ * @returns Complete dynamic plugins configuration
264
+ * @throws Error if metadata directory not found or no valid metadata files
265
+ */
266
+ export async function generateDynamicPluginsConfigFromMetadata(metadataPath = DEFAULT_METADATA_PATH) {
267
+ // Skip if metadata handling is disabled
268
+ if (!shouldInjectPluginMetadata()) {
269
+ console.log("[PluginMetadata] Returning empty config (metadata handling disabled)");
270
+ return { plugins: [] };
271
+ }
272
+ console.log("[PluginMetadata] No dynamic-plugins config provided, generating from metadata...");
273
+ // Get metadata directory
274
+ const metadataDir = getMetadataDirectory(metadataPath);
275
+ if (!metadataDir) {
276
+ throw new Error(`[PluginMetadata] Cannot generate dynamic-plugins config: metadata directory not found at: ${path.resolve(metadataPath)}`);
277
+ }
278
+ // Parse all metadata files
279
+ const metadataMap = await parseAllMetadataFiles(metadataDir);
280
+ if (metadataMap.size === 0) {
281
+ throw new Error(`[PluginMetadata] Cannot generate dynamic-plugins config: no valid metadata files found in ${metadataDir}`);
282
+ }
283
+ // Build plugin entries from metadata
284
+ let plugins = [];
285
+ for (const [pluginName, metadata] of metadataMap) {
286
+ console.log(`[PluginMetadata] Adding plugin: ${pluginName} (${metadata.packagePath})`);
287
+ plugins.push({
288
+ package: metadata.packagePath,
289
+ disabled: false,
290
+ pluginConfig: metadata.pluginConfig,
291
+ });
292
+ }
293
+ // Replace local paths with OCI URLs for PR builds
294
+ plugins = await replaceWithOCIUrls(plugins, metadataMap, metadataPath);
295
+ console.log(`[PluginMetadata] Generated dynamic-plugins config with ${plugins.length} plugins`);
296
+ return { plugins };
297
+ }
298
+ /**
299
+ * Main function to load and inject plugin metadata for PR builds.
300
+ * For non-PR builds (nightly), returns the config unchanged.
301
+ *
302
+ * @param dynamicPluginsConfig The dynamic plugins configuration
303
+ * @param metadataPath Optional custom path to metadata directory (default: ../metadata)
304
+ * @returns Augmented configuration with metadata (for PR) or unchanged (for nightly)
305
+ * @throws Error if PR build but no metadata directory found
306
+ */
307
+ export async function loadAndInjectPluginMetadata(dynamicPluginsConfig, metadataPath = DEFAULT_METADATA_PATH) {
308
+ // Skip metadata injection if disabled
309
+ if (!shouldInjectPluginMetadata()) {
310
+ return dynamicPluginsConfig;
311
+ }
312
+ console.log("[PluginMetadata] Loading plugin metadata...");
313
+ // Get metadata directory
314
+ const metadataDir = getMetadataDirectory(metadataPath);
315
+ if (!metadataDir) {
316
+ throw new Error(`[PluginMetadata] PR build requires metadata directory but not found at: ${path.resolve(metadataPath)}`);
317
+ }
318
+ // Parse all metadata files
319
+ const metadataMap = await parseAllMetadataFiles(metadataDir);
320
+ if (metadataMap.size === 0) {
321
+ throw new Error(`[PluginMetadata] PR build requires plugin metadata but no valid metadata files found in ${metadataDir}`);
322
+ }
323
+ // Inject metadata configs into the dynamic plugins config
324
+ const result = injectMetadataConfig(dynamicPluginsConfig, metadataMap);
325
+ // Replace local paths with OCI URLs for PR builds
326
+ if (result.plugins) {
327
+ result.plugins = await replaceWithOCIUrls(result.plugins, metadataMap, metadataPath);
328
+ }
329
+ return result;
330
+ }
package/package.json ADDED
@@ -0,0 +1,108 @@
1
+ {
2
+ "name": "@red-hat-developer-hub/e2e-test-utils",
3
+ "version": "1.1.10",
4
+ "description": "Test utilities for RHDH E2E tests",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/redhat-developer/rhdh-e2e-test-utils.git",
9
+ "directory": "."
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ "./eslint": {
16
+ "types": "./dist/eslint/base.config.d.ts",
17
+ "default": "./dist/eslint/base.config.js"
18
+ },
19
+ "./test": {
20
+ "types": "./dist/playwright/fixtures/test.d.ts",
21
+ "default": "./dist/playwright/fixtures/test.js"
22
+ },
23
+ "./playwright-config": {
24
+ "types": "./dist/playwright/base-config.d.ts",
25
+ "default": "./dist/playwright/base-config.js"
26
+ },
27
+ "./tsconfig": "./tsconfig.base.json",
28
+ "./rhdh": {
29
+ "types": "./dist/deployment/rhdh/index.d.ts",
30
+ "default": "./dist/deployment/rhdh/index.js"
31
+ },
32
+ "./utils": {
33
+ "types": "./dist/utils/index.d.ts",
34
+ "default": "./dist/utils/index.js"
35
+ },
36
+ "./helpers": {
37
+ "types": "./dist/playwright/helpers/index.d.ts",
38
+ "default": "./dist/playwright/helpers/index.js"
39
+ },
40
+ "./pages": {
41
+ "types": "./dist/playwright/pages/index.d.ts",
42
+ "default": "./dist/playwright/pages/index.js"
43
+ },
44
+ "./keycloak": {
45
+ "types": "./dist/deployment/keycloak/index.d.ts",
46
+ "default": "./dist/deployment/keycloak/index.js"
47
+ }
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "tsconfig.base.json"
55
+ ],
56
+ "scripts": {
57
+ "build": "yarn clean && tsc -p tsconfig.build.json && cp -r src/deployment/rhdh/config dist/deployment/rhdh/ && cp -r src/deployment/keycloak/config dist/deployment/keycloak/",
58
+ "check": "yarn typecheck && yarn lint:check && yarn prettier:check",
59
+ "clean": "rm -rf dist",
60
+ "lint:check": "eslint . --ignore-pattern dist --ignore-pattern README.md --ignore-pattern docs",
61
+ "lint:fix": "eslint . --fix --ignore-pattern dist --ignore-pattern README.md --ignore-pattern docs",
62
+ "prepublishOnly": "yarn build",
63
+ "prettier:check": "prettier --check . '!dist' '!README.md' '!docs' '!.github/workflows/deploy-docs.yml'",
64
+ "prettier:fix": "prettier --write . '!dist' '!README.md' '!docs' '!.github/workflows/deploy-docs.yml'",
65
+ "typecheck": "tsc --noEmit"
66
+ },
67
+ "keywords": [
68
+ "e2e",
69
+ "playwright",
70
+ "rhdh",
71
+ "test-utils"
72
+ ],
73
+ "engines": {
74
+ "node": ">=22",
75
+ "yarn": ">=3"
76
+ },
77
+ "peerDependencies": {
78
+ "@playwright/test": "^1.57.0"
79
+ },
80
+ "devDependencies": {
81
+ "@backstage/catalog-model": "1.7.5",
82
+ "@playwright/test": "^1.57.0",
83
+ "@types/fs-extra": "^11.0.4",
84
+ "@types/js-yaml": "^4.0.9",
85
+ "@types/lodash.clonedeepwith": "^4.5.9",
86
+ "@types/lodash.mergewith": "^4.6.9",
87
+ "@types/node": "^24.10.1"
88
+ },
89
+ "dependencies": {
90
+ "@axe-core/playwright": "^4.11.0",
91
+ "@eslint/js": "^9.39.1",
92
+ "@keycloak/keycloak-admin-client": "^26.0.0",
93
+ "@kubernetes/client-node": "^1.4.0",
94
+ "eslint": "^9.39.1",
95
+ "eslint-plugin-check-file": "^3.3.1",
96
+ "eslint-plugin-playwright": "^2.4.0",
97
+ "fs-extra": "^11.3.2",
98
+ "js-yaml": "^4.1.1",
99
+ "lodash.clonedeepwith": "^4.5.0",
100
+ "lodash.mergewith": "^4.6.2",
101
+ "otplib": "12.0.1",
102
+ "prettier": "^3.7.4",
103
+ "typescript": "^5.9.3",
104
+ "typescript-eslint": "^8.48.1",
105
+ "zx": "^8.8.5"
106
+ },
107
+ "packageManager": "yarn@3.8.7"
108
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "noEmit": true,
13
+ "types": ["node"]
14
+ },
15
+ "exclude": ["node_modules", "test-results", "playwright-report"]
16
+ }