Skip to content

feat(install-dynamic-plugins): port from Python to TypeScript/Node.js#4574

Open
gustavolira wants to merge 8 commits into
redhat-developer:mainfrom
gustavolira:feat/install-dynamic-plugins-ts
Open

feat(install-dynamic-plugins): port from Python to TypeScript/Node.js#4574
gustavolira wants to merge 8 commits into
redhat-developer:mainfrom
gustavolira:feat/install-dynamic-plugins-ts

Conversation

@gustavolira
Copy link
Copy Markdown
Member

Summary

Replaces the Python-based install-dynamic-plugins.py init-container script with a TypeScript/Node.js implementation, incorporating all improvements from the install-dynamic-plugins-fast.py POC (#4523): parallel OCI downloads, shared image cache, streaming SHA verification.

Motivation: The Python script was a longstanding duplication target for export overlays (see rhdh-plugin-export-overlays#2231). A Node.js implementation removes the Python dependency from the init-container runtime, enables reuse across RHDH core and overlays, and adopts the faster parallel architecture natively.

What changed

  • New package scripts/install-dynamic-plugins/ — 18 TypeScript modules + 9 Jest test files (105 tests)
  • Bundled output: single dist/install-dynamic-plugins.cjs (~412 KB), committed and verified fresh in CI (same pattern as .yarn/releases/yarn-*.cjs)
  • Containerfile now copies the .cjs bundle instead of .py; wrapper shell script execs node instead of python
  • CI workflow runs npm run tsc && npm test + bundle freshness check (replaces pytest)
  • Deleted: install-dynamic-plugins.py (1288 LOC), test_install-dynamic-plugins.py (3065 LOC), pytest.ini

Key design decisions

Runtime contract is unchanged

Same dynamic-plugins.yaml input schema, same app-config.dynamic-plugins.yaml output, same dynamic-plugin-config.hash / dynamic-plugin-image.hash files, same lock-file behaviour, same {{inherit}} semantics and OCI path auto-detection.

Resource-conscious concurrency

  • availableParallelism() respects cgroup CPU limits (init containers often get 0.5 CPU)
  • Default workers: max(1, min(floor(cpus/2), 6)) — cap avoids exhausting registry/network
  • NPM installs stay sequential (npm registry throttles parallel fetches)
  • Override via DYNAMIC_PLUGINS_WORKERS=<n>

Memory: streaming everywhere

  • node-tar streams extraction — no full-archive read into RAM
  • node:crypto pipeline for SHA integrity — chunks through the hash
  • Typical 10-plugin run: 20–80 MB peak RSS (well below 512 Mi init-container limit)

Security parity with Python

Check File
Path traversal in plugin path (.., absolute) tar-extract.ts
Per-entry size cap (zip bomb, MAX_ENTRY_SIZE) tar-extract.ts, catalog-index.ts
Sym/hardlink target must stay inside destination tar-extract.ts
Reject device files / FIFOs / unknown entry types tar-extract.ts
package/ prefix enforced for NPM tarballs tar-extract.ts
SRI integrity (sha256 / sha384 / sha512) integrity.ts
Registry fallback registry.access.redhat.com/rhdhquay.io/rhdh image-resolver.ts

Test plan

  • npm run tsc passes (strict mode, noUncheckedIndexedAccess)
  • npm test — 105 Jest tests pass (npm-key, oci-key, integrity, tar-extract, merger, concurrency, lock-file, image-resolver, plugin-hash)
  • npm run build produces fresh dist/install-dynamic-plugins.cjs
  • Container image builds successfully with new .cjs (CI will verify)
  • Init-container flow validated on cluster (end-to-end catalog install + RHDH startup)
  • Resource profile check: wall-clock ≤ fast.py baseline (~2:42 for full catalog)

Compatibility

  • Python 3.11 is still installed in the runtime image for techdocs/mkdocs
  • skopeo is still installed for OCI inspection
  • No new system dependencies required (Node.js 22 already runs the Backstage backend)
  • install-dynamic-plugins.sh wrapper contract unchanged (./install-dynamic-plugins.sh /dynamic-plugins-root)

Related

@openshift-ci openshift-ci Bot requested review from hopehadfield and kadel April 13, 2026 14:01
@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge Bot commented Apr 13, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. OCI prefix-collision extraction ✓ Resolved 🐞 Bug ⛨ Security
Description
extractOciPlugin filters tar entries using filePath.startsWith(pluginPath), which is not
boundary-safe and will also match sibling directories with the same prefix (e.g., extracting
plugin-one also extracts plugin-one-evil/...), installing unintended files.
Code

scripts/install-dynamic-plugins/src/tar-extract.ts[R41-45]

Evidence
The filter condition is a raw string prefix check against pluginPath, so it accepts any tar entry
whose path begins with that prefix even if it is not within the intended directory. The existing
test asserts only requested subdirectory extraction, but it doesn’t cover the prefix-collision case
(plugin-one vs plugin-one-evil).

scripts/install-dynamic-plugins/src/tar-extract.ts[41-46]
scripts/install-dynamic-plugins/tests/tar-extract.test.ts[34-47]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`extractOciPlugin()` uses `filePath.startsWith(pluginPath)` which allows prefix-collision directories (e.g., `plugin-one-evil/...`) to be extracted when requesting `plugin-one`.

### Issue Context
This function is intended to extract *only* the requested plugin subdirectory from an OCI layer.

### Fix Focus Areas
- scripts/install-dynamic-plugins/src/tar-extract.ts[41-46]
- scripts/install-dynamic-plugins/__tests__/tar-extract.test.ts[34-47]

### Suggested fix
- Replace the condition with a boundary-safe check, e.g.:
 - accept when `filePath === pluginPath` OR `filePath.startsWith(pluginPathWithSlash)` where `pluginPathWithSlash` is `pluginPath` normalized to end with `/`.
 - Consider using `path.posix` since tar entry paths are POSIX-style.
- Add a Jest test that creates both `plugin-one/` and `plugin-one-evil/` in the tarball and verifies that extracting `plugin-one` does *not* extract `plugin-one-evil`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. NaN disables entry-size cap ✓ Resolved 🐞 Bug ⛨ Security
Description
MAX_ENTRY_SIZE is parsed with Number(...) without validation; if the env var is non-numeric it
becomes NaN, making every stat.size > MAX_ENTRY_SIZE check evaluate to false and disabling
oversized-entry/zip-bomb protections during tar extraction.
Code

scripts/install-dynamic-plugins/src/types.ts[R39-40]

Evidence
MAX_ENTRY_SIZE is computed via Number(process.env.MAX_ENTRY_SIZE ?? 20_000_000), which yields
NaN for invalid values; extraction code relies on stat.size > MAX_ENTRY_SIZE (which will never
be true when MAX_ENTRY_SIZE is NaN), so the intended size guard is effectively disabled.

scripts/install-dynamic-plugins/src/types.ts[39-40]
scripts/install-dynamic-plugins/src/tar-extract.ts[43-49]
scripts/install-dynamic-plugins/src/catalog-index.ts[62-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`MAX_ENTRY_SIZE` can become `NaN` when `process.env.MAX_ENTRY_SIZE` is set to a non-numeric value, which disables the `stat.size > MAX_ENTRY_SIZE` safety checks.

### Issue Context
This value is used as a security boundary (zip bomb / oversized tar entry protection) across tar extraction code paths.

### Fix Focus Areas
- scripts/install-dynamic-plugins/src/types.ts[39-40]
- scripts/install-dynamic-plugins/src/tar-extract.ts[43-49]
- scripts/install-dynamic-plugins/src/catalog-index.ts[62-66]

### Suggested fix
- Parse with `Number.parseInt(..., 10)`.
- If the parsed value is not finite or is < 1, fall back to the default (20_000_000) and optionally log a warning.
- (Optional) Clamp to a sane upper bound to avoid accidentally setting an enormous limit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread scripts/install-dynamic-plugins/src/merger.ts Fixed
@rhdh-qodo-merge
Copy link
Copy Markdown

Review Summary by Qodo

Port install-dynamic-plugins from Python to TypeScript/Node.js with performance improvements

✨ Enhancement

Grey Divider

Walkthroughs

Description
• **Complete rewrite**: Replaces Python-based install-dynamic-plugins.py with a TypeScript/Node.js
  implementation (18 modules, 105 Jest tests)
• **Performance improvements**: Incorporates parallel OCI downloads, shared image cache, and
  streaming SHA verification from the install-dynamic-plugins-fast.py POC
• **New package structure**: scripts/install-dynamic-plugins/ with TypeScript sources, bundled to
  single dist/install-dynamic-plugins.cjs (~412 KB)
• **Runtime contract unchanged**: Same dynamic-plugins.yaml input schema, same output format, same
  lock-file behavior, same {{inherit}} semantics
• **Resource-conscious concurrency**: Respects cgroup CPU limits with default workers `max(1,
  min(floor(cpus/2), 6)), configurable via DYNAMIC_PLUGINS_WORKERS`
• **Memory-efficient**: Streaming tar extraction and SHA verification; typical 10-plugin run uses
  20–80 MB peak RSS
• **Security parity**: Path traversal prevention, zip-bomb detection, symlink validation, device
  file rejection, SRI integrity verification, registry fallback
• **CI updated**: Replaced pytest with npm run tsc && npm test plus bundle freshness
  verification
• **Container image**: Updated Containerfile to copy .cjs bundle instead of .py; wrapper shell
  script execs node instead of python
• **Deleted**: install-dynamic-plugins.py (1288 LOC), test_install-dynamic-plugins.py (3065
  LOC), pytest.ini
• **Documentation**: Comprehensive README, updated user docs and CI references
Diagram
flowchart LR
  A["Python Script<br/>install-dynamic-plugins.py"] -->|"Replaced by"| B["TypeScript/Node.js<br/>18 modules + 105 tests"]
  B -->|"Bundled to"| C["dist/install-dynamic-plugins.cjs<br/>~412 KB"]
  C -->|"Copied in"| D["Container Image<br/>Containerfile"]
  E["Parallel OCI<br/>Downloads"] -->|"Incorporated"| B
  F["Shared Image<br/>Cache"] -->|"Incorporated"| B
  G["Streaming SHA<br/>Verification"] -->|"Incorporated"| B
  H["pytest"] -->|"Replaced by"| I["npm test<br/>Jest 105 tests"]
Loading

Grey Divider

File Changes

1. scripts/install-dynamic-plugins/src/index.ts ✨ Enhancement +301/-0

Main orchestrator for dynamic plugin installation

• Main entry point orchestrating the full plugin installation flow
• Handles dynamic-plugins.yaml parsing, plugin merging, and categorization (OCI/NPM/local)
• Manages parallel OCI and sequential NPM installations with error collection
• Implements cleanup of obsolete plugins and lock file management

scripts/install-dynamic-plugins/src/index.ts


2. scripts/install-dynamic-plugins/src/merger.ts ✨ Enhancement +238/-0

Plugin configuration merging and conflict detection

• Implements deep recursive merging of plugin configurations with conflict detection
• Handles OCI plugin merging with {{inherit}} tag resolution and auto-path detection
• Manages NPM plugin merging with version stripping and deduplication
• Raises on conflicting scalar values to prevent silent overwrites

scripts/install-dynamic-plugins/src/merger.ts


3. scripts/install-dynamic-plugins/__tests__/oci-key.test.ts 🧪 Tests +197/-0

OCI package specification parsing tests

• Tests OCI package-spec parsing with tags, digests, and {{inherit}} syntax
• Validates error handling for malformed OCI references
• Tests auto-detection of plugin paths from image cache
• Covers registry with ports and multiple digest algorithms

scripts/install-dynamic-plugins/tests/oci-key.test.ts


View more (45)
4. scripts/install-dynamic-plugins/__tests__/tar-extract.test.ts 🧪 Tests +134/-0

Tar extraction security and functionality tests

• Tests streaming extraction of OCI plugin layers with security checks
• Validates path-traversal protection and zip-bomb detection
• Tests NPM tarball extraction with package/ prefix enforcement
• Covers symlink escape detection and entry size limits

scripts/install-dynamic-plugins/tests/tar-extract.test.ts


5. scripts/install-dynamic-plugins/src/tar-extract.ts ✨ Enhancement +168/-0

Secure streaming tar extraction with validation

• Streaming tar extraction for OCI layers and NPM packages using node-tar
• Security checks: path-traversal prevention, per-entry size limits, symlink validation
• Rejects device files and non-regular entry types
• Enforces package/ prefix for NPM tarballs

scripts/install-dynamic-plugins/src/tar-extract.ts


6. scripts/install-dynamic-plugins/src/catalog-index.ts ✨ Enhancement +142/-0

Catalog index image extraction and processing

• Extracts catalog index OCI image and produces dynamic-plugins.default.yaml
• Optionally extracts catalog entities to configurable directory
• Streams layer extraction with security checks (size limits, symlink validation)
• Handles manifest parsing and layer digest resolution

scripts/install-dynamic-plugins/src/catalog-index.ts


7. scripts/install-dynamic-plugins/src/oci-key.ts ✨ Enhancement +100/-0

OCI package specification parsing and validation

• Parses OCI package specifications with tags, digests, and {{inherit}} syntax
• Auto-detects plugin paths from image cache when not explicitly specified
• Validates registry format including host:port combinations
• Supports multiple digest algorithms (sha256, sha512, blake3)

scripts/install-dynamic-plugins/src/oci-key.ts


8. scripts/install-dynamic-plugins/src/image-cache.ts ✨ Enhancement +98/-0

OCI image caching and manifest inspection

• Shared cache for OCI image tarballs to avoid redundant downloads
• Caches promises to deduplicate concurrent requests for the same image
• Retrieves image digests and plugin paths from OCI manifests
• Handles io.backstage.dynamic-packages annotation parsing

scripts/install-dynamic-plugins/src/image-cache.ts


9. scripts/install-dynamic-plugins/src/installer-oci.ts ✨ Enhancement +96/-0

OCI plugin installation with change detection

• Installs single OCI-packaged plugin with pull-policy support
• Implements change detection via plugin hash and image digest comparison
• Writes hash files for tracking installed plugins
• Handles skip logic for already-installed plugins

scripts/install-dynamic-plugins/src/installer-oci.ts


10. scripts/install-dynamic-plugins/src/npm-key.ts ✨ Enhancement +68/-0

NPM package specification parsing

• Parses NPM package specifications (standard, aliases, git URLs, GitHub shorthand)
• Strips version/ref information for deduplication keys
• Handles local paths and tarball files unchanged
• Matches npm CLI v11 package-spec reference

scripts/install-dynamic-plugins/src/npm-key.ts


11. scripts/install-dynamic-plugins/__tests__/merger.test.ts 🧪 Tests +62/-0

Plugin merger and deep-merge tests

• Tests deep merge functionality with conflict detection
• Validates NPM plugin merging with level-based override semantics
• Tests duplicate detection within same configuration level
• Covers type validation for plugin package field

scripts/install-dynamic-plugins/tests/merger.test.ts


12. scripts/install-dynamic-plugins/src/installer-npm.ts ✨ Enhancement +80/-0

NPM plugin installation with integrity verification

• Installs NPM or local plugins using npm pack and streaming extraction
• Verifies SRI integrity for remote packages (unless skipped)
• Implements pull-policy and force-download logic
• Writes config hash for change detection

scripts/install-dynamic-plugins/src/installer-npm.ts


13. scripts/install-dynamic-plugins/src/skopeo.ts ✨ Enhancement +79/-0

Skopeo CLI wrapper with caching

• Wrapper around skopeo CLI with promise-based caching for inspect results
• Deduplicates concurrent requests for the same image
• Provides copy, inspect, and exists operations
• Handles both raw and parsed manifest inspection

scripts/install-dynamic-plugins/src/skopeo.ts


14. scripts/install-dynamic-plugins/src/plugin-hash.ts ✨ Enhancement +74/-0

Plugin hash computation for change detection

• Computes SHA256 hash for change detection ("already installed?")
• For local packages, includes package.json and lock-file mtimes
• Excludes version, pluginConfig, and _level from hash calculation
• Uses deterministic JSON stringification with sorted keys

scripts/install-dynamic-plugins/src/plugin-hash.ts


15. scripts/install-dynamic-plugins/__tests__/concurrency.test.ts 🧪 Tests +76/-0

Concurrency control and worker selection tests

• Tests Semaphore class for bounding concurrent operations
• Validates mapConcurrent respects concurrency limits and captures errors
• Tests getWorkers() with environment variable override and auto-detection
• Covers cgroup CPU limit awareness

scripts/install-dynamic-plugins/tests/concurrency.test.ts


16. scripts/install-dynamic-plugins/__tests__/npm-key.test.ts 🧪 Tests +48/-0

NPM package specification parsing tests

• Tests NPM package-spec parsing for standard packages, aliases, and git URLs
• Validates version/ref stripping for deduplication
• Covers local paths and tarball files (unchanged)
• Tests GitHub shorthand and various git URL formats

scripts/install-dynamic-plugins/tests/npm-key.test.ts


17. scripts/install-dynamic-plugins/src/concurrency.ts ✨ Enhancement +77/-0

Concurrency control with resource-conscious defaults

• Implements Semaphore for bounding concurrent async operations
• Provides mapConcurrent for parallel work with error capture (no cancellation)
• Implements getWorkers() with cgroup-aware CPU limit detection
• Default: max(1, min(floor(cpus/2), 6)) with DYNAMIC_PLUGINS_WORKERS override

scripts/install-dynamic-plugins/src/concurrency.ts


18. scripts/install-dynamic-plugins/__tests__/integrity.test.ts 🧪 Tests +63/-0

SRI integrity verification tests

• Tests SRI integrity verification for sha256, sha512, and sha384
• Validates error handling for malformed integrity strings and unsupported algorithms
• Tests base64 validation and hash mismatch detection
• Covers streaming verification without full archive load

scripts/install-dynamic-plugins/tests/integrity.test.ts


19. scripts/install-dynamic-plugins/src/integrity.ts ✨ Enhancement +65/-0

Streaming SRI integrity verification

• Verifies NPM package archives against SRI-style integrity strings
• Uses streaming createHash to avoid loading large files into memory
• Supports sha256, sha384, and sha512 algorithms
• Validates base64 encoding and algorithm support

scripts/install-dynamic-plugins/src/integrity.ts


20. scripts/install-dynamic-plugins/src/lock-file.ts ✨ Enhancement +69/-0

Exclusive lock file management with signal cleanup

• Acquires exclusive lock file with polling for concurrent process safety
• Registers cleanup handlers for SIGTERM, SIGINT, and process exit
• Implements atomic lock creation with wx flag
• Mirrors Python loop behavior for resilience to stale locks

scripts/install-dynamic-plugins/src/lock-file.ts


21. scripts/install-dynamic-plugins/__tests__/image-resolver.test.ts 🧪 Tests +36/-0

Registry fallback resolution tests

• Tests registry fallback from registry.access.redhat.com/rhdh to quay.io/rhdh
• Validates protocol preservation (oci:// and docker://) on fallback
• Tests non-RHDH images pass through unchanged
• Covers existence check via skopeo inspect

scripts/install-dynamic-plugins/tests/image-resolver.test.ts


22. scripts/install-dynamic-plugins/src/run.ts ✨ Enhancement +39/-0

Subprocess execution with structured error handling

• Executes subprocesses with captured stdout/stderr
• Throws InstallException with full context (exit code, stderr) on failure
• Provides structured error messages for debugging
• Matches Python run() contract

scripts/install-dynamic-plugins/src/run.ts


23. scripts/install-dynamic-plugins/src/types.ts ✨ Enhancement +41/-0

Type definitions and configuration constants

• Defines Plugin, PluginMap, and DynamicPluginsConfig types
• Exports constants for file names, protocols, and registry URLs
• Defines PullPolicy enum and Algorithm type for integrity
• Sets MAX_ENTRY_SIZE and RECOGNIZED_ALGORITHMS from environment/defaults

scripts/install-dynamic-plugins/src/types.ts


24. scripts/install-dynamic-plugins/__tests__/lock-file.test.ts 🧪 Tests +33/-0

Lock file creation and release tests

• Tests atomic lock file creation and removal
• Validates no-op behavior when lock file is absent
• Tests waiting for existing lock release before acquisition
• Covers concurrent process safety

scripts/install-dynamic-plugins/tests/lock-file.test.ts


25. scripts/install-dynamic-plugins/src/image-resolver.ts ✨ Enhancement +27/-0

OCI image reference resolution with fallback

• Resolves OCI image references with registry fallback logic
• Falls back from registry.access.redhat.com/rhdh to quay.io/rhdh when unavailable
• Preserves protocol (oci:// or docker://) on fallback
• Uses skopeo exists for availability check

scripts/install-dynamic-plugins/src/image-resolver.ts


26. scripts/install-dynamic-plugins/__tests__/plugin-hash.test.ts 🧪 Tests +29/-0

Plugin hash computation tests

• Tests deterministic hash generation for plugin change detection
• Validates that pluginConfig and version don't affect hash
• Tests hash changes when package or pullPolicy changes
• Covers local package info inclusion in hash

scripts/install-dynamic-plugins/tests/plugin-hash.test.ts


27. scripts/install-dynamic-plugins/src/which.ts ✨ Enhancement +26/-0

PATH lookup utility without external dependency

• Minimal which(1) implementation without external dependency
• Searches PATH for executable binary with platform-specific extensions
• Returns absolute path or null if not found
• Handles Windows and Unix path separators

scripts/install-dynamic-plugins/src/which.ts


28. scripts/install-dynamic-plugins/src/errors.ts ✨ Enhancement +10/-0

Custom exception type for installer errors

• Defines InstallException for installer-level failures
• Allows callers to distinguish expected failures from bugs
• Extends Error with custom name for better error handling

scripts/install-dynamic-plugins/src/errors.ts


29. scripts/install-dynamic-plugins/src/log.ts ✨ Enhancement +3/-0

Uniform logging utility

• Provides uniform stdout logging function
• Simple wrapper around process.stdout.write with newline

scripts/install-dynamic-plugins/src/log.ts


30. scripts/install-dynamic-plugins/install-dynamic-plugins.sh ✨ Enhancement +1/-1

Shell wrapper updated for Node.js execution

• Updated wrapper script to execute Node.js instead of Python
• Changed from python install-dynamic-plugins.py to node install-dynamic-plugins.cjs
• Uses exec for process replacement

scripts/install-dynamic-plugins/install-dynamic-plugins.sh


31. scripts/install-dynamic-plugins/.prettierrc.js ⚙️ Configuration changes +15/-0

Prettier code formatting configuration

• Prettier configuration for TypeScript code formatting
• Sets printWidth to 100, trailing commas, and consistent style
• Enforces consistent formatting across the package

scripts/install-dynamic-plugins/.prettierrc.js


32. scripts/install-dynamic-plugins/README.md 📝 Documentation +110/-0

Complete documentation for TypeScript implementation

• Comprehensive documentation of the TypeScript implementation
• Describes architecture, concurrency strategy, memory budget, and security checks
• Documents environment variables and development workflow
• Explains compatibility with previous Python implementation

scripts/install-dynamic-plugins/README.md


33. scripts/install-dynamic-plugins/package.json ⚙️ Configuration changes +34/-0

NPM package configuration and dependencies

• Defines npm package with Node.js 22+ requirement
• Lists dependencies: tar and yaml
• Includes dev dependencies for TypeScript, Jest, esbuild, and linting
• Provides build, test, and lint scripts

scripts/install-dynamic-plugins/package.json


34. .github/workflows/pr.yaml ⚙️ Configuration changes +16/-2

CI workflow updated for TypeScript testing

• Replaced Python pytest with TypeScript npm test workflow
• Added npm run tsc for type checking
• Added bundle freshness verification via npm run build and git diff
• Updated working directory to scripts/install-dynamic-plugins

.github/workflows/pr.yaml


35. build/containerfiles/Containerfile ⚙️ Configuration changes +2/-1

Container build updated for Node.js bundle

• Updated COPY directive to use bundled .cjs instead of .py
• Now copies dist/install-dynamic-plugins.cjs and install-dynamic-plugins.sh
• Removed reference to Python implementation

build/containerfiles/Containerfile


36. scripts/install-dynamic-plugins/tsconfig.json ⚙️ Configuration changes +22/-0

TypeScript compiler configuration

• TypeScript compiler configuration with strict mode enabled
• Sets target to ES2022 and module to NodeNext
• Enables noUncheckedIndexedAccess for safety
• Configures Jest and Node.js type definitions

scripts/install-dynamic-plugins/tsconfig.json


37. scripts/install-dynamic-plugins/jest.config.cjs ⚙️ Configuration changes +17/-0

Jest test runner configuration

• Jest test runner configuration with ts-jest preset
• Configures Node.js test environment and 20-second timeout
• Maps .js imports to .ts sources for NodeNext compatibility
• Includes coverage collection from src files

scripts/install-dynamic-plugins/jest.config.cjs


38. .cursor/rules/ci-e2e-testing.mdc 📝 Documentation +1/-1

Documentation reference updated for TypeScript

• Updated reference from install-dynamic-plugins.py to install-dynamic-plugins directory
• Reflects migration to TypeScript/Node.js implementation

.cursor/rules/ci-e2e-testing.mdc


39. docs/dynamic-plugins/installing-plugins.md 📝 Documentation +1/-1

User documentation updated for TypeScript

• Updated documentation reference from install-dynamic-plugins.py to install-dynamic-plugins
 directory
• Reflects migration to TypeScript/Node.js implementation

docs/dynamic-plugins/installing-plugins.md


40. scripts/install-dynamic-plugins/esbuild.config.mjs ⚙️ Configuration changes +15/-0

esbuild bundler configuration

• esbuild configuration for bundling TypeScript to CommonJS
• Targets Node.js 22 with shebang banner
• Produces single dist/install-dynamic-plugins.cjs file
• Disables minification and sourcemaps for readability

scripts/install-dynamic-plugins/esbuild.config.mjs


41. .claude/memories/ci-e2e-testing.md 📝 Documentation +1/-1

Memory note updated for TypeScript

• Updated reference from install-dynamic-plugins.py to install-dynamic-plugins directory
• Reflects migration to TypeScript/Node.js implementation

.claude/memories/ci-e2e-testing.md


42. .opencode/memories/ci-e2e-testing.md 📝 Documentation +1/-1

Update dynamic plugin installer documentation reference

• Updated documentation link from Python script to TypeScript/Node.js implementation directory
• Changed reference from scripts/install-dynamic-plugins/install-dynamic-plugins.py to
 scripts/install-dynamic-plugins

.opencode/memories/ci-e2e-testing.md


43. .claude/rules/ci-e2e-testing.md 📝 Documentation +1/-1

Update dynamic plugin installer documentation reference

• Updated documentation link from Python script to TypeScript/Node.js implementation directory
• Changed reference from scripts/install-dynamic-plugins/install-dynamic-plugins.py to
 scripts/install-dynamic-plugins

.claude/rules/ci-e2e-testing.md


44. .rulesync/rules/ci-e2e-testing.md 📝 Documentation +1/-1

Update dynamic plugin installer documentation reference

• Updated documentation link from Python script to TypeScript/Node.js implementation directory
• Changed reference from scripts/install-dynamic-plugins/install-dynamic-plugins.py to
 scripts/install-dynamic-plugins

.rulesync/rules/ci-e2e-testing.md


45. scripts/install-dynamic-plugins/dist/install-dynamic-plugins.cjs Additional files +11501/-0

...

scripts/install-dynamic-plugins/dist/install-dynamic-plugins.cjs


46. scripts/install-dynamic-plugins/install-dynamic-plugins.py Additional files +0/-1288

...

scripts/install-dynamic-plugins/install-dynamic-plugins.py


47. scripts/install-dynamic-plugins/pytest.ini Additional files +0/-4

...

scripts/install-dynamic-plugins/pytest.ini


48. scripts/install-dynamic-plugins/test_install-dynamic-plugins.py Additional files +0/-3065

...

scripts/install-dynamic-plugins/test_install-dynamic-plugins.py


Grey Divider

Qodo Logo

@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: failure.

@rostalan
Copy link
Copy Markdown
Contributor

my browser cannot even load the 11,5k-line file 😅

@gustavolira
Copy link
Copy Markdown
Member Author

my browser cannot even load the 11,5k-line file 😅

@rostalan No need to review that file, it's the auto-generated esbuild bundle of src/. Just pushed a commit marking it as linguist-generated so GitHub collapses it in the diff. Only src/ and tests/ need review; CI verifies the bundle is up-to-date on every push.

@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

gustavolira added a commit to gustavolira/rhdh that referenced this pull request Apr 13, 2026
… feedback

Addresses review feedback on PR redhat-developer#4574:

- Extract shared helpers into `src/util.ts`: `fileExists`, `isInside`,
  `isPlainObject`, `isAllowedEntryType`, `markAsFresh`. Removes 4x
  duplication across `index.ts`, `catalog-index.ts`, `installer-oci.ts`,
  `merger.ts`, and `tar-extract.ts`.

- Unify `catalog-index.ts` tar filter with `tar-extract.ts`: oversized
  entries now throw `InstallException` instead of being silently
  dropped; `OldFile` and `ContiguousFile` are accepted (were previously
  excluded by mistake). Uses the `isAllowedEntryType` helper.

- Extract `markAsFresh(installed, pluginPath)` helper used by both
  installers to drop stale hash entries after a successful install.

- `installer-npm.ts`: use `npm pack --json` instead of parsing the
  last line of text stdout (warnings on stdout would shift the
  filename). Also simplify the integrity-check flow — one gate that
  throws on missing hash, then one verify call (was two overlapping
  conditionals).

- Split `Plugin` → `PluginSpec` (YAML schema) + `Plugin` (internal,
  with `_level`/`plugin_hash`/`version`). Makes it explicit which
  fields originate from user YAML vs runtime state.

- `lock-file.ts`: add `DYNAMIC_PLUGINS_LOCK_TIMEOUT_MS` (default 10
  min) so a stale lock from a `kill -9`'d process no longer wedges
  the init container forever. New test covers the timeout path.

- Drop the broken `lint:check` script — it had `|| true` silencing
  every lint error and there is no ESLint config in the package.

- `README.md`: remove stale reference to non-existent `cli.ts`,
  document the new lock-timeout env var, mention `util.ts`.

Tests: 115 (was 114). Bundle rebuilt (413.4 KB).
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

gustavolira added a commit to gustavolira/rhdh that referenced this pull request Apr 13, 2026
Resolves the 21 issues flagged by SonarQube on PR redhat-developer#4574:

Prototype pollution (CodeQL, merger.ts)
- `deepMerge` now assigns via `Object.defineProperty` (bypasses the
  `__proto__` setter on `Object.prototype`) in addition to the existing
  `FORBIDDEN_KEYS` guard. CodeQL recognizes this pattern.

Redundant type assertions
- `index.ts:180`: drop `pc as Record<string, unknown>` — use the
  `isPlainObject` type guard already imported from `util.ts`.
- `installer-npm.ts:37`, `installer-oci.ts:35`: replace
  `(plugin.pluginConfig ?? {}) as Record<string, unknown>` with a
  typed local variable.
- `installer-oci.ts:41,51,71,78`: drop `as string` casts by restructuring
  the `isAlreadyInstalled` helper with proper `undefined` checks.
- `merger.ts:136-140`: replace `.slice(-1)[0] as string` with
  `.at(-1) ?? ''`.
- `merger.ts:215`: `ReadonlyArray<keyof Plugin | string>` collapses to
  `ReadonlyArray<string>`.

Cognitive complexity reductions
- `installOciPlugin` (17 → ~10): extract `resolvePullPolicy` and
  `isAlreadyInstalled` helpers.
- `mergeOciPlugin` (20 → ~12): extract `resolveInherit`.
- `npmPluginKey` (16 → ~7): extract `tryParseAlias`, `isGitLikeSpec`,
  `stripRefSuffix`.
- `ociPluginKey`: extract `autoDetectPluginPath`.

Modern JS / readability (es2015-es2022)
- `integrity.ts`: `charCodeAt` → `codePointAt` (es2015).
- `oci-key.ts`: use `String.raw` for the regex pieces containing `\s`,
  `\d`, `\]`, `\\` instead of escaped string literals (es2015).
- `oci-key.ts:escape`: `.replace(/.../g, ...)` → `.replaceAll(...)` (es2021).
- `plugin-hash.ts`: pass an explicit code-point comparator to `sort` so
  deterministic-hash behavior is spelled out. `localeCompare` is NOT
  used — it varies per-locale and would break hash stability.

All 115 tests still pass. Bundle rebuilt (415.1 KB).
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

Comment thread scripts/install-dynamic-plugins/src/merger.ts Fixed
@gustavolira
Copy link
Copy Markdown
Member Author

/review

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge Bot commented Apr 13, 2026

PR Reviewer Guide 🔍

(Review updated until commit ca0c4c6)

Warning

/review is deprecated. Use /agentic_review instead (removal date not yet scheduled).

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🏅 Score: 86
🧪 PR contains tests
🔒 No security concerns identified
🔀 Multiple PR themes

Sub-PR theme: Installer CLI + build/bundle plumbing (scripts/install-dynamic-plugins)

Relevant files:

  • scripts/install-dynamic-plugins/src/index.ts
  • scripts/install-dynamic-plugins/src/concurrency.ts
  • scripts/install-dynamic-plugins/src/lock-file.ts
  • scripts/install-dynamic-plugins/src/run.ts
  • scripts/install-dynamic-plugins/src/log.ts
  • scripts/install-dynamic-plugins/src/errors.ts
  • scripts/install-dynamic-plugins/package.json
  • scripts/install-dynamic-plugins/tsconfig.json
  • scripts/install-dynamic-plugins/esbuild.config.mjs
  • scripts/install-dynamic-plugins/README.md
  • scripts/install-dynamic-plugins/dist/install-dynamic-plugins.cjs

Sub-PR theme: Extraction + integrity + hashing (scripts/install-dynamic-plugins)

Relevant files:

  • scripts/install-dynamic-plugins/src/tar-extract.ts
  • scripts/install-dynamic-plugins/src/integrity.ts
  • scripts/install-dynamic-plugins/src/plugin-hash.ts
  • scripts/install-dynamic-plugins/tests/tar-extract.test.ts
  • scripts/install-dynamic-plugins/tests/integrity.test.ts
  • scripts/install-dynamic-plugins/tests/plugin-hash.test.ts

Sub-PR theme: Catalog index extraction + skopeo/image resolving (scripts/install-dynamic-plugins)

Relevant files:

  • scripts/install-dynamic-plugins/src/catalog-index.ts
  • scripts/install-dynamic-plugins/src/image-resolver.ts
  • scripts/install-dynamic-plugins/src/skopeo.ts
  • scripts/install-dynamic-plugins/tests/extra-catalog-index.test.ts
  • scripts/install-dynamic-plugins/tests/image-resolver.test.ts
  • scripts/install-dynamic-plugins/tests/skopeo.test.ts

⚡ Recommended focus areas for review

Possible Issue

The composite map key built for per-entry state uses an embedded NUL character as a separator. This can be hard to notice in reviews, can break logging/serialization/debug tooling, and may behave unexpectedly across environments. Consider switching to a visible separator (e.g., \u241F, |, or a tuple-like encoding) to avoid hidden control characters in keys.

const perEntryState = new Map<string, EntryState>();
const pathlessRegistries = new Map<string, string>();
const definedPaths = new Map<string, Map<string, string>>();

const keyOf = (registry: string, path: string | null): string =>
  `${registry}${path ?? ''}`;

const processEntry = (plugin: PluginSpec, level: number, sourceFile: string): void => {
  const pkg = plugin.package;
  if (typeof pkg !== 'string' || !pkg.startsWith(OCI_PROTO)) return;
Edge Case

Parsing EXTRA_CATALOG_INDEX_IMAGES allows an explicit <name>=<ref> form, but does not validate that name is non-empty or filesystem-safe. This can lead to writing into an unintended directory (e.g., empty name) or awkward paths. Consider rejecting empty names and possibly sanitizing/validating allowed characters.

export function parseExtraCatalogIndexImages(raw: string): Array<[name: string, imageRef: string]> {
  const out: Array<[string, string]> = [];
  for (const rawEntry of raw.split(',')) {
    const entry = rawEntry.trim();
    if (!entry) continue;
    let name: string;
    let imageRef: string;
    const eq = entry.indexOf('=');
    if (eq !== -1) {
      name = entry.slice(0, eq).trim();
      imageRef = entry.slice(eq + 1).trim();
    } else {
      imageRef = entry;
      name = imageRefToSubdirectory(imageRef);
    }
    if (!imageRef) {
      log(`WARNING: Skipping EXTRA_CATALOG_INDEX_IMAGES entry with empty image reference: '${entry}'`);
      continue;
    }
    out.push([name, imageRef]);
  }
  return out;
}
📄 References
  1. redhat-developer/rhdh/dynamic-plugins/_utils/src/wrappers.test.ts [141-307]
  2. redhat-developer/rhdh/plugins/dynamic-plugins-info-backend/__fixtures__/data.ts [1-68]
  3. redhat-developer/rhdh/e2e-tests/playwright/e2e/plugins/bulk-import.spec.ts [10-284]
  4. redhat-developer/rhdh/plugins/dynamic-plugins-info-backend/__fixtures__/data.ts [144-152]
  5. redhat-developer/rhdh/dynamic-plugins/_utils/src/wrappers.test.ts [1-58]

@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: failure.

gustavolira added a commit to gustavolira/rhdh that referenced this pull request Apr 13, 2026
… feedback

Addresses review feedback on PR redhat-developer#4574:

- Extract shared helpers into `src/util.ts`: `fileExists`, `isInside`,
  `isPlainObject`, `isAllowedEntryType`, `markAsFresh`. Removes 4x
  duplication across `index.ts`, `catalog-index.ts`, `installer-oci.ts`,
  `merger.ts`, and `tar-extract.ts`.

- Unify `catalog-index.ts` tar filter with `tar-extract.ts`: oversized
  entries now throw `InstallException` instead of being silently
  dropped; `OldFile` and `ContiguousFile` are accepted (were previously
  excluded by mistake). Uses the `isAllowedEntryType` helper.

- Extract `markAsFresh(installed, pluginPath)` helper used by both
  installers to drop stale hash entries after a successful install.

- `installer-npm.ts`: use `npm pack --json` instead of parsing the
  last line of text stdout (warnings on stdout would shift the
  filename). Also simplify the integrity-check flow — one gate that
  throws on missing hash, then one verify call (was two overlapping
  conditionals).

- Split `Plugin` → `PluginSpec` (YAML schema) + `Plugin` (internal,
  with `_level`/`plugin_hash`/`version`). Makes it explicit which
  fields originate from user YAML vs runtime state.

- `lock-file.ts`: add `DYNAMIC_PLUGINS_LOCK_TIMEOUT_MS` (default 10
  min) so a stale lock from a `kill -9`'d process no longer wedges
  the init container forever. New test covers the timeout path.

- Drop the broken `lint:check` script — it had `|| true` silencing
  every lint error and there is no ESLint config in the package.

- `README.md`: remove stale reference to non-existent `cli.ts`,
  document the new lock-timeout env var, mention `util.ts`.

Tests: 115 (was 114). Bundle rebuilt (413.4 KB).
@gustavolira gustavolira force-pushed the feat/install-dynamic-plugins-ts branch from b76ee6a to dd84f75 Compare April 13, 2026 19:48
gustavolira added a commit to gustavolira/rhdh that referenced this pull request Apr 13, 2026
Resolves the 21 issues flagged by SonarQube on PR redhat-developer#4574:

Prototype pollution (CodeQL, merger.ts)
- `deepMerge` now assigns via `Object.defineProperty` (bypasses the
  `__proto__` setter on `Object.prototype`) in addition to the existing
  `FORBIDDEN_KEYS` guard. CodeQL recognizes this pattern.

Redundant type assertions
- `index.ts:180`: drop `pc as Record<string, unknown>` — use the
  `isPlainObject` type guard already imported from `util.ts`.
- `installer-npm.ts:37`, `installer-oci.ts:35`: replace
  `(plugin.pluginConfig ?? {}) as Record<string, unknown>` with a
  typed local variable.
- `installer-oci.ts:41,51,71,78`: drop `as string` casts by restructuring
  the `isAlreadyInstalled` helper with proper `undefined` checks.
- `merger.ts:136-140`: replace `.slice(-1)[0] as string` with
  `.at(-1) ?? ''`.
- `merger.ts:215`: `ReadonlyArray<keyof Plugin | string>` collapses to
  `ReadonlyArray<string>`.

Cognitive complexity reductions
- `installOciPlugin` (17 → ~10): extract `resolvePullPolicy` and
  `isAlreadyInstalled` helpers.
- `mergeOciPlugin` (20 → ~12): extract `resolveInherit`.
- `npmPluginKey` (16 → ~7): extract `tryParseAlias`, `isGitLikeSpec`,
  `stripRefSuffix`.
- `ociPluginKey`: extract `autoDetectPluginPath`.

Modern JS / readability (es2015-es2022)
- `integrity.ts`: `charCodeAt` → `codePointAt` (es2015).
- `oci-key.ts`: use `String.raw` for the regex pieces containing `\s`,
  `\d`, `\]`, `\\` instead of escaped string literals (es2015).
- `oci-key.ts:escape`: `.replace(/.../g, ...)` → `.replaceAll(...)` (es2021).
- `plugin-hash.ts`: pass an explicit code-point comparator to `sort` so
  deterministic-hash behavior is spelled out. `localeCompare` is NOT
  used — it varies per-locale and would break hash stability.

All 115 tests still pass. Bundle rebuilt (415.1 KB).
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

@gustavolira
Copy link
Copy Markdown
Member Author

/review

@rhdh-qodo-merge
Copy link
Copy Markdown

Persistent review updated to latest commit ee984d5

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

This PR is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 21 days.

@github-actions github-actions Bot added the Stale label May 6, 2026
… the TS implementation

Brings the TS port back in sync with three commits that landed on main while
this PR was open:

- redhat-developer#4576 (OCI disable pre-merge): pre-compute the set of OCI registries that
  will be effectively disabled after merging, then filter them out of every
  plugin list before the merge calls skopeo. Avoids wasted remote fetches
  for plugins the operator already disabled at a higher level. New helpers
  in src/merger.ts (preMergeOciDisabledState, filterDisabledOciPlugins) and
  src/oci-key.ts (tryParseOciRegistryAndPath). loadAllPlugins() in
  src/index.ts now reads every include file up front so the pre-merge pass
  can run before mergePlugin() touches the OCI cache.

- redhat-developer#4666 (MAX_ENTRY_SIZE bump): default from 20MB to 40MB.

- redhat-developer#4655 (EXTRA_CATALOG_INDEX_IMAGES): comma-separated catalog index images,
  each extracted into <CATALOG_ENTITIES_EXTRACT_DIR>/extra/<name>/
  catalog-entities. New helpers in src/catalog-index.ts
  (extractCatalogIndexLayers refactored out of extractCatalogIndex,
  extractExtraCatalogIndex, parseExtraCatalogIndexImages,
  imageRefToSubdirectory). New maybeExtractExtraCatalogIndexes() in
  src/index.ts wires it into runInstaller after the primary catalog index.

Tests: 27 new cases under __tests__/merger-pre-merge.test.ts (15) and
__tests__/extra-catalog-index.test.ts (12) covering the level-override
matrix, ambiguous-pathless detection, same-level duplicates, invalid OCI
strings, extensions/marketplace fallback, missing-entities warning, and
the duplicate-subdir overwrite warning ordering. Existing
tar-extract.test.ts + types.test.ts updated for the new 40MB default. All
155 vitest cases pass. Bundle rebuilt.
@gustavolira gustavolira force-pushed the feat/install-dynamic-plugins-ts branch from 8fad1af to 4a2a8cb Compare May 26, 2026 13:08
@openshift-ci openshift-ci Bot removed the lgtm label May 26, 2026
@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented May 26, 2026

New changes are detected. LGTM label has been removed.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.86%. Comparing base (360e9c2) to head (5f11136).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4574      +/-   ##
==========================================
- Coverage   53.65%   52.86%   -0.80%     
==========================================
  Files         121      109      -12     
  Lines        2350     2132     -218     
  Branches      539      536       -3     
==========================================
- Hits         1261     1127     -134     
+ Misses       1084     1004      -80     
+ Partials        5        1       -4     
Flag Coverage Δ
rhdh 52.86% <ø> (-0.80%) ⬇️

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 360e9c2...5f11136. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…deQL

The shared Set.has() lookup wasn't recognised by CodeQL's
js/prototype-polluting-function rule as an exhaustive sanitizer, even
though Object.defineProperty already bypasses the __proto__ setter.
Replace it with the inlined string-literal pattern CodeQL accepts.

Closes CodeQL alert redhat-developer#116 on PR redhat-developer#4574 (safeSet at merger.ts:36).
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

@gustavolira
Copy link
Copy Markdown
Member Author

/review
--pr_reviewer.inline_code_comments=true
-i
--pr_reviewer.require_score_review=true
--pr_reviewer.require_can_be_split_review=true
--pr_reviewer.num_code_suggestions="2"

@rhdh-qodo-merge
Copy link
Copy Markdown

Persistent review updated to latest commit ca0c4c6

…-developer#4574

- catalog-index.ts: invert the eq===-1 branch so the positive case
  comes first (Sonar S3923 'Unexpected negated condition').
- index.ts: drop five redundant type assertions on PluginSpec / Plugin
  / IncludePluginList — TS already narrows correctly thanks to
  PluginSpec being a structural subset of Plugin and Array of being
  assignable to ReadonlyArray.
- merger.ts:
  * Split preMergeOciDisabledState into top-level helpers
    (processOciEntry, recordEntryState, recordRegistryPath,
    validateAmbiguousPathless, effectiveRegistryDisabled,
    computeDisabledRegistries) so the orchestrator stays well under
    the cognitive-complexity ceiling.
  * Replace the nested ternary in the explicit-paths sort with
    String#localeCompare.
  * Replace 'bucket && bucket.size === 1' with optional-chain
    'bucket?.size === 1'.
  * Drop a stray NUL byte in the entry-key template that crept in
    via an early Edit and was making the file impossible to diff in
    a few tools.

155 vitest cases still pass; bundle rebuilt.
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

…dir names

Qodo's persistent review flagged that the explicit 'name=<ref>' form of
EXTRA_CATALOG_INDEX_IMAGES does not validate 'name', so an operator
could accidentally (or maliciously) extract an extra catalog index
outside of '<entitiesDir>/extra/' by passing names containing '..' or
path separators. The auto-derived form is already safe because
imageRefToSubdirectory strips '/', ':', and '@'.

Add isSafeSubdirectoryName() that rejects empty names, '.', '..', and
any name containing '/' or '\\'. Parsing logs a warning and skips the
entry instead of crashing the installer, matching the existing 'empty
image reference' handling.

6 new vitest cases cover the rejected forms and document that the check
is character-based (so URL-encoded separators like '%2F' are accepted
verbatim rather than decoded). Bundle rebuilt (225.6 kB).
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

Targeted cleanups identified in the in-conversation review. No behaviour
changes outside of the two defense-in-depth guards listed below.

Correctness/security:
- installer-npm.ts: validate that the filename emitted by 'npm pack
  --json' is flat (no '/', no '\\', no leading '..') before joining it
  to 'destination'. A registry returning '../evil.tgz' would otherwise
  let extraction escape.
- installer-oci.ts: replace 'pkg.split("!")' with an indexOf-based
  splitOciPackage helper so plugin paths containing a literal '!' are no
  longer silently truncated. Applied to both the install path and the
  Always-policy digest comparison.
- catalog-index.ts: re-validate 'subdirectory' inside
  extractExtraCatalogIndex itself instead of trusting all callers to go
  through parseExtraCatalogIndexImages. Five new vitest cases cover the
  rejected forms.
- index.ts: drop the existsSyncSafe wrapper and use Node's existsSync
  directly (the wrapper was a literal reimplementation that also
  conflated 'doesn't exist' with 'no read permission').
- image-cache.ts: surface a typed InstallException with the image ref
  when 'io.backstage.dynamic-packages' is unparseable, instead of
  letting JSON.parse crash the install.

Maintainability:
- types.ts: new effectivePullPolicy() helper centralises the ':latest!'
  fallback that was duplicated between installer-oci.ts and
  definitelyNoOp in index.ts.
- merger.ts: copyPluginFields now uses a for-of loop with safeSet
  (same prototype-pollution guard as deepMerge) instead of building an
  intermediate object via Object.fromEntries. effectiveRegistryDisabled
  drops the 'as string' cast via destructuring + undefined check.
  IncludePluginList downgraded from exported to internal.
- tar-extract.ts: comment the POSIX assumption around the
  pluginPathBoundary check; the trailing slash is unambiguous now.
- index.ts: destructure process.argv to remove the 'as string' cast.
- installer-npm.ts: extract isNpmPackJsonEntry() type guard so the
  install path stays linear.
- concurrency.ts: replace the magic-number worker caps (6/3) with
  MAX_OCI_WORKERS / MAX_NPM_WORKERS named constants.
- catalog-index.ts: simplify isSafeSubdirectoryName (the second clause
  was unreachable once '/' and '\\' were rejected upfront).

166 vitest cases (was 161; +5 new defense-in-depth tests). Bundle
rebuilt (226.0 kB).
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

- Generic 'safeSet<T extends object>' removes the awkward
  'dst as unknown as Record<string, unknown>' double-cast in
  copyPluginFields. The Object.defineProperty signature already accepts
  any object, so the constraint widening is enough.
- Drop the redundant 'as unknown' on parseYaml's return in
  mergePluginsFromFile — yaml v2's parse() is already typed unknown.

No behaviour changes. 166 vitest cases still pass.
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

The 'Upload test results to Codecov' step at the monorepo level looks at
$RUNNER_TEMP/test-results/ for JUnit reports. Because this PR doesn't
touch any yarn workspace, 'yarn run test --affected' skips all
workspace tests and the directory ends up empty — codecov[bot] then
posts a 'JUnit XML file not found' warning on the PR.

Run vitest with the junit reporter so it writes its own report into
$RUNNER_TEMP/test-results/install-dynamic-plugins.junit.xml; the
existing test-results-action then finds it (alongside any workspace
reports) and the warning stops firing. No new upload step needed —
test-results-action already searches that directory.

Verified locally: 'JUNIT report written to /tmp/.../junit.xml' (166
testcases serialized).
@github-actions
Copy link
Copy Markdown
Contributor

The container image build workflow finished with status: cancelled.

@sonarqubecloud
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown
Contributor

Image was built and published successfully. It is available at:

@gustavolira gustavolira requested a review from jonkoops May 26, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants