The RHDHDeployment class is the core class for managing RHDH deployments in OpenShift.
import { RHDHDeployment } from "@red-hat-developer-hub/e2e-test-utils/rhdh";
// Create deployment with namespace
const deployment = new RHDHDeployment("my-test-namespace");
// Configure options
await deployment.configure({
version: "1.5",
method: "helm",
auth: "keycloak",
});
// Deploy RHDH
await deployment.deploy();
// Access the deployed instance
console.log(`RHDH URL: ${deployment.rhdhUrl}`);When using the test fixtures, RHDHDeployment is automatically created:
import { test } from "@red-hat-developer-hub/e2e-test-utils/test";
test.beforeAll(async ({ rhdh }) => {
// rhdh is already instantiated with namespace from project name
await rhdh.configure({ auth: "keycloak" });
await rhdh.deploy(); // automatically skips if already deployed
});
test("example", async ({ rhdh }) => {
console.log(`URL: ${rhdh.rhdhUrl}`);
});| Option | Type | Description |
|---|---|---|
version |
string |
RHDH version (e.g., "1.5"). Defaults to RHDH_VERSION or "next" |
namespace |
string |
Kubernetes namespace. Set via constructor |
method |
"helm" | "operator" |
Installation method. Defaults to INSTALLATION_METHOD or "helm" |
auth |
"guest" | "keycloak" |
Authentication provider. Defaults to "keycloak" |
appConfig |
string |
Path to app-config YAML |
secrets |
string |
Path to secrets YAML |
dynamicPlugins |
string |
Path to dynamic-plugins YAML |
valueFile |
string |
Helm values file (Helm only) |
subscription |
string |
Backstage CR file (Operator only) |
await deployment.configure({
version: "1.5",
method: "helm",
auth: "keycloak",
appConfig: "tests/config/app-config-rhdh.yaml",
secrets: "tests/config/rhdh-secrets.yaml",
dynamicPlugins: "tests/config/dynamic-plugins.yaml",
valueFile: "tests/config/value_file.yaml",
});Prepare for deployment by creating the namespace and setting options:
await deployment.configure({
auth: "keycloak",
appConfig: "tests/config/app-config.yaml",
});Deploy RHDH to the cluster:
await deployment.deploy();The deploy() method accepts optional parameters:
| Option | Type | Default | Description |
|---|---|---|---|
timeout |
number | null |
600_000 |
Playwright test timeout (ms) during deployment |
force |
boolean |
false |
Force redeployment even if already deployed |
// Default (600s)
await rhdh.deploy();
// Custom timeout (15 minutes)
await rhdh.deploy({ timeout: 900_000 });
// No timeout (infinite)
await rhdh.deploy({ timeout: 0 });
// Skip — let the consumer control the timeout
test.setTimeout(900_000);
await rhdh.deploy({ timeout: null });deploy() automatically skips if the deployment already succeeded in the current test run (e.g., after a worker restart due to test failure). This prevents expensive re-deployments.
Use the force option to bypass the built-in runOnce protection and force a fresh deployment. This is useful for complex test scenarios where multiple describe sections need different RHDH configurations (different app configs or dynamic plugin sets) within the same test file:
test.describe("Plugin Set A", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({
auth: "keycloak",
dynamicPlugins: "tests/config/plugins-set-a.yaml",
});
await rhdh.deploy(); // First deployment
});
test("test with plugin set A", async ({ page }) => {
// Tests using plugin set A
});
});
test.describe("Plugin Set B", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({
auth: "keycloak",
dynamicPlugins: "tests/config/plugins-set-b.yaml",
});
await rhdh.deploy({ force: true }); // Force redeploy with new config
});
test("test with plugin set B", async ({ page }) => {
// Tests using plugin set B
});
});This method:
- Merges configuration files (common → auth → project)
- Injects plugin metadata into dynamic plugins config
- Applies ConfigMaps (app-config, dynamic-plugins)
- Applies Secrets (with environment variable substitution)
- Installs RHDH via Helm or Operator
- Waits for the deployment to be ready
- Sets
RHDH_BASE_URLenvironment variable
The base URL prefix depends on the installation method:
- Helm:
https://redhat-developer-hub-<namespace>.<cluster> - Operator:
https://backstage-developer-hub-<namespace>.<cluster>
Helm deployments perform a scale-down and restart after applying configs to avoid migration locks.
Operator deployments accept only:
- Semantic versions like
1.5 "next"
Any other value will throw an error during deployment.
Wait for the RHDH deployment to be ready. Performs two-phase readiness check:
- Pod readiness — Waits for all pods to have
Ready=Truewith early failure detection (CrashLoopBackOff, ImagePullBackOff, etc.) - Route readiness — HTTP health check against the RHDH route, closing the gap between pod readiness and the OpenShift Router actually serving traffic
// Default timeout: 500 seconds (~8 minutes)
await deployment.waitUntilReady();
// Custom timeout
await deployment.waitUntilReady(600); // 10 minutesRestart the RHDH deployment (useful after config changes):
// Update configuration
await deployment.k8sClient.applyConfigMapFromObject(
"app-config-rhdh",
{ newConfig: "value" },
deployment.deploymentConfig.namespace
);
// Restart to apply changes
await deployment.rolloutRestart();Delete the namespace and all resources:
await deployment.teardown();::: warning You typically don't need to call this manually. In CI, the built-in teardown reporter automatically deletes namespaces after all tests complete. See Namespace Cleanup. :::
The URL of the deployed RHDH instance:
const url = deployment.rhdhUrl;
// e.g., "https://backstage-my-namespace.apps.cluster.example.com"The current deployment configuration:
const config = deployment.deploymentConfig;
console.log(config.namespace); // "my-namespace"
console.log(config.version); // "1.5"
console.log(config.method); // "helm"
console.log(config.auth); // "keycloak"The Kubernetes client instance for direct cluster operations:
const k8s = deployment.k8sClient;
// Get route URL
const url = await k8s.getRouteLocation(
deployment.deploymentConfig.namespace,
"my-route"
);
// Apply custom ConfigMap
await k8s.applyConfigMapFromObject(
"my-config",
{ key: "value" },
deployment.deploymentConfig.namespace
);Configuration files are looked for in:
tests/config/
├── app-config-rhdh.yaml
├── dynamic-plugins.yaml
└── rhdh-secrets.yaml
Or specify custom paths:
await deployment.configure({
appConfig: "custom/path/app-config.yaml",
secrets: "custom/path/secrets.yaml",
dynamicPlugins: "custom/path/plugins.yaml",
});- Common configs (
package/config/common/) - Auth configs (
package/config/auth/{guest|keycloak}/) - Project configs (your
tests/config/files)
Later files override earlier ones, allowing you to customize only what you need.
import { test } from "@red-hat-developer-hub/e2e-test-utils/test";
import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils";
test.beforeAll(async ({ rhdh }) => {
// Wrap in test.runOnce because the setup script is also expensive
await test.runOnce("my-plugin-setup", async () => {
const namespace = rhdh.deploymentConfig.namespace;
// Configure RHDH
await rhdh.configure({ auth: "keycloak" });
// Run custom setup before deployment
await $`bash scripts/setup.sh ${namespace}`;
// Set runtime environment variables
process.env.MY_CUSTOM_URL = await rhdh.k8sClient.getRouteLocation(
namespace,
"my-service"
);
// Deploy RHDH (has built-in protection, safe to nest inside runOnce)
await rhdh.deploy();
});
});