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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +6 -0
  3. package/dist/deployment/keycloak/config/keycloak-values.yaml +96 -0
  4. package/dist/deployment/keycloak/constants.d.ts +29 -0
  5. package/dist/deployment/keycloak/constants.d.ts.map +1 -0
  6. package/dist/deployment/keycloak/constants.js +75 -0
  7. package/dist/deployment/keycloak/deployment.d.ts +89 -0
  8. package/dist/deployment/keycloak/deployment.d.ts.map +1 -0
  9. package/dist/deployment/keycloak/deployment.js +435 -0
  10. package/dist/deployment/keycloak/index.d.ts +2 -0
  11. package/dist/deployment/keycloak/index.d.ts.map +1 -0
  12. package/dist/deployment/keycloak/index.js +1 -0
  13. package/dist/deployment/keycloak/types.d.ts +59 -0
  14. package/dist/deployment/keycloak/types.d.ts.map +1 -0
  15. package/dist/deployment/keycloak/types.js +1 -0
  16. package/dist/deployment/rhdh/config/auth/guest/app-config.yaml +5 -0
  17. package/dist/deployment/rhdh/config/auth/keycloak/app-config.yaml +19 -0
  18. package/dist/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml +3 -0
  19. package/dist/deployment/rhdh/config/auth/keycloak/secrets.yaml +12 -0
  20. package/dist/deployment/rhdh/config/common/app-config-rhdh.yaml +6 -0
  21. package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +3 -0
  22. package/dist/deployment/rhdh/config/common/rhdh-secrets.yaml +7 -0
  23. package/dist/deployment/rhdh/config/helm/value_file.yaml +7 -0
  24. package/dist/deployment/rhdh/config/operator/subscription.yaml +21 -0
  25. package/dist/deployment/rhdh/constants.d.ts +19 -0
  26. package/dist/deployment/rhdh/constants.d.ts.map +1 -0
  27. package/dist/deployment/rhdh/constants.js +27 -0
  28. package/dist/deployment/rhdh/deployment.d.ts +40 -0
  29. package/dist/deployment/rhdh/deployment.d.ts.map +1 -0
  30. package/dist/deployment/rhdh/deployment.js +303 -0
  31. package/dist/deployment/rhdh/index.d.ts +2 -0
  32. package/dist/deployment/rhdh/index.d.ts.map +1 -0
  33. package/dist/deployment/rhdh/index.js +1 -0
  34. package/dist/deployment/rhdh/types.d.ts +31 -0
  35. package/dist/deployment/rhdh/types.d.ts.map +1 -0
  36. package/dist/deployment/rhdh/types.js +1 -0
  37. package/dist/eslint/base.config.d.ts +10 -0
  38. package/dist/eslint/base.config.d.ts.map +1 -0
  39. package/dist/eslint/base.config.js +213 -0
  40. package/dist/playwright/base-config.d.ts +14 -0
  41. package/dist/playwright/base-config.d.ts.map +1 -0
  42. package/dist/playwright/base-config.js +47 -0
  43. package/dist/playwright/fixtures/test.d.ts +13 -0
  44. package/dist/playwright/fixtures/test.d.ts.map +1 -0
  45. package/dist/playwright/fixtures/test.js +48 -0
  46. package/dist/playwright/global-setup.d.ts +6 -0
  47. package/dist/playwright/global-setup.d.ts.map +1 -0
  48. package/dist/playwright/global-setup.js +67 -0
  49. package/dist/playwright/helpers/accessibility.d.ts +13 -0
  50. package/dist/playwright/helpers/accessibility.d.ts.map +1 -0
  51. package/dist/playwright/helpers/accessibility.js +24 -0
  52. package/dist/playwright/helpers/api-endpoints.d.ts +11 -0
  53. package/dist/playwright/helpers/api-endpoints.d.ts.map +1 -0
  54. package/dist/playwright/helpers/api-endpoints.js +15 -0
  55. package/dist/playwright/helpers/api-helper.d.ts +77 -0
  56. package/dist/playwright/helpers/api-helper.d.ts.map +1 -0
  57. package/dist/playwright/helpers/api-helper.js +285 -0
  58. package/dist/playwright/helpers/common.d.ts +31 -0
  59. package/dist/playwright/helpers/common.d.ts.map +1 -0
  60. package/dist/playwright/helpers/common.js +342 -0
  61. package/dist/playwright/helpers/index.d.ts +5 -0
  62. package/dist/playwright/helpers/index.d.ts.map +1 -0
  63. package/dist/playwright/helpers/index.js +4 -0
  64. package/dist/playwright/helpers/navbar.d.ts +2 -0
  65. package/dist/playwright/helpers/navbar.d.ts.map +1 -0
  66. package/dist/playwright/helpers/navbar.js +1 -0
  67. package/dist/playwright/helpers/ui-helper.d.ts +106 -0
  68. package/dist/playwright/helpers/ui-helper.d.ts.map +1 -0
  69. package/dist/playwright/helpers/ui-helper.js +439 -0
  70. package/dist/playwright/page-objects/global-obj.d.ts +25 -0
  71. package/dist/playwright/page-objects/global-obj.d.ts.map +1 -0
  72. package/dist/playwright/page-objects/global-obj.js +24 -0
  73. package/dist/playwright/page-objects/page-obj.d.ts +41 -0
  74. package/dist/playwright/page-objects/page-obj.d.ts.map +1 -0
  75. package/dist/playwright/page-objects/page-obj.js +40 -0
  76. package/dist/playwright/pages/catalog-import.d.ts +31 -0
  77. package/dist/playwright/pages/catalog-import.d.ts.map +1 -0
  78. package/dist/playwright/pages/catalog-import.js +65 -0
  79. package/dist/playwright/pages/catalog.d.ts +14 -0
  80. package/dist/playwright/pages/catalog.d.ts.map +1 -0
  81. package/dist/playwright/pages/catalog.js +37 -0
  82. package/dist/playwright/pages/extensions.d.ts +38 -0
  83. package/dist/playwright/pages/extensions.d.ts.map +1 -0
  84. package/dist/playwright/pages/extensions.js +110 -0
  85. package/dist/playwright/pages/home-page.d.ts +10 -0
  86. package/dist/playwright/pages/home-page.d.ts.map +1 -0
  87. package/dist/playwright/pages/home-page.js +46 -0
  88. package/dist/playwright/pages/index.d.ts +6 -0
  89. package/dist/playwright/pages/index.d.ts.map +1 -0
  90. package/dist/playwright/pages/index.js +5 -0
  91. package/dist/playwright/pages/notifications.d.ts +24 -0
  92. package/dist/playwright/pages/notifications.d.ts.map +1 -0
  93. package/dist/playwright/pages/notifications.js +112 -0
  94. package/dist/utils/bash.d.ts +3 -0
  95. package/dist/utils/bash.d.ts.map +1 -0
  96. package/dist/utils/bash.js +4 -0
  97. package/dist/utils/common.d.ts +3 -0
  98. package/dist/utils/common.d.ts.map +1 -0
  99. package/dist/utils/common.js +8 -0
  100. package/dist/utils/index.d.ts +5 -0
  101. package/dist/utils/index.d.ts.map +1 -0
  102. package/dist/utils/index.js +4 -0
  103. package/dist/utils/kubernetes-client.d.ts +97 -0
  104. package/dist/utils/kubernetes-client.d.ts.map +1 -0
  105. package/dist/utils/kubernetes-client.js +558 -0
  106. package/dist/utils/merge-yamls.d.ts +50 -0
  107. package/dist/utils/merge-yamls.d.ts.map +1 -0
  108. package/dist/utils/merge-yamls.js +96 -0
  109. package/dist/utils/plugin-metadata.d.ts +116 -0
  110. package/dist/utils/plugin-metadata.d.ts.map +1 -0
  111. package/dist/utils/plugin-metadata.js +330 -0
  112. package/package.json +108 -0
  113. package/tsconfig.base.json +16 -0
