Skip to content

feat: add plugin new command - #202

Open
gashcrumb wants to merge 4 commits into
redhat-developer:mainfrom
gashcrumb:feat/plugin-new
Open

gashcrumb wants to merge 4 commits into
redhat-developer:mainfrom
gashcrumb:feat/plugin-new

Conversation

@gashcrumb

@gashcrumb gashcrumb commented Sep 11, 2026

Copy link
Copy Markdown
Member

Summary

  • Add rhdh-cli plugin new for standalone frontend, backend, and catalog backend-module projects.
  • Pin generated Backstage dependencies to the selected RHDH release and configure Yarn 4.
  • Document npx @red-hat-developer-hub/cli and RHDH Dynamic Plugin Factory as the export/package routes.
  • Rebased onto main after feat: add plugin upgrade command for dynamic plugin dependency management #177 and added the 2.0.7 package/changelog release entry.

Jira

Test Plan

  • yarn prettier:check
  • yarn tsc
  • yarn lint:check
  • yarn test
  • yarn build
  • Generate, build, and export frontend, backend, and backend-module projects

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 11, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:01 PM UTC · Completed 12:21 PM UTC

Commit: c801662 · View workflow run →

Runtime: claude · Model: sonnet → claude-sonnet-4-6 · Effort: high · Cost: $4.71

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Sep 11, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 11, 2026

Copy link
Copy Markdown

Risk Assessment: moderate (2/5)

Details

Tier 1 signals are identical to the prior assessment — same 29 files, large blast radius, low test ratio (0.07), one dependency file changed, no protected paths, no security-sensitive files, no CI changes, established contributor — and Tier 2 signals are mild, confirming the prior score of 2 (moderate).

Previous run

Risk Assessment: moderate (2/5)

Details

Score of 2 (moderate): large blast radius and low test ratio from 29 new scaffolding files, with one dependency file changed, but no protected paths, security-sensitive files, or CI changes from an established contributor; prior score confirmed.

Previous run (2)

Risk Assessment: moderate (2/5)

Details

Score of 2 (moderate): substantial new scaffolding with large blast radius and low test ratio, but no protected paths, security-sensitive files, dependency, or CI changes from an established contributor.

Previous run (3)

Risk Assessment: moderate (2/5)

Details

A substantial new-feature PR (27 files, 589 additions) scaffolding a plugin-new command with templates; risk is kept moderate by zero security/protected-path exposure, no dependency changes, low churn on existing modified files, and a known non-first-time author — partially offset by a low test-file ratio (0.07) relative to total files changed.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review

Findings

Low

  • [code-organization] templates/plugin-new/backend-module/backstage.json.hbs (also backend/ and frontend/) — All three backstage.json.hbs template files are missing a trailing newline. Each ends with } as the final byte. The templates/plugin-new/ directory is excluded from Prettier via .prettierignore, so this will not self-correct.
    Remediation: Add a trailing newline to all three backstage.json.hbs template files.

  • [documentation-comment-format] src/commands/new/command.ts:61 — The exported function completeInteractiveOptions has no JSDoc comment. Both sibling command.ts files (check-versions and upgrade) place a /** ... */ block above every exported function without exception. createPluginProject (line 104) and command (line 171) in this same file follow that pattern, but completeInteractiveOptions does not.
    Remediation: Add a /** ... */ JSDoc comment above completeInteractiveOptions describing its purpose, e.g.: /** Fills in missing name and type by calling the provided prompt function interactively. */

  • [test-adequacy] src/lib/rhdhVersion.test.ts — The new validation/fallback logic in fetchRemoteRhdhMetadata (lines 161–164 of rhdhVersion.ts) has no dedicated test. The CHANGELOG documents "Fall back to the requested RHDH version when remote metadata returns an invalid version value," but no test exercises the path where the Backstage version is valid and the RHDH version fails the /^[a-z0-9._-]+$/i guard.
    Remediation: Add a test case to the fetchRemoteRhdhMetadata describe block that mocks remote metadata to return an invalid RHDH version value and asserts that rhdhVersion falls back to the input argument.

  • [test-adequacy] src/commands/new/command.test.ts — The error-cleanup branch in createPluginProject has two paths: fs.remove (when the output directory did not previously exist) and fs.emptyDir (when it did). Only the outputExisted = true path is covered. The !outputExisted path — where the newly created directory is fully removed on failure — has no test.
    Remediation: Add a test case with a non-existent output path that mocks resolveRhdhVersion to trigger a failure and asserts that the output directory no longer exists after the error.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

Low

  • [code-organization] templates/plugin-new/backend-module/backstage.json.hbs (also backend/ and frontend/) — All three backstage.json.hbs template files are missing a trailing newline. Each ends with } as the final byte (\\ No newline at end of file in the diff for all three).
    Remediation: Add a trailing newline to all three backstage.json.hbs template files.

  • [code-organization] src/commands/new/index.ts:1 — The file is missing the Apache 2.0 license header that all other subcommand index.ts files carry (upgrade/index.ts, check-versions/index.ts, start/index.ts all open with the standard copyright block). The file contains only the bare export statement.
    Remediation: Prepend the standard Apache 2.0 license header block (matching the one in src/commands/upgrade/index.ts) before the export statement.

  • [error-handling] src/commands/new/command.ts:117 — When createPluginProject targets an existing but empty directory and templatingTask fails partway through, partially-written files are left behind because the catch block only removes the output directory when it was newly created (!outputExisted). A subsequent retry will then fail with the "is not empty" guard, forcing the user to manually clean up.
    Remediation: In the catch block, when outputExisted is true, remove the files that were written (e.g., re-read the directory and delete entries) rather than leaving partial output. Alternatively, clear the directory contents while preserving the directory itself.

  • [undocumented-change] src/lib/rhdhVersion.ts:160 — A defensive sanitization was added to fetchRemoteRhdhMetadata: the resolved RHDH version from remote data is now validated against /^[a-z0-9._-]+$/i before being returned, falling back to the caller-supplied rhdhVersion if invalid. This behavioral change is not mentioned in the CHANGELOG (which lists only the matrix version correction as a Fixed item), and the function's JSDoc is silent about the fallback.
    Remediation: Add a CHANGELOG entry under the 2.0.7 Fixed section (e.g., "Sanitize the RHDH version string returned by remote metadata to prevent untrusted values from propagating") and update the JSDoc for fetchRemoteRhdhMetadata to note the sanitization fallback.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Three items need to be addressed before this PR is ready to merge.

Findings

Medium

  • [missing-doc] CHANGELOG.md — The ## Unreleased section added by this PR documents only the version-mapping correction. There is no ### Added entry for the new plugin new command, which is the headline feature of this PR — introducing full project scaffolding for three plugin types (frontend, backend, backend-module).
    Remediation: Add an ### Added subsection under ## Unreleased documenting plugin new, e.g.: **plugin new:** Add rhdh-cli plugin new command to scaffold standalone, version-pinned RHDH dynamic plugin projects for frontend, backend, and backend-module types. Resolves RHIDP-16671 / RHIDP-16668.

Low

  • [code-organization] templates/plugin-new/backend-module/backstage.json.hbs (also backend/ and frontend/) — All three backstage.json.hbs template files are still missing a trailing newline. Each ends with } as the final byte (\ No newline at end of file in the diff for all three).
    Remediation: Add a trailing newline to all three backstage.json.hbs template files.

  • [missing-doc] src/lib/rhdhVersion.ts:160 — The validation guard added to fetchRemoteRhdhMetadata — falling back to the caller-supplied rhdhVersion when the remote value fails /^[a-z0-9._-]+$/i — is a real behavioral change with no CHANGELOG entry. The Unreleased section already has an open ### Fixed subsection where this belongs.
    Remediation: Add a Fixed bullet to the ### Fixed section in ## Unreleased describing the fallback behavior, e.g.: Guard fetchRemoteRhdhMetadata against non-string or specially-encoded rhdhVersion values returned by remote metadata.

  • [data-correction] src/lib/rhdhVersion.ts:28 — The exported RHDH_COMPATIBILITY_MATRIX updates Backstage versions for keys '2.1.0', 'main', and 'next' from 1.54.0 to 1.54.6. The correction is documented in the CHANGELOG. Downstream consumers that hardcode the prior string value will receive updated output on next release; a semver patch bump at release provides a stable pin point.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Several issues should be addressed before this PR merges, including a validation gap that allows malformed plugin names, a data-contract change on an exported constant (the compatibility matrix correction), missing flag documentation, an absent changelog entry, and a cluster of low-severity correctness and style issues.

Findings

