Skip to content

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

Merged
openshift-merge-bot[bot] merged 9 commits into
redhat-developer:mainfrom
gustavolira:feat/install-dynamic-plugins-ts
May 27, 2026
Merged

feat(install-dynamic-plugins): port from Python to TypeScript/Node.js#4574
openshift-merge-bot[bot] merged 9 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

rhdh-qodo-merge Bot commented Apr 13, 2026

Copy link
Copy Markdown

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

rhdh-qodo-merge Bot commented Apr 13, 2026

Copy link
Copy Markdown

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

…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.

@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
…pload

The previous attempt at producing a JUnit report didn't take effect
because the step ordering put it AFTER the 'Upload test results to
Codecov' step. The Codecov log confirmed this:

  14:33:08  error - No JUnit XML reports found.
  14:33:16  JUNIT report written to .../install-dynamic-plugins.junit.xml

Move the install-dynamic-plugins test step before the monorepo 'Run
tests' so its JUnit report lands in $RUNNER_TEMP/test-results/ in time
for the existing upload step to pick it up. The bundle freshness check
stays where it was (after the uploads) so it doesn't delay the
upload-bearing path further.
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot removed the Stale label May 27, 2026
@openshift-ci openshift-ci Bot added the lgtm label May 27, 2026
@openshift-merge-bot
openshift-merge-bot Bot merged commit 105732d into redhat-developer:main May 27, 2026
22 checks passed
gustavolira added a commit to redhat-developer/rhdh-plugins that referenced this pull request Jun 5, 2026
…install-dynamic-plugins (#3246)

* feat(install-dynamic-plugins): import package from redhat-developer/rhdh

Migrates scripts/install-dynamic-plugins/ from redhat-developer/rhdh#4574
into this repo as @red-hat-developer-hub/install-dynamic-plugins so it
can be published to npm and consumed by the RHDH init-container without
curl-by-SHA.

Runtime contract (CLI args, env vars, plugin-hash format, on-disk layout,
tar/OCI security guards) preserved verbatim. Build remains a single
self-contained .cjs via esbuild. Tests migrated from vitest to jest to
align with the repo's backstage-cli pipeline (14 suites / 166 tests pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install-dynamic-plugins): address self-review feedback

- Add bin/install-dynamic-plugins shim and have package.json bin point at
  it (matches the convention used by extensions-cli, translations-cli, and
  rhdh-repo-tools). Split src/cli.ts as the esbuild entry so the bundle
  no longer needs the require.main guard or a shebang banner.
- Stop committing dist/install-dynamic-plugins.cjs; the release pipeline
  rebuilds via the customBuild path, and a new prepack script makes
  yarn npm publish self-healing for local runs.
- Drop the .js suffix from relative imports across src/ so the package
  matches the rest of the repo and the jest moduleNameMapper workaround
  is no longer needed.
- Consolidate the tsconfigs: the inner package extends the workspace
  tsconfig and only declares what differs.
- Add why-it's-intentional comments to the two eslint-disable lines
  (PullPolicy const+type pair, tar.x filter inside a sequential loop).
- README now leads with the npm/npx usage path; the RHDH init-container
  section is below.

tar/yaml stay in dependencies (not devDependencies as the review
suggested) — @backstage/no-undeclared-imports flagged the source
imports, and the repo convention treats bundling as opaque.

166/166 tests pass, tsc/lint/prettier clean, bin shim and bundle both
exit 0 on the empty-config smoke run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install-dynamic-plugins): unblock CI api-reports step

The CI step "check api reports and generate API reference" runs
`backstage-repo-tools api-reports --ci` before the build, and that tool
requires the bin file to introspect the CLI. The previous shim did a
plain `require('../dist/install-dynamic-plugins.cjs')`, which failed
under CI because dist/ is no longer committed and the build hasn't run
yet.

- Switch the bin shim to the local-vs-installed pattern used by every
  other CLI in the repo (extensions-cli, translations-cli,
  rhdh-repo-tools): when `src/` exists (monorepo), load TS directly via
  `@backstage/cli/config/nodeTransform`; otherwise require the built
  bundle (npm-installed scenario).
- Add `--help` / `-h` handling to main() so the api-reports tool can
  introspect the CLI usage without creating a stray `--help/` directory.
- Commit the generated `cli-report.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(install-dynamic-plugins): address SonarCloud findings

- Use String.raw for strings containing backslash literals so the source
  reads with one '\' instead of '\\\\' (catalog-index.ts log message,
  extra-catalog-index.test.ts subdirectory fixtures, skopeo.test.ts shell
  escape).
- Switch the OCI regex builder to a joined string array — eliminates the
  nested template literals SonarCloud was flagging on oci-key.ts (and
  reads much better).
- Object.prototype.hasOwnProperty.call -> Object.hasOwn in
  merger.test.ts (ES2022, available since Node 16.9).
- String#replace(/'/g, ...) -> String#replaceAll("'", ...) in
  skopeo.test.ts (ES2021).
- Hoist test helpers (stageLayer, fakeImageCache) out of their describe
  blocks so they aren't re-defined on every test.
- Drop the redundant parseMaxEntrySize(undefined) call in types.test.ts —
  the parameter already defaults to process.env.MAX_ENTRY_SIZE.

166/166 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(install-dynamic-plugins): parse argv with cleye

Switches the hand-rolled `process.argv` + USAGE-string handling in
main() to `cleye` — the same parser every `@backstage/cli-module-*`
package uses (already in our transitive deps). Aligns with the
Backstage CLI convention requested during PR review.

Existing surface preserved:
- positional `<dynamic-plugins-root>` (required, exit 1 if absent)
- `--help` / `-h` prints usage and exits 0
- normal run still exits with the installer's status code

Bundle grew from 226 kB -> 267 kB (cleye + type-flag minified).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(install-dynamic-plugins): adopt cli-module convention while keeping the bundled bin

Aligns with the @backstage/cli-module-* convention without losing the
self-contained bundled artifact:

- Rename to @red-hat-developer-hub/cli-module-install-dynamic-plugins,
  backstage.role: cli-module.
- src/installer.ts holds the install pipeline and main() (formerly
  src/index.ts).
- New src/index.ts default-exports `createCliModule(...)` registering an
  `install` command whose loader is src/command.ts. Exposes the package
  through backstage-cli discovery — `backstage-cli install <dir>` works
  when the package is a dependency.
- src/cli.ts (the esbuild entry) keeps invoking installer.main() directly,
  so the bundled .cjs stays self-contained: no @backstage/cli-node and no
  keytar gymnastics in the bin path.
- Build is dual now — `backstage-cli package build && node esbuild.config.mjs`.
  backstage-cli emits dist/index.cjs.js (the cli-module export) and the
  unbundled supporting modules; esbuild emits dist/install-dynamic-plugins.cjs
  (the standalone bin). Both are published.
- bin shim's installed branch now requires the bundled .cjs explicitly
  rather than going through `main` — that keeps direct/npx/init-container
  invocations at ~60 ms cold start instead of paying the cli-module
  dispatch cost.
- main() now takes optional `args` and `programName` so the cli-module
  loader can pass the command's argv slice and have `--help` print the
  real invocation (`install-dynamic-plugins install …`).
- @backstage/cli-node added as a runtime dependency. It is only loaded by
  the cli-module discovery path; the bundled bin never imports it.

166/166 tests pass; tsc/lint/prettier/api-reports clean. Bundle size
unchanged at ~267 KB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(install-dynamic-plugins): drop custom esbuild bundle, ship unbundled cli-module

Replaces the dual build (esbuild bundle + backstage-cli) with plain
`backstage-cli package build` — the standard Backstage cli-module pattern.
The bundle and the keytar gymnastics only existed to satisfy RHDH's
init-container `COPY` of a single self-contained `.cjs`; that consumption
model is moving to `npm install` (redhat-developer/rhdh#4908), so the
single-file requirement is going away.

What's left in the package:

- `src/installer.ts`: install pipeline + `main(args, programName)`.
- `src/index.ts`: `createCliModule(...)` default export, registers the
  `install` command. Discovered by `backstage-cli` when this package is a
  dependency of a host project.
- `src/command.ts`: thin loader that calls `installer.main`.
- `bin/install-dynamic-plugins`: fast-path shim that loads
  `dist/installer.cjs.js` directly and runs `main(process.argv.slice(2))`,
  bypassing `@backstage/cli-node`'s `runCliModule` dispatch — saves ~80 ms
  of cold start for direct/`npx`/init-container invocations. The
  cli-module discovery path still goes through `runCliModule` and pays the
  dispatch cost where it belongs.

Removed:
- `esbuild.config.mjs` (the custom bundle config)
- `src/cli.ts` (the esbuild entry)
- `dist/install-dynamic-plugins.cjs` from `files` (no longer produced)
- `esbuild` devDependency

166/166 tests pass; tsc/lint/prettier/api-reports clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(install-dynamic-plugins): drop stale references to the esbuild bundle

The README and the createCliModule docstring still referenced the
self-contained bundle that the package no longer ships:
- `npm run build` description and committed-bundle CI-check section.
- `node install-dynamic-plugins.cjs "$1"` wrapper line in "How RHDH
  consumes it" — replaced with a pointer to redhat-developer/rhdh#4908.
- `src/index.ts` describing the bin path as the bundle.
- Source layout was missing `index.ts` / `command.ts` / `installer.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install-dynamic-plugins): address PR review — align with upstream cli-module contract

Applies the four findings from @schultzp2020's review:

- package.json reshaped to match the upstream `@backstage/cli-module-*`
  contract: `main`/`types` point at `src/index.ts` for local dev,
  `publishConfig` overrides with `dist/index.cjs.js`/`dist/index.d.ts`,
  `prepack` and `postpack` wire up `backstage-cli package prepack/postpack`
  so the published artifact has the correct entry points, and `files` is
  `["bin", "dist"]` (the old `dist/**/*.js` glob excluded `.d.ts`).

- bin wrapper now uses `@backstage/cli-node/config/nodeTransform.cjs`
  (the cli-node variant) instead of `@backstage/cli/config/nodeTransform.cjs`.
  `@backstage/cli` is only a devDependency — the previous import would have
  broken `npx`/installed-package invocations.

- bin wrapper now goes through `runCliModule(...)` like every other
  `@backstage/cli-module-*` package. The earlier fast path bypassed it to
  save ~80 ms of cold start, but per @schultzp2020 that cost is one-time
  per init-container run (not per-plugin), and going through the standard
  dispatch gives us `--version`/`--help` and future runCliModule
  improvements for free.

- installer.ts: added an `isPlainObject` guard for the parsed main config,
  mirroring the existing guard on include files. A YAML scalar or array in
  `dynamic-plugins.yaml` now throws a clear `InstallException` instead of
  a confusing downstream `TypeError`.

Also drops the stale `install-dynamic-plugins.sh` wrapper from the package
— it pointed at the long-removed esbuild bundle and was no longer listed
in `files`.

166/166 tests pass; tsc/lint/prettier/api-reports clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gustavolira added a commit to gustavolira/rhdh that referenced this pull request Jun 11, 2026
… era

The dynamic-plugins installer was migrated to TypeScript in redhat-developer#4574 and the
vendored TypeScript copy was removed earlier in this PR, so the pytest
test infrastructure no longer has anything live to test.

- Deletes python/requirements-dev.in and python/requirements-dev.txt
  (pytest, pytest-cov, pytest-mock — and their transitive deps).
- Drops the .github/workflows/pr.yaml "Install Python dependencies" step
  that installed all three requirements files; no subsequent step in the
  PR workflow uses Python.
- Cleans the stale comment in codecov.yml referencing the removed
  install-dynamic-plugins pytest flag.

