From 4a9e114186bc840bf473e40326b45a9fa74ecd22 Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Tue, 23 Jun 2026 15:43:44 -0300 Subject: [PATCH 1/8] feat(e2e): add cluster-free local E2E harness (legacy Tier B + app-next) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run real Playwright E2E against RHDH without an OpenShift/Kubernetes cluster or container images — Playwright boots the backend and a frontend dev server in-process and drives the browser against them. - Legacy harness (Tier B, recommended): targets packages/app with dynamic plugins loaded via Scalprum, so the EXISTING specs run unmodified. Verified the production RHDH home page (Quick Access from the dynamic home-page plugin) renders off-cluster and the guest-signin home-page test passes. - app-next harness: targets the new frontend system; covers core/statically registered plugin UIs (dynamic frontend loading is blocked upstream — see doc). - Shared guest-auth + in-memory-SQLite overlay (app-config.local-e2e.yaml); webServer invokes backstage-cli/janus-cli from the repo-root .bin. - yarn scripts: e2e:legacy-local, e2e:app-next-local. Part of RHIDP-13501 (E2E Test Optimization), Layer 4a spike RHIDP-15075. Co-Authored-By: Claude Opus 4.8 (1M context) --- app-config.local-e2e.yaml | 25 +++++ docs/e2e-tests/local-e2e-harness.md | 105 ++++++++++++++++++ e2e-tests/package.json | 4 +- e2e-tests/playwright.app-next-local.config.ts | 94 ++++++++++++++++ e2e-tests/playwright.legacy-local.config.ts | 105 ++++++++++++++++++ .../app-next-local/guest-identity.spec.ts | 49 ++++++++ 6 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 app-config.local-e2e.yaml create mode 100644 docs/e2e-tests/local-e2e-harness.md create mode 100644 e2e-tests/playwright.app-next-local.config.ts create mode 100644 e2e-tests/playwright.legacy-local.config.ts create mode 100644 e2e-tests/playwright/app-next-local/guest-identity.spec.ts diff --git a/app-config.local-e2e.yaml b/app-config.local-e2e.yaml new file mode 100644 index 0000000000..2c2737c1ea --- /dev/null +++ b/app-config.local-e2e.yaml @@ -0,0 +1,25 @@ +# Config overlay shared by the cluster-free local E2E harnesses (legacy `packages/app` +# and new `packages/app-next`). +# +# Layered on top of app-config.yaml (and, for the legacy Tier B harness, on top of +# app-config.dynamic-plugins.yaml) to run Playwright E2E without an +# OpenShift/Kubernetes cluster or container images: +# +# yarn --cwd e2e-tests e2e:legacy-local # legacy app + dynamic plugins (Tier B) +# yarn --cwd e2e-tests e2e:app-next-local # new frontend system +# +# It enables guest sign-in (the auth backend rejects guest unless a provider is +# configured) and pins the in-memory SQLite database so a single `run` is fully +# self-contained. See docs/e2e-tests/local-e2e-harness.md. +auth: + environment: development + providers: + guest: + userEntityRef: user:default/guest + # Required because auth.environment may resolve outside "development" in CI. + dangerouslyAllowOutsideDevelopment: true + +backend: + database: + client: better-sqlite3 + connection: ":memory:" diff --git a/docs/e2e-tests/local-e2e-harness.md b/docs/e2e-tests/local-e2e-harness.md new file mode 100644 index 0000000000..0cde1b2151 --- /dev/null +++ b/docs/e2e-tests/local-e2e-harness.md @@ -0,0 +1,105 @@ +# Cluster-free local E2E harness + +Spike deliverable for **RHIDP-13501 — E2E Test Optimization (Layer 4a)**, building on +the PoC in [PR #4523](https://github.com/redhat-developer/rhdh/pull/4523) and the +backend dynamic-plugin loader from RHIDP-13508. + +## Goal + +Run real Playwright E2E against RHDH **without** an OpenShift/Kubernetes cluster or +container images — a single `run` that boots the backend and a frontend dev server +in-process and drives a browser against them. + +Two harnesses are provided: + +| Harness | Target | Command | Status | +|---------|--------|---------|--------| +| **Legacy (Tier B)** | `packages/app` (Scalprum) + dynamic plugins | `yarn --cwd e2e-tests e2e:legacy-local` | Production-faithful; runs the **existing** specs | +| **app-next** | `packages/app-next` (new frontend system) | `yarn --cwd e2e-tests e2e:app-next-local` | Forward-looking; core app only (see limits) | + +Both layer the guest-auth + in-memory-SQLite overlay `app-config.local-e2e.yaml` on +top of `app-config.yaml`. Guest sign-in must be configured explicitly — the auth +backend otherwise rejects guest with _"you must … configure the auth backend to +support guest sign in."_ + +## Legacy harness (Tier B) — recommended + +This is the production-faithful target: it is what RHDH ships today, and **the existing +Playwright specs already target it**, so they run unmodified. Dynamic frontend plugins +load through Scalprum exactly as in-cluster (the legacy `scalprum-backend` serves the +plugin config by default). + +### 1. Populate `dynamic-plugins-root` (one-time) + +Production-faithful — full plugin set and generated config, the same source CI uses: + +```bash +CATALOG_INDEX_IMAGE= \ + npx @red-hat-developer-hub/cli-module-install-dynamic-plugins install dynamic-plugins-root +``` + +Offline alternative (frontend plugins only; requires a reconciled workspace — +see "Known issues"): + +```bash +yarn --cwd dynamic-plugins export-dynamic +yarn --cwd dynamic-plugins copy-dynamic-plugins ../dynamic-plugins-root +``` + +### 2. Run + +```bash +yarn --cwd e2e-tests e2e:legacy-local +``` + +Playwright (`playwright.legacy-local.config.ts`) boots the backend and the legacy app +dev server with `app-config.yaml` + `app-config.dynamic-plugins.yaml` + +`app-config.local-e2e.yaml`, then runs the UI specs in `testMatch` that do not require +live external services. + +### Verified + +With plugins populated, the legacy app renders the full production RHDH UI off-cluster +(branding, sidebar, and Quick Access from the dynamic home-page plugin). The existing +`guest-signin-happy-path` **home-page test passes unmodified** — confirming a dynamic +frontend plugin renders with no cluster. + +## app-next harness + +`playwright.app-next-local.config.ts` + `playwright/app-next-local/guest-identity.spec.ts`. +Boots the backend + app-next dev server with guest auth. Cold start ~17–20s (warm +rspack cache); ~3s reusing servers; stable; clean teardown. + +**Limit — dynamic frontend plugins do not load on app-next yet.** app-next uses +`dynamicFrontendFeaturesLoader()` → `GET //remotes`, served by +`dynamicPluginsFrontendServiceRef`, which the RHDH backend no-ops unless +`ENABLE_STANDARD_MODULE_FEDERATION=true` — and even then returns 404 because RHDH's +exported dynamic frontend plugins do not contain standard Module Federation assets by +default (see `packages/backend/src/index.ts`). app-next also has no Home page. So +app-next currently covers core/statically-registered plugin UIs (e.g. user-settings) +only; the legacy harness is the way to exercise dynamic plugins off-cluster today. + +## vs. rhdh-local + +[`rhdh-local`](https://github.com/redhat-developer/rhdh-local) runs RHDH via +Podman/Docker Compose using the **production container image**. It is great for manual +feature testing with guest auth and UI-installed plugins, but it is **container-based**: +it requires a container runtime and pulling/running the RHDH image. For fast automated +E2E it is heavier than this in-process harness (no image pull, no container runtime — +just `run`), which is why this harness boots the dev servers directly instead. + +## Known issues / limits + +- **Workspace must be reconciled for the offline (from-source) populate path.** If + `node_modules` is out of sync with `yarn.lock` (e.g. just after a rebase that changed + dependency versions), backend dynamic-plugin builds fail with version-mismatch errors + and yarn may not surface workspace bins. Run `yarn install` first. The + `install-dynamic-plugins` populate path avoids building from source and is unaffected. +- **`global-header` plugin mounting** still needs config sorting for the legacy harness; + specs that navigate via the top-right profile dropdown depend on it. +- **Live-external-service specs** (real k8s cluster, GitHub org, Quay, Tekton, Keycloak) + still need those services or mocks; this harness covers UI/plugin-rendering scenarios + that don't require live external infra. +- **`janus-cli` / `backstage-cli`** live in the repo-root `node_modules/.bin`, which yarn + does not surface for the `app`/`backend` workspaces, so the webServer commands invoke + them directly with the root `.bin` prepended to `PATH`. diff --git a/e2e-tests/package.json b/e2e-tests/package.json index a23c85e716..d81c78e172 100644 --- a/e2e-tests/package.json +++ b/e2e-tests/package.json @@ -30,7 +30,9 @@ "tsc:check": "tsc -p tsconfig.json", "shellcheck": "git ls-files -z '*.sh' | xargs -0 shellcheck --severity=warning --color=always", "prettier:check": "prettier --ignore-unknown --check .", - "prettier:fix": "prettier --ignore-unknown --write ." + "prettier:fix": "prettier --ignore-unknown --write .", + "e2e:legacy-local": "playwright test --config=playwright.legacy-local.config.ts", + "e2e:app-next-local": "playwright test --config=playwright.app-next-local.config.ts" }, "devDependencies": { "@axe-core/playwright": "4.11.2", diff --git a/e2e-tests/playwright.app-next-local.config.ts b/e2e-tests/playwright.app-next-local.config.ts new file mode 100644 index 0000000000..a899459600 --- /dev/null +++ b/e2e-tests/playwright.app-next-local.config.ts @@ -0,0 +1,94 @@ +import { defineConfig, devices } from "@playwright/test"; +import { resolve } from "path"; + +/** + * Cluster-free local E2E harness for the new frontend system (`packages/app-next`). + * + * Layer 4a spike (RHIDP-13501): run real Playwright E2E against RHDH without an + * OpenShift/Kubernetes cluster or container images. Playwright boots the backend + * and the app-next dev server itself, then drives the browser against them. + * See docs/e2e-tests/local-e2e-harness.md (note: dynamic frontend plugins do not + * load on app-next yet — use the legacy harness for those). + * + * yarn e2e:app-next-local + * + * Both servers are started via `webServer` below with the guest-auth overlay + * `app-config.local-e2e.yaml`. Locally, an already-running pair of servers + * is reused (`reuseExistingServer`); in CI they are always started fresh. + * + * `backstage-cli` lives in the repo-root node_modules/.bin, which yarn does not + * surface for these workspaces, so both CLIs are invoked directly with the root + * .bin prepended to PATH and run from their package directory. + */ + +const frontendUrl = "http://localhost:3000"; +const backendReadiness = "http://localhost:7007/.backstage/health/v1/readiness"; +const repoRootBin = resolve(process.cwd(), "..", "node_modules", ".bin"); + +export default defineConfig({ + testDir: "./playwright/app-next-local", + timeout: 90 * 1000, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [ + ["list"], + [ + "html", + { open: "never", outputFolder: "playwright-report-app-next-local" }, + ], + [ + "junit", + { + outputFile: + process.env.JUNIT_RESULTS || "junit-results-app-next-local.xml", + }, + ], + ], + use: { + baseURL: frontendUrl, + ignoreHTTPSErrors: true, + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, + actionTimeout: 15 * 1000, + navigationTimeout: 60 * 1000, + }, + expect: { + timeout: 15 * 1000, + }, + // Two local servers, no cluster. `--config` paths are resolved relative to each + // package dir (where backstage-cli runs), hence the `../../` prefix. + webServer: [ + { + command: + "backstage-cli package start --require ./src/instrumentation.js " + + "--config ../../app-config.yaml --config ../../app-config.local-e2e.yaml", + cwd: "../packages/backend", + env: { + ...process.env, + PATH: `${repoRootBin}:${process.env.PATH}`, + NODE_OPTIONS: "--no-node-snapshot", + }, + url: backendReadiness, + reuseExistingServer: !process.env.CI, + timeout: 180 * 1000, + stdout: "pipe", + stderr: "pipe", + }, + { + command: + "backstage-cli package start --config ../../app-config.yaml " + + "--config ../../app-config.local-e2e.yaml", + cwd: "../packages/app-next", + env: { ...process.env, PATH: `${repoRootBin}:${process.env.PATH}` }, + url: frontendUrl, + reuseExistingServer: !process.env.CI, + timeout: 180 * 1000, + stdout: "pipe", + stderr: "pipe", + }, + ], +}); diff --git a/e2e-tests/playwright.legacy-local.config.ts b/e2e-tests/playwright.legacy-local.config.ts new file mode 100644 index 0000000000..88e15424e4 --- /dev/null +++ b/e2e-tests/playwright.legacy-local.config.ts @@ -0,0 +1,105 @@ +import { defineConfig, devices } from "@playwright/test"; +import { resolve } from "path"; + +/** + * Cluster-free local E2E harness for the legacy frontend (`packages/app`) — Tier B. + * + * RHIDP-13501 (E2E Test Optimization). Runs the EXISTING Playwright specs against a + * production-faithful RHDH instance with dynamic plugins loaded, without an + * OpenShift/Kubernetes cluster or container images. Playwright boots the backend and + * the legacy app dev server itself and drives the browser against them. + * + * # one-time: populate dynamic-plugins-root (production-faithful — full plugin set + * # and generated config, same source CI uses): + * CATALOG_INDEX_IMAGE= \ + * npx @red-hat-developer-hub/cli-module-install-dynamic-plugins install dynamic-plugins-root + * # (offline alternative, frontend plugins only, needs reconciled deps: + * # yarn --cwd dynamic-plugins export-dynamic && \ + * # yarn --cwd dynamic-plugins copy-dynamic-plugins ../dynamic-plugins-root) + * + * yarn --cwd e2e-tests e2e:legacy-local + * + * Both servers are started via `webServer` with the guest-auth overlay + * `app-config.local-e2e.yaml` plus the dynamic-plugins UI config. An already-running + * pair of servers is reused locally; in CI they are started fresh. + * + * `janus-cli` (legacy app dev server) lives in the repo-root node_modules/.bin, which + * yarn does not surface for the `app` workspace, so the frontend webServer prepends it + * to PATH explicitly. + */ + +const frontendUrl = "http://localhost:3000"; +const backendReadiness = "http://localhost:7007/.backstage/health/v1/readiness"; +const repoRootBin = resolve(process.cwd(), "..", "node_modules", ".bin"); + +const sharedConfigArgs = + "--config ../../app-config.yaml " + + "--config ../../app-config.dynamic-plugins.yaml " + + "--config ../../app-config.local-e2e.yaml"; + +export default defineConfig({ + testDir: "./playwright", + // Existing UI specs that do not require live external services (cluster, GitHub + // org, Quay, Tekton, Keycloak). Expand as more specs are validated off-cluster. + testMatch: [ + "e2e/guest-signin-happy-path.spec.ts", + "e2e/settings.spec.ts", + "e2e/learning-path-page.spec.ts", + "e2e/home-page-customization.spec.ts", + ], + timeout: 90 * 1000, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [ + ["list"], + ["html", { open: "never", outputFolder: "playwright-report-legacy-local" }], + [ + "junit", + { outputFile: process.env.JUNIT_RESULTS || "junit-results-legacy-local.xml" }, + ], + ], + use: { + baseURL: frontendUrl, + ignoreHTTPSErrors: true, + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, + actionTimeout: 15 * 1000, + navigationTimeout: 60 * 1000, + }, + expect: { + timeout: 15 * 1000, + }, + // backstage-cli / janus-cli live in the repo-root node_modules/.bin, which yarn does + // not surface for these workspaces, so both CLIs are invoked directly with the root + // .bin prepended to PATH and run from their package directory. + webServer: [ + { + command: `backstage-cli package start --require ./src/instrumentation.js ${sharedConfigArgs}`, + cwd: "../packages/backend", + env: { + ...process.env, + PATH: `${repoRootBin}:${process.env.PATH}`, + NODE_OPTIONS: "--no-node-snapshot", + }, + url: backendReadiness, + reuseExistingServer: !process.env.CI, + timeout: 180 * 1000, + stdout: "pipe", + stderr: "pipe", + }, + { + command: `janus-cli package start ${sharedConfigArgs}`, + cwd: "../packages/app", + env: { ...process.env, PATH: `${repoRootBin}:${process.env.PATH}` }, + url: frontendUrl, + reuseExistingServer: !process.env.CI, + timeout: 240 * 1000, + stdout: "pipe", + stderr: "pipe", + }, + ], +}); diff --git a/e2e-tests/playwright/app-next-local/guest-identity.spec.ts b/e2e-tests/playwright/app-next-local/guest-identity.spec.ts new file mode 100644 index 0000000000..a14de35ed6 --- /dev/null +++ b/e2e-tests/playwright/app-next-local/guest-identity.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from "@support/coverage/test"; + +/** + * Slice 1 of the cluster-free local E2E harness (Layer 4a spike, RHIDP-13501). + * + * Proves the full local stack works without a cluster: the app-next dev server + * and the backend are booted by Playwright (see playwright.app-next-local.config.ts), + * guest sign-in succeeds, and a real plugin page (Settings) renders the guest + * identity served by the backend. + * + * NOTE: assertions target what `packages/app-next` actually renders. The new + * frontend system registers catalog/scaffolder/search/user-settings/visualizer + * and has no Home page yet, so this spec deliberately does not assert the legacy + * "Welcome back!" landing page used by the `packages/app` E2E specs. + */ +test.describe("app-next local — guest sign-in", () => { + test.beforeAll(async () => { + test.info().annotations.push({ + type: "component", + description: "authentication", + }); + }); + + test.beforeEach(async ({ page }) => { + await page.goto("/"); + // New-frontend-system guest provider card. + await page.getByRole("button", { name: "Enter", exact: true }).click(); + // Sidebar appears once signed in. + await expect(page.getByRole("link", { name: "Settings" })).toBeVisible(); + }); + + test("signs in as guest and reaches an authenticated page", async ({ + page, + }) => { + await expect(page.getByRole("link", { name: "Catalog" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Search" })).toBeVisible(); + }); + + test("Settings page shows the guest Backstage identity", async ({ page }) => { + await page.getByRole("link", { name: "Settings" }).click(); + await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible(); + // "Backstage Identity" is an InfoCard title (not a heading role). + await expect(page.getByText("Backstage Identity")).toBeVisible(); + await expect(page.getByText("User Entity:")).toBeVisible(); + await expect( + page.getByRole("link", { name: "guest" }).first(), + ).toBeVisible(); + }); +}); From bfbe9dc66449cabceceb41183954d4cfc4c21de6 Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Tue, 23 Jun 2026 15:55:23 -0300 Subject: [PATCH 2/8] refactor(e2e): address review on local harness - Scope legacy testMatch to the spec verified to pass off-cluster (learning-path-page); document the others as pending the global-header mount fix / per-spec config so the default run is green. - Guard PATH interpolation against an undefined process.env.PATH via a shared pathWithRepoBin constant in both configs. - Default CATALOG_INDEX_IMAGE to quay.io/rhdh/plugin-catalog-index:latest for main (release branches use the matching :1.y tag). - Note that the guest-auth overlay is test-only and must never reach a production config; drop a duplicated comment. Co-Authored-By: Claude Opus 4.8 (1M context) --- app-config.local-e2e.yaml | 4 +++ docs/e2e-tests/local-e2e-harness.md | 3 ++- e2e-tests/playwright.app-next-local.config.ts | 5 ++-- e2e-tests/playwright.legacy-local.config.ts | 27 +++++++++---------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app-config.local-e2e.yaml b/app-config.local-e2e.yaml index 2c2737c1ea..1a17686238 100644 --- a/app-config.local-e2e.yaml +++ b/app-config.local-e2e.yaml @@ -11,6 +11,10 @@ # It enables guest sign-in (the auth backend rejects guest unless a provider is # configured) and pins the in-memory SQLite database so a single `run` is fully # self-contained. See docs/e2e-tests/local-e2e-harness.md. +# +# NOTE: test-only. This grants unauthenticated guest access +# (dangerouslyAllowOutsideDevelopment) and must never be layered into a +# production config. auth: environment: development providers: diff --git a/docs/e2e-tests/local-e2e-harness.md b/docs/e2e-tests/local-e2e-harness.md index 0cde1b2151..6d7e01039d 100644 --- a/docs/e2e-tests/local-e2e-harness.md +++ b/docs/e2e-tests/local-e2e-harness.md @@ -34,7 +34,8 @@ plugin config by default). Production-faithful — full plugin set and generated config, the same source CI uses: ```bash -CATALOG_INDEX_IMAGE= \ +# main branch -> :latest; release branches -> the matching :1.y tag +CATALOG_INDEX_IMAGE=quay.io/rhdh/plugin-catalog-index:latest \ npx @red-hat-developer-hub/cli-module-install-dynamic-plugins install dynamic-plugins-root ``` diff --git a/e2e-tests/playwright.app-next-local.config.ts b/e2e-tests/playwright.app-next-local.config.ts index a899459600..da8fc20ea4 100644 --- a/e2e-tests/playwright.app-next-local.config.ts +++ b/e2e-tests/playwright.app-next-local.config.ts @@ -24,6 +24,7 @@ import { resolve } from "path"; const frontendUrl = "http://localhost:3000"; const backendReadiness = "http://localhost:7007/.backstage/health/v1/readiness"; const repoRootBin = resolve(process.cwd(), "..", "node_modules", ".bin"); +const pathWithRepoBin = `${repoRootBin}:${process.env.PATH ?? ""}`; export default defineConfig({ testDir: "./playwright/app-next-local", @@ -69,7 +70,7 @@ export default defineConfig({ cwd: "../packages/backend", env: { ...process.env, - PATH: `${repoRootBin}:${process.env.PATH}`, + PATH: pathWithRepoBin, NODE_OPTIONS: "--no-node-snapshot", }, url: backendReadiness, @@ -83,7 +84,7 @@ export default defineConfig({ "backstage-cli package start --config ../../app-config.yaml " + "--config ../../app-config.local-e2e.yaml", cwd: "../packages/app-next", - env: { ...process.env, PATH: `${repoRootBin}:${process.env.PATH}` }, + env: { ...process.env, PATH: pathWithRepoBin }, url: frontendUrl, reuseExistingServer: !process.env.CI, timeout: 180 * 1000, diff --git a/e2e-tests/playwright.legacy-local.config.ts b/e2e-tests/playwright.legacy-local.config.ts index 88e15424e4..f95061e2e0 100644 --- a/e2e-tests/playwright.legacy-local.config.ts +++ b/e2e-tests/playwright.legacy-local.config.ts @@ -11,7 +11,8 @@ import { resolve } from "path"; * * # one-time: populate dynamic-plugins-root (production-faithful — full plugin set * # and generated config, same source CI uses): - * CATALOG_INDEX_IMAGE= \ + * # main -> :latest; release branches -> the matching :1.y tag + * CATALOG_INDEX_IMAGE=quay.io/rhdh/plugin-catalog-index:latest \ * npx @red-hat-developer-hub/cli-module-install-dynamic-plugins install dynamic-plugins-root * # (offline alternative, frontend plugins only, needs reconciled deps: * # yarn --cwd dynamic-plugins export-dynamic && \ @@ -22,15 +23,12 @@ import { resolve } from "path"; * Both servers are started via `webServer` with the guest-auth overlay * `app-config.local-e2e.yaml` plus the dynamic-plugins UI config. An already-running * pair of servers is reused locally; in CI they are started fresh. - * - * `janus-cli` (legacy app dev server) lives in the repo-root node_modules/.bin, which - * yarn does not surface for the `app` workspace, so the frontend webServer prepends it - * to PATH explicitly. */ const frontendUrl = "http://localhost:3000"; const backendReadiness = "http://localhost:7007/.backstage/health/v1/readiness"; const repoRootBin = resolve(process.cwd(), "..", "node_modules", ".bin"); +const pathWithRepoBin = `${repoRootBin}:${process.env.PATH ?? ""}`; const sharedConfigArgs = "--config ../../app-config.yaml " + @@ -39,14 +37,13 @@ const sharedConfigArgs = export default defineConfig({ testDir: "./playwright", - // Existing UI specs that do not require live external services (cluster, GitHub - // org, Quay, Tekton, Keycloak). Expand as more specs are validated off-cluster. - testMatch: [ - "e2e/guest-signin-happy-path.spec.ts", - "e2e/settings.spec.ts", - "e2e/learning-path-page.spec.ts", - "e2e/home-page-customization.spec.ts", - ], + // Existing UI specs validated to run off-cluster via sidebar navigation. + // Pending (see docs/e2e-tests/local-e2e-harness.md "Known issues"): + // - settings.spec.ts and guest-signin-happy-path.spec.ts navigate via the + // top-right profile dropdown, which needs the global-header plugin mounted. + // - home-page-customization.spec.ts needs that test's specific home config. + // Add them here once those are resolved. + testMatch: ["e2e/learning-path-page.spec.ts"], timeout: 90 * 1000, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, @@ -82,7 +79,7 @@ export default defineConfig({ cwd: "../packages/backend", env: { ...process.env, - PATH: `${repoRootBin}:${process.env.PATH}`, + PATH: pathWithRepoBin, NODE_OPTIONS: "--no-node-snapshot", }, url: backendReadiness, @@ -94,7 +91,7 @@ export default defineConfig({ { command: `janus-cli package start ${sharedConfigArgs}`, cwd: "../packages/app", - env: { ...process.env, PATH: `${repoRootBin}:${process.env.PATH}` }, + env: { ...process.env, PATH: pathWithRepoBin }, url: frontendUrl, reuseExistingServer: !process.env.CI, timeout: 240 * 1000, From c2b38bc3044253b977bb1cb1439fe6d14007328d Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Tue, 23 Jun 2026 16:38:39 -0300 Subject: [PATCH 3/8] refactor(e2e): scope PR to the legacy Tier B harness, drop app-next The chosen direction is the legacy cluster-free harness (packages/app), which runs the existing specs unmodified. The app-next harness can't load dynamic plugins yet (blocked upstream), so it's removed from this PR and tracked as a follow-up; a short note in the doc records why legacy is the target. - Remove playwright.app-next-local.config.ts and its guest-identity.spec.ts. - Remove the e2e:app-next-local script. - Make the overlay and docs legacy-only (keep a "why not app-next yet" note). Co-Authored-By: Claude Opus 4.8 (1M context) --- app-config.local-e2e.yaml | 11 +-- docs/e2e-tests/local-e2e-harness.md | 50 ++++------ e2e-tests/package.json | 3 +- e2e-tests/playwright.app-next-local.config.ts | 95 ------------------- .../app-next-local/guest-identity.spec.ts | 49 ---------- 5 files changed, 23 insertions(+), 185 deletions(-) delete mode 100644 e2e-tests/playwright.app-next-local.config.ts delete mode 100644 e2e-tests/playwright/app-next-local/guest-identity.spec.ts diff --git a/app-config.local-e2e.yaml b/app-config.local-e2e.yaml index 1a17686238..b604b80d83 100644 --- a/app-config.local-e2e.yaml +++ b/app-config.local-e2e.yaml @@ -1,12 +1,9 @@ -# Config overlay shared by the cluster-free local E2E harnesses (legacy `packages/app` -# and new `packages/app-next`). +# Config overlay for the cluster-free local E2E harness (legacy `packages/app`, Tier B). # -# Layered on top of app-config.yaml (and, for the legacy Tier B harness, on top of -# app-config.dynamic-plugins.yaml) to run Playwright E2E without an -# OpenShift/Kubernetes cluster or container images: +# Layered on top of app-config.yaml and app-config.dynamic-plugins.yaml to run +# Playwright E2E without an OpenShift/Kubernetes cluster or container images: # -# yarn --cwd e2e-tests e2e:legacy-local # legacy app + dynamic plugins (Tier B) -# yarn --cwd e2e-tests e2e:app-next-local # new frontend system +# yarn --cwd e2e-tests e2e:legacy-local # # It enables guest sign-in (the auth backend rejects guest unless a provider is # configured) and pins the in-memory SQLite database so a single `run` is fully diff --git a/docs/e2e-tests/local-e2e-harness.md b/docs/e2e-tests/local-e2e-harness.md index 6d7e01039d..9ac2f9f4a2 100644 --- a/docs/e2e-tests/local-e2e-harness.md +++ b/docs/e2e-tests/local-e2e-harness.md @@ -7,27 +7,18 @@ backend dynamic-plugin loader from RHIDP-13508. ## Goal Run real Playwright E2E against RHDH **without** an OpenShift/Kubernetes cluster or -container images — a single `run` that boots the backend and a frontend dev server -in-process and drives a browser against them. +container images — a single `run` that boots the backend and the legacy frontend dev +server in-process and drives a browser against them. -Two harnesses are provided: +The harness targets the legacy frontend (`packages/app`, Tier B): it is what RHDH ships +today, and **the existing Playwright specs already target it**, so they run unmodified. +Dynamic frontend plugins load through Scalprum exactly as in-cluster (the legacy +`scalprum-backend` serves the plugin config by default). -| Harness | Target | Command | Status | -|---------|--------|---------|--------| -| **Legacy (Tier B)** | `packages/app` (Scalprum) + dynamic plugins | `yarn --cwd e2e-tests e2e:legacy-local` | Production-faithful; runs the **existing** specs | -| **app-next** | `packages/app-next` (new frontend system) | `yarn --cwd e2e-tests e2e:app-next-local` | Forward-looking; core app only (see limits) | - -Both layer the guest-auth + in-memory-SQLite overlay `app-config.local-e2e.yaml` on -top of `app-config.yaml`. Guest sign-in must be configured explicitly — the auth -backend otherwise rejects guest with _"you must … configure the auth backend to -support guest sign in."_ - -## Legacy harness (Tier B) — recommended - -This is the production-faithful target: it is what RHDH ships today, and **the existing -Playwright specs already target it**, so they run unmodified. Dynamic frontend plugins -load through Scalprum exactly as in-cluster (the legacy `scalprum-backend` serves the -plugin config by default). +The guest-auth + in-memory-SQLite overlay `app-config.local-e2e.yaml` is layered on top +of `app-config.yaml`. Guest sign-in must be configured explicitly — the auth backend +otherwise rejects guest with _"you must … configure the auth backend to support guest +sign in."_ ### 1. Populate `dynamic-plugins-root` (one-time) @@ -65,20 +56,15 @@ With plugins populated, the legacy app renders the full production RHDH UI off-c `guest-signin-happy-path` **home-page test passes unmodified** — confirming a dynamic frontend plugin renders with no cluster. -## app-next harness - -`playwright.app-next-local.config.ts` + `playwright/app-next-local/guest-identity.spec.ts`. -Boots the backend + app-next dev server with guest auth. Cold start ~17–20s (warm -rspack cache); ~3s reusing servers; stable; clean teardown. +## Why the legacy app, not app-next -**Limit — dynamic frontend plugins do not load on app-next yet.** app-next uses -`dynamicFrontendFeaturesLoader()` → `GET //remotes`, served by -`dynamicPluginsFrontendServiceRef`, which the RHDH backend no-ops unless -`ENABLE_STANDARD_MODULE_FEDERATION=true` — and even then returns 404 because RHDH's -exported dynamic frontend plugins do not contain standard Module Federation assets by -default (see `packages/backend/src/index.ts`). app-next also has no Home page. So -app-next currently covers core/statically-registered plugin UIs (e.g. user-settings) -only; the legacy harness is the way to exercise dynamic plugins off-cluster today. +The harness targets the legacy app because **dynamic frontend plugins do not load on +`packages/app-next` yet**: app-next's `dynamicFrontendFeaturesLoader()` fetches Module +Federation remotes from the backend, but that endpoint is no-op'd unless +`ENABLE_STANDARD_MODULE_FEDERATION=true`, and even then RHDH's exported dynamic frontend +plugins do not contain standard MF assets (see `packages/backend/src/index.ts`). Until +that lands upstream, app-next can only exercise core/static plugin UIs. An app-next +harness is tracked as a follow-up (RHIDP-13501 / spike RHIDP-15075). ## vs. rhdh-local diff --git a/e2e-tests/package.json b/e2e-tests/package.json index d81c78e172..dea5a915b2 100644 --- a/e2e-tests/package.json +++ b/e2e-tests/package.json @@ -31,8 +31,7 @@ "shellcheck": "git ls-files -z '*.sh' | xargs -0 shellcheck --severity=warning --color=always", "prettier:check": "prettier --ignore-unknown --check .", "prettier:fix": "prettier --ignore-unknown --write .", - "e2e:legacy-local": "playwright test --config=playwright.legacy-local.config.ts", - "e2e:app-next-local": "playwright test --config=playwright.app-next-local.config.ts" + "e2e:legacy-local": "playwright test --config=playwright.legacy-local.config.ts" }, "devDependencies": { "@axe-core/playwright": "4.11.2", diff --git a/e2e-tests/playwright.app-next-local.config.ts b/e2e-tests/playwright.app-next-local.config.ts deleted file mode 100644 index da8fc20ea4..0000000000 --- a/e2e-tests/playwright.app-next-local.config.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; -import { resolve } from "path"; - -/** - * Cluster-free local E2E harness for the new frontend system (`packages/app-next`). - * - * Layer 4a spike (RHIDP-13501): run real Playwright E2E against RHDH without an - * OpenShift/Kubernetes cluster or container images. Playwright boots the backend - * and the app-next dev server itself, then drives the browser against them. - * See docs/e2e-tests/local-e2e-harness.md (note: dynamic frontend plugins do not - * load on app-next yet — use the legacy harness for those). - * - * yarn e2e:app-next-local - * - * Both servers are started via `webServer` below with the guest-auth overlay - * `app-config.local-e2e.yaml`. Locally, an already-running pair of servers - * is reused (`reuseExistingServer`); in CI they are always started fresh. - * - * `backstage-cli` lives in the repo-root node_modules/.bin, which yarn does not - * surface for these workspaces, so both CLIs are invoked directly with the root - * .bin prepended to PATH and run from their package directory. - */ - -const frontendUrl = "http://localhost:3000"; -const backendReadiness = "http://localhost:7007/.backstage/health/v1/readiness"; -const repoRootBin = resolve(process.cwd(), "..", "node_modules", ".bin"); -const pathWithRepoBin = `${repoRootBin}:${process.env.PATH ?? ""}`; - -export default defineConfig({ - testDir: "./playwright/app-next-local", - timeout: 90 * 1000, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 1 : 0, - workers: 1, - reporter: [ - ["list"], - [ - "html", - { open: "never", outputFolder: "playwright-report-app-next-local" }, - ], - [ - "junit", - { - outputFile: - process.env.JUNIT_RESULTS || "junit-results-app-next-local.xml", - }, - ], - ], - use: { - baseURL: frontendUrl, - ignoreHTTPSErrors: true, - trace: "retain-on-failure", - screenshot: "only-on-failure", - video: "retain-on-failure", - ...devices["Desktop Chrome"], - viewport: { width: 1920, height: 1080 }, - actionTimeout: 15 * 1000, - navigationTimeout: 60 * 1000, - }, - expect: { - timeout: 15 * 1000, - }, - // Two local servers, no cluster. `--config` paths are resolved relative to each - // package dir (where backstage-cli runs), hence the `../../` prefix. - webServer: [ - { - command: - "backstage-cli package start --require ./src/instrumentation.js " + - "--config ../../app-config.yaml --config ../../app-config.local-e2e.yaml", - cwd: "../packages/backend", - env: { - ...process.env, - PATH: pathWithRepoBin, - NODE_OPTIONS: "--no-node-snapshot", - }, - url: backendReadiness, - reuseExistingServer: !process.env.CI, - timeout: 180 * 1000, - stdout: "pipe", - stderr: "pipe", - }, - { - command: - "backstage-cli package start --config ../../app-config.yaml " + - "--config ../../app-config.local-e2e.yaml", - cwd: "../packages/app-next", - env: { ...process.env, PATH: pathWithRepoBin }, - url: frontendUrl, - reuseExistingServer: !process.env.CI, - timeout: 180 * 1000, - stdout: "pipe", - stderr: "pipe", - }, - ], -}); diff --git a/e2e-tests/playwright/app-next-local/guest-identity.spec.ts b/e2e-tests/playwright/app-next-local/guest-identity.spec.ts deleted file mode 100644 index a14de35ed6..0000000000 --- a/e2e-tests/playwright/app-next-local/guest-identity.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { test, expect } from "@support/coverage/test"; - -/** - * Slice 1 of the cluster-free local E2E harness (Layer 4a spike, RHIDP-13501). - * - * Proves the full local stack works without a cluster: the app-next dev server - * and the backend are booted by Playwright (see playwright.app-next-local.config.ts), - * guest sign-in succeeds, and a real plugin page (Settings) renders the guest - * identity served by the backend. - * - * NOTE: assertions target what `packages/app-next` actually renders. The new - * frontend system registers catalog/scaffolder/search/user-settings/visualizer - * and has no Home page yet, so this spec deliberately does not assert the legacy - * "Welcome back!" landing page used by the `packages/app` E2E specs. - */ -test.describe("app-next local — guest sign-in", () => { - test.beforeAll(async () => { - test.info().annotations.push({ - type: "component", - description: "authentication", - }); - }); - - test.beforeEach(async ({ page }) => { - await page.goto("/"); - // New-frontend-system guest provider card. - await page.getByRole("button", { name: "Enter", exact: true }).click(); - // Sidebar appears once signed in. - await expect(page.getByRole("link", { name: "Settings" })).toBeVisible(); - }); - - test("signs in as guest and reaches an authenticated page", async ({ - page, - }) => { - await expect(page.getByRole("link", { name: "Catalog" })).toBeVisible(); - await expect(page.getByRole("link", { name: "Search" })).toBeVisible(); - }); - - test("Settings page shows the guest Backstage identity", async ({ page }) => { - await page.getByRole("link", { name: "Settings" }).click(); - await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible(); - // "Backstage Identity" is an InfoCard title (not a heading role). - await expect(page.getByText("Backstage Identity")).toBeVisible(); - await expect(page.getByText("User Entity:")).toBeVisible(); - await expect( - page.getByRole("link", { name: "guest" }).first(), - ).toBeVisible(); - }); -}); From ebddd2cc5e53db8444a9290a74885da3171c2f80 Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Tue, 23 Jun 2026 16:48:07 -0300 Subject: [PATCH 4/8] refactor(e2e): address review on the legacy harness - Scope the default run to the one verified-green test (guest-signin home-page) via testMatch + grep, instead of an unvalidated spec whose sidebar navigation may not match the harness config. - Add a globalSetup that fails fast with the populate command when dynamic-plugins-root is empty (clear error instead of a locator timeout). - Build the shared --config args via array join; document workers=1 and that the backend command mirrors packages/backend's start script. - Doc: describe the grep-scoped default run and the fail-fast guard. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/e2e-tests/local-e2e-harness.md | 8 +++-- e2e-tests/playwright.legacy-local.config.ts | 30 +++++++++-------- .../support/local-harness-global-setup.ts | 32 +++++++++++++++++++ 3 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 e2e-tests/playwright/support/local-harness-global-setup.ts diff --git a/docs/e2e-tests/local-e2e-harness.md b/docs/e2e-tests/local-e2e-harness.md index 9ac2f9f4a2..4ac06ac07a 100644 --- a/docs/e2e-tests/local-e2e-harness.md +++ b/docs/e2e-tests/local-e2e-harness.md @@ -46,8 +46,12 @@ yarn --cwd e2e-tests e2e:legacy-local Playwright (`playwright.legacy-local.config.ts`) boots the backend and the legacy app dev server with `app-config.yaml` + `app-config.dynamic-plugins.yaml` + -`app-config.local-e2e.yaml`, then runs the UI specs in `testMatch` that do not require -live external services. +`app-config.local-e2e.yaml`. A `globalSetup` first fails fast with the populate command +if `dynamic-plugins-root` is empty. + +By default the run is scoped (via `grep`) to the one test verified green off-cluster so +far — the `guest-signin-happy-path` home-page test. Widen `testMatch`/`grep` as more +specs are validated (see "Known issues"). ### Verified diff --git a/e2e-tests/playwright.legacy-local.config.ts b/e2e-tests/playwright.legacy-local.config.ts index f95061e2e0..94cc0c8bd1 100644 --- a/e2e-tests/playwright.legacy-local.config.ts +++ b/e2e-tests/playwright.legacy-local.config.ts @@ -30,24 +30,27 @@ const backendReadiness = "http://localhost:7007/.backstage/health/v1/readiness"; const repoRootBin = resolve(process.cwd(), "..", "node_modules", ".bin"); const pathWithRepoBin = `${repoRootBin}:${process.env.PATH ?? ""}`; -const sharedConfigArgs = - "--config ../../app-config.yaml " + - "--config ../../app-config.dynamic-plugins.yaml " + - "--config ../../app-config.local-e2e.yaml"; +const sharedConfigArgs = [ + "--config ../../app-config.yaml", + "--config ../../app-config.dynamic-plugins.yaml", + "--config ../../app-config.local-e2e.yaml", +].join(" "); export default defineConfig({ testDir: "./playwright", - // Existing UI specs validated to run off-cluster via sidebar navigation. - // Pending (see docs/e2e-tests/local-e2e-harness.md "Known issues"): - // - settings.spec.ts and guest-signin-happy-path.spec.ts navigate via the - // top-right profile dropdown, which needs the global-header plugin mounted. - // - home-page-customization.spec.ts needs that test's specific home config. - // Add them here once those are resolved. - testMatch: ["e2e/learning-path-page.spec.ts"], + // Fails fast if dynamic-plugins-root has not been populated. + globalSetup: "./playwright/support/local-harness-global-setup.ts", + // Runs only what is verified green off-cluster so far: the guest-signin home-page + // test (Quick Access from the dynamic home-page plugin). `grep` scopes to that test + // because its two siblings — and several other UI specs — navigate via the top-right + // profile dropdown (needs the global-header plugin) or need per-spec config. See + // docs/e2e-tests/local-e2e-harness.md "Known issues". Widen as specs are validated. + testMatch: ["e2e/guest-signin-happy-path.spec.ts"], + grep: /Homepage renders with Search Bar/, timeout: 90 * 1000, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, - workers: 1, + workers: 1, // serial: a single shared backend + dev server reporter: [ ["list"], ["html", { open: "never", outputFolder: "playwright-report-legacy-local" }], @@ -72,7 +75,8 @@ export default defineConfig({ }, // backstage-cli / janus-cli live in the repo-root node_modules/.bin, which yarn does // not surface for these workspaces, so both CLIs are invoked directly with the root - // .bin prepended to PATH and run from their package directory. + // .bin prepended to PATH and run from their package directory. The backend command + // mirrors packages/backend's `start` script (--require instrumentation) — keep in sync. webServer: [ { command: `backstage-cli package start --require ./src/instrumentation.js ${sharedConfigArgs}`, diff --git a/e2e-tests/playwright/support/local-harness-global-setup.ts b/e2e-tests/playwright/support/local-harness-global-setup.ts new file mode 100644 index 0000000000..fbe34e62de --- /dev/null +++ b/e2e-tests/playwright/support/local-harness-global-setup.ts @@ -0,0 +1,32 @@ +import { readdirSync } from "fs"; +import { resolve } from "path"; + +/** + * globalSetup for the cluster-free legacy harness (playwright.legacy-local.config.ts). + * + * Fails fast with an actionable message when `dynamic-plugins-root` has not been + * populated — otherwise the legacy app boots with no plugins and specs fail with a + * confusing locator timeout instead of a clear "populate first" error. + */ +export default function requireDynamicPluginsPopulated(): void { + // process.cwd() is e2e-tests when Playwright runs; the plugins root is at repo root. + const root = resolve(process.cwd(), "..", "dynamic-plugins-root"); + + let pluginCount = 0; + try { + pluginCount = readdirSync(root).filter( + (entry) => entry !== ".gitkeep", + ).length; + } catch { + // root missing — treated as empty below. + } + + if (pluginCount === 0) { + throw new Error( + `dynamic-plugins-root is empty — populate it before running e2e:legacy-local:\n\n` + + ` CATALOG_INDEX_IMAGE=quay.io/rhdh/plugin-catalog-index:latest \\\n` + + ` npx @red-hat-developer-hub/cli-module-install-dynamic-plugins install dynamic-plugins-root\n\n` + + `See docs/e2e-tests/local-e2e-harness.md.`, + ); + } +} From 1bf02b56201a3a916ab08bb9097674cbe593526a Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Tue, 23 Jun 2026 18:43:01 -0300 Subject: [PATCH 5/8] style(e2e): wrap junit reporter object to satisfy prettier (printWidth 80) Co-Authored-By: Claude Opus 4.8 (1M context) --- e2e-tests/playwright.legacy-local.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e-tests/playwright.legacy-local.config.ts b/e2e-tests/playwright.legacy-local.config.ts index 94cc0c8bd1..10a4306693 100644 --- a/e2e-tests/playwright.legacy-local.config.ts +++ b/e2e-tests/playwright.legacy-local.config.ts @@ -56,7 +56,10 @@ export default defineConfig({ ["html", { open: "never", outputFolder: "playwright-report-legacy-local" }], [ "junit", - { outputFile: process.env.JUNIT_RESULTS || "junit-results-legacy-local.xml" }, + { + outputFile: + process.env.JUNIT_RESULTS || "junit-results-legacy-local.xml", + }, ], ], use: { From ab72af06aad48e08e69c865dcd89882e6ac14108 Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Wed, 24 Jun 2026 11:11:28 -0300 Subject: [PATCH 6/8] ci(e2e): run cluster-free harness on GitHub Actions Add .github/workflows/e2e-cluster-free.yaml: a no-cluster job that installs deps + skopeo, populates dynamic-plugins-root from the public catalog index via the install-dynamic-plugins CLI (same mechanism as the nightly sanity check), boots the backend + legacy app dev servers in-process, and runs yarn e2e:legacy-local. Triggers on e2e-tests/** and app-config*.yaml. Follows the project workflow-security rules: pull_request (no secrets, public image), pinned action SHAs, minimal permissions, concurrency control. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e-cluster-free.yaml | 86 +++++++++++++++++++++++++ docs/e2e-tests/local-e2e-harness.md | 9 +++ 2 files changed, 95 insertions(+) create mode 100644 .github/workflows/e2e-cluster-free.yaml diff --git a/.github/workflows/e2e-cluster-free.yaml b/.github/workflows/e2e-cluster-free.yaml new file mode 100644 index 0000000000..754abfc8f0 --- /dev/null +++ b/.github/workflows/e2e-cluster-free.yaml @@ -0,0 +1,86 @@ +name: E2E Cluster-free Harness + +# Runs the cluster-free local E2E harness (RHIDP-13501 / RHIDP-15075): boots the +# backend and the legacy app dev server in-process and drives Playwright against +# them, with dynamic plugins pulled from the public catalog index via the +# install-dynamic-plugins CLI (skopeo). No OpenShift/Kubernetes cluster or image. +# See docs/e2e-tests/local-e2e-harness.md. + +on: + pull_request: + paths: + - "e2e-tests/**" + - "app-config*.yaml" + - ".github/workflows/e2e-cluster-free.yaml" + push: + branches: + - "main" + - "release-*" + paths: + - "e2e-tests/**" + - "app-config*.yaml" + - ".github/workflows/e2e-cluster-free.yaml" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # Public index; release branches can override to the matching :1.y tag. + CATALOG_INDEX_IMAGE: quay.io/rhdh/plugin-catalog-index:latest + +jobs: + legacy-local: + name: Cluster-free E2E (legacy app) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: ".nvmrc" + + - name: Install skopeo + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends skopeo + + - name: Install dependencies (root) + run: yarn install + + - name: Install dependencies (e2e-tests) + working-directory: ./e2e-tests + run: yarn install --mode=skip-build + + - name: Install Playwright browser + working-directory: ./e2e-tests + run: yarn playwright install --with-deps chromium + + - name: Populate dynamic-plugins-root from the catalog index + # Mirrors the install-dynamic-plugins flow used by the nightly sanity check: + # an empty plugin list + CATALOG_INDEX_IMAGE pulls the default plugin set + # (incl. the dynamic home-page plugin) from the public index via skopeo. + run: | + mkdir -p dynamic-plugins-root + printf 'plugins: []\n' > dynamic-plugins.yaml + cp dynamic-plugins.yaml dynamic-plugins-root/dynamic-plugins.yaml + npx -y @red-hat-developer-hub/cli-module-install-dynamic-plugins@0.2.0 install dynamic-plugins-root + + - name: Run cluster-free E2E (legacy app) + working-directory: ./e2e-tests + run: yarn e2e:legacy-local + + - name: Upload Playwright report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playwright-report-legacy-local + path: e2e-tests/playwright-report-legacy-local + retention-days: 7 diff --git a/docs/e2e-tests/local-e2e-harness.md b/docs/e2e-tests/local-e2e-harness.md index 4ac06ac07a..81f4008d52 100644 --- a/docs/e2e-tests/local-e2e-harness.md +++ b/docs/e2e-tests/local-e2e-harness.md @@ -60,6 +60,15 @@ With plugins populated, the legacy app renders the full production RHDH UI off-c `guest-signin-happy-path` **home-page test passes unmodified** — confirming a dynamic frontend plugin renders with no cluster. +## CI + +`.github/workflows/e2e-cluster-free.yaml` runs this harness on GitHub Actions in a +cluster-free phase: it installs deps + skopeo, populates `dynamic-plugins-root` from the +public catalog index via the `install-dynamic-plugins` CLI (the same mechanism the +nightly sanity check uses), then runs `yarn e2e:legacy-local`. No cluster or container +image is built. It triggers on `e2e-tests/**` and `app-config*.yaml` changes; the scope +can widen to `packages/app/**` / `packages/backend/**` once it is proven stable. + ## Why the legacy app, not app-next The harness targets the legacy app because **dynamic frontend plugins do not load on From f2019bbce0b5dc7980e0906de370de5cd641098d Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Wed, 24 Jun 2026 14:05:08 -0300 Subject: [PATCH 7/8] =?UTF-8?q?ci(e2e):=20fix=20cluster-free=20job=20?= =?UTF-8?q?=E2=80=94=20drop=20hanging=20--with-deps,=20cache=20yarn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'Install Playwright browser' step hung on 'playwright install --with-deps' (its apt phase); drop --with-deps since ubuntu-latest already has the libs headless chromium needs. Add setup-node yarn caching for both lockfiles to cut the slow root install on subsequent runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e-cluster-free.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-cluster-free.yaml b/.github/workflows/e2e-cluster-free.yaml index 754abfc8f0..64725ab566 100644 --- a/.github/workflows/e2e-cluster-free.yaml +++ b/.github/workflows/e2e-cluster-free.yaml @@ -46,6 +46,10 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: ".nvmrc" + cache: yarn + cache-dependency-path: | + yarn.lock + e2e-tests/yarn.lock - name: Install skopeo run: | @@ -59,9 +63,11 @@ jobs: working-directory: ./e2e-tests run: yarn install --mode=skip-build + # Browser only (no --with-deps): the apt step hung on the runner, and + # ubuntu-latest already ships the libs headless chromium needs. - name: Install Playwright browser working-directory: ./e2e-tests - run: yarn playwright install --with-deps chromium + run: yarn playwright install chromium - name: Populate dynamic-plugins-root from the catalog index # Mirrors the install-dynamic-plugins flow used by the nightly sanity check: From 39ab02f803c632e9ade96981f74653e9e5896a77 Mon Sep 17 00:00:00 2001 From: Gustavo Lira Date: Wed, 24 Jun 2026 17:12:54 -0300 Subject: [PATCH 8/8] ci(e2e): run cluster-free job in the Playwright container Use mcr.microsoft.com/playwright:v1.59.1-noble (browsers + OS deps preinstalled, matching @playwright/test 1.59.1) to eliminate the playwright-install step that hung on plain ubuntu runners. Enable corepack for the vendored yarn 4 and cache the yarn global cache explicitly (the container lacks the yarn binary setup-node's cache relies on). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e-cluster-free.yaml | 29 +++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e-cluster-free.yaml b/.github/workflows/e2e-cluster-free.yaml index 64725ab566..840f8c55cf 100644 --- a/.github/workflows/e2e-cluster-free.yaml +++ b/.github/workflows/e2e-cluster-free.yaml @@ -36,6 +36,10 @@ jobs: legacy-local: name: Cluster-free E2E (legacy app) runs-on: ubuntu-latest + # Playwright image: browsers + OS deps preinstalled (matches @playwright/test + # 1.59.1), so no browser-install step (which hung on plain ubuntu runners). + container: + image: mcr.microsoft.com/playwright:v1.59.1-noble steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -46,15 +50,22 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: ".nvmrc" - cache: yarn - cache-dependency-path: | - yarn.lock - e2e-tests/yarn.lock + + - name: Enable Corepack (vendored yarn 4) + run: corepack enable + + - name: Cache yarn global cache + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + with: + path: ~/.yarn/berry/cache + key: yarn-${{ hashFiles('yarn.lock', 'e2e-tests/yarn.lock') }} + restore-keys: | + yarn- - name: Install skopeo run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends skopeo + apt-get update + apt-get install -y --no-install-recommends skopeo - name: Install dependencies (root) run: yarn install @@ -63,12 +74,6 @@ jobs: working-directory: ./e2e-tests run: yarn install --mode=skip-build - # Browser only (no --with-deps): the apt step hung on the runner, and - # ubuntu-latest already ships the libs headless chromium needs. - - name: Install Playwright browser - working-directory: ./e2e-tests - run: yarn playwright install chromium - - name: Populate dynamic-plugins-root from the catalog index # Mirrors the install-dynamic-plugins flow used by the nightly sanity check: # an empty plugin list + CATALOG_INDEX_IMAGE pulls the default plugin set