|
| 1 | +# Native (Docker-free) smoke harness |
| 2 | + |
| 3 | +Validates RHDH dynamic **backend** plugins in-process — install via the published |
| 4 | +`install-dynamic-plugins` CLI, then boot with `startTestBackend` — with **no Docker |
| 5 | +container and no cluster**. About **20x faster** than the per-workspace `docker run rhdh` |
| 6 | +smoke-test it replaces for that scope. |
| 7 | + |
| 8 | +**Tickets:** RHIDP-15075, RHIDP-15076, RHIDP-13530 (epic RHIDP-13501). |
| 9 | + |
| 10 | +## Why |
| 11 | + |
| 12 | +The repo's container smoke-test boots RHDH in Docker (`docker run rhdh …`) just to check a |
| 13 | +plugin loads. An earlier native attempt (PR #2231) was closed because it was 694 lines of |
| 14 | +bespoke OCI parsing and predated the npm CLI. Now that `install-dynamic-plugins` is |
| 15 | +published — and RHDH already uses it in-process in `plugin-dynamic-loading.spec.ts` |
| 16 | +(PR #4967) — that extraction collapses to one CLI call, so the smoke validation runs |
| 17 | +in-process with no container. |
| 18 | + |
| 19 | +## What it does |
| 20 | + |
| 21 | +``` |
| 22 | +install CLI (extract OCI → dynamic-plugins-root, run with cwd=root) |
| 23 | + → discoverPlugins() # scan install dirs, classify by package.json backstage.role |
| 24 | + → loadBackendPlugins() # require() each, assert default BackendFeature |
| 25 | + → startTestBackend() # boot core + loaded features in-process (+ rootConfig) |
| 26 | + → validateFrontendBundle() # scalprum/remoteEntry present (presence check, not executed) |
| 27 | + → results.json + exit code |
| 28 | +``` |
| 29 | + |
| 30 | +`src/loader.ts` and `src/{module-resolution,plugin-config}.ts` are ported from RHDH |
| 31 | +PR #4967; `discoverPlugins()` replaces RHDH's `loadManifest()` because this CLI version |
| 32 | +lays out one dir per plugin instead of emitting a `manifest.json`. |
| 33 | + |
| 34 | +## What it deliberately does NOT do |
| 35 | + |
| 36 | +It does **not render frontend UI**. `startTestBackend` is backend-only. UI-behaviour tests |
| 37 | +(the 24 overlay `e2e-tests`, which are ~all Playwright `uiHelper`-driven) need a real |
| 38 | +frontend — that is the **NFS / app-next** path (RHIDP-15082), intentionally out of scope. |
| 39 | + |
| 40 | +## Run |
| 41 | + |
| 42 | +Requires Node 24 and Yarn 4 (matching the repo's `versions.json` and the sibling |
| 43 | +`workspaces/*/e2e-tests`), plus registry access to pull the OCI plugin images. |
| 44 | + |
| 45 | +```bash |
| 46 | +yarn install |
| 47 | + |
| 48 | +cat > dp.yaml <<'YAML' |
| 49 | +plugins: |
| 50 | + - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/<plugin>:<tag>!<name> |
| 51 | +YAML |
| 52 | +yarn smoke --dynamic-plugins dp.yaml |
| 53 | +``` |
| 54 | + |
| 55 | +`yarn check` runs `tsc --noEmit`. This is a standalone tool dir, not a |
| 56 | +`workspaces/*/e2e-tests` one, so it is outside `e2e-code-quality.yaml` (which only scans |
| 57 | +`workspaces/*/e2e-tests/**`). |
| 58 | + |
| 59 | +### CI |
| 60 | + |
| 61 | +`.github/workflows/native-smoke.yaml` runs the harness two ways: |
| 62 | + |
| 63 | +- **`pull_request`** (paths `smoke-tests-native/**`): validates the harness itself on every |
| 64 | + change here, against a known-good pure-backend plugin. |
| 65 | +- **`workflow_dispatch`**: Actions → "Native Smoke Harness" → Run workflow, with an optional |
| 66 | + `plugin_ref` to validate any plugin on demand. |
| 67 | + |
| 68 | +It installs skopeo, builds, runs `yarn smoke`, uploads `results.json`, and fails the job on |
| 69 | +a non-passing plugin. |
| 70 | + |
| 71 | +Exit code `0` = pass; non-zero with `results.json` detailing `fail-load` / `fail-start` / |
| 72 | +`fail-bundle`. |
| 73 | + |
| 74 | +## Best fit (from the 64-workspace analysis, RHIDP-15076) |
| 75 | + |
| 76 | +- **12 pure-backend workspaces** → fully covered here (load + backend start): |
| 77 | + `3scale, ai-integrations, apiconnect, github-notifications, keycloak, |
| 78 | + mcp-integrations, pingidentity, scaffolder-backend-module-{kubernetes,regex,servicenow,sonarqube}, |
| 79 | + scaffolder-relation-processor`. |
| 80 | +- **32 smoke-tests** → replace the Docker container with this harness (backend start + |
| 81 | + frontend bundle/registration check). |
| 82 | +- **24 UI e2e-tests** → NOT this harness; need the NFS/app-next render harness. |
| 83 | + |
| 84 | +## Status of validation |
| 85 | + |
| 86 | +- ✅ Install CLI interface confirmed: `@red-hat-developer-hub/cli-module-install-dynamic-plugins@0.3.0` |
| 87 | + (`install <dynamic-plugins-root>`), fetchable via `npx`. |
| 88 | +- ✅ Harness logic ported from the **already-green** RHDH nightly test (PR #4967). |
| 89 | +- ✅ Builds clean (esbuild → `dist/native-smoke.mjs`, run with plain `node`); `tsc --noEmit` passes. |
| 90 | +- ✅ `patchModuleResolution()` ported (`src/module-resolution.ts`) so extracted plugins |
| 91 | + resolve their `@backstage/*` peers against this harness's `node_modules`. Requires a |
| 92 | + node-modules linker — see `.yarnrc.yml`. |
| 93 | +- ✅ End-to-end run done locally (Node 24) against a real catalog-index plugin: `pass`, |
| 94 | + backend loaded 1/1, `startTestBackend` booted — see the Benchmark section below. |
| 95 | + |
| 96 | +## Module resolution |
| 97 | + |
| 98 | +Extracted plugins live under a temp dir with no `node_modules` of their own, so their bare |
| 99 | +`@backstage/*` imports must resolve against this harness. `patchModuleResolution()` (ported |
| 100 | +from RHDH PR #4967) extends `Module._nodeModulePaths` to append `HARNESS_NODE_MODULES` |
| 101 | +before any plugin is `require`d. This is why the package uses `nodeLinker: node-modules` |
| 102 | +(`.yarnrc.yml`) rather than Yarn PnP — the patch needs a real `node_modules` directory to |
| 103 | +point at. |
| 104 | + |
| 105 | +## Benchmark: native vs Docker (real run) |
| 106 | + |
| 107 | +Same plugin both ways: `roadiehq-scaffolder-backend-module-http-request` |
| 108 | +(`bs_1.49.4__5.6.0`), from the real catalog index |
| 109 | +`quay.io/rhdh-community/plugin-catalog-index:1.11-bs_1.49.4`. Same minimal app-config |
| 110 | +(sqlite `:memory:` + guest). Node 24. The RHDH base image (`quay.io/rhdh-community/rhdh:next`, |
| 111 | +6.55 GB) was pre-pulled and is excluded from the Docker timing (one-time infra, amortized |
| 112 | +across all workspaces in a CI run). |
| 113 | + |
| 114 | +| Approach | What it does | Wall-clock | |
| 115 | +|----------|--------------|------------| |
| 116 | +| **Native (this harness)** | skopeo pull plugin → load → `startTestBackend` boot | **5 s cold, 3–4 s warm** | |
| 117 | +| **Docker smoke** (`run-workspace-smoke-tests.yaml`) | container start → in-container `install-dynamic-plugins` (pulls same plugin) → full `node packages/backend` boot → `/healthcheck` 200 | **104 s** | |
| 118 | + |
| 119 | +Roughly **20× faster cold, ~25–35× warm.** Both confirm the plugin loads; the Docker run |
| 120 | +additionally boots the entire RHDH backend (that extra work is exactly the overhead the |
| 121 | +in-process approach removes). Note the comparison is per-workspace — the Docker smoke boots |
| 122 | +one container per workspace, which is the unit this harness replaces. |
| 123 | + |
| 124 | +Caveat: the native harness currently boots a minimal backend scoped to the plugin's needs |
| 125 | +(e.g. scaffolder for scaffolder modules). Catalog-extending modules need the catalog core, |
| 126 | +which does not yet boot cleanly standalone — see the coreFeatures note in `src/native-smoke.ts`. |
0 commit comments