Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/e2e-cluster-free.yaml
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions app-config.local-e2e.yaml
Original file line number Diff line number Diff line change
@@ -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:"
105 changes: 105 additions & 0 deletions docs/e2e-tests/local-e2e-harness.md
Original file line number Diff line number Diff line change
@@ -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`.
3 changes: 2 additions & 1 deletion e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
109 changes: 109 additions & 0 deletions e2e-tests/playwright.legacy-local.config.ts
Original file line number Diff line number Diff line change
@@ -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",
},
],
});
32 changes: 32 additions & 0 deletions e2e-tests/playwright/support/local-harness-global-setup.ts
Original file line number Diff line number Diff line change
@@ -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.`,
);
}
}
Loading