Medium

  • [input-validation] src/commands/new/command.ts:40 — The regex /^[a-z][a-z0-9-]*$/ in assertPluginName allows trailing hyphens (my-plugin-) and consecutive hyphens (my--plugin). A trailing-hyphen name produces a double-hyphen package name in the backend-module template (@internal/backstage-plugin-my-plugin--catalog-backend-module). No test covers this edge case.
    Remediation: Change to /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/ to require each hyphen-separated segment to contain at least one alphanumeric character.

  • [api-shape] src/commands/new/command.ts:174 — The success message uses process.stdout.write(), but every other command in the project uses Task.log() for non-JSON user-facing output (process.stdout.write in check-versions is gated exclusively behind --json). Confirmed by codebase grep across all command files.
    Remediation: Replace with Task.log(...) and add Task to the import from '../../lib/tasks'.

  • [missing-doc] README.md:54 — The 'Creating a Plugin' section documents --type, --rhdh-version, and --output but omits --manifest-file (with RHDH_OFFLINE=true for air-gapped environments) and --name. The analogous 'Checking Plugin Versions' section explicitly documents both of these flags for the same scenario.
    Remediation: Document --manifest-file (with RHDH_OFFLINE=true) and --name in the 'Creating a Plugin' README section.

  • [missing-doc] CHANGELOG.md — The PR introduces a significant new plugin new command with full project scaffolding for three plugin types, but CHANGELOG.md has no entry (the file does not appear in the diff at all).
    Remediation: Add an entry under the appropriate version heading in an 'Added' section.

  • [breaking-api] src/lib/rhdhVersion.ts:29 — The exported RHDH_COMPATIBILITY_MATRIX changes the Backstage version for keys '2.1.0', 'main', and 'next' from '1.54.0' to '1.54.6'. Because this is a named export, downstream consumers asserting against the prior value will silently receive a different string. Derived exported functions findStaticMatrixBackstageVersion('2.1.0') and resolveRhdhVersion('2.1.0', { offline: true }) are also affected. The test change in the diff confirms an existing assertion was updated from '1.54.0' to '1.54.6'.
    Remediation: Add a changelog/release note for this data correction so downstream consumers know to update hardcoded expectations.

Low

  • [error-handling] src/commands/new/command.ts:133fs.ensureDir(outputDir) creates the output directory before templatingTask runs. If rendering fails mid-run, partially-written template files are left on disk. On retry, the non-empty-directory guard at line 114 rejects the directory, forcing users to delete it manually.
    Remediation: Wrap templatingTask in a try/catch that removes outputDir on failure, or document in the error message that the partial directory must be deleted before retrying.

  • [logic-error] src/commands/new/command.ts:77 — The non-interactive error path hard-codes "Plugin name and --type are required" regardless of which field is actually missing. A user who provides --name but omits --type receives an inaccurate error.
    Remediation: Build the message dynamically from the missing fields.

  • [logic-error] src/commands/new/command.ts:61completeInteractiveOptions does not guard against an empty prompt response. Pressing Enter without typing a name sets options.name to ''; createPluginProject then throws "Plugin name is required. Pass it as an argument or with --name." — a message that implies a non-interactive invocation.
    Remediation: Validate the trimmed result is non-empty and re-prompt or throw a prompt-specific error.

  • [naming] templates/plugin-new/backend-module/src/index.ts.hbs:11const module = createBackendModule({...}) shadows the Node.js CJS module global. While TypeScript's emit avoids a runtime break, this will trigger no-shadow/no-restricted-globals ESLint rules in generated projects.
    Remediation: Rename to catalogBackendModule or similar.

  • [injection] src/commands/new/command.ts:175result.rhdhVersion is interpolated into stdout without format validation. In fetchRemoteRhdhMetadata, the RHDH version string is taken verbatim from remote JSON fields with no sanitization — unlike backstageVersion, which is guarded by semver.clean(). A MITM on the upstream fetch or a compromised RHDH_METADATA_BASE_URL could inject :: sequences that GitHub Actions interprets as workflow commands.
    Remediation: Apply semver.coerce/semver.clean validation to the resolved RHDH version in fetchRemoteRhdhMetadata, or strip characters outside /^[a-z0-9._-]+$/.

  • [scope-creep] src/lib/rhdhVersion.ts — The compatibility matrix correction (changing the Backstage version for '2.1.0', 'main', 'next' from '1.54.0' to '1.54.6') is bundled with this PR without mention in the title or body. The README explicitly calls out matrix updates as a distinct maintenance action. See also: [breaking-api] finding at src/lib/rhdhVersion.ts:29.
    Remediation: Call out the matrix correction explicitly in the PR body with its own justification, or split into a separate commit.

  • [code-organization] templates/plugin-new/backend-module/backstage.json.hbs — All three backstage.json.hbs files (backend-module, backend, frontend) are missing a trailing newline. Confirmed: each file ends with } as the final byte.
    Remediation: Add a trailing newline to all three backstage.json.hbs template files.

  • [incomplete-doc] README.md:54 — The frontend plugin type is described as "an NFS page". "NFS" is not defined or expanded anywhere in the documentation and is not a recognized standard abbreviation in public Backstage documentation.
    Remediation: Expand on first use, e.g. "an NFS (New Frontend System) page".

  • [api-shape] src/commands/new/command.ts:96createPluginProject returns an anonymous inline object type (Promise<{ outputDir: string; rhdhVersion: string; backstageVersion: string }>). A named exported interface would give the shape a stable identity across versions.
    Remediation: Introduce and export a named interface (e.g. PluginProjectResult) for the return type.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 11, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:27 PM UTC · Completed 12:42 PM UTC

