Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions workspaces/boost/dynamic-plugins-filesystem-reference.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ plugins:
# Core backend plugin — required
# -----------------------------------------------------------------------
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-boost-backend-dynamic
disabled: false
enabled: true
pluginConfig:
boost:
# Security mode: 'full' for production, 'development-only-no-auth' for local dev
Expand All @@ -38,8 +38,8 @@ plugins:

# OGX provider: connects to a OGX Responses API endpoint
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-boost-backend-module-ogx-dynamic
disabled: false
enabled: true

# Kagenti provider: connects to a Kagenti multi-agent orchestrator
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-boost-backend-module-kagenti-dynamic
disabled: true
enabled: false
6 changes: 3 additions & 3 deletions workspaces/boost/dynamic-plugins-image-reference.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ plugins:
# Core backend plugin — required
# -----------------------------------------------------------------------
- package: oci://<registry>/backstage-plugin-boost-backend:<tag>!red-hat-developer-hub-backstage-plugin-boost-backend-dynamic
disabled: false
enabled: true
pluginConfig:
boost:
# Security mode: 'full' for production, 'development-only-no-auth' for local dev
Expand All @@ -37,8 +37,8 @@ plugins:

# OGX provider: connects to a OGX Responses API endpoint
- package: oci://<registry>/backstage-plugin-boost-backend-module-ogx:<tag>!red-hat-developer-hub-backstage-plugin-boost-backend-module-ogx-dynamic
disabled: false
enabled: true

# Kagenti provider: connects to a Kagenti multi-agent orchestrator
- package: oci://<registry>/backstage-plugin-boost-backend-module-kagenti:<tag>!red-hat-developer-hub-backstage-plugin-boost-backend-module-kagenti-dynamic
disabled: true
enabled: false
12 changes: 6 additions & 6 deletions workspaces/extensions/dynamic-plugins.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
plugins:
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-bulk-import-backend-dynamic
disabled: false
enabled: true
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-bulk-import
disabled: true
enabled: false
pluginConfig:
dynamicPlugins:
frontend:
Expand All @@ -17,7 +17,7 @@ plugins:
icon: bulkImportIcon
text: Bulk import
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-extensions
disabled: false
enabled: true
pluginConfig:
dynamicPlugins:
frontend:
Expand All @@ -38,9 +38,9 @@ plugins:
path: extensions
title: Catalog
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-extensions-backend-dynamic
disabled: false
enabled: true
- package: ./dynamic-plugins/dist/backstage-community-plugin-quay
disabled: false
enabled: true
# TODO: enable
pluginConfig:
dynamicPlugins:
Expand All @@ -56,4 +56,4 @@ plugins:
anyOf:
- isQuayAvailable
- package: ./dynamic-plugins/dist/backstage-community-plugin-topology
disabled: false
enabled: true
4 changes: 2 additions & 2 deletions workspaces/global-header/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The RHDH Global Header can be disabled competely by disabling the right plugin:
```yaml
# Disabling global header
- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-global-header
disabled: true
enabled: false
```

