Skip to content

feat(poc): replace Docker-based smoke tests with native Node.js harness#2231

Open
rostalan wants to merge 8 commits intoredhat-developer:mainfrom
rostalan:smoke-test-poc
Open

feat(poc): replace Docker-based smoke tests with native Node.js harness#2231
rostalan wants to merge 8 commits intoredhat-developer:mainfrom
rostalan:smoke-test-poc

Conversation

@rostalan
Copy link
Copy Markdown
Contributor

@rostalan rostalan commented Apr 9, 2026

Boot a minimal Backstage backend directly on the runner using createBackend() + dynamicPluginsFeatureLoader, probe /api/<pluginId> routes, and report results as structured JSON. Includes core bundled plugins (catalog, auth, permission, scaffolder, events, search, proxy) so dynamic plugins resolve their dependencies correctly.

Frontend plugins are also validated via two layers:

  • Static bundle checks — verifies dist-scalprum/ exists and contains JavaScript files after OCI download
  • Loaded-plugin probe — an inline backend plugin queries dynamicPluginsServiceRef to confirm frontend plugins were registered without errors

Results are merged into a single report with per-plugin status (pass, fail-bundle, fail-load, warn, skip) and the process exits non-zero on any failure.

Made-with: Cursor

@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Apr 9, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@rostalan
Copy link
Copy Markdown
Contributor Author

rostalan commented Apr 9, 2026

/publish

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

PR action (/publish) cancelled: PR doesn't touch only 1 workspace.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

⚠️
Smoke test workflow skipped: PR doesn't touch exactly one workspace.

@github-actions github-actions bot added the non-workspace-changes PR changes files outside workspace directories label Apr 9, 2026
@rostalan rostalan changed the title Replace Docker-based smoke tests with native Node.js harness feat(poc): replace Docker-based smoke tests with native Node.js harness Apr 9, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

The file versions.json could not be synced from branch main into this because your PR is from a fork.

You should update the versions.json file with the following content:

{
    "backstage": "1.48.3",
    "node": "22.22.0",
    "cli": "1.10.4",
    "cliPackage": "@red-hat-developer-hub/cli"
}

Comment thread smoke-tests/smoke-test.mjs Outdated
}

// ---------------------------------------------------------------------------
// OCI Download (mirrors install-dynamic-plugins.py §663-715)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment itself acknowledges this mirrors install-dynamic-plugins.py. Should we just call the Python script directly as a pre-step instead of reimplementing it in JS?

The overlay repo currently gets the script from the RHDH container image — by removing Docker, we lose access to it. But we could copy the script here (with a version marker) as a short-term solution. That would:

  • Eliminate ~60 lines of JS OCI code
  • Inherit all security protections for free (zip bomb, symlink traversal, integrity checks)
  • Leverage the 130KB+ test suite that already covers this logic

Longer-term options: publish as a pip package or container tool so any repo can consume it.

For context, there's also a parallel POC in RHDH core (redhat-developer/rhdh#4523) that extends the same Python script with parallel downloads (ThreadPoolExecutor) achieving ~34% speedup. Reusing it here would get that performance benefit too.

Copy link
Copy Markdown
Contributor Author

@rostalan rostalan Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now, I added a step that downloads install-dynamic-plugins script and its requirements and uses it directly. Not sure if the would benefit from the parallel downloads, since it only runs on a single workspace, so only a couple OCIs at a time.

Comment thread smoke-tests/smoke-test.mjs Outdated

function extractPlugin(tarFile, pluginPath, dest) {
mkdirSync(join(dest, pluginPath), { recursive: true });
execSync(`tar xf "${tarFile}" -C "${dest}" "${pluginPath}/"`, {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extracts tar contents with a bare execSync('tar xf ...') — no validation of archive members before extraction. The Python script in RHDH core (install-dynamic-plugins.py) checks for:

  • Zip bomb: member.size > MAX_ENTRY_SIZE (20MB default per entry)
  • Symlink traversal: os.path.realpath() validation against the destination directory
  • Hardlink traversal: same check
  • Device files/FIFOs: rejected entirely
  • Safe tar filter: tar.extractall(..., filter='tar')

Even in CI, a crafted OCI layer could exploit path traversal via symlinks. If we keep the JS implementation, all of these checks need to be ported.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced by python script.

Comment thread smoke-tests/smoke-test.mjs Outdated
if (existsSync(p)) process.argv.push("--config", p);
}

const { createBackend } = await import("@backstage/backend-defaults");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createBackend() is the production API. Backstage provides startTestBackend() from @backstage/backend-test-utils specifically for this use case — it handles lifecycle management, automatic cleanup, built-in in-memory SQLite, and exposes mock services. It would also reduce the manual plugin registration below (~20 lines of backend.add() calls).

redhat-developer/rhdh#4523 uses startTestBackend() for the same plugin loadability validation and it works well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was meant to keep the environment somewhat real, but I guess it makes sense to have this as a simple "plugin loads" check rather than anything more complicated.

Comment thread smoke-tests/smoke-test.mjs Outdated
// Config generation
// ---------------------------------------------------------------------------

function deepMerge(src, dst) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reimplements config deep-merge that already exists in install-dynamic-plugins.py (the merge() function). The Python version also detects key conflicts and raises errors — this one silently overwrites. If we use the Python script as a pre-step, this function becomes unnecessary since the script already generates app-config.dynamic-plugins.yaml with all plugin configs merged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced with python script

Comment thread smoke-tests/smoke-test.mjs Outdated
);

backend.add(
createBackendPlugin({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probe plugin concept is great — using dynamicPluginsServiceRef to verify frontend plugins actually loaded at runtime is more thorough than filesystem-only checks. Worth keeping regardless of the OCI download approach. The sanity check POC in RHDH core (redhat-developer/rhdh#4523) currently only validates bundles via filesystem (dist-scalprum/plugin-manifest.json); this runtime probe approach would complement it nicely.

Comment thread smoke-tests/smoke-test.mjs Outdated
});
}

async function downloadPlugins(plugins, dest) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downloads are sequential here. The install-dynamic-plugins-fast.py in redhat-developer/rhdh#4523 uses ThreadPoolExecutor for parallel OCI downloads with a shared image cache (one download per unique image, not per plugin), achieving ~34% speedup. Another reason to reuse the existing script rather than maintaining a separate implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced with the basic python script, same reason as above.

@github-actions
Copy link
Copy Markdown
Contributor

The file versions.json could not be synced from branch main into this because your PR is from a fork.

You should update the versions.json file with the following content:

{
    "backstage": "1.49.4",
    "node": "22.22.0",
    "cli": "1.10.4",
    "cliPackage": "@red-hat-developer-hub/cli"
}

@rostalan rostalan marked this pull request as ready for review April 15, 2026 07:49
@rostalan rostalan requested review from a team, gashcrumb and kadel as code owners April 15, 2026 07:49
@rostalan
Copy link
Copy Markdown
Contributor Author

/publish

@github-actions
Copy link
Copy Markdown
Contributor

PR action (/publish) cancelled: PR doesn't touch only 1 workspace.

@rostalan
Copy link
Copy Markdown
Contributor Author

/publish

@github-actions
Copy link
Copy Markdown
Contributor

PR action (/publish) cancelled: PR doesn't touch only 1 workspace.

@rostalan
Copy link
Copy Markdown
Contributor Author

/publish

@github-actions
Copy link
Copy Markdown
Contributor

PR action (/publish) cancelled: PR doesn't touch only 1 workspace.

- name: Log in to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@v4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be pinned to a hash and match the other workflows:

Suggested change
uses: actions/setup-node@v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0

registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
node-version: "20"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two LTS releases after this one that are currently active, let's not target older Node.js versions if we can avoid it.

Suggested change
node-version: "20"
node-version: "24"

echo "RHDH did not become ready in time."
exit 1
curl -fsSL \
"https://raw.githubusercontent.com/redhat-developer/rhdh/3efb9cc140ff/scripts/install-dynamic-plugins/install-dynamic-plugins.py" \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be pinned to a specific commit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will get replaced with the ts version after redhat-developer/rhdh#4574 gets merged anyway...

Comment thread smoke-tests/package.json Outdated
"type": "module",
"description": "Lightweight smoke test harness for RHDH dynamic plugins — no Docker required",
"scripts": {
"test": "node smoke-test.mjs",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this script is not triggered in the CI, but rather executed manually, let's use npm test for consistency.

Comment thread smoke-tests/package.json
Comment on lines +13 to +21
"@backstage/backend-dynamic-feature-service": "0.7.9",
"@backstage/backend-plugin-api": "1.7.0",
"@backstage/backend-test-utils": "1.4.0",
"@backstage/cli-node": "0.2.18",
"@backstage/config": "1.3.6",
"@backstage/config-loader": "1.10.8",
"@backstage/errors": "1.2.7",
"@backstage/types": "1.2.2",
"js-yaml": "4.1.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not rely on pinned versions here, but use caret ranges ^ and a lockfile.

Comment thread smoke-tests/smoke-test.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to use .mjs, you can use .js and Node will be able to detect it, also because the type is already set in the package.

Comment thread smoke-tests/smoke-test.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node.js can run TypeScript these days, so using .ts would also work, and make your code type-safe.

Comment thread smoke-tests/smoke-test.mjs Outdated
// CLI
// ---------------------------------------------------------------------------

function parseArgs(argv) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node.js has built-in support for parsing CLI arguments, there is no need to implement this yourself.

Comment thread smoke-tests/smoke-test.mjs Outdated
return args;
}

function loadEnvFile(filePath) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node.js has built-in support for parsing .env files, so there is no need to implement this yourself.

Comment thread smoke-tests/smoke-test.mjs Outdated
// Config helpers
// ---------------------------------------------------------------------------

function deepMerge(src, dst) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom deepMerge and loadConfigs can be removed entirely — Backstage's config loader already handles deep merging of multiple --config files. Just push the config paths onto process.argv before calling startTestBackend() and let the default rootConfig service pick them up. This is the same approach the old Docker-based smoke test used (chaining --config flags on the node packages/backend command).

This also removes the need for mockServices.rootConfig.factory({ data }).

Boot a minimal Backstage backend directly on the runner using
createBackend() + dynamicPluginsFeatureLoader, probe /api/<pluginId>
routes, and report results as structured JSON. Includes core bundled
plugins (catalog, auth, permission, scaffolder, events, search, proxy)
so dynamic plugins resolve their dependencies correctly.

Made-with: Cursor
…aded-plugin probe

Add two-layer frontend plugin validation to the smoke test harness:
- Layer 1: static validation of dist-scalprum/ after OCI download
- Layer 2: inline backend probe plugin querying dynamicPluginsServiceRef

Made-with: Cursor
…to install-dynamic-plugins.py

Replace createBackend() with startTestBackend() for a minimal, focused
smoke test that only validates dynamic plugin loading. Remove 23 static
plugin dependencies and the in-process OCI download logic in favor of
the upstream install-dynamic-plugins.py script fetched at CI time.

Made-with: Cursor

Signed-of-by: rlan@redhat.com
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

non-workspace-changes PR changes files outside workspace directories

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants