@red-hat-developer-hub/e2e-test-utils 1.1.39 → 1.1.41

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.
@@ -0,0 +1,5 @@
1
+ plugins:
2
+ - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-app-auth:bs_1.49.4__0.0.1
3
+ disabled: false
4
+ - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-app-integrations:bs_1.49.4__0.0.1
5
+ disabled: false
@@ -0,0 +1,8 @@
1
+ apiVersion: v1
2
+ kind: Secret
3
+ metadata:
4
+ name: rhdh-secrets
5
+ type: Opaque
6
+ stringData:
7
+ APP_CONFIG_app_packageName: app-next
8
+ ENABLE_STANDARD_MODULE_FEDERATION: "true"
@@ -0,0 +1,2 @@
1
+ # Helm values merge layer for the new frontend system (app-next); merged when NFS layers are active (additive; extend as needed).
2
+ {}
@@ -4,6 +4,12 @@ export declare const DEFAULT_CONFIG_PATHS: {
4
4
  appConfig: string;
5
5
  secrets: string;
6
6
  dynamicPlugins: string;
7
+ /** New frontend system (app-next): paths merged when the deployment uses NFS layers (explicit flag, `-app-next` namespace, or `USE_NEW_FRONTEND_SYSTEM`). */
8
+ newFrontendSystem: {
9
+ secrets: string;
10
+ dynamicPlugins: string;
11
+ valueFile: string;
12
+ };
7
13
  helm: {
8
14
  valueFile: string;
9
15
  };
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/constants.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAK1D,eAAO,MAAM,oBAAoB;;;;;;;;;;CAyBhC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,MAAM,CACpC,YAAY,EACZ;IACE,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,YAAY,CAAC;CAC9B,CAoCF,CAAC;AAEF,eAAO,MAAM,SAAS,6BAA6B,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/constants.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAK1D,eAAO,MAAM,oBAAoB;;;;IAa/B,6JAA6J;;;;;;;;;;;;CA2B9J,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,MAAM,CACpC,YAAY,EACZ;IACE,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,YAAY,CAAC;CAC9B,CAoCF,CAAC;AAEF,eAAO,MAAM,SAAS,6BAA6B,CAAC"}
@@ -5,6 +5,12 @@ export const DEFAULT_CONFIG_PATHS = {
5
5
  appConfig: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/common/app-config-rhdh.yaml"),
6
6
  secrets: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/common/rhdh-secrets.yaml"),
7
7
  dynamicPlugins: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/common/dynamic-plugins.yaml"),
8
+ /** New frontend system (app-next): paths merged when the deployment uses NFS layers (explicit flag, `-app-next` namespace, or `USE_NEW_FRONTEND_SYSTEM`). */
9
+ newFrontendSystem: {
10
+ secrets: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/new-frontend-system/secrets.yaml"),
11
+ dynamicPlugins: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/new-frontend-system/dynamic-plugins.yaml"),
12
+ valueFile: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/new-frontend-system/value_file.yaml"),
13
+ },
8
14
  helm: {
9
15
  valueFile: path.join(PACKAGE_ROOT, "dist/deployment/rhdh/config/helm/value_file.yaml"),
10
16
  },
@@ -13,8 +13,8 @@ export declare class RHDHDeployment {
13
13
  /** Shared merge strategy for dynamic plugin arrays. */
14
14
  private static readonly pluginMergeOpts;
15
15
  /**
16
- * Merges package defaults + auth config (+ optional user config) into a
17
- * single dynamic plugins configuration.
16
+ * Merges package defaults + auth + optional new-frontend-system defaults +
17
+ * optional user config into a single dynamic plugins configuration.
18
18
  */
19
19
  private _mergeBaseConfigs;
20
20
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAwB1E,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAClB,SAAS,yBAAgC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;gBAE9B,SAAS,EAAE,MAAM;IAKvB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YA0CpD,eAAe;YAmBf,aAAa;IAqB3B,uDAAuD;IACvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAE5B;IAEX;;;OAGG;YACW,iBAAiB;IAY/B;;OAEG;YACW,uBAAuB;IAgBrC;;;;;;;;OAQG;YACW,0BAA0B;YAsC1B,oBAAoB;YAWpB,eAAe;YA6Df,mBAAmB;IAoE3B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAWrC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC,cAAc,CAAC,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BpD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,iBAAiB;YASjB,oBAAoB;IAsClC;;;OAGG;YACW,0BAA0B;IAwCxC,OAAO,CAAC,sBAAsB;IAoCxB,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAMlB"}
1
+ {"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAwB1E,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAClB,SAAS,yBAAgC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;gBAE9B,SAAS,EAAE,MAAM;IAKvB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YA0CpD,eAAe;YAmBf,aAAa;IA2B3B,uDAAuD;IACvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAE5B;IAEX;;;OAGG;YACW,iBAAiB;IAe/B;;OAEG;YACW,uBAAuB;IAgBrC;;;;;;;;OAQG;YACW,0BAA0B;YAsC1B,oBAAoB;YAWpB,eAAe;YAuEf,mBAAmB;IAoE3B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAWrC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC,cAAc,CAAC,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BpD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,iBAAiB;YASjB,oBAAoB;IAsClC;;;OAGG;YACW,0BAA0B;IAwCxC,OAAO,CAAC,sBAAsB;IA2CxB,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAMlB"}
@@ -62,32 +62,40 @@ export class RHDHDeployment {
62
62
  }
63
63
  async _applySecrets() {
64
64
  const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth];
65
- const secretsYaml = await mergeYamlFilesIfExists([
65
+ const secretsPaths = [
66
66
  DEFAULT_CONFIG_PATHS.secrets,
67
67
  authConfig.secrets,
68
+ ...(this.deploymentConfig.useNewFrontendSystem
69
+ ? [DEFAULT_CONFIG_PATHS.newFrontendSystem.secrets]
70
+ : []),
68
71
  this.deploymentConfig.secrets,
69
- ]);
72
+ ];
73
+ const secretsYaml = await mergeYamlFilesIfExists(secretsPaths);
70
74
  // Use cloneDeepWith to substitute env vars in-place, avoiding JSON.parse issues
71
75
  // with control characters in secrets (e.g., private keys with newlines)
72
76
  const substituted = cloneDeepWith(secretsYaml, (value) => {
73
77
  if (typeof value === "string")
74
78
  return envsubst(value);
75
79
  });
76
- await this.k8sClient.applySecretFromObject("rhdh-secrets", substituted, this.deploymentConfig.namespace);
80
+ const secretPayload = substituted;
81
+ await this.k8sClient.applySecretFromObject("rhdh-secrets", secretPayload, this.deploymentConfig.namespace);
77
82
  }
78
83
  /** Shared merge strategy for dynamic plugin arrays. */
79
84
  static pluginMergeOpts = {
80
85
  arrayMergeStrategy: { byKey: "package" },
81
86
  };
82
87
  /**
83
- * Merges package defaults + auth config (+ optional user config) into a
84
- * single dynamic plugins configuration.
88
+ * Merges package defaults + auth + optional new-frontend-system defaults +
89
+ * optional user config into a single dynamic plugins configuration.
85
90
  */
86
91
  async _mergeBaseConfigs(userConfigPath) {
87
92
  const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth];
88
93
  const paths = [
89
94
  DEFAULT_CONFIG_PATHS.dynamicPlugins,
90
95
  authConfig.dynamicPlugins,
96
+ ...(this.deploymentConfig.useNewFrontendSystem
97
+ ? [DEFAULT_CONFIG_PATHS.newFrontendSystem.dynamicPlugins]
98
+ : []),
91
99
  ...(userConfigPath ? [userConfigPath] : []),
92
100
  ];
93
101
  return await mergeYamlFilesIfExists(paths, RHDHDeployment.pluginMergeOpts);
@@ -148,10 +156,18 @@ export class RHDHDeployment {
148
156
  async _deployWithHelm(valueFile) {
149
157
  const chartVersion = await this._resolveChartVersion(this.deploymentConfig.version);
150
158
  this._log(`Helm chart version resolved to: ${chartVersion}`);
151
- const valueFileObject = (await mergeYamlFilesIfExists([
159
+ const helmValuePaths = [
152
160
  DEFAULT_CONFIG_PATHS.helm.valueFile,
161
+ ...(this.deploymentConfig.useNewFrontendSystem
162
+ ? [DEFAULT_CONFIG_PATHS.newFrontendSystem.valueFile]
163
+ : []),
153
164
  valueFile,
154
- ]));
165
+ ...(this.deploymentConfig.useNewFrontendSystem &&
166
+ fs.existsSync(WorkspacePaths.valueFileAppNext)
167
+ ? [WorkspacePaths.valueFileAppNext]
168
+ : []),
169
+ ];
170
+ const valueFileObject = (await mergeYamlFilesIfExists(helmValuePaths));
155
171
  this._logBoxen("Value File", valueFileObject);
156
172
  // Merge dynamic plugins into the values file (including auth-specific plugins)
157
173
  if (!valueFileObject.global) {
@@ -358,14 +374,19 @@ export class RHDHDeployment {
358
374
  const method = input.method ??
359
375
  process.env.INSTALLATION_METHOD ??
360
376
  "helm";
377
+ const namespace = input.namespace ?? this.deploymentConfig.namespace;
378
+ const useNewFrontendSystem = input.useNewFrontendSystem ??
379
+ (namespace.endsWith("-app-next") ||
380
+ process.env.USE_NEW_FRONTEND_SYSTEM === "true");
361
381
  const base = {
362
382
  version,
363
- namespace: input.namespace ?? this.deploymentConfig.namespace,
383
+ namespace,
364
384
  auth: input.auth ?? "keycloak",
365
385
  appConfig: input.appConfig ?? WorkspacePaths.appConfig,
366
386
  secrets: input.secrets ?? WorkspacePaths.secrets,
367
387
  dynamicPlugins: input.dynamicPlugins ?? WorkspacePaths.dynamicPlugins,
368
388
  disableWrappers: input.disableWrappers ?? [],
389
+ useNewFrontendSystem,
369
390
  };
370
391
  if (method === "helm") {
371
392
  return {
@@ -38,4 +38,54 @@ describe("dynamic-plugins merge (no user config path)", () => {
38
38
  assert.strictEqual(plugins.length, 1, "merged config must have exactly one keycloak plugin");
39
39
  assert.ok(plugins[0].package?.startsWith("oci://"), "metadata (OCI) must win over auth local path");
40
40
  });
41
+ it("keeps user app-auth OCI ref over NFS defaults for same logical plugin", () => {
42
+ const nfsDefaults = {
43
+ plugins: [
44
+ {
45
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-app-auth:bs_1.49.4__0.0.1",
46
+ disabled: false,
47
+ },
48
+ ],
49
+ };
50
+ const userPlugins = {
51
+ plugins: [
52
+ {
53
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-app-auth:older__0.0.1",
54
+ disabled: false,
55
+ },
56
+ ],
57
+ };
58
+ const merged = deepMerge(nfsDefaults, userPlugins, {
59
+ arrayMergeStrategy: {
60
+ byKey: "package",
61
+ normalizeKey: (item) => getNormalizedPluginMergeKey(item),
62
+ },
63
+ });
64
+ const plugins = merged.plugins;
65
+ assert.strictEqual(plugins.length, 1);
66
+ assert.ok(plugins[0].package?.includes("older__0.0.1"), "user layer must win over NFS defaults for the same logical plugin");
67
+ });
68
+ it("does not duplicate includes when NFS layer omits includes", () => {
69
+ const common = {
70
+ includes: ["dynamic-plugins.default.yaml"],
71
+ plugins: [],
72
+ };
73
+ const nfsLayer = {
74
+ plugins: [
75
+ {
76
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-app-auth:bs_1.49.4__0.0.1",
77
+ disabled: false,
78
+ },
79
+ ],
80
+ };
81
+ const merged = deepMerge(common, nfsLayer, {
82
+ arrayMergeStrategy: {
83
+ byKey: "package",
84
+ normalizeKey: (item) => getNormalizedPluginMergeKey(item),
85
+ },
86
+ });
87
+ const includes = merged.includes;
88
+ assert.strictEqual(includes?.length, 1);
89
+ assert.strictEqual(includes[0], "dynamic-plugins.default.yaml");
90
+ });
41
91
  });
@@ -1,2 +1,3 @@
1
1
  export { RHDHDeployment } from "./deployment.js";
2
+ export * from "./types.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,cAAc,YAAY,CAAC"}
@@ -1 +1,2 @@
1
1
  export { RHDHDeployment } from "./deployment.js";
2
+ export * from "./types.js";
@@ -11,6 +11,8 @@ export type DeploymentOptions = {
11
11
  valueFile?: string;
12
12
  subscription?: string;
13
13
  disableWrappers?: string[];
14
+ /** When true, merge new-frontend-system (app-next) layers. When omitted, auto-detect: namespace ends with `-app-next` or `USE_NEW_FRONTEND_SYSTEM=true`. Pass false to disable. */
15
+ useNewFrontendSystem?: boolean;
14
16
  };
15
17
  export type HelmDeploymentConfig = {
16
18
  method: "helm";
@@ -28,6 +30,8 @@ export type DeploymentConfigBase = {
28
30
  secrets: string;
29
31
  dynamicPlugins: string;
30
32
  disableWrappers: string[];
33
+ /** New frontend system (Backstage app-next / NFS shell). */
34
+ useNewFrontendSystem: boolean;
31
35
  };
32
36
  export type DeploymentConfig = DeploymentConfigBase & (HelmDeploymentConfig | OperatorDeploymentConfig);
33
37
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,UAAU,CAAC;AACnD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE3D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GACjD,CAAC,oBAAoB,GAAG,wBAAwB,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,UAAU,CAAC;AACnD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE3D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,mLAAmL;IACnL,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,4DAA4D;IAC5D,oBAAoB,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GACjD,CAAC,oBAAoB,GAAG,wBAAwB,CAAC,CAAC"}
@@ -26,6 +26,25 @@ export interface DynamicPluginsConfig {
26
26
  * - E2E_NIGHTLY_MODE is set (manual override for local testing)
27
27
  */
28
28
  export declare function isNightlyJob(): boolean;
29
+ /**
30
+ * Fetches the list of packages from default.packages.yaml (source for the
31
+ * dynamic-plugins.default.yaml — DPDY — in the catalog index image shipped
32
+ * with RHDH). Used in nightly mode to determine which plugins support
33
+ * {{inherit}} tag resolution vs which need full OCI refs from metadata.
34
+ *
35
+ * Branch is determined by RELEASE_BRANCH_NAME (set by OpenShift CI),
36
+ * defaulting to "main" for local development.
37
+ */
38
+ export declare function fetchDefaultPackages(): Promise<Set<string>>;
39
+ /**
40
+ * Resolves the OCI registry for a plugin's {{inherit}} ref in nightly mode.
41
+ *
42
+ * Resolution priority:
43
+ * 1. NIGHTLY_DPDY_OCI_REGISTRY_MAP — JSON object mapping registry → array of package names
44
+ * 2. NIGHTLY_DPDY_OCI_REGISTRY — blanket override for all plugins using {{inherit}}
45
+ * 3. Default: registry.access.redhat.com/rhdh
46
+ */
47
+ export declare function getDpdyRegistry(packageName: string): string;
29
48
  /**
30
49
  * Extracts the plugin name from a package path or OCI reference.
31
50
  * Strips the `-dynamic` suffix so local paths and OCI refs for the same
@@ -82,15 +101,20 @@ export declare function generatePluginsFromMetadata(metadataPath?: string): Prom
82
101
  * Single entry point for both PR and nightly flows.
83
102
  *
84
103
  * Operations (in order):
85
- * 1. Inject appConfigExamples from metadata (PR mode only, unless RHDH_SKIP_PLUGIN_METADATA_INJECTION is set)
86
- * 2. Resolve all packages to OCI references:
87
- * - PR with GIT_PR_NUMBER: workspace plugins in PR build → pr_ tags, rest unchanged
88
- * - PR without GIT_PR_NUMBER: OCI plugins with metadata → metadata refs, rest unchanged
89
- * - Nightly: OCI plugins with metadata → metadata refs, rest unchanged
104
+ * 1. Inject appConfigExamples from metadata:
105
+ * - PR/local: all plugins with metadata (unless RHDH_SKIP_PLUGIN_METADATA_INJECTION)
106
+ * - Nightly: only plugins NOT in default.packages.yaml with OCI metadata
107
+ * (plugins in default.packages.yaml get both config and OCI tag via {{inherit}})
108
+ * 2. Resolve all packages:
109
+ * - PR with GIT_PR_NUMBER: workspace plugins → pr_ OCI tags
110
+ * - Nightly DPDY + OCI: {{inherit}} tag with configurable registry
111
+ * (NIGHTLY_DPDY_OCI_REGISTRY_MAP > NIGHTLY_DPDY_OCI_REGISTRY > registry.access.redhat.com/rhdh)
112
+ * - Nightly (not in default.packages.yaml) / local: metadata's dynamicArtifact as-is
90
113
  *
91
114
  * @param config The merged dynamic plugins configuration
92
115
  * @param metadataPath Optional custom path to metadata directory
116
+ * @param dpdyPackages Optional pre-loaded DPDY package set (for testing; fetched automatically if omitted in nightly)
93
117
  * @returns Processed configuration ready for deployment
94
118
  */
95
- export declare function processPluginsForDeployment(config: DynamicPluginsConfig, metadataPath?: string): Promise<DynamicPluginsConfig>;
119
+ export declare function processPluginsForDeployment(config: DynamicPluginsConfig, metadataPath?: string, dpdyPackages?: Set<string>): Promise<DynamicPluginsConfig>;
96
120
  //# sourceMappingURL=plugin-metadata.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAaD,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,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;AAID;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAqBtC;AAID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI5D;AAYD,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD,wBAAgB,oBAAoB,CAClC,YAAY,GAAE,MAA8B,GAC3C,MAAM,GAAG,IAAI,CAQf;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CAyBzB;AAED,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwBtC;AA2JD;;;;;;;GAOG;AACH;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAMT;AA0GD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAW7E;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAwB/B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,oBAAoB,EAC5B,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CA6B/B"}
1
+ {"version":3,"file":"plugin-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAaD,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,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;AAID;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAqBtC;AAgBD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CA2CjE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAc3D;AAID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI5D;AAYD,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD,wBAAgB,oBAAoB,CAClC,YAAY,GAAE,MAA8B,GAC3C,MAAM,GAAG,IAAI,CAQf;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CAyBzB;AAED,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwBtC;AA2JD;;;;;;;GAOG;AACH;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAMT;AAoHD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAW7E;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAwB/B;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,oBAAoB,EAC5B,YAAY,GAAE,MAA8B,EAC5C,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAsC/B"}
@@ -32,6 +32,69 @@ export function isNightlyJob() {
32
32
  }
33
33
  return false;
34
34
  }
35
+ // ── Default Packages (DPDY) ──────────────────────────────────────────────────
36
+ const DEFAULT_PACKAGES_BASE_URL = "https://raw.githubusercontent.com/redhat-developer/rhdh/refs/heads";
37
+ const DEFAULT_DPDY_OCI_REGISTRY = "registry.access.redhat.com/rhdh";
38
+ /**
39
+ * Fetches the list of packages from default.packages.yaml (source for the
40
+ * dynamic-plugins.default.yaml — DPDY — in the catalog index image shipped
41
+ * with RHDH). Used in nightly mode to determine which plugins support
42
+ * {{inherit}} tag resolution vs which need full OCI refs from metadata.
43
+ *
44
+ * Branch is determined by RELEASE_BRANCH_NAME (set by OpenShift CI),
45
+ * defaulting to "main" for local development.
46
+ */
47
+ export async function fetchDefaultPackages() {
48
+ const branch = process.env.RELEASE_BRANCH_NAME;
49
+ if (!branch) {
50
+ if (process.env.CI) {
51
+ throw new Error("[PluginMetadata] RELEASE_BRANCH_NAME is required in CI to fetch default.packages.yaml");
52
+ }
53
+ console.log("[PluginMetadata] RELEASE_BRANCH_NAME not set — defaulting to 'main' (local dev)");
54
+ }
55
+ const resolvedBranch = branch || "main";
56
+ const url = `${DEFAULT_PACKAGES_BASE_URL}/${resolvedBranch}/default.packages.yaml`;
57
+ console.log(`[PluginMetadata] Fetching default packages from ${url} (branch: ${resolvedBranch})...`);
58
+ const response = await fetch(url);
59
+ if (!response.ok) {
60
+ throw new Error(`[PluginMetadata] Failed to fetch default.packages.yaml: ${response.status} ${response.statusText}\n` +
61
+ ` URL: ${url}\n` +
62
+ ` Branch: ${resolvedBranch} (from RELEASE_BRANCH_NAME)`);
63
+ }
64
+ const content = await response.text();
65
+ const parsed = yaml.load(content);
66
+ const packages = new Set();
67
+ for (const list of [parsed?.packages?.enabled, parsed?.packages?.disabled]) {
68
+ for (const entry of list || []) {
69
+ if (entry.package)
70
+ packages.add(entry.package);
71
+ }
72
+ }
73
+ console.log(`[PluginMetadata] Found ${packages.size} packages in default.packages.yaml (branch: ${resolvedBranch})`);
74
+ return packages;
75
+ }
76
+ /**
77
+ * Resolves the OCI registry for a plugin's {{inherit}} ref in nightly mode.
78
+ *
79
+ * Resolution priority:
80
+ * 1. NIGHTLY_DPDY_OCI_REGISTRY_MAP — JSON object mapping registry → array of package names
81
+ * 2. NIGHTLY_DPDY_OCI_REGISTRY — blanket override for all plugins using {{inherit}}
82
+ * 3. Default: registry.access.redhat.com/rhdh
83
+ */
84
+ export function getDpdyRegistry(packageName) {
85
+ const map = process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP;
86
+ if (map) {
87
+ const parsed = JSON.parse(map);
88
+ for (const [registry, packages] of Object.entries(parsed)) {
89
+ if (packages.includes(packageName))
90
+ return registry;
91
+ }
92
+ }
93
+ if (process.env.NIGHTLY_DPDY_OCI_REGISTRY) {
94
+ return process.env.NIGHTLY_DPDY_OCI_REGISTRY;
95
+ }
96
+ return DEFAULT_DPDY_OCI_REGISTRY;
97
+ }
35
98
  // ── Utilities ─────────────────────────────────────────────────────────────────
36
99
  /**
37
100
  * Extracts the plugin name from a package path or OCI reference.
@@ -211,7 +274,7 @@ export function getNormalizedPluginMergeKey(entry) {
211
274
  }
212
275
  return extractPluginName(pkg);
213
276
  }
214
- async function resolvePluginPackages(plugins, metadataMap, metadataPath) {
277
+ async function resolvePluginPackages(plugins, metadataMap, metadataPath, dpdyPackages = null) {
215
278
  // Build PR OCI URLs if applicable
216
279
  const prNumber = process.env.GIT_PR_NUMBER;
217
280
  let prOciUrls = null;
@@ -224,7 +287,7 @@ async function resolvePluginPackages(plugins, metadataMap, metadataPath) {
224
287
  const pkg = plugin.package;
225
288
  const pluginName = extractPluginName(pkg);
226
289
  const metadata = metadataMap.get(pluginName);
227
- // 1. With metadata: resolve to PR OCI URL or metadata's dynamicArtifact
290
+ // 1. With metadata: resolve to PR OCI URL, {{inherit}}, or metadata's dynamicArtifact
228
291
  if (metadata?.packageName) {
229
292
  const displayName = toDisplayName(metadata.packageName);
230
293
  // PR: use PR-specific OCI URL if this plugin is part of the PR build
@@ -235,9 +298,18 @@ async function resolvePluginPackages(plugins, metadataMap, metadataPath) {
235
298
  return { ...plugin, package: prUrl };
236
299
  }
237
300
  }
238
- // Use metadata's dynamicArtifact directly (latest published version).
239
- // This is more accurate than {{inherit}} because metadata is updated daily
240
- // while the DPDY in the catalog index may lag behind.
301
+ // Nightly: if the plugin is in default.packages.yaml and its metadata
302
+ // spec.dynamicArtifact is an OCI ref, use {{inherit}} — RHDH resolves
303
+ // both the OCI tag and default config from its built-in DPDY.
304
+ // Registry: getDpdyRegistry() (env var overrides > default RHEC).
305
+ if (dpdyPackages?.has(metadata.packageName) &&
306
+ metadata.packagePath.startsWith("oci://")) {
307
+ const registry = getDpdyRegistry(metadata.packageName);
308
+ const inheritRef = `oci://${registry}/${displayName}:{{inherit}}`;
309
+ console.log(`[PluginMetadata] DPDY inherit: ${pkg} → ${inheritRef}`);
310
+ return { ...plugin, package: inheritRef };
311
+ }
312
+ // OCI: use metadata's dynamicArtifact directly (not in default.packages.yaml, or not nightly).
241
313
  if (metadata.packagePath.startsWith("oci://")) {
242
314
  console.log(`[PluginMetadata] ${pkg} → ${metadata.packagePath}`);
243
315
  return { ...plugin, package: metadata.packagePath };
@@ -249,10 +321,7 @@ async function resolvePluginPackages(plugins, metadataMap, metadataPath) {
249
321
  }
250
322
  return { ...plugin, package: metadata.packagePath };
251
323
  }
252
- // 2. Local paths (./dynamic-plugins/dist/...) and other formats — keep as-is.
253
- // Local paths reference plugins bundled in the RHDH container image and work
254
- // without OCI resolution. When the catalog index moves all plugins to OCI refs,
255
- // they'll be handled by step 1 or 2 above automatically.
324
+ // 2. No metadata — keep as-is (cross-workspace, npm packages, etc.)
256
325
  return plugin;
257
326
  });
258
327
  }
@@ -327,38 +396,60 @@ export async function generatePluginsFromMetadata(metadataPath = DEFAULT_METADAT
327
396
  console.log(`[PluginMetadata] Generated ${plugins.length} plugin entries from metadata`);
328
397
  return { plugins };
329
398
  }
399
+ function selectMetadataForInjection(metadataMap, nightly, dpdyPackages) {
400
+ if (!process.env.CI &&
401
+ process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION === "true")
402
+ return null;
403
+ if (metadataMap.size === 0)
404
+ return null;
405
+ if (!nightly)
406
+ return metadataMap;
407
+ if (!dpdyPackages)
408
+ return null;
409
+ // Nightly: only inject config for plugins NOT in default.packages.yaml whose
410
+ // metadata is OCI. DPDY plugins get both config and OCI tag via {{inherit}}.
411
+ return new Map([...metadataMap].filter(([, m]) => !dpdyPackages.has(m.packageName) && m.packagePath.startsWith("oci://")));
412
+ }
330
413
  /**
331
414
  * Processes a dynamic plugins configuration for deployment.
332
415
  * Single entry point for both PR and nightly flows.
333
416
  *
334
417
  * Operations (in order):
335
- * 1. Inject appConfigExamples from metadata (PR mode only, unless RHDH_SKIP_PLUGIN_METADATA_INJECTION is set)
336
- * 2. Resolve all packages to OCI references:
337
- * - PR with GIT_PR_NUMBER: workspace plugins in PR build → pr_ tags, rest unchanged
338
- * - PR without GIT_PR_NUMBER: OCI plugins with metadata → metadata refs, rest unchanged
339
- * - Nightly: OCI plugins with metadata → metadata refs, rest unchanged
418
+ * 1. Inject appConfigExamples from metadata:
419
+ * - PR/local: all plugins with metadata (unless RHDH_SKIP_PLUGIN_METADATA_INJECTION)
420
+ * - Nightly: only plugins NOT in default.packages.yaml with OCI metadata
421
+ * (plugins in default.packages.yaml get both config and OCI tag via {{inherit}})
422
+ * 2. Resolve all packages:
423
+ * - PR with GIT_PR_NUMBER: workspace plugins → pr_ OCI tags
424
+ * - Nightly DPDY + OCI: {{inherit}} tag with configurable registry
425
+ * (NIGHTLY_DPDY_OCI_REGISTRY_MAP > NIGHTLY_DPDY_OCI_REGISTRY > registry.access.redhat.com/rhdh)
426
+ * - Nightly (not in default.packages.yaml) / local: metadata's dynamicArtifact as-is
340
427
  *
341
428
  * @param config The merged dynamic plugins configuration
342
429
  * @param metadataPath Optional custom path to metadata directory
430
+ * @param dpdyPackages Optional pre-loaded DPDY package set (for testing; fetched automatically if omitted in nightly)
343
431
  * @returns Processed configuration ready for deployment
344
432
  */
345
- export async function processPluginsForDeployment(config, metadataPath = DEFAULT_METADATA_PATH) {
433
+ export async function processPluginsForDeployment(config, metadataPath = DEFAULT_METADATA_PATH, dpdyPackages) {
346
434
  if (!config.plugins)
347
435
  return config;
348
- const metadataMap = await tryLoadMetadata(metadataPath);
436
+ const nightly = isNightlyJob();
437
+ const [metadataMap, resolvedDpdyPackages] = await Promise.all([
438
+ tryLoadMetadata(metadataPath),
439
+ nightly ? (dpdyPackages ?? fetchDefaultPackages()) : Promise.resolve(null),
440
+ ]);
349
441
  let result = { ...config };
350
- // Inject appConfigExamples from metadata (PR mode only)
351
- if (!isNightlyJob() &&
352
- process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION !== "true" &&
353
- metadataMap.size > 0) {
354
- console.log("[PluginMetadata] Injecting metadata configs...");
355
- result = injectMetadataConfig(result, metadataMap);
442
+ // Inject appConfigExamples from metadata
443
+ const metadataToInject = selectMetadataForInjection(metadataMap, nightly, resolvedDpdyPackages);
444
+ if (metadataToInject && metadataToInject.size > 0) {
445
+ console.log(`[PluginMetadata] Injecting metadata configs for ${metadataToInject.size} plugin(s)...`);
446
+ result = injectMetadataConfig(result, metadataToInject);
356
447
  }
357
448
  // Resolve all packages to OCI references
358
- console.log("[PluginMetadata] Resolving plugin packages to OCI...");
449
+ console.log("[PluginMetadata] Resolving plugin packages...");
359
450
  result = {
360
451
  ...result,
361
- plugins: await resolvePluginPackages(result.plugins, metadataMap, metadataPath),
452
+ plugins: await resolvePluginPackages(result.plugins, metadataMap, metadataPath, resolvedDpdyPackages),
362
453
  };
363
454
  return result;
364
455
  }
@@ -179,11 +179,12 @@ describe("processPluginsForDeployment — workspace fixtures", () => {
179
179
  },
180
180
  ],
181
181
  };
182
- const result = await processPluginsForDeployment(config, metadataDir);
182
+ // Empty DPDY set — these plugins are not in default.packages.yaml
183
+ const result = await processPluginsForDeployment(config, metadataDir, new Set());
183
184
  const plugins = result.plugins;
184
185
  assert.ok(plugins[0].package.startsWith("oci://quay.io/rhdh/"), "must use quay.io registry from metadata, not the ghcr.io from user config");
185
186
  assert.ok(plugins[0].package.includes("@sha256:c1d17d47aaa"), "must preserve digest from metadata");
186
- assert.deepStrictEqual(plugins[0].pluginConfig, { events: { http: { topics: ["github"] } } }, "nightly must preserve user pluginConfig without metadata injection");
187
+ assert.deepStrictEqual(plugins[0].pluginConfig, { events: { http: { topics: ["github"] } } }, "nightly must preserve user pluginConfig for non-DPDY OCI plugin");
187
188
  assert.strictEqual(plugins[1].package, "./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic", "local path from metadata must stay unchanged");
188
189
  assert.deepStrictEqual(plugins[1].pluginConfig, { catalog: { providers: { github: { org: "janus-qe" } } } }, "nightly must preserve user pluginConfig for local path plugin");
189
190
  }
@@ -234,9 +235,9 @@ describe("processPluginsForDeployment — workspace fixtures", () => {
234
235
  const prResult = await processPluginsForDeployment(config, metadataDir);
235
236
  assert.ok(prResult.plugins[0].pluginConfig, "PR mode must inject pluginConfig for topology");
236
237
  assert.strictEqual(prResult.plugins[0].package, "./dynamic-plugins/dist/backstage-community-plugin-topology", "local path must stay unchanged in PR mode");
237
- // Nightly mode
238
+ // Nightly mode (empty DPDY set to avoid network fetch in unit tests)
238
239
  process.env.E2E_NIGHTLY_MODE = "true";
239
- const nightlyResult = await processPluginsForDeployment({ ...config, plugins: config.plugins.map((p) => ({ ...p })) }, metadataDir);
240
+ const nightlyResult = await processPluginsForDeployment({ ...config, plugins: config.plugins.map((p) => ({ ...p })) }, metadataDir, new Set());
240
241
  assert.strictEqual(nightlyResult.plugins[0].pluginConfig, undefined, "nightly must not inject pluginConfig");
241
242
  for (const result of [prResult, nightlyResult]) {
242
243
  for (const plugin of result.plugins) {
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Nightly mode tests — isNightlyJob detection and nightly plugin resolution.
3
3
  */
4
+ /* eslint-disable @typescript-eslint/naming-convention -- test fixtures use real plugin config keys with dots/dashes */
4
5
  import { describe, it, beforeEach, afterEach } from "node:test";
5
6
  import assert from "node:assert";
6
7
  import fs from "fs-extra";
7
- import { isNightlyJob, processPluginsForDeployment, } from "../plugin-metadata.js";
8
+ import { isNightlyJob, processPluginsForDeployment, getDpdyRegistry, } from "../plugin-metadata.js";
8
9
  import { withCleanEnv, createMetadataFixture } from "./helpers.js";
9
10
  // ── isNightlyJob ─────────────────────────────────────────────────────────────
10
11
  describe("isNightlyJob", () => {
@@ -69,7 +70,7 @@ describe("processPluginsForDeployment — nightly mode", () => {
69
70
  process.env.E2E_NIGHTLY_MODE = "true";
70
71
  });
71
72
  afterEach(() => env.restore());
72
- it("skips metadata injection in nightly mode", async () => {
73
+ it("skips metadata injection for wrapper plugins in nightly mode", async () => {
73
74
  const metadataDir = await createMetadataFixture([
74
75
  {
75
76
  name: "backstage-community-plugin-tech-radar",
@@ -89,8 +90,8 @@ describe("processPluginsForDeployment — nightly mode", () => {
89
90
  },
90
91
  ],
91
92
  };
92
- const result = await processPluginsForDeployment(config, metadataDir);
93
- assert.strictEqual(result.plugins[0].pluginConfig, undefined, "nightly mode must NOT inject metadata pluginConfig");
93
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-tech-radar"]));
94
+ assert.strictEqual(result.plugins[0].pluginConfig, undefined, "nightly mode must NOT inject metadata pluginConfig for wrapper plugins");
94
95
  }
95
96
  finally {
96
97
  await fs.remove(metadataDir);
@@ -120,14 +121,14 @@ describe("processPluginsForDeployment — nightly mode", () => {
120
121
  },
121
122
  ],
122
123
  };
123
- const result = await processPluginsForDeployment(config, metadataDir);
124
+ const result = await processPluginsForDeployment(config, metadataDir, new Set());
124
125
  assert.deepStrictEqual(result.plugins[0].pluginConfig, userPluginConfig, "nightly mode must preserve user pluginConfig exactly as-is");
125
126
  }
126
127
  finally {
127
128
  await fs.remove(metadataDir);
128
129
  }
129
130
  });
130
- it("resolves OCI plugin to metadata dynamicArtifact in nightly", async () => {
131
+ it("resolves non-DPDY OCI plugin to metadata dynamicArtifact in nightly", async () => {
131
132
  const metadataDir = await createMetadataFixture([
132
133
  {
133
134
  name: "backstage-community-plugin-tekton",
@@ -144,8 +145,9 @@ describe("processPluginsForDeployment — nightly mode", () => {
144
145
  },
145
146
  ],
146
147
  };
147
- const result = await processPluginsForDeployment(config, metadataDir);
148
- assert.ok(result.plugins[0].package.includes("bs_1.45.3__3.33.3"), "nightly must resolve to metadata dynamicArtifact (latest published version)");
148
+ // Empty DPDY set — plugin is NOT in default.packages.yaml
149
+ const result = await processPluginsForDeployment(config, metadataDir, new Set());
150
+ assert.ok(result.plugins[0].package.includes("bs_1.45.3__3.33.3"), "non-DPDY OCI plugin must resolve to metadata dynamicArtifact");
149
151
  }
150
152
  finally {
151
153
  await fs.remove(metadataDir);
@@ -172,7 +174,7 @@ describe("processPluginsForDeployment — nightly mode", () => {
172
174
  },
173
175
  ],
174
176
  };
175
- const result = await processPluginsForDeployment(config, metadataDir);
177
+ const result = await processPluginsForDeployment(config, metadataDir, new Set());
176
178
  assert.strictEqual(result.plugins[0].package, "./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-org-dynamic", "when metadata has a wrapper path, nightly must resolve to wrapper — not pass through stale OCI ref from user config");
177
179
  }
178
180
  finally {
@@ -196,7 +198,7 @@ describe("processPluginsForDeployment — nightly mode", () => {
196
198
  },
197
199
  ],
198
200
  };
199
- const result = await processPluginsForDeployment(config, metadataDir);
201
+ const result = await processPluginsForDeployment(config, metadataDir, new Set());
200
202
  assert.strictEqual(result.plugins[0].package, "./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-quickstart", "local path plugins must not be converted to OCI in nightly");
201
203
  }
202
204
  finally {
@@ -204,3 +206,413 @@ describe("processPluginsForDeployment — nightly mode", () => {
204
206
  }
205
207
  });
206
208
  });
209
+ // ── {{inherit}} resolution (DPDY plugins) ──────────────────────────────────
210
+ describe("processPluginsForDeployment — nightly {{inherit}}", () => {
211
+ const env = withCleanEnv();
212
+ beforeEach(() => {
213
+ env.save();
214
+ delete process.env.GIT_PR_NUMBER;
215
+ process.env.E2E_NIGHTLY_MODE = "true";
216
+ });
217
+ afterEach(() => env.restore());
218
+ it("resolves DPDY OCI plugin to {{inherit}} tag with default RHEC registry", async () => {
219
+ const metadataDir = await createMetadataFixture([
220
+ {
221
+ name: "backstage-community-plugin-tekton",
222
+ packageName: "@backstage-community/plugin-tekton",
223
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton",
224
+ },
225
+ ]);
226
+ try {
227
+ const config = {
228
+ plugins: [
229
+ {
230
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old_tag",
231
+ disabled: false,
232
+ },
233
+ ],
234
+ };
235
+ const dpdyPackages = new Set(["@backstage-community/plugin-tekton"]);
236
+ const result = await processPluginsForDeployment(config, metadataDir, dpdyPackages);
237
+ assert.strictEqual(result.plugins[0].package, "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton:{{inherit}}", "DPDY OCI plugin must resolve to {{inherit}} with default RHEC registry");
238
+ }
239
+ finally {
240
+ await fs.remove(metadataDir);
241
+ }
242
+ });
243
+ it("{{inherit}} ref has no !alias suffix", async () => {
244
+ const metadataDir = await createMetadataFixture([
245
+ {
246
+ name: "backstage-community-plugin-topology",
247
+ packageName: "@backstage-community/plugin-topology",
248
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-topology:bs_1.49.4__1.2.0!backstage-community-plugin-topology",
249
+ },
250
+ ]);
251
+ try {
252
+ const config = {
253
+ plugins: [
254
+ {
255
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-topology:old",
256
+ disabled: false,
257
+ },
258
+ ],
259
+ };
260
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-topology"]));
261
+ assert.strictEqual(result.plugins[0].package, "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-topology:{{inherit}}", "{{inherit}} ref must use default RHEC registry with no alias suffix");
262
+ assert.ok(!result.plugins[0].package.includes("!"), "{{inherit}} ref must NOT include !alias suffix");
263
+ }
264
+ finally {
265
+ await fs.remove(metadataDir);
266
+ }
267
+ });
268
+ it("{{inherit}} uses default RHEC even when metadata has ghcr.io (PR #2449 scenario)", async () => {
269
+ const metadataDir = await createMetadataFixture([
270
+ {
271
+ name: "red-hat-developer-hub-backstage-plugin-orchestrator",
272
+ packageName: "@red-hat-developer-hub/backstage-plugin-orchestrator",
273
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:bs_1.49.4__5.7.10!red-hat-developer-hub-backstage-plugin-orchestrator",
274
+ },
275
+ ]);
276
+ try {
277
+ const config = {
278
+ plugins: [
279
+ {
280
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:old",
281
+ disabled: false,
282
+ },
283
+ ],
284
+ };
285
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@red-hat-developer-hub/backstage-plugin-orchestrator"]));
286
+ assert.strictEqual(result.plugins[0].package, "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", "{{inherit}} must use default RHEC registry regardless of metadata's ghcr.io");
287
+ }
288
+ finally {
289
+ await fs.remove(metadataDir);
290
+ }
291
+ });
292
+ it("NIGHTLY_DPDY_OCI_REGISTRY overrides default registry for all plugins", async () => {
293
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY =
294
+ "ghcr.io/redhat-developer/rhdh-plugin-export-overlays";
295
+ const metadataDir = await createMetadataFixture([
296
+ {
297
+ name: "backstage-community-plugin-tekton",
298
+ packageName: "@backstage-community/plugin-tekton",
299
+ dynamicArtifact: "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc",
300
+ },
301
+ ]);
302
+ try {
303
+ const config = {
304
+ plugins: [
305
+ {
306
+ package: "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc",
307
+ disabled: false,
308
+ },
309
+ ],
310
+ };
311
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-tekton"]));
312
+ assert.strictEqual(result.plugins[0].package, "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:{{inherit}}", "NIGHTLY_DPDY_OCI_REGISTRY must override default RHEC registry");
313
+ }
314
+ finally {
315
+ await fs.remove(metadataDir);
316
+ }
317
+ });
318
+ it("NIGHTLY_DPDY_OCI_REGISTRY_MAP overrides registry for specific plugins", async () => {
319
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({
320
+ "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [
321
+ "@backstage-community/plugin-tekton",
322
+ ],
323
+ });
324
+ const metadataDir = await createMetadataFixture([
325
+ {
326
+ name: "backstage-community-plugin-tekton",
327
+ packageName: "@backstage-community/plugin-tekton",
328
+ dynamicArtifact: "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc",
329
+ },
330
+ {
331
+ name: "red-hat-developer-hub-backstage-plugin-orchestrator",
332
+ packageName: "@red-hat-developer-hub/backstage-plugin-orchestrator",
333
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:bs_1.49.4__5.7.10!red-hat-developer-hub-backstage-plugin-orchestrator",
334
+ },
335
+ ]);
336
+ try {
337
+ const config = {
338
+ plugins: [
339
+ {
340
+ package: "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc",
341
+ disabled: false,
342
+ },
343
+ {
344
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:old",
345
+ disabled: false,
346
+ },
347
+ ],
348
+ };
349
+ const dpdyPackages = new Set([
350
+ "@backstage-community/plugin-tekton",
351
+ "@red-hat-developer-hub/backstage-plugin-orchestrator",
352
+ ]);
353
+ const result = await processPluginsForDeployment(config, metadataDir, dpdyPackages);
354
+ assert.strictEqual(result.plugins[0].package, "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:{{inherit}}", "tekton must use ghcr.io from NIGHTLY_DPDY_OCI_REGISTRY_MAP");
355
+ assert.strictEqual(result.plugins[1].package, "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", "orchestrator must use default RHEC (not in map)");
356
+ }
357
+ finally {
358
+ await fs.remove(metadataDir);
359
+ }
360
+ });
361
+ it("NIGHTLY_DPDY_OCI_REGISTRY_MAP takes precedence over NIGHTLY_DPDY_OCI_REGISTRY", async () => {
362
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY = "quay.io/rhdh";
363
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({
364
+ "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [
365
+ "@backstage-community/plugin-tekton",
366
+ ],
367
+ });
368
+ const metadataDir = await createMetadataFixture([
369
+ {
370
+ name: "backstage-community-plugin-tekton",
371
+ packageName: "@backstage-community/plugin-tekton",
372
+ dynamicArtifact: "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc",
373
+ },
374
+ {
375
+ name: "red-hat-developer-hub-backstage-plugin-orchestrator",
376
+ packageName: "@red-hat-developer-hub/backstage-plugin-orchestrator",
377
+ dynamicArtifact: "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator@sha256:def",
378
+ },
379
+ ]);
380
+ try {
381
+ const config = {
382
+ plugins: [
383
+ {
384
+ package: "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton@sha256:abc",
385
+ disabled: false,
386
+ },
387
+ {
388
+ package: "oci://registry.access.redhat.com/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator@sha256:def",
389
+ disabled: false,
390
+ },
391
+ ],
392
+ };
393
+ const dpdyPackages = new Set([
394
+ "@backstage-community/plugin-tekton",
395
+ "@red-hat-developer-hub/backstage-plugin-orchestrator",
396
+ ]);
397
+ const result = await processPluginsForDeployment(config, metadataDir, dpdyPackages);
398
+ assert.strictEqual(result.plugins[0].package, "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:{{inherit}}", "tekton must use ghcr.io from MAP (takes precedence over blanket)");
399
+ assert.strictEqual(result.plugins[1].package, "oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}", "orchestrator must use quay.io from NIGHTLY_DPDY_OCI_REGISTRY (blanket fallback)");
400
+ }
401
+ finally {
402
+ await fs.remove(metadataDir);
403
+ }
404
+ });
405
+ it("DPDY wrapper plugin keeps wrapper path (no {{inherit}})", async () => {
406
+ const metadataDir = await createMetadataFixture([
407
+ {
408
+ name: "backstage-community-plugin-tech-radar",
409
+ packageName: "@backstage-community/plugin-tech-radar",
410
+ dynamicArtifact: "./dynamic-plugins/dist/backstage-community-plugin-tech-radar",
411
+ },
412
+ ]);
413
+ try {
414
+ const config = {
415
+ plugins: [
416
+ {
417
+ package: "./dynamic-plugins/dist/backstage-community-plugin-tech-radar",
418
+ disabled: false,
419
+ },
420
+ ],
421
+ };
422
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-tech-radar"]));
423
+ assert.strictEqual(result.plugins[0].package, "./dynamic-plugins/dist/backstage-community-plugin-tech-radar", "DPDY wrapper plugin must keep wrapper path, not use {{inherit}}");
424
+ assert.ok(!result.plugins[0].package.includes("inherit"), "wrapper plugin must not contain {{inherit}}");
425
+ }
426
+ finally {
427
+ await fs.remove(metadataDir);
428
+ }
429
+ });
430
+ it("non-DPDY OCI plugin uses full metadata ref (not {{inherit}})", async () => {
431
+ const metadataDir = await createMetadataFixture([
432
+ {
433
+ name: "red-hat-developer-hub-backstage-plugin-scorecard",
434
+ packageName: "@red-hat-developer-hub/backstage-plugin-scorecard",
435
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:bs_1.49.4__1.0.0!red-hat-developer-hub-backstage-plugin-scorecard",
436
+ },
437
+ ]);
438
+ try {
439
+ const config = {
440
+ plugins: [
441
+ {
442
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:old",
443
+ disabled: false,
444
+ },
445
+ ],
446
+ };
447
+ // Scorecard is NOT in the DPDY
448
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-tekton"]));
449
+ assert.ok(result.plugins[0].package.includes("bs_1.49.4__1.0.0"), "non-DPDY OCI plugin must use full metadata ref");
450
+ assert.ok(!result.plugins[0].package.includes("inherit"), "non-DPDY OCI plugin must NOT use {{inherit}}");
451
+ }
452
+ finally {
453
+ await fs.remove(metadataDir);
454
+ }
455
+ });
456
+ it("skips config injection for DPDY OCI plugins", async () => {
457
+ const metadataDir = await createMetadataFixture([
458
+ {
459
+ name: "backstage-community-plugin-tekton",
460
+ packageName: "@backstage-community/plugin-tekton",
461
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton",
462
+ appConfigExamples: {
463
+ dynamicPlugins: {
464
+ frontend: {
465
+ "backstage-community.plugin-tekton": { enabled: true },
466
+ },
467
+ },
468
+ },
469
+ },
470
+ ]);
471
+ try {
472
+ const config = {
473
+ plugins: [
474
+ {
475
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old",
476
+ disabled: false,
477
+ },
478
+ ],
479
+ };
480
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-tekton"]));
481
+ assert.strictEqual(result.plugins[0].pluginConfig, undefined, "DPDY plugin must NOT get metadata config injected — RHDH provides it via {{inherit}}");
482
+ }
483
+ finally {
484
+ await fs.remove(metadataDir);
485
+ }
486
+ });
487
+ it("injects config for non-DPDY OCI plugins", async () => {
488
+ const metadataDir = await createMetadataFixture([
489
+ {
490
+ name: "red-hat-developer-hub-backstage-plugin-scorecard",
491
+ packageName: "@red-hat-developer-hub/backstage-plugin-scorecard",
492
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:bs_1.49.4__1.0.0!red-hat-developer-hub-backstage-plugin-scorecard",
493
+ appConfigExamples: {
494
+ scorecard: { apiUrl: "http://scorecard.example.com" },
495
+ },
496
+ },
497
+ ]);
498
+ try {
499
+ const config = {
500
+ plugins: [
501
+ {
502
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:old",
503
+ disabled: false,
504
+ },
505
+ ],
506
+ };
507
+ // Scorecard NOT in DPDY
508
+ const result = await processPluginsForDeployment(config, metadataDir, new Set());
509
+ assert.deepStrictEqual(result.plugins[0].pluginConfig, { scorecard: { apiUrl: "http://scorecard.example.com" } }, "non-DPDY OCI plugin must get metadata config injected in nightly");
510
+ }
511
+ finally {
512
+ await fs.remove(metadataDir);
513
+ }
514
+ });
515
+ it("mixed scenario: DPDY OCI → RHEC inherit, non-DPDY OCI → full ref + config", async () => {
516
+ const metadataDir = await createMetadataFixture([
517
+ {
518
+ name: "backstage-community-plugin-tekton",
519
+ packageName: "@backstage-community/plugin-tekton",
520
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton",
521
+ appConfigExamples: {
522
+ tekton: { enabled: true },
523
+ },
524
+ },
525
+ {
526
+ name: "red-hat-developer-hub-backstage-plugin-scorecard",
527
+ packageName: "@red-hat-developer-hub/backstage-plugin-scorecard",
528
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:bs_1.49.4__1.0.0!red-hat-developer-hub-backstage-plugin-scorecard",
529
+ appConfigExamples: {
530
+ scorecard: { apiUrl: "http://scorecard.example.com" },
531
+ },
532
+ },
533
+ ]);
534
+ try {
535
+ const config = {
536
+ plugins: [
537
+ {
538
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old",
539
+ disabled: false,
540
+ },
541
+ {
542
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scorecard:old",
543
+ disabled: false,
544
+ },
545
+ ],
546
+ };
547
+ // Only tekton is in DPDY
548
+ const result = await processPluginsForDeployment(config, metadataDir, new Set(["@backstage-community/plugin-tekton"]));
549
+ // Tekton: DPDY → {{inherit}} with default RHEC, no config injection
550
+ assert.strictEqual(result.plugins[0].package, "oci://registry.access.redhat.com/rhdh/backstage-community-plugin-tekton:{{inherit}}", "DPDY plugin must use {{inherit}} with default RHEC registry");
551
+ assert.strictEqual(result.plugins[0].pluginConfig, undefined, "DPDY plugin must not have config injected");
552
+ // Scorecard: non-DPDY → full OCI ref, config injected
553
+ assert.ok(result.plugins[1].package.includes("bs_1.49.4__1.0.0"), "non-DPDY plugin must use full metadata ref");
554
+ assert.deepStrictEqual(result.plugins[1].pluginConfig, { scorecard: { apiUrl: "http://scorecard.example.com" } }, "non-DPDY OCI plugin must have config injected");
555
+ }
556
+ finally {
557
+ await fs.remove(metadataDir);
558
+ }
559
+ });
560
+ });
561
+ // ── getDpdyRegistry unit tests ──────────────────────────────────────────────
562
+ describe("getDpdyRegistry", () => {
563
+ const env = withCleanEnv();
564
+ beforeEach(() => env.save());
565
+ afterEach(() => env.restore());
566
+ it("returns default RHEC registry when no env vars set", () => {
567
+ delete process.env.NIGHTLY_DPDY_OCI_REGISTRY;
568
+ delete process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP;
569
+ assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "registry.access.redhat.com/rhdh");
570
+ });
571
+ it("NIGHTLY_DPDY_OCI_REGISTRY overrides default for all plugins", () => {
572
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY =
573
+ "ghcr.io/redhat-developer/rhdh-plugin-export-overlays";
574
+ delete process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP;
575
+ assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "ghcr.io/redhat-developer/rhdh-plugin-export-overlays");
576
+ assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "ghcr.io/redhat-developer/rhdh-plugin-export-overlays");
577
+ });
578
+ it("NIGHTLY_DPDY_OCI_REGISTRY_MAP returns mapped registry for listed plugin", () => {
579
+ delete process.env.NIGHTLY_DPDY_OCI_REGISTRY;
580
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({
581
+ "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [
582
+ "@backstage-community/plugin-tekton",
583
+ "@backstage-community/plugin-argocd",
584
+ ],
585
+ });
586
+ assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "ghcr.io/redhat-developer/rhdh-plugin-export-overlays");
587
+ assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-argocd"), "ghcr.io/redhat-developer/rhdh-plugin-export-overlays");
588
+ });
589
+ it("NIGHTLY_DPDY_OCI_REGISTRY_MAP falls back to default for unlisted plugin", () => {
590
+ delete process.env.NIGHTLY_DPDY_OCI_REGISTRY;
591
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({
592
+ "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [
593
+ "@backstage-community/plugin-tekton",
594
+ ],
595
+ });
596
+ assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "registry.access.redhat.com/rhdh", "unlisted plugin must fall back to default RHEC");
597
+ });
598
+ it("NIGHTLY_DPDY_OCI_REGISTRY_MAP takes precedence over NIGHTLY_DPDY_OCI_REGISTRY", () => {
599
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY = "quay.io/rhdh";
600
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({
601
+ "ghcr.io/custom": ["@backstage-community/plugin-tekton"],
602
+ });
603
+ assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "ghcr.io/custom", "MAP must take precedence over blanket");
604
+ assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "quay.io/rhdh", "unlisted plugin must fall back to blanket NIGHTLY_DPDY_OCI_REGISTRY");
605
+ });
606
+ it("supports multiple registries in NIGHTLY_DPDY_OCI_REGISTRY_MAP", () => {
607
+ delete process.env.NIGHTLY_DPDY_OCI_REGISTRY;
608
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY_MAP = JSON.stringify({
609
+ "ghcr.io/redhat-developer/rhdh-plugin-export-overlays": [
610
+ "@backstage-community/plugin-tekton",
611
+ ],
612
+ "quay.io/rhdh": ["@red-hat-developer-hub/backstage-plugin-orchestrator"],
613
+ });
614
+ assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-tekton"), "ghcr.io/redhat-developer/rhdh-plugin-export-overlays");
615
+ assert.strictEqual(getDpdyRegistry("@red-hat-developer-hub/backstage-plugin-orchestrator"), "quay.io/rhdh");
616
+ assert.strictEqual(getDpdyRegistry("@backstage-community/plugin-argocd"), "registry.access.redhat.com/rhdh", "unlisted plugin falls back to default");
617
+ });
618
+ });
@@ -4,7 +4,7 @@
4
4
  import { describe, it, beforeEach, afterEach } from "node:test";
5
5
  import assert from "node:assert";
6
6
  import fs from "fs-extra";
7
- import { isNightlyJob, processPluginsForDeployment, } from "../plugin-metadata.js";
7
+ import { processPluginsForDeployment, } from "../plugin-metadata.js";
8
8
  import { withCleanEnv, createMetadataFixture, createWorkspaceFixture, } from "./helpers.js";
9
9
  describe("processPluginsForDeployment — PR mode", () => {
10
10
  const env = withCleanEnv();
@@ -163,6 +163,7 @@ describe("processPluginsForDeployment — PR mode", () => {
163
163
  }
164
164
  });
165
165
  it("skips injection when RHDH_SKIP_PLUGIN_METADATA_INJECTION is 'true'", async () => {
166
+ delete process.env.CI;
166
167
  process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION = "true";
167
168
  const metadataDir = await createMetadataFixture([
168
169
  {
@@ -190,6 +191,35 @@ describe("processPluginsForDeployment — PR mode", () => {
190
191
  await fs.remove(metadataDir);
191
192
  }
192
193
  });
194
+ it("ignores RHDH_SKIP_PLUGIN_METADATA_INJECTION in CI", async () => {
195
+ process.env.CI = "true";
196
+ process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION = "true";
197
+ const metadataDir = await createMetadataFixture([
198
+ {
199
+ name: "backstage-community-plugin-tech-radar",
200
+ packageName: "@backstage-community/plugin-tech-radar",
201
+ dynamicArtifact: "./dynamic-plugins/dist/backstage-community-plugin-tech-radar",
202
+ appConfigExamples: {
203
+ techRadar: { url: "http://default.example.com" },
204
+ },
205
+ },
206
+ ]);
207
+ try {
208
+ const config = {
209
+ plugins: [
210
+ {
211
+ package: "./dynamic-plugins/dist/backstage-community-plugin-tech-radar",
212
+ disabled: false,
213
+ },
214
+ ],
215
+ };
216
+ const result = await processPluginsForDeployment(config, metadataDir);
217
+ assert.ok(result.plugins[0].pluginConfig, "pluginConfig must be injected in CI even when RHDH_SKIP_PLUGIN_METADATA_INJECTION=true");
218
+ }
219
+ finally {
220
+ await fs.remove(metadataDir);
221
+ }
222
+ });
193
223
  it("does not skip injection when RHDH_SKIP_PLUGIN_METADATA_INJECTION is 'false'", async () => {
194
224
  process.env.RHDH_SKIP_PLUGIN_METADATA_INJECTION = "false";
195
225
  const metadataDir = await createMetadataFixture([
@@ -311,13 +341,66 @@ describe("processPluginsForDeployment — PR mode", () => {
311
341
  }
312
342
  });
313
343
  });
314
- // ── PR vs nightly precedence ────────────────────────────────────────────
315
- describe("PR vs nightly precedence", () => {
316
- it("isNightlyJob returns false when both GIT_PR_NUMBER and E2E_NIGHTLY_MODE are set", () => {
344
+ // ── {{inherit}} must NOT apply in PR/local mode ─────────────────────────
345
+ describe("{{inherit}} does not apply in PR/local mode", () => {
346
+ it("DPDY OCI plugin uses metadata ref (not {{inherit}}) when GIT_PR_NUMBER is set", async () => {
317
347
  process.env.GIT_PR_NUMBER = "42";
318
348
  process.env.E2E_NIGHTLY_MODE = "true";
319
- assert.strictEqual(isNightlyJob(), false, "GIT_PR_NUMBER must make isNightlyJob return false");
349
+ const { wsDir, metadataDir } = await createWorkspaceFixture([
350
+ {
351
+ name: "backstage-community-plugin-tekton",
352
+ packageName: "@backstage-community/plugin-tekton",
353
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton",
354
+ },
355
+ ]);
356
+ try {
357
+ const config = {
358
+ plugins: [
359
+ {
360
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old_tag",
361
+ disabled: false,
362
+ },
363
+ ],
364
+ };
365
+ const result = await processPluginsForDeployment(config, metadataDir);
366
+ assert.ok(!result.plugins[0].package.includes("{{inherit}}"), "{{inherit}} must NOT be used when GIT_PR_NUMBER is set (PR takes precedence)");
367
+ }
368
+ finally {
369
+ await fs.remove(wsDir);
370
+ }
320
371
  });
372
+ it("NIGHTLY_DPDY_OCI_REGISTRY env vars are ignored in PR/local mode", async () => {
373
+ delete process.env.GIT_PR_NUMBER;
374
+ delete process.env.E2E_NIGHTLY_MODE;
375
+ process.env.NIGHTLY_DPDY_OCI_REGISTRY = "quay.io/rhdh";
376
+ const metadataDir = await createMetadataFixture([
377
+ {
378
+ name: "backstage-community-plugin-tekton",
379
+ packageName: "@backstage-community/plugin-tekton",
380
+ dynamicArtifact: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:bs_1.49.4__3.33.3!backstage-community-plugin-tekton",
381
+ },
382
+ ]);
383
+ try {
384
+ const config = {
385
+ plugins: [
386
+ {
387
+ package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-tekton:old_tag",
388
+ disabled: false,
389
+ },
390
+ ],
391
+ };
392
+ const result = await processPluginsForDeployment(config, metadataDir);
393
+ assert.ok(!result.plugins[0].package.includes("quay.io"), "NIGHTLY_DPDY_OCI_REGISTRY must be ignored in local/PR mode");
394
+ assert.ok(!result.plugins[0].package.includes("{{inherit}}"), "{{inherit}} must NOT be used in local/PR mode");
395
+ assert.ok(result.plugins[0].package.includes("bs_1.49.4__3.33.3"), "must use metadata dynamicArtifact in local/PR mode");
396
+ }
397
+ finally {
398
+ await fs.remove(metadataDir);
399
+ }
400
+ });
401
+ });
402
+ // ── PR vs nightly precedence ────────────────────────────────────────────
403
+ describe("PR vs nightly precedence", () => {
321
404
  it("injects metadata config when GIT_PR_NUMBER is set (PR mode despite nightly env)", async () => {
322
405
  process.env.GIT_PR_NUMBER = "42";
323
406
  process.env.E2E_NIGHTLY_MODE = "true";
@@ -37,6 +37,8 @@ export declare class WorkspacePaths {
37
37
  static get dynamicPlugins(): string;
38
38
  /** Default Helm value file path: `tests/config/value_file.yaml` */
39
39
  static get valueFile(): string;
40
+ /** Optional Helm values merged after `value_file.yaml` when the new frontend system is active (explicit option, namespace suffix `-app-next`, or `USE_NEW_FRONTEND_SYSTEM=true`) and this file exists. */
41
+ static get valueFileAppNext(): string;
40
42
  /** Default operator subscription path: `tests/config/subscription.yaml` */
41
43
  static get subscription(): string;
42
44
  }
@@ -1 +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"}
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,0MAA0M;IAC1M,MAAM,KAAK,gBAAgB,IAAI,MAAM,CAEpC;IAED,2EAA2E;IAC3E,MAAM,KAAK,YAAY,IAAI,MAAM,CAEhC;CACF"}
@@ -58,6 +58,10 @@ export class WorkspacePaths {
58
58
  static get valueFile() {
59
59
  return path.resolve(this.e2eRoot, "tests/config/value_file.yaml");
60
60
  }
61
+ /** Optional Helm values merged after `value_file.yaml` when the new frontend system is active (explicit option, namespace suffix `-app-next`, or `USE_NEW_FRONTEND_SYSTEM=true`) and this file exists. */
62
+ static get valueFileAppNext() {
63
+ return path.resolve(this.e2eRoot, "tests/config/value_file-app-next.yaml");
64
+ }
61
65
  /** Default operator subscription path: `tests/config/subscription.yaml` */
62
66
  static get subscription() {
63
67
  return path.resolve(this.e2eRoot, "tests/config/subscription.yaml");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/e2e-test-utils",
3
- "version": "1.1.39",
3
+ "version": "1.1.41",
4
4
  "description": "Test utilities for RHDH E2E tests",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {