From 629e8080dc45cafb414bd918391a18893fd38a91 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Sun, 19 Apr 2026 03:34:55 +0300 Subject: [PATCH 1/4] feat(deploy): add force deploy option Signed-off-by: Oleksandr Andriienko --- src/deployment/rhdh/deployment.ts | 55 ++++++++++++++++++------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/deployment/rhdh/deployment.ts b/src/deployment/rhdh/deployment.ts index 9f8093c..9c86d6e 100644 --- a/src/deployment/rhdh/deployment.ts +++ b/src/deployment/rhdh/deployment.ts @@ -39,40 +39,49 @@ export class RHDHDeployment { this.rhdhUrl = this._buildBaseUrl(); } - async deploy(options?: { timeout?: number | null }): Promise { + async deploy(options?: { timeout?: number | null, forceUpdate?: boolean }): Promise { // Default 600s, custom number to override, null to skip and let consumer control the timeout const timeout = options?.timeout === undefined ? 600_000 : options.timeout; if (timeout !== null) { test.setTimeout(timeout); } - const executed = await runOnce( - `deploy-${this.deploymentConfig.namespace}`, - async () => { - this._log("Starting RHDH deployment..."); - this._log("RHDH Base URL: " + this.rhdhUrl); - console.table(this.deploymentConfig); + const deployFunc = async () => { + this._log("Starting RHDH deployment..."); + this._log("RHDH Base URL: " + this.rhdhUrl); + console.table(this.deploymentConfig); - await this.k8sClient.createNamespaceIfNotExists( - this.deploymentConfig.namespace, - ); + await this.k8sClient.createNamespaceIfNotExists( + this.deploymentConfig.namespace, + ); - await this._applyAppConfig(); - await this._applySecrets(); + await this._applyAppConfig(); + await this._applySecrets(); - if (this.deploymentConfig.method === "helm") { - const isUpgrade = await this._deploymentExists(); + if (this.deploymentConfig.method === "helm") { + const isUpgrade = await this._deploymentExists(); await this._deployWithHelm(this.deploymentConfig.valueFile); - if (isUpgrade) { - await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes - } - } else { - await this._applyDynamicPlugins(); - await this._deployWithOperator(this.deploymentConfig.subscription); + if (isUpgrade) { + await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes } - await this.waitUntilReady(); - }, - ); + } else { + await this._applyDynamicPlugins(); + await this._deployWithOperator(this.deploymentConfig.subscription); + } + await this.waitUntilReady(); + }; + + let executed = false; + + if (options?.forceUpdate) { + await deployFunc(); + executed = true; + } else { + executed = await runOnce( + `deploy-${this.deploymentConfig.namespace}`, + deployFunc + ); + } if (!executed) { this._log( From f01448a19a1a481c15f94776c39c6d37084a3c76 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Mon, 20 Apr 2026 04:22:58 +0300 Subject: [PATCH 2/4] feat(deploy): update version, docs, changeset Signed-off-by: Oleksandr Andriienko --- docs/.vitepress/config.ts | 2 +- docs/api/deployment/rhdh-deployment.md | 7 +++- docs/api/playwright/test-fixtures.md | 10 ++++- docs/changelog.md | 8 +++- .../core-concepts/playwright-fixtures.md | 32 +++++++++++++++ docs/guide/deployment/rhdh-deployment.md | 41 ++++++++++++++++++- package.json | 2 +- 7 files changed, 96 insertions(+), 6 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 2aacaf0..29ca87f 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -33,7 +33,7 @@ export default defineConfig({ { text: "Examples", link: "/examples/" }, { text: "Overlay Testing", link: "/overlay/" }, { - text: "v1.1.30", + text: "v1.1.32", items: [{ text: "Changelog", link: "/changelog" }], }, ], diff --git a/docs/api/deployment/rhdh-deployment.md b/docs/api/deployment/rhdh-deployment.md index 085dfd8..6e14eec 100644 --- a/docs/api/deployment/rhdh-deployment.md +++ b/docs/api/deployment/rhdh-deployment.md @@ -70,7 +70,7 @@ await rhdh.configure({ ### `deploy()` ```typescript -async deploy(options?: { timeout?: number | null }): Promise +async deploy(options?: { timeout?: number | null; force?: boolean }): Promise ``` Deploy RHDH to the cluster. This: @@ -83,6 +83,7 @@ Deploy RHDH to the cluster. This: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `options.timeout` | `number \| null` | `600_000` | Playwright test timeout (ms) for the deployment. Pass a custom number to override, `0` for no timeout, or `null` to skip and let the consumer control the timeout. | +| `options.force` | `boolean` | `false` | Force redeployment even if already deployed. Bypasses the built-in `runOnce` protection. Useful for complex test scenarios where multiple `describe` sections need different RHDH configurations. | ```typescript // Default (600s timeout) @@ -97,6 +98,10 @@ await rhdh.deploy({ timeout: 0 }); // Skip — consumer controls the timeout test.setTimeout(900_000); await rhdh.deploy({ timeout: null }); + +// Force redeploy with new configuration +await rhdh.configure({ dynamicPlugins: "tests/config/new-plugins.yaml" }); +await rhdh.deploy({ force: true }); ``` ### `waitUntilReady()` diff --git a/docs/api/playwright/test-fixtures.md b/docs/api/playwright/test-fixtures.md index 605d2bf..e05aa56 100644 --- a/docs/api/playwright/test-fixtures.md +++ b/docs/api/playwright/test-fixtures.md @@ -16,7 +16,7 @@ import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test"; **Type:** `RHDHDeployment` -Shared RHDH deployment across all tests in a worker. `deploy()` automatically skips if the deployment already succeeded, even after worker restarts. +Shared RHDH deployment across all tests in a worker. `deploy()` automatically skips if the deployment already succeeded, even after worker restarts. Use `deploy({ force: true })` to bypass this protection when you need to redeploy with different configurations in the same test file. ```typescript test.beforeAll(async ({ rhdh }) => { @@ -28,6 +28,14 @@ test("access rhdh", async ({ rhdh }) => { console.log(rhdh.rhdhUrl); console.log(rhdh.deploymentConfig.namespace); }); + +// Force redeploy with new config +test.describe("Different Config", () => { + test.beforeAll(async ({ rhdh }) => { + await rhdh.configure({ appConfig: "tests/config/new-app-config-rhdh.yaml" }); + await rhdh.deploy({ force: true }); + }); +}); ``` ### `uiHelper` diff --git a/docs/changelog.md b/docs/changelog.md index bf7ced9..4d7ce42 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,13 @@ All notable changes to this project will be documented in this file. -## [1.1.31] - Current +## [1.1.32] - Current + +### Added + +- **Force redeploy option for `rhdh.deploy()`**: Added optional `force` parameter to `rhdh.deploy({ force: true })` to bypass the built-in `runOnce` protection and force a fresh deployment. This enables complex test scenarios where multiple `describe` sections need different RHDH configurations (different app configs or dynamic plugin sets) within the same test file. Test writers can now call `rhdh.configure()` with new settings and `rhdh.deploy({ force: true })` to redeploy with the desired configuration. + +## [1.1.31] ### Fixed diff --git a/docs/guide/core-concepts/playwright-fixtures.md b/docs/guide/core-concepts/playwright-fixtures.md index accda7a..6fa0019 100644 --- a/docs/guide/core-concepts/playwright-fixtures.md +++ b/docs/guide/core-concepts/playwright-fixtures.md @@ -171,6 +171,38 @@ test.beforeAll(async ({ rhdh }) => { Playwright's `beforeAll` runs once **per worker**, not once per test run. When a test fails, Playwright kills the worker and creates a new one for remaining tests — causing `beforeAll` to run again. Without protection, this would re-deploy RHDH from scratch every time a test fails. ::: +### Bypassing Protection with `force` + +For complex test scenarios where multiple `describe` sections need different RHDH configurations (different app configs or dynamic plugin sets), you can use the `force` option to bypass the built-in protection: + +```typescript +test.describe("Plugin Set A", () => { + test.beforeAll(async ({ rhdh }) => { + await rhdh.configure({ + 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({ + 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 + }); +}); +``` + ## `test.runOnce` — Run Any Expensive Operation Once While `rhdh.deploy()` has built-in protection, you may have **other expensive operations** in your `beforeAll` that also shouldn't repeat on worker restart — deploying external services, seeding databases, running setup scripts, etc. diff --git a/docs/guide/deployment/rhdh-deployment.md b/docs/guide/deployment/rhdh-deployment.md index 985152e..92fbdf9 100644 --- a/docs/guide/deployment/rhdh-deployment.md +++ b/docs/guide/deployment/rhdh-deployment.md @@ -93,7 +93,12 @@ Deploy RHDH to the cluster: await deployment.deploy(); ``` -The `deploy()` method accepts an optional `{ timeout }` parameter to control the Playwright test timeout during deployment. By default, it sets the timeout to 600 seconds (10 minutes). +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 | ```typescript // Default (600s) @@ -112,6 +117,40 @@ 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. +#### Force Redeploy + +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: + +```typescript +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: 1. Merges configuration files (common → auth → project) 2. [Injects plugin metadata](/guide/configuration/config-files#plugin-metadata-injection) into dynamic plugins config diff --git a/package.json b/package.json index 88a80e3..b380d9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@red-hat-developer-hub/e2e-test-utils", - "version": "1.1.31", + "version": "1.1.32", "description": "Test utilities for RHDH E2E tests", "license": "Apache-2.0", "repository": { From 0f411beb04e4df575d789007519a6da60123542b Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Mon, 20 Apr 2026 04:32:28 +0300 Subject: [PATCH 3/4] feat(deploy): fixup Signed-off-by: Oleksandr Andriienko --- src/deployment/rhdh/deployment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deployment/rhdh/deployment.ts b/src/deployment/rhdh/deployment.ts index 9c86d6e..ac7c0b1 100644 --- a/src/deployment/rhdh/deployment.ts +++ b/src/deployment/rhdh/deployment.ts @@ -71,8 +71,8 @@ export class RHDHDeployment { await this.waitUntilReady(); }; - let executed = false; - + let executed: boolean; + if (options?.forceUpdate) { await deployFunc(); executed = true; From 7bd83322b9a4a67db71e2ae678dc8b6ddb8bdc23 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Mon, 20 Apr 2026 04:39:59 +0300 Subject: [PATCH 4/4] feat(deploy): fixup Signed-off-by: Oleksandr Andriienko --- src/deployment/rhdh/deployment.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/deployment/rhdh/deployment.ts b/src/deployment/rhdh/deployment.ts index ac7c0b1..38b67c6 100644 --- a/src/deployment/rhdh/deployment.ts +++ b/src/deployment/rhdh/deployment.ts @@ -39,7 +39,10 @@ export class RHDHDeployment { this.rhdhUrl = this._buildBaseUrl(); } - async deploy(options?: { timeout?: number | null, forceUpdate?: boolean }): Promise { + async deploy(options?: { + timeout?: number | null; + forceUpdate?: boolean; + }): Promise { // Default 600s, custom number to override, null to skip and let consumer control the timeout const timeout = options?.timeout === undefined ? 600_000 : options.timeout; if (timeout !== null) { @@ -60,12 +63,12 @@ export class RHDHDeployment { if (this.deploymentConfig.method === "helm") { const isUpgrade = await this._deploymentExists(); - await this._deployWithHelm(this.deploymentConfig.valueFile); + await this._deployWithHelm(this.deploymentConfig.valueFile); if (isUpgrade) { await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes } } else { - await this._applyDynamicPlugins(); + await this._applyDynamicPlugins(); await this._deployWithOperator(this.deploymentConfig.subscription); } await this.waitUntilReady(); @@ -79,7 +82,7 @@ export class RHDHDeployment { } else { executed = await runOnce( `deploy-${this.deploymentConfig.namespace}`, - deployFunc + deployFunc, ); }