Commit: 22d55c2 · View workflow run →

Runtime: claude · Model: sonnet → claude-sonnet-4-6 · Effort: high · Cost: $4.37

fullsend-ai-review[bot]

This comment was marked as outdated.

Assisted-By: openai/gpt-5.6-terra
Signed-off-by: Stan Lewis <gashcrumb@gmail.com>

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
Assisted-By: openai/gpt-5.6-terra
Signed-off-by: Stan Lewis <gashcrumb@gmail.com>

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
Assisted-By: openai/gpt-5.6-terra

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@gashcrumb
gashcrumb marked this pull request as ready for review September 14, 2026 12:23
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 14, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:24 PM UTC · Completed 12:42 PM UTC

Commit: a204e70 · View workflow run →

Runtime: claude · Model: sonnet → claude-sonnet-4-6 · Effort: high · Cost: $5.55

fullsend-ai-review[bot]

This comment was marked as outdated.

Assisted-By: openai/gpt-5.6-terra

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@sonarqubecloud

Copy link
Copy Markdown

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 14, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:14 PM UTC · Completed 1:31 PM UTC

Commit: e4107f6 · View workflow run →

Runtime: claude · Model: sonnet → claude-sonnet-4-6 · Effort: high · Cost: $4.47

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

}
}

export async function completeInteractiveOptions(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] documentation-comment-format

The exported function completeInteractiveOptions has no JSDoc comment. Both sibling command.ts files (check-versions and upgrade) place a JSDoc block above every exported function without exception. createPluginProject (line 104) and command (line 171) in this same file follow that pattern, but completeInteractiveOptions does not.

Suggested fix: Add a JSDoc comment above completeInteractiveOptions describing its purpose, e.g.: /** Fills in missing name and type by calling the provided prompt function interactively. */

@PatAKnight PatAKnight left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Left a few in-lines.

Also, the generated files do not seem to contain a .gitignore. It would be beneficial to include one.

resolveRhdhVersion: jest.fn(),
}));

describe('createPluginProject', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be beneficial to include some snapshot tests of the generated files here to hopefully catch any template drift that might happen.


const plugin = createBackendPlugin({
pluginId: '{{pluginId}}',
register() {},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's scaffold the backend plugin a bit more: registerInit with coreServices.httpRouter and coreServices.logger (for example a simple /health route). That shows developers how to wire a couple of core services instead of an empty register().

Comment thread README.md
rhdh-cli plugin new my-plugin --type frontend --rhdh-version 2.1.0
```

Supported types are `frontend` (a New Frontend System, or NFS, page), `backend` (a minimal new-backend-system plugin), and `backend-module` (a catalog processor module). Use `--name <plugin-name>` as an alternative to the positional name, and `--output <directory>` to select a destination. The generated project uses the target RHDH release's Backstage manifest for every `@backstage/*` dependency. For air-gapped environments, provide `--manifest-file` and set `RHDH_OFFLINE=true`. Export and package generated plugins with `npx @red-hat-developer-hub/cli`, or through RHDH Dynamic Plugin Factory, rather than adding the CLI as a project dependency.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see that we mention "backend-module (a catalog processor module)". Do we want to keep it catalog-only for now, or is the longer-term plan to support other targets (scaffolder/search/techdocs) or a generic “which plugin to extend” prompt?

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

Labels

risk/moderate PR risk: moderate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants