From 729a7d6e76556f250e370379fcfc56479d4bf249 Mon Sep 17 00:00:00 2001 From: "Avery, Mark" Date: Sat, 22 Mar 2025 13:57:29 -0700 Subject: [PATCH 1/5] feat(backend-app-api): add config to permit module failures on startup to not effect the plugin from starting up Signed-off-by: Avery, Mark --- .changeset/eight-toys-feel.md | 35 +++++ packages/backend-app-api/config.d.ts | 21 +++ .../src/wiring/BackendInitializer.test.ts | 135 +++++++++++++++++- .../src/wiring/BackendInitializer.ts | 59 ++++++-- .../src/wiring/createInitializationLogger.ts | 26 ++++ 5 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 .changeset/eight-toys-feel.md diff --git a/.changeset/eight-toys-feel.md b/.changeset/eight-toys-feel.md new file mode 100644 index 0000000000..c0f5a12c40 --- /dev/null +++ b/.changeset/eight-toys-feel.md @@ -0,0 +1,35 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added a configuration to permit backend plugin module failures on startup: + +```yaml +backend: + ... + startup: + plugins: + plugin-x: + modules: + module-y: + onPluginModuleBootFailure: continue +``` + +This configuration permits `plugin-x` with `module-y` to fail on startup. Omitting the +`onPluginModuleBootFailure` configuration matches the previous behavior, wherein any +individual plugin module failure is forwarded to the plugin and aborts backend startup. + +The default can also be changed, so that continuing on failure is the default +unless otherwise specified: + +```yaml +backend: + startup: + default: + onPluginModuleBootFailure: continue + plugins: + catalog: + modules: + github: + onPluginModuleBootFailure: abort +``` diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index 0cf91852a1..58511c91fb 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -34,6 +34,14 @@ export interface Config { * `onPluginBootFailure: abort` to be required. */ onPluginBootFailure?: 'continue' | 'abort'; + /** + * The default value for `onPluginModuleBootFailure` if not specified for a particular plugin module. + * This defaults to 'abort', which means `onPluginModuleBootFailure: continue` must be specified + * for backend startup to continue on plugin module boot failure. This can also be set to + * 'continue', which flips the logic for individual plugin modules so that they must be set to + * `onPluginModuleBootFailure: abort` to be required. + */ + onPluginModuleBootFailure?: 'continue' | 'abort'; }; plugins?: { [pluginId: string]: { @@ -46,6 +54,19 @@ export interface Config { * setting). */ onPluginBootFailure?: 'continue' | 'abort'; + modules?: { + [moduleId: string]: { + /** + * Used to control backend startup behavior when this plugin module fails to boot up. Setting + * this to `continue` allows the backend to continue starting up, even if this plugin + * module fails. This can enable leaving a crashing plugin installed, but still permit backend + * startup, which may help troubleshoot data-dependent issues. Plugin module failures for plugin modules + * set to `abort` are fatal (this is the default unless overridden by the `default` + * setting). + */ + onPluginModuleBootFailure?: 'continue' | 'abort'; + }; + }; }; }; }; diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index fe0a4f526b..3b0596cd13 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -541,7 +541,7 @@ describe('BackendInitializer', () => { }); it('should forward errors when plugins fail to start', async () => { - const init = new BackendInitializer([]); + const init = new BackendInitializer(baseFactories); init.add( createBackendPlugin({ pluginId: 'test', @@ -562,8 +562,7 @@ describe('BackendInitializer', () => { it('should permit startup errors for plugins with onPluginBootFailure: continue', async () => { const init = new BackendInitializer([ - mockServices.rootLifecycle.factory(), - mockServices.rootLogger.factory(), + ...baseFactories, mockServices.rootConfig.factory({ data: { backend: { @@ -590,8 +589,7 @@ describe('BackendInitializer', () => { it('should permit startup errors if the default onPluginBootFailure is continue', async () => { const init = new BackendInitializer([ - mockServices.rootLifecycle.factory(), - mockServices.rootLogger.factory(), + ...baseFactories, mockServices.rootConfig.factory({ data: { backend: { @@ -618,8 +616,7 @@ describe('BackendInitializer', () => { it('should forward errors for plugins explicitly marked to abort when the default is continue', async () => { const init = new BackendInitializer([ - mockServices.rootLifecycle.factory(), - mockServices.rootLogger.factory(), + ...baseFactories, mockServices.rootConfig.factory({ data: { backend: { @@ -649,6 +646,130 @@ describe('BackendInitializer', () => { ); }); + it('should forward errors when plugin modules fail to start', async () => { + const init = new BackendInitializer(baseFactories); + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'mod', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw new Error('NOPE'); + }, + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + "Module 'mod' for plugin 'test' startup failed; caused by Error: NOPE", + ); + }); + + it('should permit startup errors for plugin modules with onPluginModuleBootFailure: continue', async () => { + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + plugins: { + test: { + modules: { mod: { onPluginModuleBootFailure: 'continue' } }, + }, + }, + }, + }, + }, + }), + ]); + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'mod', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw new Error('NOPE'); + }, + }); + }, + }), + ); + await expect(init.start()).resolves.not.toThrow(); + }); + + it('should permit startup errors if the default onPluginModuleBootFailure is continue', async () => { + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { default: { onPluginModuleBootFailure: 'continue' } }, + }, + }, + }), + ]); + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'mod', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw new Error('NOPE'); + }, + }); + }, + }), + ); + await expect(init.start()).resolves.not.toThrow(); + }); + + it('should forward errors for plugin modules explicitly marked to abort when the default is continue', async () => { + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + default: { onPluginModuleBootFailure: 'continue' }, + plugins: { + test: { + modules: { mod: { onPluginModuleBootFailure: 'abort' } }, + }, + }, + }, + }, + }, + }), + ]); + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'mod', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw new Error('NOPE'); + }, + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + "Module 'mod' for plugin 'test' startup failed; caused by Error: NOPE", + ); + }); + it('should forward errors when multiple plugins fail to start', async () => { const init = new BackendInitializer([]); init.add( diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index f521966d4d..a666f53f29 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -365,17 +365,38 @@ export class BackendInitializer { } await tree.parallelTopologicalTraversal( async ({ moduleId, moduleInit }) => { - const moduleDeps = await this.#getInitDeps( - moduleInit.init.deps, - pluginId, - moduleId, - ); - await moduleInit.init.func(moduleDeps).catch(error => { - throw new ForwardedError( - `Module '${moduleId}' for plugin '${pluginId}' startup failed`, - error, + const isModuleBootFailurePermitted = + this.#getPluginModuleBootFailurePredicate( + pluginId, + moduleId, + rootConfig, ); - }); + + try { + const moduleDeps = await this.#getInitDeps( + moduleInit.init.deps, + pluginId, + moduleId, + ); + await moduleInit.init.func(moduleDeps).catch(error => { + throw new ForwardedError( + `Module '${moduleId}' for plugin '${pluginId}' startup failed`, + error, + ); + }); + } catch (error: unknown) { + assertError(error); + if (isModuleBootFailurePermitted) { + initLogger.onPermittedPluginModuleFailure( + pluginId, + moduleId, + error, + ); + } else { + initLogger.onPluginModuleFailed(pluginId, moduleId, error); + throw error; + } + } }, ); } @@ -649,6 +670,24 @@ export class BackendInitializer { return pluginStartupBootFailureValue === 'continue'; } + + #getPluginModuleBootFailurePredicate( + pluginId: string, + moduleId: string, + config?: Config, + ): boolean { + const defaultStartupBootFailureValue = + config?.getOptionalString( + 'backend.startup.default.onPluginModuleBootFailure', + ) ?? 'abort'; + + const pluginModuleStartupBootFailureValue = + config?.getOptionalString( + `backend.startup.plugins.${pluginId}.modules.${moduleId}.onPluginModuleBootFailure`, + ) ?? defaultStartupBootFailureValue; + + return pluginModuleStartupBootFailureValue === 'continue'; + } } function toInternalBackendFeature( diff --git a/packages/backend-app-api/src/wiring/createInitializationLogger.ts b/packages/backend-app-api/src/wiring/createInitializationLogger.ts index 0978cfb0c8..600826a435 100644 --- a/packages/backend-app-api/src/wiring/createInitializationLogger.ts +++ b/packages/backend-app-api/src/wiring/createInitializationLogger.ts @@ -29,6 +29,12 @@ export function createInitializationLogger( onPluginStarted(pluginId: string): void; onPluginFailed(pluginId: string, error: Error): void; onPermittedPluginFailure(pluginId: string, error: Error): void; + onPluginModuleFailed(pluginId: string, moduleId: string, error: Error): void; + onPermittedPluginModuleFailure( + pluginId: string, + moduleId: string, + error: Error, + ): void; onAllStarted(): void; } { const logger = rootLogger?.child({ type: 'initialization' }); @@ -87,6 +93,26 @@ export function createInitializationLogger( error, ); }, + onPluginModuleFailed(pluginId: string, moduleId: string, error: Error) { + const status = + starting.size > 0 + ? `, waiting for ${starting.size} other plugins to finish before shutting down the process` + : ''; + logger?.error( + `Module ${moduleId} in Plugin '${pluginId}' threw an error during startup${status}.`, + error, + ); + }, + onPermittedPluginModuleFailure( + pluginId: string, + moduleId: string, + error: Error, + ) { + logger?.error( + `Module ${moduleId} in Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin module so startup will continue.`, + error, + ); + }, onAllStarted() { logger?.info(`Plugin initialization complete${getInitStatus()}`); From 928538513adc371b5136ee5034e4a8b422a34c42 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 26 Apr 2025 15:08:57 +0200 Subject: [PATCH 2/5] cli: add info to alpha context Signed-off-by: Patrik Oldsberg --- .changeset/good-islands-drive.md | 5 +++++ .../cli/cli-report.backstage-cli-alpha.md | 7 +++--- packages/cli/src/modules/config/alpha.ts | 22 +++++++++---------- packages/cli/src/wiring/CliInitializer.ts | 9 +++++++- packages/cli/src/wiring/types.ts | 18 ++++++++++++++- 5 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 .changeset/good-islands-drive.md diff --git a/.changeset/good-islands-drive.md b/.changeset/good-islands-drive.md new file mode 100644 index 0000000000..5b909dc84f --- /dev/null +++ b/.changeset/good-islands-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added `info` object to the context of the alpha CLI. diff --git a/packages/cli/cli-report.backstage-cli-alpha.md b/packages/cli/cli-report.backstage-cli-alpha.md index 917a915fe5..386a642c21 100644 --- a/packages/cli/cli-report.backstage-cli-alpha.md +++ b/packages/cli/cli-report.backstage-cli-alpha.md @@ -56,12 +56,11 @@ Commands: ### `backstage-cli-alpha config docs` ``` -Usage: +Usage: backstage-cli config docs [options] Options: - --help - --package - --version + --package + -h, --help ``` ### `backstage-cli-alpha config schema` diff --git a/packages/cli/src/modules/config/alpha.ts b/packages/cli/src/modules/config/alpha.ts index a0f2624922..a49fddfa24 100644 --- a/packages/cli/src/modules/config/alpha.ts +++ b/packages/cli/src/modules/config/alpha.ts @@ -40,22 +40,21 @@ export default createCliPlugin({ reg.addCommand({ path: ['config', 'docs'], description: 'Browse the configuration reference documentation', - execute: async ({ args }) => { - const argv = await yargs - .options({ - package: { type: 'string' }, - }) - .help() - .parse(args); - const m = - (await require('./commands/docs')) as typeof import('./commands/docs'); - await m.default(argv); + execute: async ({ args, info }) => { + await new Command(info.usage) + .option( + '--package ', + 'Only include the schema that applies to the given package', + ) + .description(info.description) + .action(lazy(() => import('./commands/docs'), 'default')) + .parseAsync(args, { from: 'user' }); }, }); reg.addCommand({ path: ['config:print'], description: 'Print the app configuration for the current package', - execute: async ({ args }) => { + execute: async ({ args, info }) => { const argv = await yargs .options({ package: { type: 'string' }, @@ -65,6 +64,7 @@ export default createCliPlugin({ format: { type: 'string' }, config: { type: 'string', array: true }, }) + .usage(`$0 ${info.usageWithoutProgram}`, info.description) .help() .parse(args); const m = diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index d3cba34f60..8ad3e77cdf 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -62,9 +62,11 @@ export class CliInitializer { async run() { await this.#doInit(); + const programName = 'backstage-cli'; + const program = new Command(); program - .name('backstage-cli') + .name(programName) .version(version) .allowUnknownOption(true) .allowExcessArguments(true); @@ -117,6 +119,11 @@ export class CliInitializer { } await node.command.execute({ args: [...positionalArgs, ...args.unknown], + info: { + usage: [programName, ...node.command.path].join(' '), + usageWithoutProgram: node.command.path.join(' '), + description: node.command.description, + }, }); process.exit(0); } catch (error) { diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index 0decdc6479..43ca84d604 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -19,7 +19,23 @@ export interface BackstageCommand { path: string[]; description: string; deprecated?: boolean; - execute: (options: { args: string[] }) => Promise; + execute: (context: { + args: string[]; + info: { + /** + * The usage string of the current command, for example: "backstage-cli repo test" + */ + usage: string; + /** + * The usage string of the current command, without the program name, for example: "repo test" + */ + usageWithoutProgram: string; + /** + * The description provided for the command + */ + description: string; + }; + }) => Promise; } export interface CliFeature { From bf788ea80da276b9f00b372a157873673a80df56 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 26 Apr 2025 16:05:42 +0200 Subject: [PATCH 3/5] cli: remove usage update from yargs example Signed-off-by: Patrik Oldsberg --- packages/cli/src/modules/config/alpha.ts | 4 ++-- packages/cli/src/wiring/CliInitializer.ts | 1 - packages/cli/src/wiring/types.ts | 4 ---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/modules/config/alpha.ts b/packages/cli/src/modules/config/alpha.ts index a49fddfa24..43bce858a1 100644 --- a/packages/cli/src/modules/config/alpha.ts +++ b/packages/cli/src/modules/config/alpha.ts @@ -62,9 +62,9 @@ export default createCliPlugin({ frontend: { type: 'boolean' }, 'with-secrets': { type: 'boolean' }, format: { type: 'string' }, - config: { type: 'string', array: true }, + config: { type: 'string', array: true, default: [] }, }) - .usage(`$0 ${info.usageWithoutProgram}`, info.description) + .usage('$0', info.description) .help() .parse(args); const m = diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index 8ad3e77cdf..1c6f885842 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -121,7 +121,6 @@ export class CliInitializer { args: [...positionalArgs, ...args.unknown], info: { usage: [programName, ...node.command.path].join(' '), - usageWithoutProgram: node.command.path.join(' '), description: node.command.description, }, }); diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index 43ca84d604..fad29ca241 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -26,10 +26,6 @@ export interface BackstageCommand { * The usage string of the current command, for example: "backstage-cli repo test" */ usage: string; - /** - * The usage string of the current command, without the program name, for example: "repo test" - */ - usageWithoutProgram: string; /** * The description provided for the command */ From 9aaec544b8f7a090d8316dc510205ef9f3240507 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 27 Apr 2025 11:39:40 +0200 Subject: [PATCH 4/5] cli: refactor alpha to use common helper for opaque types Signed-off-by: Patrik Oldsberg --- .changeset/four-peaches-talk.md | 5 +++ packages/cli/src/wiring/CliInitializer.ts | 29 +++------------ .../cli/src/wiring/describeParentCallSite.ts | 20 ++++++++++ packages/cli/src/wiring/factory.ts | 21 +++++------ packages/cli/src/wiring/types.ts | 37 +++++++++---------- 5 files changed, 57 insertions(+), 55 deletions(-) create mode 100644 .changeset/four-peaches-talk.md create mode 100644 packages/cli/src/wiring/describeParentCallSite.ts diff --git a/.changeset/four-peaches-talk.md b/.changeset/four-peaches-talk.md new file mode 100644 index 0000000000..4b0d2e8a47 --- /dev/null +++ b/.changeset/four-peaches-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Internal refactor of opaque type handling. diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index d3cba34f60..b310a07a12 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -15,7 +15,7 @@ */ import { CommandGraph } from './CommandGraph'; -import { CliFeature, InternalCliFeature, InternalCliPlugin } from './types'; +import { CliFeature, OpaqueCliPlugin } from './types'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from '../lib/version'; @@ -42,10 +42,11 @@ export class CliInitializer { } async #register(feature: CliFeature) { - if (isCliPlugin(feature)) { - await feature.init(this.commandRegistry); + if (OpaqueCliPlugin.isType(feature)) { + const internal = OpaqueCliPlugin.toInternal(feature); + await internal.init(this.commandRegistry); } else { - throw new Error(`Unsupported feature type: ${feature.$$type}`); + throw new Error(`Unsupported feature type: ${(feature as any).$$type}`); } } @@ -146,26 +147,6 @@ export class CliInitializer { } } -function toInternalCliFeature(feature: CliFeature): InternalCliFeature { - if (feature.$$type !== '@backstage/CliFeature') { - throw new Error(`Invalid CliFeature, bad type '${feature.$$type}'`); - } - const internal = feature as InternalCliFeature; - if (internal.version !== 'v1') { - throw new Error(`Invalid CliFeature, bad version '${internal.version}'`); - } - return internal; -} - -function isCliPlugin(feature: CliFeature): feature is InternalCliPlugin { - const internal = toInternalCliFeature(feature); - if (internal.featureType === 'plugin') { - return true; - } - // Backwards compatibility for v1 registrations that use duck typing - return 'plugin' in internal; -} - /** @internal */ export function unwrapFeature( feature: CliFeature | { default: CliFeature }, diff --git a/packages/cli/src/wiring/describeParentCallSite.ts b/packages/cli/src/wiring/describeParentCallSite.ts new file mode 100644 index 0000000000..35603e33b0 --- /dev/null +++ b/packages/cli/src/wiring/describeParentCallSite.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ + +// Single re-export to avoid doing this import in multiple places, but still +// avoid duplicate declarations because this one is a bit tricky. +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite'; diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts index eec058e180..6374f37c14 100644 --- a/packages/cli/src/wiring/factory.ts +++ b/packages/cli/src/wiring/factory.ts @@ -14,19 +14,18 @@ * limitations under the License. */ -import { CommandRegistry } from './CommandRegistry'; -import { InternalCliPlugin } from './types'; +import { describeParentCallSite } from './describeParentCallSite'; +import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; export function createCliPlugin(options: { pluginId: string; - init: (registry: CommandRegistry) => Promise; -}): InternalCliPlugin { - return { - id: options.pluginId, + init: (registry: { + addCommand: (command: BackstageCommand) => void; + }) => Promise; +}): CliPlugin { + return OpaqueCliPlugin.createInstance('v1', { + pluginId: options.pluginId, init: options.init, - $$type: '@backstage/CliFeature', - version: 'v1', - featureType: 'plugin', - description: 'A Backstage CLI plugin', - }; + description: `created at '${describeParentCallSite()}'`, + }); } diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index 0decdc6479..86b66f229e 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CommandRegistry } from './CommandRegistry'; +import { OpaqueType } from '@internal/opaque'; export interface BackstageCommand { path: string[]; @@ -22,26 +22,23 @@ export interface BackstageCommand { execute: (options: { args: string[] }) => Promise; } -export interface CliFeature { - $$type: '@backstage/CliFeature'; -} +export type CliFeature = CliPlugin; export interface CliPlugin { - id: string; - init: (registry: CommandRegistry) => Promise; - $$type: '@backstage/CliFeature'; + readonly pluginId: string; + readonly $$type: '@backstage/CliPlugin'; } -/** - * @public - */ -export interface InternalCliPlugin extends CliFeature { - version: 'v1'; - featureType: 'plugin'; - description: string; - id: string; - init: (registry: CommandRegistry) => Promise; -} - -/** @internal */ -export type InternalCliFeature = InternalCliPlugin; +export const OpaqueCliPlugin = OpaqueType.create<{ + public: CliPlugin; + versions: { + readonly version: 'v1'; + readonly description: string; + init: (registry: { + addCommand: (command: BackstageCommand) => void; + }) => Promise; + }; +}>({ + type: '@backstage/CliPlugin', + versions: ['v1'], +}); From 75a58f50d136cf48c55545a8495b5f7cb9ba6ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 29 Apr 2025 08:57:01 +0200 Subject: [PATCH 5/5] adjust the footer links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- microsite/docusaurus.config.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 664f6e1e6e..71331710d5 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -453,7 +453,7 @@ const config: Config = { }, { label: 'Adopting', - to: 'https://backstage.spotify.com', + to: 'https://backstage.io/docs/getting-started/', }, { label: 'Subscribe to our newsletter', @@ -468,18 +468,6 @@ const config: Config = { { title: 'More', items: [ - { - label: 'Open Source @ Spotify', - to: 'https://spotify.github.io/', - }, - { - label: 'Spotify Engineering Blog', - to: 'https://engineering.atspotify.com/', - }, - { - label: 'Spotify for Developers', - to: 'https://developer.spotify.com/', - }, { label: 'GitHub', to: 'https://github.com/backstage/',