@red-hat-developer-hub/e2e-test-utils 1.1.23 → 1.1.25
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.
- package/dist/deployment/rhdh/deployment.d.ts +18 -1
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +135 -51
- package/dist/playwright/fixtures/test.d.ts.map +1 -1
- package/dist/playwright/fixtures/test.js +8 -0
- package/dist/playwright/helpers/ui-helper.d.ts +7 -0
- package/dist/playwright/helpers/ui-helper.d.ts.map +1 -1
- package/dist/playwright/helpers/ui-helper.js +16 -0
- package/dist/playwright/teardown-reporter.d.ts +17 -5
- package/dist/playwright/teardown-reporter.d.ts.map +1 -1
- package/dist/playwright/teardown-reporter.js +55 -14
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/kubernetes-client.d.ts.map +1 -1
- package/dist/utils/kubernetes-client.js +0 -2
- package/dist/utils/plugin-metadata.d.ts +52 -96
- package/dist/utils/plugin-metadata.d.ts.map +1 -1
- package/dist/utils/plugin-metadata.js +201 -211
- package/dist/utils/tests/helpers.d.ts +26 -0
- package/dist/utils/tests/helpers.d.ts.map +1 -0
- package/dist/utils/tests/helpers.js +84 -0
- package/dist/utils/tests/plugin-metadata.fixtures.test.d.ts +2 -0
- package/dist/utils/tests/plugin-metadata.fixtures.test.d.ts.map +1 -0
- package/dist/utils/tests/plugin-metadata.fixtures.test.js +563 -0
- package/dist/utils/tests/plugin-metadata.nightly.test.d.ts +2 -0
- package/dist/utils/tests/plugin-metadata.nightly.test.d.ts.map +1 -0
- package/dist/utils/tests/plugin-metadata.nightly.test.js +178 -0
- package/dist/utils/tests/plugin-metadata.pr.test.d.ts +2 -0
- package/dist/utils/tests/plugin-metadata.pr.test.d.ts.map +1 -0
- package/dist/utils/tests/plugin-metadata.pr.test.js +296 -0
- package/dist/utils/tests/plugin-metadata.test.d.ts.map +1 -0
- package/dist/utils/tests/plugin-metadata.test.js +156 -0
- package/dist/utils/workspace-paths.d.ts +43 -0
- package/dist/utils/workspace-paths.d.ts.map +1 -0
- package/dist/utils/workspace-paths.js +65 -0
- package/package.json +1 -1
- package/dist/utils/plugin-metadata.test.d.ts.map +0 -1
- package/dist/utils/plugin-metadata.test.js +0 -39
- /package/dist/utils/{plugin-metadata.test.d.ts → tests/plugin-metadata.test.d.ts} +0 -0
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
222
|
+
const pkg = plugin.package;
|
|
223
|
+
const pluginName = extractPluginName(pkg);
|
|
220
224
|
const metadata = metadataMap.get(pluginName);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
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
|
|
300
|
-
* @returns
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
337
|
-
*
|
|
324
|
+
* Processes a dynamic plugins configuration for deployment.
|
|
325
|
+
* Single entry point for both PR and nightly flows.
|
|
338
326
|
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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,26 @@
|
|
|
1
|
+
/** Saves and restores process.env around each test. */
|
|
2
|
+
export declare function withCleanEnv(): {
|
|
3
|
+
save(): void;
|
|
4
|
+
restore(): void;
|
|
5
|
+
};
|
|
6
|
+
/** Creates a temporary metadata directory with Package CRD YAML files. */
|
|
7
|
+
export declare function createMetadataFixture(plugins: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
packageName: string;
|
|
10
|
+
dynamicArtifact: string;
|
|
11
|
+
appConfigExamples?: Record<string, unknown>;
|
|
12
|
+
}>): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a workspace-like directory structure with metadata, source.json,
|
|
15
|
+
* and plugins-list.yaml. Used for tests that trigger PR OCI URL fetching.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createWorkspaceFixture(plugins: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
packageName: string;
|
|
20
|
+
dynamicArtifact: string;
|
|
21
|
+
appConfigExamples?: Record<string, unknown>;
|
|
22
|
+
}>): Promise<{
|
|
23
|
+
wsDir: string;
|
|
24
|
+
metadataDir: string;
|
|
25
|
+
}>;
|
|
26
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/tests/helpers.ts"],"names":[],"mappings":"AAQA,uDAAuD;AACvD,wBAAgB,YAAY;;;EAa3B;AAED,0EAA0E;AAC1E,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C,CAAC,GACD,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C,CAAC,GACD,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCjD"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test helpers for plugin-metadata tests.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
/** Saves and restores process.env around each test. */
|
|
9
|
+
export function withCleanEnv() {
|
|
10
|
+
let savedEnv;
|
|
11
|
+
return {
|
|
12
|
+
save() {
|
|
13
|
+
savedEnv = { ...process.env };
|
|
14
|
+
},
|
|
15
|
+
restore() {
|
|
16
|
+
for (const key of Object.keys(process.env)) {
|
|
17
|
+
if (!(key in savedEnv))
|
|
18
|
+
delete process.env[key];
|
|
19
|
+
}
|
|
20
|
+
Object.assign(process.env, savedEnv);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Creates a temporary metadata directory with Package CRD YAML files. */
|
|
25
|
+
export async function createMetadataFixture(plugins) {
|
|
26
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "metadata-test-"));
|
|
27
|
+
for (const plugin of plugins) {
|
|
28
|
+
const content = {
|
|
29
|
+
apiVersion: "extensions.backstage.io/v1alpha1",
|
|
30
|
+
kind: "Package",
|
|
31
|
+
metadata: { name: plugin.name },
|
|
32
|
+
spec: {
|
|
33
|
+
packageName: plugin.packageName,
|
|
34
|
+
dynamicArtifact: plugin.dynamicArtifact,
|
|
35
|
+
...(plugin.appConfigExamples
|
|
36
|
+
? {
|
|
37
|
+
appConfigExamples: [
|
|
38
|
+
{ title: "Default", content: plugin.appConfigExamples },
|
|
39
|
+
],
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
await fs.writeFile(path.join(tmpDir, `${plugin.name}.yaml`), yaml.dump(content));
|
|
45
|
+
}
|
|
46
|
+
return tmpDir;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates a workspace-like directory structure with metadata, source.json,
|
|
50
|
+
* and plugins-list.yaml. Used for tests that trigger PR OCI URL fetching.
|
|
51
|
+
*/
|
|
52
|
+
export async function createWorkspaceFixture(plugins) {
|
|
53
|
+
const wsDir = await fs.mkdtemp(path.join(os.tmpdir(), "workspace-test-"));
|
|
54
|
+
const metadataDir = path.join(wsDir, "metadata");
|
|
55
|
+
await fs.mkdir(metadataDir);
|
|
56
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
57
|
+
await fs.writeFile(path.join(wsDir, "source.json"), JSON.stringify({
|
|
58
|
+
repo: "https://github.com/test/repo",
|
|
59
|
+
"repo-ref": "main",
|
|
60
|
+
"repo-flat": false,
|
|
61
|
+
}));
|
|
62
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
63
|
+
await fs.writeFile(path.join(wsDir, "plugins-list.yaml"), "{}");
|
|
64
|
+
for (const plugin of plugins) {
|
|
65
|
+
const content = {
|
|
66
|
+
apiVersion: "extensions.backstage.io/v1alpha1",
|
|
67
|
+
kind: "Package",
|
|
68
|
+
metadata: { name: plugin.name },
|
|
69
|
+
spec: {
|
|
70
|
+
packageName: plugin.packageName,
|
|
71
|
+
dynamicArtifact: plugin.dynamicArtifact,
|
|
72
|
+
...(plugin.appConfigExamples
|
|
73
|
+
? {
|
|
74
|
+
appConfigExamples: [
|
|
75
|
+
{ title: "Default", content: plugin.appConfigExamples },
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
await fs.writeFile(path.join(metadataDir, `${plugin.name}.yaml`), yaml.dump(content));
|
|
82
|
+
}
|
|
83
|
+
return { wsDir, metadataDir };
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-metadata.fixtures.test.d.ts","sourceRoot":"","sources":["../../../src/utils/tests/plugin-metadata.fixtures.test.ts"],"names":[],"mappings":""}
|