A comprehensive tool for building and exporting dynamic plugins for Red Hat Developer Hub (RHDH) from Backstage plugin source code.
- RHDH Dynamic Plugin Factory
The RHDH Plugin Factory automates the process of converting Backstage plugins into RHDH dynamic plugins. It provides:
- Source Repository Management: Clone and checkout plugin source repositories
- Patch & Overlay System: Apply custom modifications to plugin source code before exporting
- Dependency Management: Automated yarn installation with TypeScript compilation
- Dynamic Plugin Packaging: Build, export and package plugins using the RHDH CLI
- Container Image Publishing: Optionally push to container registries (Quay, OpenShift, etc.)
A backstage plugin workspace is a yarn workspace within a Backstage repository that contains plugins to export. Note that there are cases where the backstage repository itself is the plugin workspace.
A Backstage plugin workspace is a yarn workspace (either a root workspace or nested within a monorepo) that typically follows this structure:
<backstage-workspace>/
├── package.json # Workspace root package.json (defines the yarn workspace)
├── plugins/ # Contains the plugins to export as dynamic plugins
│ ├── my-plugin/
│ └── my-plugin-backend/
└── packages/ # Optional: Contains frontend/backend apps for local development
├── app/ # (Usually unused for dynamic plugin export)
└── backend/
Examples:
- In the Backstage Community Plugins repository, each directory under
workspaces/is a Backstage workspace (e.g.,workspaces/todo,workspaces/announcements) - A standalone Backstage repository may have its workspace be the repository itself such as in the case of the PagerDuty plugins
These two options work together to locate your plugin workspace:
--repo-path: Where the backstage repository containing the plugin workspace is located (the cloned repository destination or your local repo)- Note: this is automatically resolved to
/sourceif not provided - Most of the time this is NOT the same as the plugin workspace containing the plugins you want to export, the exception is when the backstage plugin repository itself is a standalone plugin workspace.
- Note: this is automatically resolved to
--workspace-path: The relative path from the repository root to the workspace containing your plugins
Ex: To build plugins from the TODO workspace in the community-plugins repository:
--repo-path /source # Repository cloned to the /source directory
--workspace-path workspaces/todo # Workspace is at /source/workspaces/todo
The factory will then search for plugins defined in the plugins-list.yaml file with respect to the workspace <repo-path>/<workspace-path>/
The RHDH Plugin Factory is distributed as a pre-built container image. It is recommended to use Podman for all platforms.
Pre-built container images are published to quay.io/rhdh-community/dynamic-plugins-factory with tags corresponding to the version of RHDH they were designed for:
# Pull the latest version
podman pull quay.io/rhdh-community/dynamic-plugins-factory:latest# Or pull a specific RHDH version
podman pull quay.io/rhdh-community/dynamic-plugins-factory:1.8The container requires specific capabilities and device access for building dynamic plugins:
- Volume Mounts: Mount your configuration, plugin repository, and/or output directory to the
/config,/sourceand/outputsdirectories respectively - Device Access: Mount
/dev/fusefor filesystem operations (required for buildah) - SELinux Context: Use
:zflag for volume mounts on SELinux-enabled systems (RHEL/Fedora/CentOS)
The --device /dev/fuse flag passes the FUSE device from the Linux environment (native on Linux, or from Podman Machine's VM on macOS/Windows) to the container, enabling buildah operations.
podman run --rm -it \
--device /dev/fuse \
-v ./config:/config:z \
-v ./source:/source:z \
-v ./outputs:/outputs:z \
quay.io/rhdh-community/dynamic-plugins-factory:latest \
--workspace-path <path-to-workspace>Note: The --config-dir, --repo-path, and --output-dir options use default values of /config, /source, and /outputs respectively, which map to your local directories through volume mounts.
For local execution without containers, see CONTRIBUTING.md.
The factory expects the following directory structure:
./
├── config/ # Configuration directory (Can be set with --config-dir)
│ ├── .env # Optional (if not pushing): Override environment variables + provide registry credentials
│ ├── source.json # Source repository configuration
│ ├── plugins-list.yaml # List of plugins to build
│ ├── patches/ # Optional: Patch files to apply
│ └── <path-to-plugin-in-workspace>/overlays/ # Optional: Files to overlay on plugin source
├── source/ # Source code location (Can be set with --repo-path)
└── outputs/ # Build output directory (Can be set with --output-dir)Note: source/ in this case refers to the default source code location if not provided by --repo-path and is not to be mistaken with the workspace containing the plugins to export. Refer to Key Terminology for more details.
This file contains required version settings and defaults for RHDH CLI:
# Tooling versions
RHDH_CLI_VERSION="1.8.0"Defines the source repository to clone:
{
"repo": "https://github.com/backstage/community-plugins",
"repo-ref": "main",
}Fields:
repo: Repository URL (HTTPS or SSH)repo-ref: Git reference (branch, tag, or commit SHA)
A list of plugin paths (with respect to root of workspace) to plugins to build along with optional build arguments:
# Simple plugins (no additional arguments)
plugins/todo:
plugins/todo-backend:# Plugins with embed packages
plugins/scaffolder-backend: --embed-package @backstage/plugin-scaffolder-backend-module-github# Multiple embed packages
plugins/search-backend: |
--embed-package @backstage/plugin-search-backend-module-catalog
--embed-package @backstage/plugin-search-backend-module-techdocsOverride default settings to publish to a remote image registry:
# Registry configuration (required only with --push-images)
REGISTRY_URL=quay.io
REGISTRY_USERNAME=your_username
REGISTRY_PASSWORD=your_password
REGISTRY_NAMESPACE=your_namespace
REGISTRY_INSECURE=false
# Logging
LOG_LEVEL=DEBUG
WORKSPACE_PATH=<path_to_workspace_with_respect_to_plugin_repo_root>LOG_LEVEL can be set to one of DEBUG, INFO (default), WARN, ERROR, or CRITICAL
WORKSPACE_PATH can be set in lieu of the --workspace-path argument
Alternatively, you can pass the .env file directly through podman using the --env-file argument instead of placing a .env file in the config directory:
podman run --rm -it \
--device /dev/fuse \
--env-file ./my-env-file.env \
-v ./config:/config:z \
quay.io/rhdh-community/dynamic-plugins-factory:latest \
--workspace-path workspaces/todo \
--push-imagesThis approach keeps your credentials separate from the config directory and can be useful for CI/CD pipelines or when you want to reuse the same environment file across different configurations.
WARNING: This is a destructive operation
Patches and overlays modify files directly in the
--repo-pathdirectory. These operations are destructive and will permanently change the repository contents.
- When using
--use-localwith a local repository, patches and overlays WILL modify your local files- Consider using version control OR cloning a fresh copy of your repository if you need to preserve the original state
Place .patch files to apply modifications to the source code:
config/
└── patches/
└── 001-fix-dependency.patchPatches are applied using the override-sources.sh script before building.
See the AWS ECS plugin example config for an example on how patches are applied
Place files that should be copied over the source code:
config/
└── plugins/
└── my-plugin/
└── overlay/
└── custom-config.tsSee the TODO plugin example config and Gitlab plugin example config for an example on using overlays.
| Option | Default | Description |
|---|---|---|
--config-dir |
/config |
Configuration directory containing source.json, plugins-list.yaml, patches, and overlays |
--repo-path |
/source |
Path where plugin source code will be cloned/stored |
--workspace-path |
(required) | Path to the workspace from repository root (e.g., workspaces/todo) |
--output-dir |
/outputs |
Directory for build artifacts (.tgz files and container images) |
--push-images / --no-push-images |
--no-push-images |
Whether to push container images to registry. Defaults to not pushing if no argument is provided |
--use-local |
false |
Use local repository instead of cloning from source.json |
--log-level |
INFO |
Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
--verbose |
false |
Show verbose output with file and line numbers |
When using the container, you can mount directories based on your needs:
| Volume Mount | Required? | Purpose | When to Use |
|---|---|---|---|
-v ./config:/config:z |
Required | Configuration files | Always - contains your plugins-list.yaml, source.json, patches, and overlays |
-v ./source:/source:z |
Optional | Source code location | Only if using --use-local OR if you want to preserve/inspect the cloned/patched remote repository |
-v ./outputs:/outputs:z |
Optional | Stores bBuild artifacts | Only if you want the output .tgz files saved locally (otherwise they stay in the container) |
Important: These volume mount paths (/config, /source, /outputs) correspond to the default values of --config-dir, --repo-path, and --output-dir. If you override these arguments with custom paths, adjust your volume mounts accordingly.
Note: Use the :z flag for systems with SELinux enabled (RHEL/Fedora/CentOS). On other systems, you can omit it.
The following examples demonstrate common use cases with the container image. All examples assume you have the necessary configuration files (source.json, plugins-list.yaml, and optionally patches/overlays) in your configuration directory. See the Configuration section for details.
This minimal example builds the TODO plugins without saving the workspace or output files locally:
podman run --rm -it \
--device /dev/fuse \
-v ./examples/example-config-todo:/config:z \
quay.io/rhdh-community/dynamic-plugins-factory:latest \
--workspace-path workspaces/todoThis will clone the repository, build the plugins, and NOT push the result to a remote repository.
This example builds plugins and saves the .tgz files to your local ./outputs/ directory:
podman run --rm -it \
--device /dev/fuse \
-v ./config:/config:z \
-v ./outputs:/outputs:z \
quay.io/rhdh-community/dynamic-plugins-factory:latest \
--workspace-path workspaces/todoThis will clone the repository specified in ./config/source.json, build the plugins listed in ./config/plugins-list.yaml, and save the .tgz files to ./outputs/.
This example builds plugins and pushes them directly to a container registry (no local .tgz files saved).
First, create a ./config/.env file with your registry credentials:
REGISTRY_URL=quay.io
REGISTRY_USERNAME=myuser
REGISTRY_PASSWORD=mytoken
REGISTRY_NAMESPACE=mynamespaceThen run the factory with --push-images:
podman run --rm -it \
--device /dev/fuse \
-v ./config:/config:z \
quay.io/rhdh-community/dynamic-plugins-factory:latest \
--workspace-path workspaces/announcements \
--push-imagesThe factory will automatically read the load the environmental variables from ./config/.env.
Important: If the destination repository is a quay.io repository and does not exist, the factory will attempt to create a private repository. This may lead to issues described below. If you are having issues, please create the repositories before running the factory.
If you do need to manually create the quay repository, the expected naming scheme for the repository is quay.io/${REGISTRY_NAMESPACE}/${REPO_NAME} where ${REPO_NAME} is the name field of the package.json for the plugin except with @ removed and instances of / replaced with -.
Ex: @red-hat-developer-hub/backstage-plugin-quickstart -> red-hat-developer-hub-backstage-plugin-quickstart
If you already have the source code locally, use the --use-local flag and mount your existing workspace:
podman run --rm -it \
--device /dev/fuse \
-v ./config:/config:z \
-v /path/to/existing-source-code:/source:z \
-v ./outputs:/outputs:z \
quay.io/rhdh-community/dynamic-plugins-factory:latest \
--workspace-path path/to/workspace \
--use-localNote: When using --use-local, patches and overlays will still be applied to your local repository. Make sure you have backups or are using version control.
The factory also produces the following outputs in the directory specified by --output-dir:
outputs/
├── plugin-name-dynamic-1.0.0.tgz # Plugin tarball
├── plugin-name-dynamic-1.0.0.tgz.integrity # Integrity checksum
└── ...When --push-images is enabled, images are tagged as:
${REGISTRY_URL}/${REGISTRY_NAMESPACE}/plugin-name-dynamic:1.0.0NOTE: If the repository name (ex: plugin-name-dynamic) in the namespace specified by REGISTRY_NAMESPACE does not exist, the dynamic plugin factory will create a new registry. Depending on the registry specified by REGISTRY_URL, the newly created repository may be private. This will be the case for quay.io.
The examples directory contains ready-to-use configuration examples demonstrating different use cases and features.
| Example | Description | Details |
|---|---|---|
| TODO | Basic workspace with custom scalprum-config | View README |
| GitLab | Overlays for non Backstage Community Plugins workspace format | View README |
| AWS ECS | Patches and embed packages in plugins-list.yaml | View README |
Build the TODO plugin from Backstage community plugins:
podman run --rm -it \
--device /dev/fuse \
-v ./examples/example-config-todo:/config:z \
quay.io/rhdh-community/dynamic-plugins-factory:latest \
--workspace-path workspaces/todo \
--no-push-imagesThis example includes:
- Custom
scalprum-config.jsonconfiguration - A source repository using the standard Backstage Community Plugins workspace format
- Both frontend and backend plugins in the workspace
For detailed instructions, package verification steps, and additional examples, see the individual README files linked in the table above.
This section covers common issues encountered when building, publishing, and installing dynamic plugins generated with the factory.
When dynamically installing frontend plugins, they may fail to load or display incorrectly in RHDH.
To begin debugging, open your browser's developer console (F12) and check for loading errors. These errors are typically informative and indicate the root cause.
Example Error:
Plugin backstage-community.plugin-entity-feedback is not configured properly: PluginRoot.default not found, ignoring mountPoint: "entity.page.feedback/cards"
In most cases, the issue arise from missing or incorrect plugin configuration for the frontend wiring for the plugin.
To fix this, ensure all required mount points, routes, and bindings are correctly defined. Refer to the RHDH frontend wiring documentation for more details on how to do this.
When dynamically installing backend plugins, they may fail to load due to a MODULE_NOT_FOUND error.
Example Error:
backstage error an error occurred while loading dynamic backend plugin '@internal/backstage-plugin-catalog-backend-module-github-org-transformer-dynamic' from 'file:///opt/app-root/src/dynamic-plugins-root/backstage-plugin-catalog-backend-module-github-org-transformer' Cannot find module '@backstage/plugin-catalog-backend-module-github'
Require stack:
- /opt/app-root/src/dynamic-plugins-root/backstage-plugin-catalog-backend-module-github-org-transformer/dist/module.cjs.js
- /opt/app-root/src/dynamic-plugins-root/backstage-plugin-catalog-backend-module-github-org-transformer/dist/index.cjs.js
code="MODULE_NOT_FOUND" requireStack=["/opt/app-root/src/dynamic-plugins-root/backstage-plugin-catalog-backend-module-github-org-transformer ...
This indicates the backend plugin has dependencies that were not bundled in the dynamic plugin package when exporting with the factory.
To solve this, embed the missing dependency/dependencies using the --embed-package flag in your plugins-list.yaml:
plugins/my-backend-plugin: --embed-package @backstage/plugin-catalog-backend-module-github --embed-package <any-other-required-modules>Note: By default, the rhdh-cli only embeds -common and -node packages from your backend plugin's dependencies. Any non-@backstage dependencies not included in your RHDH instance must be explicitly embedded.
Note: The MODULE_NOT_FOUND error is thrown for the first missing module. It might not be the only missing module, so be sure to verify all the relevant private dependencies are embedded during the export.
During plugin installation via helm chart or operator, it may fail with a Skopeo error such as:
subprocess.CalledProcessError: Command '['/usr/bin/skopeo', 'inspect', '--raw', 'docker://quay.io/my-test-organization/red-hat-developer-hub-backstage-plugin-scaffolder-backend-module-orchestrator:1.3.1']' returned non-zero exit status 1
The main cause of this issue are:
- The repository is private and authentication is not configured, in which case you should set it to public or configure the proper authentication to pull from the repository.
- The repository does not exist (see Quay.io Repository Publishing Issues below), if so, you may need to manually create the repository and rebuild/publish with the factory (see Build and Push to Registry for the expected repository naming scheme)
The factory logs may indicate successful image publication, but the image does not appear in your Quay.io repository.
This may be due to Quay.io silently failing to publish images since your account has reached its private repository quota limit. When pushing to a non-existent repository, Quay.io automatically creates a private repository. If your account or organization has exhausted its private repository allocation, the creation may silently fails.
To mitigate this, you may need to pre-create the repositories on quay.io before publishing to avoid having the factory attempt to create the repositories. Alternatively, you can upgrade your quay.io plan to increase the private repository allocation.
For users who want to run the factory locally without containers or contribute to the project, see CONTRIBUTING.md.
To learn more about how dynamic plugins work refer to the dynamic plugins documentation in the RHDH Repository