diff --git a/workspaces/boost/dynamic-plugins-filesystem-reference.yaml b/workspaces/boost/dynamic-plugins-filesystem-reference.yaml index 87bd1c63d74..474e4d72693 100644 --- a/workspaces/boost/dynamic-plugins-filesystem-reference.yaml +++ b/workspaces/boost/dynamic-plugins-filesystem-reference.yaml @@ -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 @@ -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 diff --git a/workspaces/boost/dynamic-plugins-image-reference.yaml b/workspaces/boost/dynamic-plugins-image-reference.yaml index 2c0314d55cb..19011f899aa 100644 --- a/workspaces/boost/dynamic-plugins-image-reference.yaml +++ b/workspaces/boost/dynamic-plugins-image-reference.yaml @@ -16,7 +16,7 @@ plugins: # Core backend plugin — required # ----------------------------------------------------------------------- - package: oci:///backstage-plugin-boost-backend:!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 @@ -37,8 +37,8 @@ plugins: # OGX provider: connects to a OGX Responses API endpoint - package: oci:///backstage-plugin-boost-backend-module-ogx:!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:///backstage-plugin-boost-backend-module-kagenti:!red-hat-developer-hub-backstage-plugin-boost-backend-module-kagenti-dynamic - disabled: true + enabled: false diff --git a/workspaces/extensions/dynamic-plugins.yaml b/workspaces/extensions/dynamic-plugins.yaml index 713beba63db..edd51d3c2ee 100644 --- a/workspaces/extensions/dynamic-plugins.yaml +++ b/workspaces/extensions/dynamic-plugins.yaml @@ -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: @@ -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: @@ -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: @@ -56,4 +56,4 @@ plugins: anyOf: - isQuayAvailable - package: ./dynamic-plugins/dist/backstage-community-plugin-topology - disabled: false + enabled: true diff --git a/workspaces/global-header/docs/configuration.md b/workspaces/global-header/docs/configuration.md index f75f31d165c..1b4b3dc9ce0 100644 --- a/workspaces/global-header/docs/configuration.md +++ b/workspaces/global-header/docs/configuration.md @@ -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 @@ -36,7 +36,7 @@ After that customers can implement and install their own header as a dynamic plu ```yaml # Custom header implementation - package: - disabled: false + enabled: true pluginConfig: dynamicPlugins: frontend: diff --git a/workspaces/install-dynamic-plugins/.changeset/quiet-plugin-warnings.md b/workspaces/install-dynamic-plugins/.changeset/quiet-plugin-warnings.md new file mode 100644 index 00000000000..a0c06976ab5 --- /dev/null +++ b/workspaces/install-dynamic-plugins/.changeset/quiet-plugin-warnings.md @@ -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. diff --git a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-deprecation.test.ts b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-deprecation.test.ts new file mode 100644 index 00000000000..3df017aecce --- /dev/null +++ b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-deprecation.test.ts @@ -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); + }); +}); diff --git a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer.ts b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer.ts index bba28c9f352..6bf3b4f167e 100644 --- a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer.ts +++ b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer.ts @@ -333,6 +333,18 @@ export function resolveInheritPlugins( } } +function warnAboutDeprecatedDisabledKeys( + includeLists: IncludePluginList[], + mainPlugins: PluginSpec[], +): void { + // 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); +} + /** Resolve include paths, substitute the catalog-index placeholder, merge * everything into a single `PluginMap`, and compute change-detection hashes. * @@ -380,6 +392,8 @@ async function loadAllPlugins( } const mainPlugins = content.plugins ?? []; + warnAboutDeprecatedDisabledKeys(includeLists, mainPlugins); + resolveRefPlugins(mainPlugins, includeLists); // Collision validation must use the packages the user declared. Resolving // an inherit reference replaces its requested registry with the catalog's @@ -486,7 +500,7 @@ function categorize(allPlugins: PluginMap): Categorized { 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; } diff --git a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.test.ts b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.test.ts index b0da589c58a..686de366b1d 100644 --- a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.test.ts +++ b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.test.ts @@ -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[] = []; @@ -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("new 'enabled: true'"); + expect(warnings[0]).toContain( + "Remove deprecated 'disabled' in favour of preferred 'enabled' syntax.", + ); + expect(warnings[0]).not.toContain('Replace it with'); }); it('enabled takes precedence over disabled when both set (enabled: false, disabled: false)', () => { @@ -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("new 'enabled: false'"); + expect(warnings[0]).toContain( + "Remove deprecated 'disabled' in favour of preferred 'enabled' syntax.", + ); + expect(warnings[0]).not.toContain('Replace it with'); }); it('does not warn when no callback provided', () => { @@ -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'); }); }); diff --git a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts index 81ee2c37599..d04a2970f26 100644 --- a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts +++ b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/types.ts @@ -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; @@ -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. @@ -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 BOTH deprecated 'disabled: ${plugin.disabled}' and new 'enabled: ${plugin.enabled}'. Remove deprecated 'disabled' in favour of preferred 'enabled' syntax.`, + ); + } else { + warn?.( + `WARNING: Plugin ${plugin.package} uses deprecated 'disabled: ${plugin.disabled}' syntax. Replace it with 'enabled: ${!plugin.disabled}'.`, + ); + } } if (hasEnabled) return !plugin.enabled; if (hasDisabled) return plugin.disabled === true; diff --git a/workspaces/intelligent-assistant/.changeset/nasty-hairs-call.md b/workspaces/intelligent-assistant/.changeset/nasty-hairs-call.md new file mode 100644 index 00000000000..eb8748f2006 --- /dev/null +++ b/workspaces/intelligent-assistant/.changeset/nasty-hairs-call.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-intelligent-assistant': patch +--- + +Update README to use new `enabled` field when configuring plugins, as `disabled` is now deprecated diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md b/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md index b1f7f5e84f4..a7b7fb00e1a 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md @@ -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`: @@ -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: diff --git a/workspaces/orchestrator/docs/dynamic-plugin-installation.md b/workspaces/orchestrator/docs/dynamic-plugin-installation.md index 8bd7598bed4..d27d3fe5b4d 100644 --- a/workspaces/orchestrator/docs/dynamic-plugin-installation.md +++ b/workspaces/orchestrator/docs/dynamic-plugin-installation.md @@ -10,7 +10,7 @@ Include the following to the RHDH dynamic plugin configuration, replace `` ```yaml plugins: - - disabled: false + - enabled: true package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator:!red-hat-developer-hub-backstage-plugin-orchestrator pluginConfig: dynamicPlugins: @@ -43,13 +43,13 @@ plugins: if: anyOf: - IsOrchestratorCatalogTabAvailable - - disabled: false + - enabled: true package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator-backend:!red-hat-developer-hub-backstage-plugin-orchestrator-backend pluginConfig: orchestrator: dataIndexService: url: - - disabled: false + - enabled: true package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-orchestrator-form-widgets:!red-hat-developer-hub-backstage-plugin-orchestrator-form-widgets pluginConfig: dynamicPlugins: @@ -57,6 +57,6 @@ plugins: # OFS: load BackstagePlugin from Legacy (PluginRoot is NFS after graduation) red-hat-developer-hub.backstage-plugin-orchestrator-form-widgets: pluginModule: Legacy - - disabled: false + - enabled: true package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scaffolder-backend-module-orchestrator:!red-hat-developer-hub-backstage-plugin-scaffolder-backend-module-orchestrator ```