Skip to content

feat(install-dynamic-plugins): cli-module variant (alternative to #3246)#3254

Open
gustavolira wants to merge 7 commits into
redhat-developer:mainfrom
gustavolira:feat/install-dynamic-plugins-cli-module
Open

feat(install-dynamic-plugins): cli-module variant (alternative to #3246)#3254
gustavolira wants to merge 7 commits into
redhat-developer:mainfrom
gustavolira:feat/install-dynamic-plugins-cli-module

Conversation

@gustavolira
Copy link
Copy Markdown
Member

Summary

Alternative to #3246, opened so the trade-offs are reviewable side by side (cc @schultzp2020).

#3246 ships the installer as a standalone CLI (backstage.role: cli) that goes through backstage-cli's customBuild escape hatch. This PR restructures it as a Backstage CLI module (backstage.role: cli-module, @backstage/cli-node's createCliModule), per review feedback, so it integrates natively with backstage-cli discovery.

Both keep cleye as the argv parser and both produce a single bundled .cjs for the RHDH init-container.

What changed vs #3246

  • Renamed to @red-hat-developer-hub/cli-module-install-dynamic-plugins, backstage.role: cli-module.
  • src/module.tscreateCliModule registering an install command; src/command.ts loads it; src/cli.ts (esbuild entry) runs it via runCliModule. main() now takes explicit argv from the command context.
  • @backstage/cli-node added as a dependency. cleye stays as the in-command parser (same as @backstage/cli-module-lint).

Trade-offs to weigh

#3246 (role: cli) This PR (role: cli-module)
Bundle size 267 KB 1.45 MB (~5.4x)
CLI surface install-dynamic-plugins <root> install-dynamic-plugins install <root> (adds subcommand)
backstage-cli discovery no (standalone bin) yes (auto-discovered as a dep)
Native deps none @backstage/cli-node pulls in keytar (native .node)
Build esbuild via customBuild esbuild via customBuild + keytar stub

The keytar caveat

@backstage/cli-node depends on keytar (a native credential store, for @backstage/cli-module-auth). esbuild can't bundle .node binaries into a single file, so this PR aliases keytar to no-ops at build time (esbuild-keytar-stub.cjs). The install command never touches credentials, so it's safe today — but it's load-bearing on cli-node never lazy-loading keytar in a path our command hits. If a future cli-node release changes that, the init-container would break.

Test plan

  • CI green (node 22 + 24, Verify)
  • yarn tsc / lint / prettier:check / build:api-reports:only --ci clean
  • 166/166 tests pass
  • install-dynamic-plugins install <dir> exits 0 on an empty config

Recommendation

Both work. #3246 is leaner and keeps the init-container artifact small with no native-dep gymnastics; this PR aligns with the cli-module convention at the cost of ~5x bundle size and a keytar stub. Posting both so the maintainers can pick the direction.

🤖 Generated with Claude Code

gustavolira and others added 6 commits May 27, 2026 16:06
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>
- 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>
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>
- 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>
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>
Alternative to redhat-developer#3246 — packages the installer as a Backstage CLI module
per review feedback, so it integrates natively with backstage-cli.

- Rename to @red-hat-developer-hub/cli-module-install-dynamic-plugins,
  backstage.role: cli-module.
- src/module.ts exports createCliModule registering an `install` command;
  src/command.ts loads it; src/cli.ts (esbuild entry) runs it via
  runCliModule. main() now takes explicit argv from the command context.
- @backstage/cli-node added as a dependency; cleye is still the argv
  parser inside the command (same as @backstage/cli-module-lint).

Tradeoffs vs redhat-developer#3246:
- CLI surface gains an `install` subcommand:
  `install-dynamic-plugins install <root>` (wrapper + README updated).
- Bundle grows 267 KB -> 1.45 MB (~5.4x) from inlining cli-node + deps.
- @backstage/cli-node pulls in keytar (native .node). esbuild can't
  bundle native binaries, so keytar is aliased to no-ops at build time
  (esbuild-keytar-stub.cjs). Safe today since install never touches
  credentials, but it's load-bearing on cli-node not lazy-loading keytar
  elsewhere.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gustavolira gustavolira requested review from a team as code owners May 29, 2026 19:25
@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge Bot commented May 29, 2026

Code Review by Qodo

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

Grey Divider


Action required

1. Arch forced to amd64 🐞 Bug ≡ Correctness
Description
Skopeo.copy always passes --override-arch=amd64, so OCI plugin/image downloads will fetch amd64
artifacts even when running on arm64 (or other) nodes, leading to install failures or wrong-arch
payloads.
Code

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/skopeo.ts[R40-49]

Relevance

⭐⭐ Medium

No historical evidence found around skopeo platform/arch override handling in this repo.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code hard-codes the architecture for every skopeo copy, which directly controls which platform
manifest is pulled/extracted.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/skopeo.ts[40-52]

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

### Issue description
`skopeo copy` is executed with a hard-coded `--override-arch=amd64`, which breaks installs on non-amd64 nodes.

### Issue Context
Node reports host arch via `process.arch` (e.g. `x64`, `arm64`). `skopeo` expects OCI arch strings (e.g. `amd64`, `arm64`).

### Fix Focus Areas
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/skopeo.ts[40-51]

### Suggested change
- Replace the constant `--override-arch=amd64` with:
 - a mapping from `process.arch` to skopeo arch (`x64 -> amd64`, `arm64 -> arm64`, etc), and/or
 - an override env var (e.g. `DYNAMIC_PLUGINS_OCI_ARCH`) for explicit control.
- Consider whether `--override-os=linux` is necessary; if not, letting skopeo resolve platform from the runtime may be safer.

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



Remediation recommended

2. Wrapper depends on CWD 🐞 Bug ☼ Reliability
Description
install-dynamic-plugins.sh executes node install-dynamic-plugins.cjs without anchoring the path
to the script location, so it fails unless the current working directory contains that file.
Code

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/install-dynamic-plugins.sh[18]

Relevance

⭐⭐⭐ High

Repo has accepted shell reliability fixes avoiding CWD dependence/side effects (pushd/popd
suggestion accepted in #3181).

PR-#3181

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper uses a relative bundle path, while the package declares the built bundle under dist/,
making execution sensitive to the current working directory and the on-disk layout.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/install-dynamic-plugins.sh[1-18]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/package.json[26-33]

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

### Issue description
The wrapper script assumes `install-dynamic-plugins.cjs` exists in the current working directory. That is brittle and can fail depending on how the init container invokes the script.

### Issue Context
The built bundle path in the package is `dist/install-dynamic-plugins.cjs` (see `package.json`). The wrapper should locate the bundle relative to the script (`$0`) rather than relying on `cwd`.

### Fix Focus Areas
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/install-dynamic-plugins.sh[1-18]
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/package.json[26-33]

### Suggested change
- Update the wrapper to compute its directory and execute the bundle via an absolute path, e.g.:
 - `SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)`
 - `exec node "$SCRIPT_DIR/dist/install-dynamic-plugins.cjs" install "$@"`
- Consider passing `"$@"` (not only `$1`) to avoid accidental argument loss if options are added later.

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


3. LEGAL output not shipped 🐞 Bug ⚙ Maintainability
Description
esbuild is configured to emit external sourcemaps and external legal comments, but the npm package
files list only includes the .cjs, so the generated .map/.LEGAL.txt outputs will be omitted
from published artifacts.
Code

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/package.json[R28-33]

Relevance

⭐⭐⭐ High

They accept fixing published package files lists to include required runtime artifacts (templates
added in #2383).

PR-#2383

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The build config explicitly requests external outputs, while the package manifest only whitelists
the main bundle for publication.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/esbuild.config.mjs[23-41]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/package.json[26-33]

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

### Issue description
The build emits external sourcemaps and a separate legal comments file, but those outputs are not included in the published package.

### Issue Context
With `sourcemap: 'external'` and `legalComments: 'external'`, esbuild writes additional files alongside the bundle (typically `*.map` and `*.LEGAL.txt`). Excluding them reduces debuggability and can create license attribution/compliance risk.

### Fix Focus Areas
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/esbuild.config.mjs[23-41]
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/package.json[28-33]

### Suggested change
- Add the generated artifacts to `files`, for example:
 - `dist/install-dynamic-plugins.cjs.map`
 - `dist/install-dynamic-plugins.cjs.LEGAL.txt`
- Alternatively, if you intentionally do not want to ship these, change esbuild to inline/omit them (and ensure license obligations are still met).

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


4. npm pack flag injection 🐞 Bug ⛨ Security
Description
npmPack passes the package spec as a positional argument without a -- terminator, so a malformed
package value starting with - can be parsed as an npm flag and alter npm pack behavior.
Code

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-npm.ts[R110-114]

Relevance

⭐⭐ Medium

Security hardening is often accepted (#3097), but CLI-arg parsing edge-case hardening was sometimes
rejected (#2864).

PR-#3097
PR-#2864

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code builds an argv array where actualPkg is the final argument; without --, npm may parse
it as an option if it begins with -.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-npm.ts[101-114]

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

### Issue description
The `npm pack` invocation is susceptible to **flag injection** because `actualPkg` is appended without `--` to stop option parsing.

### Issue Context
`actualPkg` ultimately comes from `dynamic-plugins.yaml` (`plugin.package`). While this is not shell injection (spawn uses argv), npm can still interpret values beginning with `-` as options.

### Fix Focus Areas
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-npm.ts[101-114]

### Suggested change
- Change the argv to include an explicit terminator:
 - `['npm', 'pack', '--json', '--ignore-scripts', '--', actualPkg]`
- Optionally add a defensive check that rejects non-local `pkg` values starting with `-` with a clear error message.

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



Advisory comments

5. README defaults are wrong 🐞 Bug ⚙ Maintainability
Description
The README claims MAX_ENTRY_SIZE defaults to 20000000 and that
dist/install-dynamic-plugins.cjs is committed, but the code default is 40_000_000 and dist/ is
gitignored, so operators and CI expectations are misdocumented.
Code

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/README.md[R99-125]

Relevance

⭐⭐⭐ High

Team often accepts docs-to-code alignment fixes (e.g., README/config consistency in PRs #2005,
#2861).

PR-#2005
PR-#2861

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The README's documented defaults and repository policy contradict the implemented default and ignore
rules.

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/README.md[99-125]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts[81-96]
workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/.gitignore[1-6]

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

### Issue description
README states defaults/CI behavior that do not match the actual repo/code state:
- `MAX_ENTRY_SIZE` default is documented as `20000000`, but code uses `40_000_000`.
- README says `dist/install-dynamic-plugins.cjs` is committed, but `dist/` is in `.gitignore`.

### Issue Context
This can mislead operators debugging extraction failures and contributors trying to follow the documented CI workflow.

### Fix Focus Areas
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/README.md[99-125]
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts[81-96]
- workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/.gitignore[1-6]

### Suggested change
- Update README to reflect the real `DEFAULT_MAX_ENTRY_SIZE`.
- Either remove the claim that `dist/` is committed, or change repo policy (stop ignoring `dist/` and commit the bundle) to match the documentation.

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


Grey Divider

Qodo Logo

@github-actions
Copy link
Copy Markdown
Contributor

This pull request adds a new top-level directory under workspaces/. Please follow Submitting a Pull Request for a New Workspace in CONTRIBUTING.md.

@rhdh-gh-app
Copy link
Copy Markdown

rhdh-gh-app Bot commented May 29, 2026

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/cli-module-install-dynamic-plugins workspaces/install-dynamic-plugins/packages/install-dynamic-plugins minor v0.1.0

@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 64.55331% with 369 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.37%. Comparing base (c901f52) to head (1d43006).
⚠️ Report is 8 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3254      +/-   ##
==========================================
+ Coverage   53.23%   53.37%   +0.13%     
==========================================
  Files        2413     2436      +23     
  Lines       86358    87399    +1041     
  Branches    23897    24154     +257     
==========================================
+ Hits        45975    46647     +672     
- Misses      40049    40418     +369     
  Partials      334      334              
Flag Coverage Δ *Carryforward flag
adoption-insights 83.58% <ø> (ø) Carriedforward from f774c45
ai-integrations 70.03% <ø> (ø) Carriedforward from f774c45
app-defaults 69.60% <ø> (ø) Carriedforward from f774c45
augment 46.39% <ø> (ø) Carriedforward from f774c45
bulk-import 72.86% <ø> (ø) Carriedforward from f774c45
cost-management 16.49% <ø> (ø) Carriedforward from f774c45
dcm 32.85% <ø> (ø) Carriedforward from f774c45
extensions 61.79% <ø> (ø) Carriedforward from f774c45
global-floating-action-button 74.30% <ø> (ø) Carriedforward from f774c45
global-header 61.68% <ø> (ø) Carriedforward from f774c45
homepage 51.52% <ø> (ø) Carriedforward from f774c45
install-dynamic-plugins 64.55% <64.55%> (?)
konflux 91.01% <ø> (ø) Carriedforward from f774c45
lightspeed 68.33% <ø> (ø) Carriedforward from f774c45
mcp-integrations 81.59% <ø> (ø) Carriedforward from f774c45
orchestrator 36.36% <ø> (ø) Carriedforward from f774c45
quickstart 62.88% <ø> (ø) Carriedforward from f774c45
sandbox 79.56% <ø> (ø) Carriedforward from f774c45
scorecard 83.84% <ø> (ø) Carriedforward from f774c45
theme 64.54% <ø> (ø) Carriedforward from f774c45
translations 8.49% <ø> (ø) Carriedforward from f774c45
x2a 78.47% <ø> (ø) Carriedforward from f774c45

*This pull request uses carry forward flags. Click here to find out more.


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 c901f52...1d43006. 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.

@rhdh-qodo-merge
Copy link
Copy Markdown

Review Summary by Qodo

Implement install-dynamic-plugins as Backstage CLI module with OCI/NPM plugin support and security hardening

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implements @red-hat-developer-hub/cli-module-install-dynamic-plugins as a Backstage CLI module
  (backstage.role: cli-module) for native integration with backstage-cli discovery
• Restructures the installer from a standalone CLI to use @backstage/cli-node's createCliModule
  pattern, registering an install subcommand
• Core installation orchestration: loads and merges dynamic plugin configurations from YAML,
  categorizes plugins (OCI, NPM, skipped), and installs concurrently with worker pooling
• Security hardening: implements path-traversal validation, zip-bomb detection, symlink containment,
  prototype-pollution protection, and SRI integrity verification for NPM packages
• OCI plugin support: extracts plugins from OCI images with hash-based change detection, implements
  pull policies (IfNotPresent, Always), and registry fallback logic (registry.access.redhat.com →
  quay.io)
• NPM plugin support: orchestrates npm pack with integrity verification and secure tarball
  extraction
• Concurrency control: implements Semaphore class and mapConcurrent() for bounded parallel
  execution with adaptive worker counts
• Exclusive locking: prevents concurrent installs via lock file with polling and timeout (default 10
  min)
• Comprehensive test coverage: 166 tests validating security checks, configuration merging, OCI/NPM
  parsing, hash computation, and error handling
• Trade-off vs alternative #3246: ~5.4x larger bundle (1.45 MB vs 267 KB) due to
  @backstage/cli-node dependency, adds keytar native dependency (stubbed at build time), but gains
  native CLI discovery and follows Backstage CLI module convention
Diagram
flowchart LR
  A["CLI Entry<br/>cli.ts"] -->|runCliModule| B["Module Registration<br/>module.ts"]
  B -->|createCliModule| C["Command Handler<br/>command.ts"]
  C -->|main argv| D["Config Loading<br/>index.ts"]
  D -->|merge configs| E["Plugin Merger<br/>merger.ts"]
  E -->|categorize| F["OCI Installer<br/>installer-oci.ts"]
  E -->|categorize| G["NPM Installer<br/>installer-npm.ts"]
  F -->|extract| H["Tar Extraction<br/>tar-extract.ts"]
  G -->|verify| I["Integrity Check<br/>integrity.ts"]
  H -->|security| J["Path Validation<br/>util.ts"]
  I -->|security| J
  F -->|image ops| K["Skopeo Wrapper<br/>skopeo.ts"]
  G -->|package ops| L["NPM Pack<br/>installer-npm.ts"]
  D -->|finalize| M["Lock & Cleanup<br/>lock-file.ts"]

Loading

Grey Divider

File Changes

1. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/index.ts ✨ Enhancement +646/-0

Core plugin installation orchestration and configuration loading

• Implements the main entry point main() function that parses CLI arguments and orchestrates the
 plugin installation workflow
• Loads and merges dynamic plugin configurations from YAML files, handling includes and catalog
 index extraction
• Categorizes plugins into OCI, NPM, and skipped categories, then installs them concurrently with
 worker pooling
• Manages plugin hashing, cleanup of obsolete plugins, and error handling with graceful state
 preservation on failures

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/index.ts


2. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.ts ✨ Enhancement +562/-0

Plugin configuration merging with security and OCI disable pre-pass

• Implements deepMerge() with prototype-pollution protection via forbidden-key filtering and
 Object.defineProperty
• Provides OCI and NPM plugin merging logic with version/path resolution and inheritance support
• Implements preMergeOciDisabledState() pre-pass to compute disabled registries before any remote
 fetches
• Includes filterDisabledOciPlugins() to drop disabled entries from plugin lists

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.ts


3. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/extra-catalog-index.test.ts 🧪 Tests +358/-0

Extra catalog index parsing and extraction test coverage

• Tests for imageRefToSubdirectory() function converting OCI image refs to filesystem-safe names
• Tests for parseExtraCatalogIndexImages() parsing comma-separated image entries with
 explicit/auto-derived subdirectory names
• Tests for extractExtraCatalogIndex() extraction flow including path-traversal defense, duplicate
 warnings, and fallback to marketplace directory
• Validates rejection of unsafe subdirectory names and proper ordering of extraction headers vs
 warnings

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/extra-catalog-index.test.ts


View more (53)
4. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/catalog-index.ts ✨ Enhancement +328/-0

OCI catalog index extraction with security hardening

• Implements extractCatalogIndex() to pull OCI images and extract plugin configs and catalog
 entities
• Implements extractCatalogIndexLayers() shared layer extraction with security filters (size caps,
 path-traversal rejection, link containment)
• Implements extractExtraCatalogIndex() for multi-image catalog index support with subdirectory
 isolation
• Provides parseExtraCatalogIndexImages() and imageRefToSubdirectory() for parsing
 EXTRA_CATALOG_INDEX_IMAGES env var

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/catalog-index.ts


5. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger-pre-merge.test.ts 🧪 Tests +265/-0

Pre-merge OCI disabled state computation test coverage

• Tests for preMergeOciDisabledState() level-override behavior (include vs main config precedence)
• Tests for path-less reference ambiguity detection with multiple explicit-path entries
• Tests for same-level duplicate detection (warns on disabled, throws on enabled)
• Tests for invalid OCI string handling and filterDisabledOciPlugins() filtering logic

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger-pre-merge.test.ts


6. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/tar-extract.test.ts 🧪 Tests +200/-0

Tar extraction security and functionality test coverage

• Tests for extractOciPlugin() including path-traversal rejection, zip-bomb detection, symlink
 containment, and sibling-prefix filtering
• Tests for extractNpmPackage() including package/ prefix stripping, archive validation, and
 link-target escape checks
• Validates security guards against absolute paths, .. segments, empty segments, and oversized
 entries

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/tar-extract.test.ts


7. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/oci-key.test.ts 🧪 Tests +222/-0

OCI package key parsing and validation test coverage

• Tests for ociPluginKey() parsing of OCI package specs with tags, digests (sha256/sha512/blake3),
 and {{inherit}} placeholders
• Tests for explicit ! suffix handling and auto-detection from image cache for single-plugin
 images
• Tests for rejection of invalid OCI formats, unsupported digest algorithms, and ambiguous
 multi-plugin images
• Validates host:port registry support and path-less inherit resolution

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/oci-key.test.ts


8. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/tar-extract.ts ✨ Enhancement +194/-0

Secure tar extraction for OCI plugins and NPM packages

• Implements extractOciPlugin() with streaming tar extraction, path-traversal validation, and
 security filters (size, link targets, entry types)
• Implements extractNpmPackage() with package/ prefix stripping and realpath-based symlink
 escape detection
• Provides assertSafePluginPath() segment-based validation rejecting absolute paths, ..
 segments, and empty segments

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/tar-extract.ts


9. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/oci-key.ts ✨ Enhancement +156/-0

OCI package specification parsing and auto-detection

• Implements ociPluginKey() regex-based parser for OCI package specs with tag/digest/inherit
 support
• Provides autoDetectPluginPath() to resolve single-plugin images via image cache annotation
 lookup
• Implements tryParseOciRegistryAndPath() synchronous parser for the pre-merge disable pass
• Supports sha256, sha512, and blake3 digest algorithms with host:port registry parsing

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/oci-key.ts


10. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-npm.ts ✨ Enhancement +155/-0

NPM plugin installation with integrity verification

• Implements installNpmPlugin() orchestrating npm pack, integrity verification, and tarball
 extraction
• Provides npmPack() wrapper running npm pack --json with --ignore-scripts for security
• Implements isSafeArchiveName() validation rejecting path-traversal attempts in npm output
 filenames

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-npm.ts


11. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/module.ts ✨ Enhancement +29/-0

Backstage CLI module registration for install command

• Exports a createCliModule() registration that adds an install subcommand to the Backstage CLI
• Lazy-loads the command implementation via dynamic import for efficient module discovery

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/module.ts


12. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/.eslintrc.js ⚙️ Configuration changes +1/-0

ESLint configuration for package

• Adds ESLint configuration file delegating to Backstage CLI's standard config factory

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/.eslintrc.js


13. workspaces/install-dynamic-plugins/.eslintrc.js ⚙️ Configuration changes +1/-0

ESLint configuration for workspace

• Adds ESLint configuration file delegating to root monorepo config

workspaces/install-dynamic-plugins/.eslintrc.js


14. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.ts ✨ Enhancement +152/-0

OCI plugin installer with pull policy support

• New module implementing OCI plugin installation with installOciPlugin() function
• Handles plugin extraction from OCI images with hash-based change detection
• Implements isAlreadyInstalled() logic for IfNotPresent and Always pull policies
• Manages image and config hash files for tracking installed plugins

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.ts


15. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-hash.ts ✨ Enhancement +128/-0

Plugin hash computation for change detection

• Computes SHA256 hash for plugin change detection, byte-compatible with Python implementation
• Extracts local package metadata (package.json, mtimes, lock files) for hash computation
• Implements deterministic JSON stringification with sorted keys for cross-version compatibility

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-hash.ts


16. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/image-cache.ts ✨ Enhancement +125/-0

OCI image caching and metadata extraction

• Shared cache for OCI image tarballs to avoid redundant downloads across plugins
• Caches promises to deduplicate concurrent requests for the same image
• Extracts plugin paths from OCI annotations and retrieves image digests

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/image-cache.ts


17. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/concurrency.test.ts 🧪 Tests +131/-0

Concurrency control tests

• Unit tests for Semaphore class bounding concurrent operations
• Tests for mapConcurrent() capturing both successes and failures without cancellation
• Tests for getWorkers() and getNpmWorkers() respecting env vars and auto-detection

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/concurrency.test.ts


18. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts ✨ Enhancement +114/-0

Core type definitions and constants

• Defines PullPolicy enum and plugin-related types (PluginSpec, Plugin, PluginMap)
• Exports constants for OCI/Docker protocols, registry URLs, hash file names
• Implements effectivePullPolicy() and parseMaxEntrySize() utility functions

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts


19. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.test.ts 🧪 Tests +101/-0

Plugin merging and config tests

• Tests for deepMerge() handling nested objects and detecting conflicts
• Tests for mergePlugin() with include-file precedence and duplicate detection
• Includes prototype-pollution prevention tests

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/merger.test.ts


20. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/integrity.ts ✨ Enhancement +120/-0

SRI integrity verification for packages

• Streaming SRI integrity verification for NPM packages (sha256/sha384/sha512)
• Validates base64 encoding without regex to avoid ReDoS vulnerabilities
• Processes large archives without loading into memory

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/integrity.ts


21. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/finalize-install.test.ts 🧪 Tests +123/-0

Installation finalization tests

• Tests for finalizeInstall() writing global config and cleaning obsolete plugins
• Verifies error handling preserves previous config and skips cleanup on failure
• Tests lock file and error reporting behavior

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/finalize-install.test.ts


22. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/concurrency.ts ✨ Enhancement +120/-0

Concurrency control and worker management

Semaphore class for bounding concurrent async operations
• mapConcurrent() for parallel execution with error capture and concurrency limits
• getWorkers() and getNpmWorkers() for adaptive worker count based on CPU and env vars

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/concurrency.ts


23. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/skopeo.ts ✨ Enhancement +117/-0

Skopeo CLI wrapper with caching

• Wrapper around skopeo CLI with promise-based caching for inspect results
• Implements copy(), inspect(), inspectRaw(), and exists() methods
• Deduplicates concurrent requests for the same image

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/skopeo.ts


24. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/skopeo.test.ts 🧪 Tests +108/-0

Skopeo wrapper cache tests

• Tests for Skopeo cache deduplication across concurrent calls
• Verifies memoization of exists() results and negative caching
• Uses fake skopeo binary to validate invocation counts

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/skopeo.test.ts


25. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/lock-file.ts ✨ Enhancement +104/-0

Exclusive lock file management

• Exclusive lock file acquisition with polling and timeout (default 10 min)
• Registers cleanup handlers for process exit and signals (SIGTERM, SIGINT)
• Prevents concurrent installs and handles stale locks

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/lock-file.ts


26. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/npm-key.test.ts 🧪 Tests +81/-0

NPM package key parsing tests

• Tests for npmPluginKey() parsing standard NPM packages, aliases, and git URLs
• Verifies version/ref stripping and handling of local paths and tarballs
• Comprehensive test cases for cross-version compatibility

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/npm-key.test.ts


27. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-hash.test.ts 🧪 Tests +73/-0

Plugin hash computation tests

• Tests for deterministic hash computation and change detection
• Verifies Python compatibility with reference hashes
• Tests that pluginConfig and version do not affect hash

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/plugin-hash.test.ts


28. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/npm-key.ts ✨ Enhancement +71/-0

NPM package spec parsing

• Parses NPM package specs to extract stable keys for deduplication
• Handles standard packages, aliases, git URLs, and local paths
• Strips version/ref suffixes while preserving scope and package name

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/npm-key.ts


29. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/integrity.test.ts 🧪 Tests +81/-0

Integrity verification tests

• Tests for SRI integrity verification with sha256/sha512/sha384
• Validates error handling for malformed hashes and unsupported algorithms
• Tests base64 validation and hash mismatch detection

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/integrity.test.ts


30. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/lock-file.test.ts 🧪 Tests +66/-0

Lock file tests

• Tests for atomic lock file creation and removal
• Verifies waiting for existing locks and timeout behavior
• Tests stale lock handling with configurable timeout

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/lock-file.test.ts


31. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/util.ts ✨ Enhancement +72/-0

Shared utility functions

• Utility functions for file existence checks, path containment, and object type detection
• Tar entry type validation for security (rejects devices, FIFOs, unknown types)
• markAsFresh() to prevent stale hash cleanup after successful install

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/util.ts


32. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/image-resolver.test.ts 🧪 Tests +51/-0

Image resolver fallback tests

• Tests for registry fallback from registry.access.redhat.com/rhdh to quay.io/rhdh
• Verifies protocol preservation (oci:// vs docker://) during fallback
• Tests non-RHDH images pass through unchanged

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/image-resolver.test.ts


33. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/run.ts ✨ Enhancement +59/-0

Subprocess execution wrapper

• Subprocess execution wrapper capturing stdout/stderr with structured error reporting
• Throws InstallException on non-zero exit with full context (code, stderr, command)
• Matches Python run() contract for consistency

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/run.ts


34. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/image-resolver.ts ✨ Enhancement +46/-0

OCI image reference resolver

• Resolves OCI image references with registry fallback logic
• Falls back from registry.access.redhat.com/rhdh to quay.io/rhdh on missing images
• Preserves protocol (oci:// vs docker://) during resolution

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/image-resolver.ts


35. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.test.ts 🧪 Tests +48/-0

Type utilities tests

• Tests for parseMaxEntrySize() env var parsing with fallback to default
• Validates handling of non-numeric, zero, and negative values

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.test.ts


36. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/which.ts ✨ Enhancement +43/-0

PATH binary lookup utility

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

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/which.ts


37. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/cli.ts ✨ Enhancement +24/-0

CLI module entry point

• Entry point for the Backstage CLI module using runCliModule()
• Loads the CLI module and passes version from package.json

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/cli.ts


38. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/command.ts ✨ Enhancement +21/-0

Install command handler

• Command handler for the install subcommand
• Extracts argv from CLI context and delegates to main()

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/command.ts


39. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/errors.ts ✨ Enhancement +22/-0

Custom error class

• Custom InstallException error class for structured error handling

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/errors.ts


40. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/log.ts ✨ Enhancement +18/-0

Logging utility

• Uniform logging function writing to stdout

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/log.ts


41. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/install-dynamic-plugins.sh ✨ Enhancement +18/-0

Init-container shell wrapper

• Shell wrapper script for the init-container entry point
• Invokes the bundled CommonJS with Node.js, passing the root directory

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/install-dynamic-plugins.sh


42. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/README.md 📝 Documentation +130/-0

Project documentation

• Comprehensive documentation of the CLI module architecture and usage
• Lists environment variables, security checks, and concurrency strategy
• Explains compatibility with previous Python implementation

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/README.md


43. workspaces/install-dynamic-plugins/package.json ⚙️ Configuration changes +54/-0

Root workspace configuration

• Root workspace configuration for the monorepo
• Defines scripts for build, test, lint, and type-checking
• Configures Node.js 22/24 requirement and Prettier/ESLint settings

workspaces/install-dynamic-plugins/package.json


44. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/package.json ⚙️ Configuration changes +60/-0

Package metadata and dependencies

• Package metadata for @red-hat-developer-hub/cli-module-install-dynamic-plugins
• Declares backstage.role: cli-module and dependencies (@backstage/cli-node, tar, yaml,
 cleye)
• Configures esbuild build script and bin entry point

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/package.json


45. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/esbuild.config.mjs ⚙️ Configuration changes +41/-0

esbuild bundler configuration

• esbuild configuration bundling TypeScript to single CommonJS file
• Aliases keytar to stub module to avoid native binary bundling
• Enables minification and external sourcemap for production

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/esbuild.config.mjs


46. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/bin/install-dynamic-plugins ✨ Enhancement +34/-0

Executable bin entry point

• Executable bin entry point detecting monorepo vs installed package context
• Routes to TypeScript source in monorepo or bundled CommonJS in installed package

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/bin/install-dynamic-plugins


47. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/esbuild-keytar-stub.cjs ⚙️ Configuration changes +28/-0

Keytar native dependency stub

• No-op stub module replacing keytar native dependency at build time
• Provides dummy implementations of credential store functions

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/esbuild-keytar-stub.cjs


48. workspaces/install-dynamic-plugins/.changeset/initial-release.md 📝 Documentation +5/-0

Initial release changeset

• Changeset documenting initial release of the CLI module
• References Python predecessor and highlights TypeScript port with Backstage CLI module integration

workspaces/install-dynamic-plugins/.changeset/initial-release.md


49. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/cli-report.md 📝 Documentation +26/-0

CLI command report

• CLI report documenting command structure and options
• Generated by yarn build:api-reports for documentation

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/cli-report.md


50. workspaces/install-dynamic-plugins/.changeset/README.md 📝 Documentation +8/-0

Changesets configuration documentation

• Changesets documentation and setup guide

workspaces/install-dynamic-plugins/.changeset/README.md


51. workspaces/install-dynamic-plugins/.changeset/config.json ⚙️ Configuration changes +10/-0

Changesets configuration

• Changesets configuration for versioning and publishing

workspaces/install-dynamic-plugins/.changeset/config.json


52. workspaces/install-dynamic-plugins/tsconfig.json ⚙️ Configuration changes +10/-0

Root TypeScript configuration

• Root TypeScript configuration extending Backstage CLI defaults
• Configures strict type checking and output directory

workspaces/install-dynamic-plugins/tsconfig.json


53. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/report.api.md 📝 Documentation +7/-0

API Extractor report

• API Extractor report (empty for this package)

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/report.api.md


54. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/tsconfig.json ⚙️ Configuration changes +8/-0

Package TypeScript configuration

• Package-level TypeScript configuration with no-emit for type-checking only

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/tsconfig.json


55. workspaces/install-dynamic-plugins/.prettierignore ⚙️ Configuration changes +6/-0

Prettier ignore configuration

• Prettier ignore patterns for build artifacts and bundled output

workspaces/install-dynamic-plugins/.prettierignore


56. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/.prettierignore ⚙️ Configuration changes +5/-0

Package Prettier ignore configuration

• Package-level Prettier ignore patterns

workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/.prettierignore


Grey Divider

Qodo Logo

@rhdh-qodo-merge rhdh-qodo-merge Bot added documentation Improvements or additions to documentation enhancement New feature or request Tests Other labels May 29, 2026
- bin: switch to object form so the installed command stays
  `install-dynamic-plugins`. With the string form, npm derived the command
  name from the renamed scoped package (cli-module-install-dynamic-plugins),
  contradicting the README, runCliModule name, and the shell wrapper.
- keytar stub now throws on every method instead of silently returning
  null. Install never touches credentials, so a throw only fires if a
  future @backstage/cli-node release reaches keytar in our path — better a
  loud failure in CI than a silent wrong credential in an init-container.
- Thread the command's `info.usage` into the inner cleye parser so
  `install-dynamic-plugins install --help` prints the real invocation
  (`install-dynamic-plugins install <dynamic-plugins-root>`) instead of
  dropping the subcommand.
- main() now requires explicit args (the process.argv default was
  unreachable in the cli-module path).

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

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

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

Labels

documentation Improvements or additions to documentation enhancement New feature or request Other Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant