A frontend dynamic plugin for Red Hat Developer Hub (RHDH) 1.8+ that provides a Scaffolder Field Extension for selecting GitLab groups and repositories. Only shows groups where the authenticated user has Owner access (min_access_level=50).
- Owner-scoped group listing with full pagination
- OAuth token passthrough to downstream scaffolder steps (
publish:gitlab) - Multi-environment auth probe (
development→production→staging→default) - Scope validation with actionable error messages
- State restoration when navigating back in the scaffolder wizard
- Input validation and host sanitization
- Tech Stack
- Prerequisites
- Getting Started
- Architecture
- Available Scripts
- Testing
- Installation & Deployment
- Required Configuration
- Usage in Scaffolder Templates
- How It Works
- Environment Variables
- Troubleshooting
- Changelog
- License
- Language: TypeScript 5 (strict mode, ES2021 target)
- Framework: React 17/18 with Backstage Plugin API
- UI Components: Material UI v4 (
@material-ui/core,@material-ui/lab) - Plugin System: Backstage
createPlugin+createScaffolderFieldExtension - Dynamic Loading: Scalprum (via
@red-hat-developer-hub/cli) - Testing: Jest 29 + ts-jest + Testing Library
- Linting: ESLint with
@typescript-eslint - Package Manager: npm (no monorepo, no Yarn)
- License: Apache-2.0
- Node.js 18 or higher
- npm 9 or higher
- Podman or Docker (for OCI image packaging)
- A running RHDH 1.8+ instance with GitLab OAuth configured
git clone https://github.com/LiorHaim/gitlab-fe-selector.git
cd gitlab-fe-selectornpm install --legacy-peer-depsThe --legacy-peer-deps flag is needed because @backstage/* packages have strict peer dependency trees.
npm run buildThis runs tsc --build and compiles TypeScript to the dist/ directory.
npm testRuns all 50 tests (utility functions + React component smoke tests) via Jest with jsdom.
npm run lintRuns ESLint with TypeScript-aware rules against all src/**/*.{ts,tsx} files.
npm run typecheckRuns tsc --noEmit to validate types without emitting output.
src/
├── index.ts # Re-exports plugin + field extension
├── plugin.ts # createPlugin + createScaffolderFieldExtension
├── components/
│ └── GitLabGroupPicker.tsx # Main UI component (Autocomplete selectors)
├── hooks/
│ └── useGitLabAuth.ts # OAuth token acquisition hook
├── utils/
│ └── gitlab.ts # Types, validators, fetchAllPages, URL builders
└── __tests__/
├── gitlab.test.ts # Unit tests for utils/gitlab.ts (40 tests)
└── GitLabGroupPicker.test.tsx # Component smoke tests (10 tests)
User opens Scaffolder Template
│
▼
┌─────────────────────┐
│ useGitLabAuth() │ ──► /api/auth/gitlab/refresh?env=development
│ │ ──► /api/auth/gitlab/refresh?env=production
│ (probes envs) │ ──► /api/auth/gitlab/refresh?env=staging
│ │ ──► /api/auth/gitlab/refresh?env=default
└────────┬────────────┘
│ token + scope
▼
┌─────────────────────┐
│ GitLabGroupPicker │
│ │
│ 1. Validate scope │ ──► Checks for read_api or api
│ 2. Fetch groups │ ──► GET https://{host}/api/v4/groups?min_access_level=50
│ 3. Fetch projects │ ──► GET https://{host}/api/v4/groups/{id}/projects
│ 4. Set secrets │ ──► useTemplateSecrets().setSecrets({ key: token })
│ 5. Emit value │ ──► onChange("host?owner=group&repo=name")
└─────────────────────┘
- Direct browser fetch — API calls go directly from the browser to the GitLab instance using the user's OAuth token. No backend proxy or plugin backend is needed.
- Repo URL encoding — The output format
host?owner=<group>&repo=<name>is compatible with Backstage'spublish:gitlabaction (not a standard HTTPS URL). - Input validation —
isValidHostrejects path traversal, query injection, and other malicious host values.isValidSecretsKeyblocks prototype pollution vectors. - Rate limit resilience —
fetchAllPagesretries up to 3 times on HTTP 429, respecting theRetry-Afterheader (capped at 10 seconds).
| Command | Description |
|---|---|
npm run build |
Compile TypeScript to dist/ |
npm test |
Run all 50 tests via Jest |
npm run lint |
Run ESLint on source files |
npm run lint:fix |
Run ESLint with auto-fix |
npm run typecheck |
Type-check without emitting |
npm run export-dynamic |
Export Scalprum bundles to dist-dynamic/ via rhdh-cli |
npm run package |
Full pipeline: build → export → npm pack tarball |
npm run clean |
Remove dist/, dist-dynamic/, dist-pkg/ |
| Suite | File | Tests | Coverage |
|---|---|---|---|
| Utility functions | src/__tests__/gitlab.test.ts |
40 | hasRequiredScope, buildRepoUrl, parseRepoUrl, fetchAllPages, isValidHost, isValidSecretsKey, AUTH_ENVIRONMENTS |
| Component states | src/__tests__/GitLabGroupPicker.test.tsx |
10 | Auth loading, sign-in prompt, error display, scope warning, happy path, disabled picker, validation errors, invalid host, custom host |
# All tests
npm test
# Verbose output
npm test -- --verbose
# Single file
npx jest src/__tests__/gitlab.test.ts
# Watch mode
npx jest --watch- Runner: Jest 29 with ts-jest preset
- Environment: jsdom
- Config:
jest.config.js
A pre-built OCI image is available on quay.io:
quay.io/rh_ee_lhaim/plugin-gitlab-fe-selector:1.5.0
quay.io/rh_ee_lhaim/plugin-gitlab-fe-selector:latest
Configure in dynamic-plugins.yaml:
plugins:
- package: oci://quay.io/rh_ee_lhaim/plugin-gitlab-fe-selector:1.5.0!liorhaim-gitlab-fe-selector-dynamic
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
liorhaim.gitlab-fe-selector-dynamic:
scaffolderFieldExtensions:
- importName: GitLabGroupPickerFieldExtension# Build and export
npm run build
npm run export-dynamic
# Package as OCI image (requires podman or docker)
npx @red-hat-developer-hub/cli@latest plugin package \
--tag quay.io/YOUR_ORG/plugin-gitlab-fe-selector:1.5.0
# Push to registry
podman push quay.io/YOUR_ORG/plugin-gitlab-fe-selector:1.5.0| Option | Description |
|---|---|
-t, --tag <tag> |
Required — Image tag (e.g., quay.io/user/plugin:1.0.0) |
--container-tool <tool> |
podman (default), docker, or buildah |
--platform <platform> |
Target platform (default: linux/amd64) |
--force-export |
Rebuild dist-dynamic even if it exists |
# Full build + export + pack pipeline
npm run packageThis generates liorhaim-gitlab-fe-selector-dynamic-1.5.0.tgz in the project root.
Configure in dynamic-plugins.yaml:
plugins:
- package: ./local-plugins/liorhaim-gitlab-fe-selector-dynamic-1.5.0.tgz
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
liorhaim.gitlab-fe-selector-dynamic:
scaffolderFieldExtensions:
- importName: GitLabGroupPickerFieldExtensionThe GitLab OAuth provider must include read_api scope:
auth:
environment: development # See "Auth Environment Detection" below
providers:
gitlab:
development: # Must match auth.environment value
clientId: ${AUTH_GITLAB_CLIENT_ID}
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
audience: https://gitlab.example.com
## REQUIRED: read_api scope is needed to list groups ##
additionalScopes:
- read_api
signIn:
resolvers:
- resolver: emailMatchingUserEntityProfileEmail
integrations:
gitlab:
- host: gitlab.example.com
apiBaseUrl: https://gitlab.example.com/api/v4
token: ${GITLAB_TOKEN}Important: After adding
additionalScopes, users must revoke the existing RHDH/Backstage app in GitLab (User Settings → Applications) and re-authenticate to get a token with the new scopes.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: create-app
title: Create Application
spec:
parameters:
- title: Choose Repository
required:
- repoUrl
properties:
repoUrl:
title: Repository Location
type: string
ui:field: GitLabGroupPicker
ui:options:
allowedHosts:
- gitlab.example.com
requestUserCredentials:
secretsKey: USER_OAUTH_TOKEN
steps:
- id: publish
name: Publish to GitLab
action: publish:gitlab
input:
repoUrl: ${{ parameters.repoUrl }}
token: ${{ secrets.USER_OAUTH_TOKEN }}| Option | Description |
|---|---|
allowedHosts |
Array of allowed GitLab hosts. First host is used for API calls. Default: gitlab.com |
requestUserCredentials.secretsKey |
Key name for storing the user's OAuth token (used by backend steps) |
- Authentication Check — Probes the RHDH auth backend for a GitLab OAuth token across multiple environment names
- Scope Validation — Verifies the token has
read_apiorapiscope; shows actionable guidance if not - Host Validation — Validates the configured
allowedHostsvalue against injection attacks - Group Selection — Lists all groups where the user has Owner access, with full pagination
- Repository Selection — Lists projects within the selected group
- Token Passthrough — The
requestUserCredentialsoption stores the token viasetSecretsfor downstream scaffolder steps
The plugin automatically tries common auth environment names in order:
| Attempt | Environment Name |
|---|---|
| 1st | development |
| 2nd | production |
| 3rd | staging |
| 4th | default |
If your RHDH uses a different environment name, modify
AUTH_ENVIRONMENTSinsrc/utils/gitlab.tsand rebuild.
This plugin runs entirely in the browser and has no server-side environment variables. All configuration is done through app-config.yaml and template YAML as documented above.
For development, no environment variables are needed — just npm install and npm test.
The OAuth token doesn't have read_api scope.
- Add
additionalScopes: [read_api]toapp-config.yaml(see Required Configuration) - Restart RHDH
- Have users revoke the app in GitLab:
https://gitlab.example.com/-/profile/applications - Users re-authenticate via RHDH
- Ensure the user is logged in via GitLab (RHDH sidebar)
- Verify the GitLab auth provider is configured in
app-config.yaml - Check that
auth.environmentmatches one of:development,production,staging,default
The allowedHosts value in the template YAML contains invalid characters (path separators, query strings, etc.). Fix the template to use a plain hostname like gitlab.example.com.
- The user may not have Owner access to any GitLab groups
- Verify
allowedHostsmatches the GitLab instance configured inapp-config.yaml
If the sign-in popup is blocked, the plugin shows a message. Users need to allow popups for the RHDH domain in their browser settings.
The plugin automatically retries up to 3 times with the server's Retry-After delay (capped at 10 seconds). If retries are exhausted, an error is shown. This typically indicates heavy GitLab API usage — wait a moment and retry.
- Add input validation for host (
isValidHost) and secrets key (isValidSecretsKey) - Add retry loop with up to 3 retries on HTTP 429 rate limiting
- Cap
Retry-Afterdelay at 10s for better UX in browser context - Fix
parseRepoUrlto preserve query values containing encoded question marks - URL-encode
selectedGroup.idin projects fetch URL - Prevent stacking popup polling intervals on rapid sign-in clicks
- Add
console.warnwhenformDatagroup restoration fails - Add ESLint with
@typescript-eslintfor static analysis - Add 10 React component smoke tests covering all render states
- Bump test count from 25 to 50
- OCI image:
quay.io/rh_ee_lhaim/plugin-gitlab-fe-selector:1.5.0
- Fix OCI packaging — use
rhdh-cli plugin packageinstead of manual Containerfile
- Harden pagination, validation, accessibility, and error handling
- Align plugin packaging with RHDH best practices
- Full pagination for groups and repositories (no longer limited to first 100 results)
- State restoration when navigating back in the scaffolder wizard
- Field validation ensuring both group and repository are selected
- Repository fetch error reporting in the UI
- Unmount safety for the auth popup polling interval
- Extracted
useGitLabAuthhook and shared utilities - Added unit tests for all utility functions and pagination logic
Apache-2.0