python/requirements.txt and python/requirements-build.txt stay — they are
consumed by the TechDocs venv build in the Containerfile (line 237) for
mkdocs / mkdocs-techdocs-core / plantuml-markdown, which is unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
openshift-merge-bot Bot pushed a commit that referenced this pull request Jun 16, 2026
…arn workspace (#4908)

* [DRAFT] chore(install-dynamic-plugins): consume installer from npm

Replaces the COPY of scripts/install-dynamic-plugins/{install-dynamic-plugins.cjs,
install-dynamic-plugins.sh} with an `npm install` of
@red-hat-developer-hub/cli-module-install-dynamic-plugins (built and published
out of redhat-developer/rhdh-plugins).

This unblocks the cli-module structure on the rhdh-plugins side — it lets
that package use the standard `backstage-cli package build` (unbundled,
multi-file dist) instead of a custom esbuild bundle with a keytar stub. See
the conversation context: redhat-developer/rhdh-plugins#3254

Backward compatibility is preserved by writing a tiny
`/opt/app-root/src/install-dynamic-plugins.sh` shim that delegates to the
npm-installed bin, so the Helm chart and Operator init-container spec
continue to invoke `./install-dynamic-plugins.sh /dynamic-plugins-root`
unchanged.

DRAFT — DO NOT MERGE: blocked on
redhat-developer/rhdh-plugins#3254 (or the unbundled successor) being
merged and published to npm. Opened for review of the consumption pattern
and to back the cold-start benchmark posted in Slack.

Trade-off summary (cold-start benchmark on empty config):

- Current (bundled .cjs, 231 KB single file): ~89 ms warm cache (median)
- Proposed (npm install, 25 MB node_modules): ~180 ms warm cache (median)

The ~90 ms gap is the module-resolution overhead of unbundled Node — paid
once per pod start. Image build time also gets +`npm install` of ~25 MB
(one extra layer), offset by deleting ~7000 lines of vendored installer
script from this repo in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup: address PR review on the install-dynamic-plugins Containerfile step

- Install into /opt/dynamic-plugins-installer (its own dir, no package.json)
  instead of /opt/app-root/src so npm cannot honor the yarn workspace and
  perturb the production tree that `yarn workspaces focus` built at line 208.
- Delegate the shim to `node_modules/.bin/install-dynamic-plugins` (the
  symlink npm creates from the package's bin field) instead of reaching
  into the package's internal layout.
- Add `--no-save --omit=dev` so npm doesn't write a package-lock.json into
  the installer dir and doesn't fetch devDependencies.
- Pin the installer to an exact version (0.1.0) so image builds are
  reproducible.
- Add a build-time smoke check (`install-dynamic-plugins --help`) so a
  missing or renamed CLI entrypoint fails the image build instead of the
  init container at pod start.

The hermetic-build concern (npm reaching the public registry when this
Containerfile runs under Konflux with networking disabled) is acknowledged
separately in the PR description — it's the real gating work and is not
addressed by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup: drop synthetic `install` subcommand from the shim

The unbundled cli-module variant ships a fast-path bin that calls the
installer directly (bypassing @backstage/cli-node's runCliModule dispatch),
so the published binary takes the dynamic-plugins-root as a positional
without a subcommand prefix — matching the original CLI surface.

Verified locally:
  $ /opt/dynamic-plugins-installer/node_modules/.bin/install-dynamic-plugins /dynamic-plugins-root
exits 0 on an empty config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(install-dynamic-plugins): consume installer through dynamic-plugins yarn workspace

Pivots #4908 from `RUN npm install …` to a plain yarn dependency in
dynamic-plugins/package.json. The existing `yarn install --immutable` at
line 151 of this Containerfile pulls the installer as part of the same
yarn run that already brings in the rest of the dynamic-plugins deps, so
the bin lands at /opt/app-root/src/dynamic-plugins/node_modules/.bin/install-dynamic-plugins
and the final stage just writes a shim pointing there.

Why yarn and not npm: scripts/local-hermeto-build.sh:213 already
prefetches yarn deps for ./dynamic-plugins via cachi2/hermeto. No infra
change is needed in this repo or in the midstream Konflux pipeline — the
hermetic build "just works". The earlier npm-install approach required
wiring npm into hermeto's fetch-deps, which is real work spanning two
repos.

Validated locally: yarn install of the unbundled cli-module variant
(via a tarball of the fast-path build) produces .bin/install-dynamic-plugins
and the smoke invocation on an empty dynamic-plugins.yaml exits 0.

dynamic-plugins/yarn.lock is intentionally NOT regenerated in this
commit — that has to happen against the published
@red-hat-developer-hub/cli-module-install-dynamic-plugins@0.1.0 once
redhat-developer/rhdh-plugins ships it. Marking the PR as Draft until
then.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup: shim uses install subcommand + forwards all positional args

The rhdh-plugins side reverted the bin shim to the standard `runCliModule`
dispatch (redhat-developer/rhdh-plugins#3246 review), so the installed bin
now expects an `install` subcommand. Updating the wrapper to match and to
forward `"$@"` instead of `"$1"` so any extra positional argument the
Helm chart or Operator passes is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: pin cli-module-install-dynamic-plugins to 0.2.0 and regen yarn.lock

redhat-developer/rhdh-plugins#3246 merged and the changesets release
published @red-hat-developer-hub/cli-module-install-dynamic-plugins as
0.2.0 (the changesets minor bump rolled past 0.1.0 because of the
existing `0.0.0` workspace version).

Updates the dynamic-plugins/package.json pin to 0.2.0 and regenerates
dynamic-plugins/yarn.lock so `yarn install --immutable` (line 151 of the
Containerfile) and the hermeto yarn prefetch at
scripts/local-hermeto-build.sh:213 both resolve the package. The Build
Image GitHub Actions job should now go green.

Verified locally: yarn install resolves the package, the bin symlink
lands at dynamic-plugins/node_modules/.bin/install-dynamic-plugins, and
invoking it with `install <dir>` exits 0 on an empty config (going
through the standard @backstage/cli-node runCliModule dispatch like the
final Containerfile shim does).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop vendored scripts/install-dynamic-plugins/ now that it's on npm

The dynamic-plugins installer is consumed from
@red-hat-developer-hub/cli-module-install-dynamic-plugins via the yarn
dependency declared in dynamic-plugins/package.json, so the vendored
copy under scripts/install-dynamic-plugins/ is no longer used by the
Containerfile and can be removed.

Deletes:
- scripts/install-dynamic-plugins/ (46 files, ~7000 lines)

Cleans up the references that pointed at it:
- .gitattributes: drop linguist-generated marker for the bundled .cjs
- .dockerignore: drop the dist/ exceptions that kept the bundle in the
  build context
- codecov.yml: drop the install-dynamic-plugins flag (used to track
  Python coverage from the pre-TS era)
- .github/workflows/pr.yaml: drop the vitest + bundle-up-to-date checks
- .github/workflows/coverage-baseline.yml: drop the pytest baseline
- docs/dynamic-plugins/installing-plugins.md: link to the rhdh-plugins
  source instead of the vendored path
- docs/coverage/e2e-rhdh.md: drop the vitest row from the coverage table
- .rulesync/rules/ci-e2e-testing.md + the derived files under
  .claude/, .cursor/, .opencode/: update the installer link

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: move installer dep to root package.json so the bin survives stage 3

The Containerfile's build stage at line 175 wipes everything under
dynamic-plugins/ except dist/, which deleted dynamic-plugins/node_modules/.bin/
along with everything else. The final stage then failed at runtime with
exit code 127 because the bin wasn't there:

  /bin/sh: line 1: /opt/app-root/src/dynamic-plugins/node_modules/.bin/install-dynamic-plugins:
  No such file or directory

Moves @red-hat-developer-hub/cli-module-install-dynamic-plugins from
dynamic-plugins/package.json to the repo root package.json. The bin then
survives the `yarn workspaces focus --all --production` at line 208 and
lands at /opt/app-root/src/node_modules/.bin/ where the final stage's shim
can reach it. Hermeto already prefetches yarn deps for the repo root
(scripts/local-hermeto-build.sh:213, `{"type": "yarn", "path": "."}`), so
no infra change.

Also corrects the shim path: the published package uses the string form of
`bin` ("bin/install-dynamic-plugins"), and yarn names the symlink after
the package's base name (after the scope), so the actual bin lands at
node_modules/.bin/cli-module-install-dynamic-plugins, not
node_modules/.bin/install-dynamic-plugins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop pytest residue left by the Python install-dynamic-plugins era

The dynamic-plugins installer was migrated to TypeScript in #4574 and the
vendored TypeScript copy was removed earlier in this PR, so the pytest
test infrastructure no longer has anything live to test.

- Deletes python/requirements-dev.in and python/requirements-dev.txt
  (pytest, pytest-cov, pytest-mock — and their transitive deps).
- Drops the .github/workflows/pr.yaml "Install Python dependencies" step
  that installed all three requirements files; no subsequent step in the
  PR workflow uses Python.
- Cleans the stale comment in codecov.yml referencing the removed
  install-dynamic-plugins pytest flag.

python/requirements.txt and python/requirements-build.txt stay — they are
consumed by the TechDocs venv build in the Containerfile (line 237) for
mkdocs / mkdocs-techdocs-core / plantuml-markdown, which is unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: stop tracking and ignore the Claude Code session lock file

`.claude/scheduled_tasks.lock` is a Claude Code runtime artifact (per-session
pid lock); it got accidentally included by `git add -A` in the previous
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address PR review — smoke check `install`, stop pointing at line numbers, drop orphaned comments

- Build-time smoke check now invokes `install --help`, not just `--help`.
  A top-level `--help` only proves the bin resolves; the shim hardcodes
  the `install` subcommand, so if a future release of the package
  renames/removes it, the failure should land at image build time, not at
  pod start. cleye prints the subcommand help and exits 0 before
  validating the required `<dynamic-plugins-root>` positional, so this is
  a strict upgrade with no behavioural change for the current published
  package.

- Replace hardcoded line-number references in the Containerfile comment
  (already drifted: line 175 → actual 173) with the step banners
  `=== YARN WORKSPACES FOCUS ===` and `=== DELETE DYNAMIC PLUGINS/* ===`,
  which survive edits to the surrounding RUN steps. Same treatment for
  scripts/local-hermeto-build.sh: point at the
  `{"type": "yarn", "path": "."}` fetch-deps entry instead of a line
  number.

- Drop the orphaned `.dockerignore` comment that explained the now-removed
  `!scripts/install-dynamic-plugins/dist` re-include lines and was reading
  as if it described `**/node_modules`.

- Drop the orphaned `.gitattributes` section header
  `# Generated bundles — collapsed in GitHub diffs, …` left behind when
  the `dist/install-dynamic-plugins.cjs linguist-generated=true` entry
  was removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: move installer dep to packages/backend to satisfy monorepo:check

The sherif rule in `yarn run monorepo:check` rejects `dependencies` on
the private root package.json — the rationale being that
dependencies vs devDependencies is a no-op for a private package and
creates confusion. CI fails on that check today.

Moves `@red-hat-developer-hub/cli-module-install-dynamic-plugins` from
the root package.json to `packages/backend/package.json`, keeping the
load-bearing behaviour: backend is `private: true` and gets included by
`yarn workspaces focus --all --production`, so the bin still hoists to
`/opt/app-root/src/node_modules/.bin/cli-module-install-dynamic-plugins`
where the Containerfile shim picks it up. Semantically this is the
right home anyway — the backend is the runtime that the init-container
runs alongside.

Containerfile comment updated to point at the new declaration site.

Verified locally:
- yarn run monorepo:check → No issues found
- node_modules/.bin/cli-module-install-dynamic-plugins install --help
  exits 0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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