## Replacing the Global Header with your own header
Expand All @@ -36,7 +36,7 @@ After that customers can implement and install their own header as a dynamic plu
```yaml
# Custom header implementation
- package: <npm or oci package-reference>
disabled: false
enabled: true
pluginConfig:
dynamicPlugins:
frontend:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/cli-module-install-dynamic-plugins': patch
---

Warn once per dynamic plugin entry that uses the deprecated `disabled` field, including entries filtered or overridden during installation. Preserve `enabled` precedence and existing plugin loading behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { main } from './installer';
import { log } from './log';
import type { PluginSpec } from './types';

jest.mock('./log', () => ({ log: jest.fn() }));
const warnings = () =>
(log as jest.Mock).mock.calls
.map(([message]: [string]) => message)
.filter((message: string) => message.includes('deprecated'));

describe('dynamic-plugins.yaml disabled deprecation', () => {
let dir: string;
const originalCwd = process.cwd();
const originalCatalogIndex = process.env.CATALOG_INDEX_IMAGE;
const originalExtraIndexes = process.env.EXTRA_CATALOG_INDEX_IMAGES;

beforeEach(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dp-deprecation-'));
process.chdir(dir);
delete process.env.CATALOG_INDEX_IMAGE;
delete process.env.EXTRA_CATALOG_INDEX_IMAGES;
jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
jest.clearAllMocks();
});

afterEach(async () => {
process.chdir(originalCwd);
if (originalCatalogIndex === undefined)
delete process.env.CATALOG_INDEX_IMAGE;
else process.env.CATALOG_INDEX_IMAGE = originalCatalogIndex;
if (originalExtraIndexes === undefined)
delete process.env.EXTRA_CATALOG_INDEX_IMAGES;
else process.env.EXTRA_CATALOG_INDEX_IMAGES = originalExtraIndexes;
jest.restoreAllMocks();
await fs.rm(dir, { recursive: true, force: true });
});

it('warns once per raw entry, including an overridden include and a filtered OCI entry', async () => {
const included: PluginSpec[] = [
{ package: './overridden-local', disabled: true },
{ package: 'oci://example.com/unused:1!plugin', disabled: true },
];
await fs.writeFile(
path.join(dir, 'included.yaml'),
JSON.stringify({ plugins: included }),
);
const plugins: PluginSpec[] = [
{ package: './overridden-local', enabled: true },
{ package: './disabled-false-local', disabled: false },
{ package: './both-local', enabled: false, disabled: false },
{ package: './enabled-only-local', enabled: false },
{ package: './neither-local' },
];
await fs.writeFile(
path.join(dir, 'dynamic-plugins.yaml'),
JSON.stringify({ includes: ['included.yaml'], plugins }),
);
// Missing local paths skip installation, so this exercises startup without
// npm, OCI downloads, or a live registry.
await main([path.join(dir, 'dynamic-plugins-root')]);
expect(process.exit).toHaveBeenCalledWith(0);

expect(warnings()).toHaveLength(4);
expect(
warnings().filter(message => message.includes('./overridden-local')),
).toHaveLength(1);
expect(
warnings().filter(message =>
message.includes('oci://example.com/unused:1!plugin'),
),
).toHaveLength(1);
expect(
warnings().filter(message => message.includes('./disabled-false-local')),
).toHaveLength(1);
expect(
warnings().filter(message => message.includes('./both-local')),
).toHaveLength(1);
expect(
warnings().some(message => message.includes('./enabled-only-local')),
).toBe(false);
expect(
warnings().some(message => message.includes('./neither-local')),
).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@
* at level 1 would still trigger a `skopeo` round-trip during the level-0
* merge — wasted work and a footgun in restricted-network init containers.
*/
async function loadAllPlugins(

Check failure on line 346 in workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AaDqMmM3e8jR-TndulRL&open=AaDqMmM3e8jR-TndulRL&pullRequest=5023
content: DynamicPluginsConfig,
configFileAbs: string,
configDir: string,
Expand Down Expand Up @@ -380,6 +380,13 @@
}
const mainPlugins = content.plugins ?? [];

// Warn once per declared entry, including disabled OCI entries removed by
// the pre-merge filter and included entries overridden by the main config.
for (const [, plugins] of includeLists) {
for (const plugin of plugins) isPluginDisabled(plugin, log);
}
for (const plugin of mainPlugins) isPluginDisabled(plugin, log);

resolveRefPlugins(mainPlugins, includeLists);
// Collision validation must use the packages the user declared. Resolving
// an inherit reference replaces its requested registry with the catalog's
Expand Down Expand Up @@ -486,7 +493,7 @@
const npm: Plugin[] = [];
const skipped: Plugin[] = [];
for (const plugin of Object.values(allPlugins)) {
if (isPluginDisabled(plugin, log)) {
if (isPluginDisabled(plugin)) {
log(`\n======= Skipping disabled plugin ${plugin.package}`);
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,35 @@ describe('isPluginDisabled', () => {
expect(isPluginDisabled({ package: 'pkg@1.0', enabled: false })).toBe(true);
});

it('returns true when disabled: true (backward compat)', () => {
expect(isPluginDisabled({ package: 'pkg@1.0', disabled: true })).toBe(true);
});

it('returns false when disabled: false (backward compat)', () => {
expect(isPluginDisabled({ package: 'pkg@1.0', disabled: false })).toBe(
false,
);
});
it.each([
{ disabled: true, expected: true, replacement: 'enabled: false' },
{ disabled: false, expected: false, replacement: 'enabled: true' },
])(
'preserves disabled: $disabled and warns once',
({ disabled, expected, replacement }) => {
const warnings: string[] = [];
expect(
isPluginDisabled({ package: 'pkg@1.0', disabled }, msg =>
warnings.push(msg),
),
).toBe(expected);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('Plugin pkg@1.0');
expect(warnings[0]).toContain(`deprecated 'disabled: ${disabled}'`);
expect(warnings[0]).toContain(replacement);
},
);

it.each([{ enabled: true }, { enabled: false }, {}])(
'does not warn without disabled: %p',
fields => {
const warnings: string[] = [];
isPluginDisabled({ package: 'pkg@1.0', ...fields }, msg =>
warnings.push(msg),
);
expect(warnings).toEqual([]);
},
);

it('enabled takes precedence over disabled when both set (enabled: true, disabled: true)', () => {
const warnings: string[] = [];
Expand All @@ -78,7 +98,12 @@ describe('isPluginDisabled', () => {
);
expect(result).toBe(false);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toMatch(/both 'enabled' and 'disabled'/);
expect(warnings[0]).toContain("deprecated 'disabled: true'");
expect(warnings[0]).toContain("alongside 'enabled: true'");
expect(warnings[0]).toContain(
"Remove 'disabled'; 'enabled' takes precedence",
);
expect(warnings[0]).not.toContain('Replace it with');
});

it('enabled takes precedence over disabled when both set (enabled: false, disabled: false)', () => {
Expand All @@ -89,6 +114,12 @@ describe('isPluginDisabled', () => {
);
expect(result).toBe(true);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain("deprecated 'disabled: false'");
expect(warnings[0]).toContain("alongside 'enabled: false'");
expect(warnings[0]).toContain(
"Remove 'disabled'; 'enabled' takes precedence",
);
expect(warnings[0]).not.toContain('Replace it with');
});

it('does not warn when no callback provided', () => {
Expand Down Expand Up @@ -141,7 +172,8 @@ describe('isPluginDisabled', () => {
msg => warnings.push(msg),
);
expect(result).toBe(true);
expect(warnings).toHaveLength(1);
expect(warnings).toHaveLength(2);
expect(warnings[0]).toMatch(/non-boolean 'enabled/);
expect(warnings[1]).toContain('enabled: false');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type PullPolicy = (typeof PullPolicy)[keyof typeof PullPolicy];
export type PluginSpec = {
package: string;
/**
* Recommended: Use `enabled` instead.
* @deprecated Use `enabled` instead (`enabled: !disabled`).
* When both `enabled` and `disabled` are present, `enabled` takes precedence.
*/
disabled?: boolean;
Expand Down Expand Up @@ -133,10 +133,12 @@ export function effectivePullPolicy(plugin: {
*
* 1. When only `enabled` is set → `disabled = !enabled`.
* 2. When only `disabled` is set → use it directly (backward compat).
* 3. When both are set → `enabled` wins and a warning is emitted
* via the optional `warn` callback.
* 3. When both are set → `enabled` wins.
* 4. When neither is set → default to `false` (not disabled).
*
* An explicit boolean `disabled` produces one deprecation warning through
* the optional `warn` callback, including when `enabled` is also set.
*
* Non-boolean values (e.g. the quoted string `enabled: 'false'` or
* `enabled: null`) are treated as unset and a warning is emitted,
* preventing JS truthiness from silently flipping activation state.
Expand Down Expand Up @@ -164,12 +166,16 @@ export function isPluginDisabled(
);
}

if (hasEnabled && hasDisabled) {
warn?.(
`WARNING: Plugin ${plugin.package} specifies both 'enabled' and 'disabled'. ` +
`The 'enabled' field takes precedence; please use only 'enabled'.`,
);
return !plugin.enabled;
if (hasDisabled) {
if (hasEnabled) {
warn?.(
`WARNING: Plugin ${plugin.package} uses deprecated 'disabled: ${plugin.disabled}' alongside 'enabled: ${plugin.enabled}'. Remove 'disabled'; 'enabled' takes precedence.`,
);
} else {
warn?.(
`WARNING: Plugin ${plugin.package} uses deprecated 'disabled: ${plugin.disabled}'. Replace it with 'enabled: ${!plugin.disabled}'.`,
);
}
}
if (hasEnabled) return !plugin.enabled;
if (hasDisabled) return plugin.disabled === true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-intelligent-assistant': patch
---

Update README to use new enabled field
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ Enable the plugin and its extensions in your dynamic plugins configuration:
```yaml
plugins:
- package: './local-plugins/red-hat-developer-hub-backstage-plugin-intelligent-assistant'
disabled: false
enabled: true
```

Then configure extensions in `app-config.yaml`:
Expand Down Expand Up @@ -323,7 +323,7 @@ OFS mode uses Scalprum for dynamic plugin loading. Legacy exports require `modul
```yaml
plugins:
- package: './local-plugins/red-hat-developer-hub-backstage-plugin-intelligent-assistant'
disabled: false
enabled: true
pluginConfig:
dynamicPlugins:
frontend:
Expand Down
Loading
Loading