Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4a9e114
feat(e2e): add cluster-free local E2E harness (legacy Tier B + app-next)
gustavolira Jun 23, 2026
bfbe9dc
refactor(e2e): address review on local harness
gustavolira Jun 23, 2026
c2b38bc
refactor(e2e): scope PR to the legacy Tier B harness, drop app-next
gustavolira Jun 23, 2026
ebddd2c
refactor(e2e): address review on the legacy harness
gustavolira Jun 23, 2026
1bf02b5
style(e2e): wrap junit reporter object to satisfy prettier (printWidt…
gustavolira Jun 23, 2026
ab72af0
ci(e2e): run cluster-free harness on GitHub Actions
gustavolira Jun 24, 2026
f2019bb
ci(e2e): fix cluster-free job — drop hanging --with-deps, cache yarn
gustavolira Jun 24, 2026
39ab02f
ci(e2e): run cluster-free job in the Playwright container
gustavolira Jun 24, 2026
4813459
Merge remote-tracking branch 'origin/main' into rhidp-15075-cluster-f…
gustavolira Jun 30, 2026
c85e002
ci(e2e): conform harness config to Oxc (oxlint/oxfmt) after main migr…
gustavolira Jun 30, 2026
3d62adf
ci(e2e): bump Playwright container to v1.61.1-noble
gustavolira Jul 1, 2026
59f581b
ci(e2e): install dynamic-home-page from OCI instead of empty plugin set
gustavolira Jul 1, 2026
f82e8ce
test(e2e): widen cluster-free run to all guest-signin specs via globa…
gustavolira Jul 1, 2026
9c56ab1
test(e2e): keep cluster-free run scoped to the green home-page test
gustavolira Jul 1, 2026
203f7aa
refactor(e2e): single populate script, @cluster-free tag scoping, rev…
gustavolira Jul 2, 2026
faa8069
test(e2e): widen cluster-free run to full guest-signin + learning-paths
gustavolira Jul 2, 2026
863abe0
ci(e2e): rename cluster-free workflow/job for a production-ready chec…
gustavolira Jul 2, 2026
ee3120b
docs(e2e): fix skopeo availability claim — it installs on macOS via brew
gustavolira Jul 2, 2026
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
92 changes: 92 additions & 0 deletions .github/workflows/e2e-cluster-free.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: E2E Cluster-free

# 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 installed from the public OCI registry (ghcr) 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

jobs:
e2e:
name: e2e
runs-on: ubuntu-latest
# Playwright image: browsers + OS deps preinstalled, so no browser-install step
# (which hung on plain ubuntu runners). Keep the tag in sync with the
# @playwright/test version in e2e-tests/package.json (currently 1.61.1) — a
# mismatch fails with "Executable doesn't exist at /ms-playwright/...".
container:
image: mcr.microsoft.com/playwright:v1.61.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 (OCI, no source build)
# install-dynamic-plugins pulls the harness plugin set from the public OCI
# registry (ghcr) via skopeo — the core plugins in the catalog index's
# default.yaml reference local ./dynamic-plugins/dist paths that only exist
# after a source build, so we install the OCI-published builds instead.
run: ./e2e-tests/local-harness/populate.sh

- 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ site
# Dynamic plugins root content
dynamic-plugins-root/*
!dynamic-plugins-root/.gitkeep
# install-dynamic-plugins config copied to the repo root by e2e-tests/local-harness/populate.sh
/dynamic-plugins.yaml

#dev caches
.webpack-cache
Expand Down
42 changes: 42 additions & 0 deletions app-config.local-e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 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:"

# The e2e specs are written against the CI deployment's menu customization
# (.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml): APIs and
# Learning Paths nest under a "References" sidebar group. Mirror it here so
# navigation helpers like openSidebarButton("References") work off-cluster.
dynamicPlugins:
frontend:
default.main-menu-items:
menuItems:
default.list:
title: References
icon: bookmarks
default.apis:
parent: default.list
default.learning-path:
parent: default.list
138 changes: 138 additions & 0 deletions docs/e2e-tests/local-e2e-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# 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)

Run the same script CI uses — it installs the harness plugin set
(`e2e-tests/local-harness/dynamic-plugins.yaml`) from the public OCI registry (ghcr)
via `install-dynamic-plugins` + skopeo, pinned to the same CLI version as CI. No
source build needed; works from a fresh clone. Requires skopeo (preinstalled in CI;
`brew install skopeo` on macOS):

```bash
./e2e-tests/local-harness/populate.sh
```

Alternatives:

- **Catalog index** — the index's `dynamic-plugins.default.yaml` references the core
plugins by local `./dynamic-plugins/dist/…` paths that only exist after a source
build, so on a fresh clone most plugins are skipped. Use only after building
`dynamic-plugins` from source (main -> `:latest`; release branches -> the matching
`:1.y` tag):

```bash
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 from-source** (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` has no plugins.

The run is scoped to tests tagged `@cluster-free` within the spec files allowlisted in
`testMatch`. To widen coverage, tag a validated test with `@cluster-free` and add its
spec file to `testMatch`; if the test needs extra plugins, add them (with their
`pluginConfig`) to `e2e-tests/local-harness/dynamic-plugins.yaml` and re-run
`populate.sh` (see "Known issues").

### Verified

With plugins populated, the legacy app renders the full production RHDH UI off-cluster
(branding, sidebar, global header, and Quick Access from the dynamic plugins). The
existing specs **pass unmodified**:

- `guest-signin-happy-path` — all three tests: home page (dynamic-home-page plugin),
Settings and Sign-out (navigation via the global-header profile dropdown, using the
plugin's canonical `pluginConfig` merged through the generated
`dynamic-plugins-root/app-config.dynamic-plugins.yaml`, exactly as in-cluster).
- `learning-path-page` — renders from the static fallback data bundled with
`packages/app`; the "References" sidebar group mirrors the CI menu customization via
`app-config.local-e2e.yaml`.

## 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` via
`./e2e-tests/local-harness/populate.sh` (the harness plugin set from the public OCI
registry, ghcr), 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.
- **Re-run `populate.sh` after changing the harness plugin set.** The `pluginConfig`
blocks in `e2e-tests/local-harness/dynamic-plugins.yaml` (e.g. the global-header
mount points) only take effect through the generated
`dynamic-plugins-root/app-config.dynamic-plugins.yaml`, which the webServer loads
last. A stale populate leaves plugins loaded but unconfigured (the header renders
empty).
- **Specs that need CI test data are not enabled yet.** `settings.spec.ts` asserts
ownership entities ("Guest User, team-a") that come from catalog locations in the CI
config map; `home-page-customization.spec.ts` needs the home-page card customization
from `.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml`. Enabling them
means mirroring that data/config into the harness overlay.
- **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`.
Loading
Loading