@@ -0,0 +1,40 @@
1
+ import { KubernetesClientHelper } from "../../utils/kubernetes-client.js";
2
+ import type { DeploymentOptions, DeploymentConfig } from "./types.js";
3
+ export declare class RHDHDeployment {
4
+ k8sClient: KubernetesClientHelper;
5
+ rhdhUrl: string;
6
+ deploymentConfig: DeploymentConfig;
7
+ constructor(namespace: string);
8
+ deploy(): Promise<void>;
9
+ private _applyAppConfig;
10
+ private _applySecrets;
11
+ /**
12
+ * Builds the merged dynamic plugins configuration.
13
+ * Merges: package defaults + auth config + user config + metadata (for PR builds).
14
+ */
15
+ private _buildDynamicPluginsConfig;
16
+ private _applyDynamicPlugins;
17
+ private _deployWithHelm;
18
+ private _deployWithOperator;
19
+ rolloutRestart(): Promise<void>;
20
+ /**
21
+ * Performs a clean restart by scaling down to 0 first, waiting for pods to terminate,
22
+ * then scaling back up. This prevents MigrationLocked errors by ensuring no pods
23
+ * hold database locks when new pods start.
24
+ */
25
+ scaleDownAndRestart(): Promise<void>;
26
+ waitUntilReady(timeout?: number): Promise<void>;
27
+ teardown(): Promise<void>;
28
+ private _resolveChartVersion;
29
+ /**
30
+ * Resolve the semantic version from the "next" tag by looking up the
31
+ * downstream image (rhdh-hub-rhel9) and finding tags with the same digest.
32
+ */
33
+ private _resolveVersionFromNextTag;
34
+ private _buildDeploymentConfig;
35
+ configure(deploymentOptions?: DeploymentOptions): Promise<void>;
36
+ private _buildBaseUrl;
37
+ private _log;
38
+ private _logBoxen;
39
+ }
40
+ //# sourceMappingURL=deployment.d.ts.map
@@ -0,0 +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;AAiB1E,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;IAUvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;YAqBf,eAAe;YAgBf,aAAa;IAqB3B;;;OAGG;YACW,0BAA0B;YAuC1B,oBAAoB;YAWpB,eAAe;YAmCf,mBAAmB;IAiD3B,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;IAwBpD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,oBAAoB;IAsClC;;;OAGG;YACW,0BAA0B;IAwCxC,OAAO,CAAC,sBAAsB;IAoCxB,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrE,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAMlB"}
@@ -0,0 +1,303 @@
1
+ import { KubernetesClientHelper } from "../../utils/kubernetes-client.js";
2
+ import { $ } from "../../utils/bash.js";
3
+ import yaml from "js-yaml";
4
+ import { test } from "@playwright/test";
5
+ import { mergeYamlFilesIfExists, deepMerge } from "../../utils/merge-yamls.js";
6
+ import { loadAndInjectPluginMetadata, generateDynamicPluginsConfigFromMetadata, } from "../../utils/plugin-metadata.js";
7
+ import { envsubst } from "../../utils/common.js";
8
+ import cloneDeepWith from "lodash.clonedeepwith";
9
+ import fs from "fs-extra";
10
+ import { DEFAULT_CONFIG_PATHS, AUTH_CONFIG_PATHS, CHART_URL, } from "./constants.js";
11
+ export class RHDHDeployment {
12
+ k8sClient = new KubernetesClientHelper();
13
+ rhdhUrl;
14
+ deploymentConfig;
15
+ constructor(namespace) {
16
+ this.deploymentConfig = this._buildDeploymentConfig({ namespace });
17
+ this.rhdhUrl = this._buildBaseUrl();
18
+ this._log(`RHDH deployment initialized (namespace: ${this.deploymentConfig.namespace})`);
19
+ this._log("RHDH Base URL: " + this.rhdhUrl);
20
+ console.table(this.deploymentConfig);
21
+ }
22
+ async deploy() {
23
+ this._log("Starting RHDH deployment...");
24
+ test.setTimeout(600_000);
25
+ await this.k8sClient.createNamespaceIfNotExists(this.deploymentConfig.namespace);
26
+ await this._applyAppConfig();
27
+ await this._applySecrets();
28
+ if (this.deploymentConfig.method === "helm") {
29
+ await this._deployWithHelm(this.deploymentConfig.valueFile);
30
+ await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes
31
+ }
32
+ else {
33
+ await this._applyDynamicPlugins();
34
+ await this._deployWithOperator(this.deploymentConfig.subscription);
35
+ }
36
+ await this.waitUntilReady();
37
+ }
38
+ async _applyAppConfig() {
39
+ const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth];
40
+ const appConfigYaml = await mergeYamlFilesIfExists([
41
+ DEFAULT_CONFIG_PATHS.appConfig,
42
+ authConfig.appConfig,
43
+ this.deploymentConfig.appConfig,
44
+ ]);
45
+ this._logBoxen("App Config", appConfigYaml);
46
+ await this.k8sClient.applyConfigMapFromObject("app-config-rhdh", appConfigYaml, this.deploymentConfig.namespace);
47
+ }
48
+ async _applySecrets() {
49
+ const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth];
50
+ const secretsYaml = await mergeYamlFilesIfExists([
51
+ DEFAULT_CONFIG_PATHS.secrets,
52
+ authConfig.secrets,
53
+ this.deploymentConfig.secrets,
54
+ ]);
55
+ // Use cloneDeepWith to substitute env vars in-place, avoiding JSON.parse issues
56
+ // with control characters in secrets (e.g., private keys with newlines)
57
+ const substituted = cloneDeepWith(secretsYaml, (value) => {
58
+ if (typeof value === "string")
59
+ return envsubst(value);
60
+ });
61
+ await this.k8sClient.applySecretFromObject("rhdh-secrets", substituted, this.deploymentConfig.namespace);
62
+ }
63
+ /**
64
+ * Builds the merged dynamic plugins configuration.
65
+ * Merges: package defaults + auth config + user config + metadata (for PR builds).
66
+ */
67
+ async _buildDynamicPluginsConfig() {
68
+ const userConfigPath = this.deploymentConfig.dynamicPlugins;
69
+ const userConfigExists = userConfigPath && fs.existsSync(userConfigPath);
70
+ const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth];
71
+ // If user's dynamic-plugins config doesn't exist, auto-generate from metadata
72
+ if (!userConfigExists) {
73
+ this._log(`Dynamic plugins config not found at '${userConfigPath}', auto-generating from metadata...`);
74
+ const metadataConfig = await generateDynamicPluginsConfigFromMetadata();
75
+ // Merge with package defaults and auth config
76
+ const authPlugins = await mergeYamlFilesIfExists([DEFAULT_CONFIG_PATHS.dynamicPlugins, authConfig.dynamicPlugins], { arrayMergeStrategy: { byKey: "package" } });
77
+ return deepMerge(metadataConfig, authPlugins, {
78
+ arrayMergeStrategy: { byKey: "package" },
79
+ });
80
+ }
81
+ // User config exists - merge provided configs and inject metadata for listed plugins only
82
+ let dynamicPluginsConfig = await mergeYamlFilesIfExists([
83
+ DEFAULT_CONFIG_PATHS.dynamicPlugins,
84
+ authConfig.dynamicPlugins,
85
+ userConfigPath,
86
+ ], { arrayMergeStrategy: { byKey: "package" } });
87
+ // Inject plugin metadata configuration for plugins in the config
88
+ dynamicPluginsConfig =
89
+ await loadAndInjectPluginMetadata(dynamicPluginsConfig);
90
+ return dynamicPluginsConfig;
91
+ }
92
+ async _applyDynamicPlugins() {
93
+ const dynamicPluginsYaml = await this._buildDynamicPluginsConfig();
94
+ this._logBoxen("Dynamic Plugins", dynamicPluginsYaml);
95
+ await this.k8sClient.applyConfigMapFromObject("dynamic-plugins", dynamicPluginsYaml, this.deploymentConfig.namespace);
96
+ }
97
+ async _deployWithHelm(valueFile) {
98
+ const chartVersion = await this._resolveChartVersion(this.deploymentConfig.version);
99
+ this._log(`Helm chart version resolved to: ${chartVersion}`);
100
+ const valueFileObject = (await mergeYamlFilesIfExists([
101
+ DEFAULT_CONFIG_PATHS.helm.valueFile,
102
+ valueFile,
103
+ ]));
104
+ this._logBoxen("Value File", valueFileObject);
105
+ // Merge dynamic plugins into the values file (including auth-specific plugins)
106
+ if (!valueFileObject.global) {
107
+ valueFileObject.global = {};
108
+ }
109
+ valueFileObject.global.dynamic = await this._buildDynamicPluginsConfig();
110
+ this._logBoxen("Dynamic Plugins", valueFileObject.global.dynamic);
111
+ fs.writeFileSync(`/tmp/${this.deploymentConfig.namespace}-value-file.yaml`, yaml.dump(valueFileObject));
112
+ await $ `
113
+ helm upgrade redhat-developer-hub -i "${process.env.CHART_URL || CHART_URL}" --version "${chartVersion}" \
114
+ -f "/tmp/${this.deploymentConfig.namespace}-value-file.yaml" \
115
+ --set global.clusterRouterBase="${process.env.K8S_CLUSTER_ROUTER_BASE}" \
116
+ --namespace="${this.deploymentConfig.namespace}"
117
+ `;
118
+ this._log(`Helm deployment completed successfully`);
119
+ }
120
+ async _deployWithOperator(subscription) {
121
+ const subscriptionObject = await mergeYamlFilesIfExists([
122
+ DEFAULT_CONFIG_PATHS.operator.subscription,
123
+ subscription,
124
+ ]);
125
+ this._logBoxen("Subscription", subscriptionObject);
126
+ fs.writeFileSync(`/tmp/${this.deploymentConfig.namespace}-subscription.yaml`, yaml.dump(subscriptionObject));
127
+ const version = this.deploymentConfig.version;
128
+ const isSemanticVersion = /^\d+(\.\d+)?$/.test(version);
129
+ // Use main branch for non-semantic versions (e.g., "next", "latest")
130
+ const branch = isSemanticVersion ? `release-${version}` : "main";
131
+ // Build version argument based on version type
132
+ let versionArg;
133
+ if (isSemanticVersion) {
134
+ versionArg = `-v ${version}`;
135
+ }
136
+ else if (version === "next") {
137
+ versionArg = "--next";
138
+ }
139
+ else {
140
+ throw new Error(`Invalid RHDH version "${version}". Use semantic version (e.g., "1.5") or "next".`);
141
+ }
142
+ this._log(`Using operator branch: ${branch}, version arg: ${versionArg}`);
143
+ await $ `
144
+ set -e;
145
+ curl -sf https://raw.githubusercontent.com/redhat-developer/rhdh-operator/refs/heads/${branch}/.rhdh/scripts/install-rhdh-catalog-source.sh | bash -s -- ${versionArg} --install-operator rhdh
146
+
147
+ timeout 300 bash -c '
148
+ while ! oc get crd/backstages.rhdh.redhat.com -n "${this.deploymentConfig.namespace}" >/dev/null 2>&1; do
149
+ echo "Waiting for Backstage CRD to be created..."
150
+ sleep 20
151
+ done
152
+ echo "Backstage CRD is created."
153
+ ' || echo "Error: Timed out waiting for Backstage CRD creation."
154
+
155
+ oc apply -f "/tmp/${this.deploymentConfig.namespace}-subscription.yaml" -n "${this.deploymentConfig.namespace}"
156
+ `;
157
+ this._log("Operator deployment executed successfully.");
158
+ }
159
+ async rolloutRestart() {
160
+ this._log(`Restarting RHDH deployment in namespace ${this.deploymentConfig.namespace}...`);
161
+ await $ `oc rollout restart deployment -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' -n ${this.deploymentConfig.namespace}`;
162
+ this._log(`RHDH deployment restarted successfully in namespace ${this.deploymentConfig.namespace}`);
163
+ await this.waitUntilReady();
164
+ }
165
+ /**
166
+ * Performs a clean restart by scaling down to 0 first, waiting for pods to terminate,
167
+ * then scaling back up. This prevents MigrationLocked errors by ensuring no pods
168
+ * hold database locks when new pods start.
169
+ */
170
+ async scaleDownAndRestart() {
171
+ const namespace = this.deploymentConfig.namespace;
172
+ await $ `oc scale deployment -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' --replicas=0 -n ${namespace}`;
173
+ await $ `oc wait --for=delete pod -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub),app.kubernetes.io/name!=postgresql' -n ${namespace} --timeout=120s || true`;
174
+ await $ `oc scale deployment -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' --replicas=1 -n ${namespace}`;
175
+ }
176
+ async waitUntilReady(timeout = 500) {
177
+ this._log(`Waiting for RHDH deployment to be ready in namespace ${this.deploymentConfig.namespace}...`);
178
+ const labelSelector = "app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)";
179
+ try {
180
+ await this.k8sClient.waitForPodsWithFailureDetection(this.deploymentConfig.namespace, labelSelector, timeout);
181
+ this._log(`RHDH deployment is ready in namespace ${this.deploymentConfig.namespace}`);
182
+ }
183
+ catch (error) {
184
+ throw new Error(`RHDH deployment failed in namespace ${this.deploymentConfig.namespace}: ${error instanceof Error ? error.message : error}`);
185
+ }
186
+ }
187
+ async teardown() {
188
+ await this.k8sClient.deleteNamespace(this.deploymentConfig.namespace);
189
+ }
190
+ async _resolveChartVersion(version) {
191
+ let resolvedVersion = version;
192
+ // Handle "next" tag by looking up the corresponding version from downstream image
193
+ if (version === "next") {
194
+ resolvedVersion = await this._resolveVersionFromNextTag();
195
+ this._log(`Resolved "next" tag to version: ${resolvedVersion}`);
196
+ }
197
+ // Semantic versions (e.g., 1.2, 1.10)
198
+ if (/^(\d+(\.\d+)?)$/.test(resolvedVersion)) {
199
+ const response = await fetch("https://quay.io/api/v1/repository/rhdh/chart/tag/?onlyActiveTags=true&limit=600");
200
+ if (!response.ok)
201
+ throw new Error(`Failed to fetch chart versions: ${response.statusText}`);
202
+ const data = (await response.json());
203
+ const matching = data.tags
204
+ .map((t) => t.name)
205
+ .filter((name) => name.startsWith(`${resolvedVersion}-`))
206
+ .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
207
+ const latest = matching.at(-1);
208
+ if (!latest)
209
+ throw new Error(`No chart version found for ${resolvedVersion}`);
210
+ return latest;
211
+ }
212
+ // CI build versions (e.g., 1.2.3-CI)
213
+ if (resolvedVersion.endsWith("CI"))
214
+ return resolvedVersion;
215
+ throw new Error(`Invalid Helm chart version format: "${version}"`);
216
+ }
217
+ /**
218
+ * Resolve the semantic version from the "next" tag by looking up the
219
+ * downstream image (rhdh-hub-rhel9) and finding tags with the same digest.
220
+ */
221
+ async _resolveVersionFromNextTag() {
222
+ // Fetch all active tags in a single API call
223
+ const response = await fetch("https://quay.io/api/v1/repository/rhdh/rhdh-hub-rhel9/tag/?onlyActiveTags=true&limit=75");
224
+ if (!response.ok) {
225
+ throw new Error(`Failed to fetch image tags: ${response.statusText}`);
226
+ }
227
+ // Use Record to avoid snake_case linting issues with Quay API response
228
+ const data = (await response.json());
229
+ // Find the "next" tag and get its digest
230
+ const nextTag = data.tags.find((t) => t["name"] === "next");
231
+ if (!nextTag) {
232
+ throw new Error('No "next" tag found in rhdh-hub-rhel9 repository');
233
+ }
234
+ const digest = nextTag["manifest_digest"];
235
+ this._log(`"next" tag digest: ${digest}`);
236
+ // Find semantic version tag (e.g., "1.10") with the same digest
237
+ const semanticVersionTag = data.tags.find((t) => t["manifest_digest"] === digest &&
238
+ /^\d+\.\d+$/.test(t["name"]));
239
+ if (!semanticVersionTag) {
240
+ throw new Error(`Could not find semantic version tag for "next" (digest: ${digest})`);
241
+ }
242
+ return semanticVersionTag["name"];
243
+ }
244
+ _buildDeploymentConfig(input) {
245
+ // Default to "next" if RHDH_VERSION not set
246
+ const version = input.version ?? process.env.RHDH_VERSION ?? "next";
247
+ // Default to "helm" if INSTALLATION_METHOD not set
248
+ const method = input.method ??
249
+ process.env.INSTALLATION_METHOD ??
250
+ "helm";
251
+ const base = {
252
+ version,
253
+ namespace: input.namespace ?? this.deploymentConfig.namespace,
254
+ auth: input.auth ?? "keycloak",
255
+ appConfig: input.appConfig ?? `tests/config/app-config-rhdh.yaml`,
256
+ secrets: input.secrets ?? `tests/config/rhdh-secrets.yaml`,
257
+ dynamicPlugins: input.dynamicPlugins ?? `tests/config/dynamic-plugins.yaml`,
258
+ };
259
+ if (method === "helm") {
260
+ return {
261
+ ...base,
262
+ method,
263
+ valueFile: input.valueFile ?? `tests/config/value_file.yaml`,
264
+ };
265
+ }
266
+ else if (method === "operator") {
267
+ return {
268
+ ...base,
269
+ method,
270
+ subscription: input.subscription ?? `tests/config/subscription.yaml`,
271
+ };
272
+ }
273
+ else {
274
+ throw new Error(`Invalid RHDH installation method: ${method}`);
275
+ }
276
+ }
277
+ async configure(deploymentOptions) {
278
+ if (deploymentOptions) {
279
+ this.deploymentConfig = this._buildDeploymentConfig(deploymentOptions);
280
+ this.rhdhUrl = this._buildBaseUrl();
281
+ this._log(`RHDH deployment initialized (namespace: ${this.deploymentConfig.namespace})`);
282
+ console.table(this.deploymentConfig);
283
+ }
284
+ await this.k8sClient.createNamespaceIfNotExists(this.deploymentConfig.namespace);
285
+ }
286
+ _buildBaseUrl() {
287
+ const prefix = this.deploymentConfig.method === "helm"
288
+ ? "redhat-developer-hub"
289
+ : "backstage-developer-hub";
290
+ const baseUrl = `https://${prefix}-${this.deploymentConfig.namespace}.${process.env.K8S_CLUSTER_ROUTER_BASE}`;
291
+ process.env.RHDH_BASE_URL = baseUrl;
292
+ return baseUrl;
293
+ }
294
+ _log(...args) {
295
+ console.log("[RHDHDeployment]", ...args);
296
+ }
297
+ _logBoxen(title, data) {
298
+ const content = yaml.dump(data, { lineWidth: -1 });
299
+ console.log(`\n┌─ ${title} ${"─".repeat(60)}`);
300
+ console.log(content);
301
+ console.log(`└${"─".repeat(60 + title.length + 3)}\n`);
302
+ }
303
+ }
@@ -0,0 +1,2 @@
1
+ export { RHDHDeployment } from "./deployment.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ export { RHDHDeployment } from "./deployment.js";
@@ -0,0 +1,31 @@
1
+ export type DeploymentMethod = "helm" | "operator";
2
+ export type AuthProvider = "guest" | "keycloak";
3
+ export type DeploymentOptions = {
4
+ version?: string;
5
+ namespace?: string;
6
+ auth?: AuthProvider;
7
+ appConfig?: string;
8
+ secrets?: string;
9
+ dynamicPlugins?: string;
10
+ method?: DeploymentMethod;
11
+ valueFile?: string;
12
+ subscription?: string;
13
+ };
14
+ export type HelmDeploymentConfig = {
15
+ method: "helm";
16
+ valueFile: string;
17
+ };
18
+ export type OperatorDeploymentConfig = {
19
+ method: "operator";
20
+ subscription: string;
21
+ };
22
+ export type DeploymentConfigBase = {
23
+ version: string;
24
+ namespace: string;
25
+ auth: AuthProvider;
26
+ appConfig: string;
27
+ secrets: string;
28
+ dynamicPlugins: string;
29
+ };
30
+ export type DeploymentConfig = DeploymentConfigBase & (HelmDeploymentConfig | OperatorDeploymentConfig);
31
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +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,CAAC;AAEhD,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;CACvB,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;CACxB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GACjD,CAAC,oBAAoB,GAAG,wBAAwB,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { Linter } from "eslint";
2
+ /**
3
+ * Creates a base ESLint configuration for RHDH E2E tests.
4
+ * This configuration includes TypeScript, Playwright, and file naming conventions.
5
+ *
6
+ * @param tsconfigRootDir - The root directory for tsconfig.json resolution
7
+ * @returns ESLint flat config array
8
+ */
9
+ export declare function createEslintConfig(tsconfigRootDir: string): Linter.Config[];
10
+ //# sourceMappingURL=base.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.config.d.ts","sourceRoot":"","sources":["../../src/eslint/base.config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CA4M3E"}
@@ -0,0 +1,213 @@
1
+ import js from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+ import checkFile from "eslint-plugin-check-file";
4
+ import playwright from "eslint-plugin-playwright";
5
+ /**
6
+ * Creates a base ESLint configuration for RHDH E2E tests.
7
+ * This configuration includes TypeScript, Playwright, and file naming conventions.
8
+ *
9
+ * @param tsconfigRootDir - The root directory for tsconfig.json resolution
10
+ * @returns ESLint flat config array
11
+ */
12
+ export function createEslintConfig(tsconfigRootDir) {
13
+ return [
14
+ // Global ignores - must be first for ESLint flat config
15
+ {
16
+ ignores: [
17
+ "node_modules/**",
18
+ "playwright-report/**",
19
+ "test-results/**",
20
+ "blob-report/**",
21
+ "*.config.js",
22
+ "dist/**",
23
+ ],
24
+ },
25
+ js.configs.recommended,
26
+ ...tseslint.configs.recommended,
27
+ {
28
+ files: ["**/*.ts"],
29
+ languageOptions: {
30
+ parserOptions: {
31
+ project: "./tsconfig.json",
32
+ tsconfigRootDir,
33
+ },
34
+ },
35
+ rules: {
36
+ // TypeScript naming conventions
37
+ "@typescript-eslint/naming-convention": [
38
+ "error",
39
+ {
40
+ selector: "variable",
41
+ format: ["camelCase", "PascalCase"],
42
+ leadingUnderscore: "allow",
43
+ },
44
+ {
45
+ selector: "variable",
46
+ modifiers: ["const"],
47
+ format: ["camelCase", "PascalCase", "UPPER_CASE"],
48
+ },
49
+ {
50
+ selector: "function",
51
+ format: ["camelCase", "PascalCase"],
52
+ },
53
+ {
54
+ selector: "parameter",
55
+ format: ["camelCase", "PascalCase"],
56
+ leadingUnderscore: "allow",
57
+ },
58
+ {
59
+ selector: "typeLike",
60
+ format: ["PascalCase"],
61
+ },
62
+ {
63
+ selector: "enumMember",
64
+ format: ["PascalCase"],
65
+ },
66
+ {
67
+ selector: "memberLike",
68
+ modifiers: ["private"],
69
+ format: ["camelCase"],
70
+ leadingUnderscore: "allow",
71
+ },
72
+ {
73
+ selector: "memberLike",
74
+ modifiers: ["public"],
75
+ format: ["camelCase"],
76
+ },
77
+ // Allow HTTP headers in object literals which require specific formats
78
+ {
79
+ selector: "objectLiteralProperty",
80
+ format: null,
81
+ filter: {
82
+ regex: "^(Accept|Authorization|Content-Type|X-GitHub-Api-Version|X-[A-Za-z-]+)$",
83
+ match: true,
84
+ },
85
+ },
86
+ ],
87
+ // Promise handling
88
+ "@typescript-eslint/no-floating-promises": "error",
89
+ "@typescript-eslint/await-thenable": "error",
90
+ "@typescript-eslint/no-misused-promises": "error",
91
+ // Allow any type in tests (for mocking, test data)
92
+ "@typescript-eslint/no-explicit-any": "warn",
93
+ // Prefer modern syntax
94
+ "@typescript-eslint/prefer-optional-chain": "error",
95
+ // Allow unused vars starting with underscore
96
+ "@typescript-eslint/no-unused-vars": [
97
+ "error",
98
+ {
99
+ argsIgnorePattern: "^_",
100
+ varsIgnorePattern: "^_",
101
+ },
102
+ ],
103
+ // Allow empty functions (for test stubs)
104
+ "@typescript-eslint/no-empty-function": "off",
105
+ },
106
+ },
107
+ {
108
+ files: ["**/*.{js,ts}"],
109
+ plugins: {
110
+ "check-file": checkFile,
111
+ },
112
+ rules: {
113
+ "check-file/filename-naming-convention": [
114
+ "error",
115
+ {
116
+ "**/*.{js,ts}": "KEBAB_CASE",
117
+ },
118
+ {
119
+ ignoreMiddleExtensions: true,
120
+ },
121
+ ],
122
+ "check-file/folder-naming-convention": [
123
+ "error",
124
+ {
125
+ "**": "KEBAB_CASE",
126
+ },
127
+ ],
128
+ },
129
+ },
130
+ // Playwright test files
131
+ {
132
+ ...playwright.configs["flat/recommended"],
133
+ files: [
134
+ "**/*.spec.ts",
135
+ "**/*.test.ts",
136
+ "**/tests/**/*.ts",
137
+ "**/e2e/**/*.ts",
138
+ ],
139
+ rules: {
140
+ ...playwright.configs["flat/recommended"].rules,
141
+ // Playwright best practices
142
+ "playwright/expect-expect": "warn",
143
+ "playwright/max-nested-describe": ["warn", { max: 2 }],
144
+ "playwright/missing-playwright-await": "error",
145
+ "playwright/no-conditional-in-test": "warn",
146
+ "playwright/no-element-handle": "warn",
147
+ "playwright/no-eval": "error",
148
+ "playwright/no-focused-test": "error",
149
+ "playwright/no-force-option": "warn",
150
+ "playwright/no-page-pause": "warn",
151
+ "playwright/no-skipped-test": [
152
+ "warn",
153
+ {
154
+ allowConditional: true,
155
+ },
156
+ ],
157
+ "playwright/no-useless-await": "warn",
158
+ "playwright/no-useless-not": "warn",
159
+ "playwright/no-wait-for-selector": "warn",
160
+ "playwright/no-wait-for-timeout": "warn",
161
+ "playwright/prefer-web-first-assertions": "error",
162
+ "playwright/require-top-level-describe": "off",
163
+ "playwright/valid-describe-callback": "off",
164
+ "playwright/valid-expect": "error",
165
+ "playwright/valid-title": "warn",
166
+ // Custom restrictions
167
+ "no-restricted-syntax": [
168
+ "error",
169
+ {
170
+ selector: "CallExpression[callee.property.name='fixme'][callee.object.property.name='describe'][callee.object.object.name='test']",
171
+ message: "test.describe.fixme() is not valid. Use test.fixme() on individual tests instead.",
172
+ },
173
+ ],
174
+ // Disallow console.log in tests (use test.info() instead)
175
+ "no-console": [
176
+ "warn",
177
+ {
178
+ allow: ["warn", "error"],
179
+ },
180
+ ],
181
+ },
182
+ },
183
+ // Page Object Models - require class suffix
184
+ {
185
+ files: ["**/page-objects/**/*.ts", "**/page/**/*.ts", "**/pages/**/*.ts"],
186
+ rules: {
187
+ "@typescript-eslint/naming-convention": [
188
+ "error",
189
+ {
190
+ selector: "class",
191
+ format: ["PascalCase"],
192
+ suffix: ["Page", "Component", "PO"],
193
+ },
194
+ ],
195
+ },
196
+ },
197
+ // Fixtures
198
+ {
199
+ files: ["**/fixtures/**/*.ts"],
200
+ rules: {
201
+ "@typescript-eslint/no-explicit-any": "off",
202
+ },
203
+ },
204
+ // Config files
205
+ {
206
+ files: ["playwright.config.ts", "**/*.config.ts"],
207
+ rules: {
208
+ "@typescript-eslint/naming-convention": "off",
209
+ "check-file/filename-naming-convention": "off",
210
+ },
211
+ },
212
+ ];
213
+ }
@@ -0,0 +1,14 @@
1
+ import { PlaywrightTestConfig } from "@playwright/test";
2
+ /**
3
+ * Base Playwright configuration that can be extended by workspace-specific configs.
4
+ * Provides sensible defaults for RHDH plugin e2e testing.
5
+ */
6
+ export declare const baseConfig: PlaywrightTestConfig;
7
+ /**
8
+ * Defines a workspace-specific config by merging with base config.
9
+ * Only allows overriding the projects configuration.
10
+ * @param overrides - Object containing projects to override
11
+ * @returns Merged Playwright configuration
12
+ */
13
+ export declare function defineConfig(overrides?: Pick<PlaywrightTestConfig, "projects">): PlaywrightTestConfig;
14
+ //# sourceMappingURL=base-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-config.d.ts","sourceRoot":"","sources":["../../src/playwright/base-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,oBA4BxB,CAAC;AAEF;;;;;GAKG;AAEH,wBAAgB,YAAY,CAC1B,SAAS,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACrD,oBAAoB,CAKtB"}