diff --git a/.github/workflows/e2e-cluster-free.yaml b/.github/workflows/e2e-cluster-free.yaml new file mode 100644 index 0000000000..840f8c55cf --- /dev/null +++ b/.github/workflows/e2e-cluster-free.yaml @@ -0,0 +1,97 @@ +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 + # 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 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: ".nvmrc" + + - 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: | + apt-get update + 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: 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/app-config.local-e2e.yaml b/app-config.local-e2e.yaml new file mode 100644 index 0000000000..b604b80d83 --- /dev/null +++ b/app-config.local-e2e.yaml @@ -0,0 +1,26 @@ +# Config overlay for the cluster-free local E2E harness (legacy `packages/app`, Tier B). +# +# 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 +# +# 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: + 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..81f4008d52 --- /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 the legacy frontend dev +server in-process and drives a browser against them. + +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). + +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) + +Production-faithful — full plugin set and generated config, the same source CI uses: + +```bash +# 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 +``` + +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`. 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 + +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. + +## 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 +`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 + +[`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..dea5a915b2 100644 --- a/e2e-tests/package.json +++ b/e2e-tests/package.json @@ -30,7 +30,8 @@ "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" }, "devDependencies": { "@axe-core/playwright": "4.11.2", diff --git a/e2e-tests/playwright.legacy-local.config.ts b/e2e-tests/playwright.legacy-local.config.ts new file mode 100644 index 0000000000..10a4306693 --- /dev/null +++ b/e2e-tests/playwright.legacy-local.config.ts @@ -0,0 +1,109 @@ +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): + * # 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 && \ + * # 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. + */ + +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", + "--config ../../app-config.dynamic-plugins.yaml", + "--config ../../app-config.local-e2e.yaml", +].join(" "); + +export default defineConfig({ + testDir: "./playwright", + // 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, // serial: a single shared backend + dev server + 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. 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}`, + 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: `janus-cli package start ${sharedConfigArgs}`, + cwd: "../packages/app", + env: { ...process.env, PATH: pathWithRepoBin }, + url: frontendUrl, + reuseExistingServer: !process.env.CI, + timeout: 240 * 1000, + stdout: "pipe", + stderr: "pipe", + }, + ], +}); 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.`, + ); + } +}