From fe47a3e15ea2af602b8bef6a6f5ba3db43071f74 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 28 May 2024 16:29:51 +0200 Subject: [PATCH 001/212] refactor: rename service config to options Signed-off-by: Camila Belo --- .changeset/tall-walls-learn.md | 9 +++ packages/backend-plugin-api/api-report.md | 47 +++++++++++--- packages/backend-plugin-api/src/deprecated.ts | 52 ++++++++++++++++ packages/backend-plugin-api/src/index.ts | 1 + .../src/services/system/index.ts | 6 +- .../src/services/system/types.ts | 62 ++++++++++--------- 6 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 .changeset/tall-walls-learn.md create mode 100644 packages/backend-plugin-api/src/deprecated.ts diff --git a/.changeset/tall-walls-learn.md b/.changeset/tall-walls-learn.md new file mode 100644 index 0000000000..d79c439221 --- /dev/null +++ b/.changeset/tall-walls-learn.md @@ -0,0 +1,9 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +All service config types were renamed to option types in order to standardize frontend and backend `create*` function signatures: + +- The `ServiceRefConfig` type was renamed to`ServiceRefOptions`; +- The `RootServiceFactoryConfig` type was renamed to `RootServiceFactoryOptions`; +- The `PluginServiceFactoryConfig` type was renamed to `PluginServiceFactoryOptions` diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 5e59378760..e157187d26 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -256,7 +256,7 @@ export function createServiceFactory< }, TOpts extends object | undefined = undefined, >( - config: RootServiceFactoryConfig, + options: RootServiceFactoryOptions, ): () => ServiceFactory; // @public @@ -268,7 +268,9 @@ export function createServiceFactory< }, TOpts extends object | undefined = undefined, >( - config: (options?: TOpts) => RootServiceFactoryConfig, + options: ( + options?: TOpts, + ) => RootServiceFactoryOptions, ): (options?: TOpts) => ServiceFactory; // @public @@ -281,7 +283,7 @@ export function createServiceFactory< TContext = undefined, TOpts extends object | undefined = undefined, >( - config: PluginServiceFactoryConfig, + options: PluginServiceFactoryOptions, ): () => ServiceFactory; // @public @@ -294,19 +296,19 @@ export function createServiceFactory< TContext = undefined, TOpts extends object | undefined = undefined, >( - config: ( + options: ( options?: TOpts, - ) => PluginServiceFactoryConfig, + ) => PluginServiceFactoryOptions, ): (options?: TOpts) => ServiceFactory; // @public export function createServiceRef( - config: ServiceRefConfig, + options: ServiceRefOptions, ): ServiceRef; // @public export function createServiceRef( - config: ServiceRefConfig, + options: ServiceRefOptions, ): ServiceRef; // @public @@ -450,8 +452,18 @@ export interface PluginMetadataService { getId(): string; } +// @public @deprecated (undocumented) +export type PluginServiceFactoryConfig< + TService, + TContext, + TImpl extends TService, + TDeps extends { + [name in string]: ServiceRef; + }, +> = PluginServiceFactoryOptions; + // @public (undocumented) -export interface PluginServiceFactoryConfig< +export interface PluginServiceFactoryOptions< TService, TContext, TImpl extends TService, @@ -549,8 +561,17 @@ export interface RootLifecycleService extends LifecycleService {} // @public (undocumented) export interface RootLoggerService extends LoggerService {} +// @public @deprecated (undocumented) +export type RootServiceFactoryConfig< + TService, + TImpl extends TService, + TDeps extends { + [name in string]: ServiceRef; + }, +> = RootServiceFactoryOptions; + // @public (undocumented) -export interface RootServiceFactoryConfig< +export interface RootServiceFactoryOptions< TService, TImpl extends TService, TDeps extends { @@ -674,8 +695,14 @@ export type ServiceRef< $$type: '@backstage/ServiceRef'; }; +// @public @deprecated (undocumented) +export type ServiceRefConfig< + TService, + TScope extends 'root' | 'plugin', +> = ServiceRefOptions; + // @public (undocumented) -export interface ServiceRefConfig { +export interface ServiceRefOptions { // (undocumented) defaultFactory?: ( service: ServiceRef, diff --git a/packages/backend-plugin-api/src/deprecated.ts b/packages/backend-plugin-api/src/deprecated.ts new file mode 100644 index 0000000000..17ccccf320 --- /dev/null +++ b/packages/backend-plugin-api/src/deprecated.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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. + */ + +import { + ServiceRef, + ServiceRefOptions, + RootServiceFactoryOptions, + PluginServiceFactoryOptions, +} from './services'; + +/** + * @public + * @deprecated Use {@link ServiceRefOptions} instead + */ +export type ServiceRefConfig< + TService, + TScope extends 'root' | 'plugin', +> = ServiceRefOptions; + +/** + * @public + * @deprecated Use {@link RootServiceFactoryOptions} instead + */ +export type RootServiceFactoryConfig< + TService, + TImpl extends TService, + TDeps extends { [name in string]: ServiceRef }, +> = RootServiceFactoryOptions; + +/** + * @public + * @deprecated Use {@link PluginServiceFactoryOptions} instead + */ +export type PluginServiceFactoryConfig< + TService, + TContext, + TImpl extends TService, + TDeps extends { [name in string]: ServiceRef }, +> = PluginServiceFactoryOptions; diff --git a/packages/backend-plugin-api/src/index.ts b/packages/backend-plugin-api/src/index.ts index c2e2a13a89..342b33dd2e 100644 --- a/packages/backend-plugin-api/src/index.ts +++ b/packages/backend-plugin-api/src/index.ts @@ -24,3 +24,4 @@ export * from './services'; export type { BackendFeature } from './types'; export * from './paths'; export * from './wiring'; +export * from './deprecated'; diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index 2b91758cbc..9f62968ecd 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -16,10 +16,10 @@ export type { ServiceRef, - ServiceRefConfig, + ServiceRefOptions, ServiceFactory, - PluginServiceFactoryConfig, - RootServiceFactoryConfig, + PluginServiceFactoryOptions, + RootServiceFactoryOptions, ServiceFactoryOrFunction, } from './types'; export { createServiceRef, createServiceFactory } from './types'; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 13252ba6e4..2d6951ffea 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -78,7 +78,7 @@ export interface InternalServiceFactory< export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); /** @public */ -export interface ServiceRefConfig { +export interface ServiceRefOptions { id: string; scope?: TScope; defaultFactory?: ( @@ -92,7 +92,7 @@ export interface ServiceRefConfig { * @public */ export function createServiceRef( - config: ServiceRefConfig, + options: ServiceRefOptions, ): ServiceRef; /** @@ -101,12 +101,13 @@ export function createServiceRef( * @public */ export function createServiceRef( - config: ServiceRefConfig, + options: ServiceRefOptions, ): ServiceRef; + export function createServiceRef( - config: ServiceRefConfig, + options: ServiceRefOptions, ): ServiceRef { - const { id, scope = 'plugin', defaultFactory } = config; + const { id, scope = 'plugin', defaultFactory } = options; return { id, scope, @@ -114,7 +115,7 @@ export function createServiceRef( throw new Error(`tried to read ServiceRef.T of ${this}`); }, toString() { - return `serviceRef{${config.id}}`; + return `serviceRef{${options.id}}`; }, $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, @@ -134,7 +135,7 @@ type ServiceRefsToInstances< }; /** @public */ -export interface RootServiceFactoryConfig< +export interface RootServiceFactoryOptions< TService, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, @@ -156,7 +157,7 @@ export interface RootServiceFactoryConfig< } /** @public */ -export interface PluginServiceFactoryConfig< +export interface PluginServiceFactoryOptions< TService, TContext, TImpl extends TService, @@ -188,7 +189,7 @@ export interface PluginServiceFactoryConfig< * Creates a root scoped service factory without options. * * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, @@ -196,13 +197,13 @@ export function createServiceFactory< TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - config: RootServiceFactoryConfig, + options: RootServiceFactoryOptions, ): () => ServiceFactory; /** * Creates a root scoped service factory with optional options. * * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, @@ -210,13 +211,15 @@ export function createServiceFactory< TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - config: (options?: TOpts) => RootServiceFactoryConfig, + options: ( + options?: TOpts, + ) => RootServiceFactoryOptions, ): (options?: TOpts) => ServiceFactory; /** * Creates a plugin scoped service factory without options. * * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, @@ -225,13 +228,13 @@ export function createServiceFactory< TContext = undefined, TOpts extends object | undefined = undefined, >( - config: PluginServiceFactoryConfig, + options: PluginServiceFactoryOptions, ): () => ServiceFactory; /** * Creates a plugin scoped service factory with optional options. * * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, @@ -240,9 +243,9 @@ export function createServiceFactory< TContext = undefined, TOpts extends object | undefined = undefined, >( - config: ( + options: ( options?: TOpts, - ) => PluginServiceFactoryConfig, + ) => PluginServiceFactoryOptions, ): (options?: TOpts) => ServiceFactory; export function createServiceFactory< TService, @@ -251,23 +254,24 @@ export function createServiceFactory< TContext, TOpts extends object | undefined = undefined, >( - config: - | RootServiceFactoryConfig - | PluginServiceFactoryConfig - | ((options: TOpts) => RootServiceFactoryConfig) + options: + | RootServiceFactoryOptions + | PluginServiceFactoryOptions + | ((options: TOpts) => RootServiceFactoryOptions) | (( options: TOpts, - ) => PluginServiceFactoryConfig) - | (() => RootServiceFactoryConfig) - | (() => PluginServiceFactoryConfig), + ) => PluginServiceFactoryOptions) + | (() => RootServiceFactoryOptions) + | (() => PluginServiceFactoryOptions), ): (options: TOpts) => ServiceFactory { - const configCallback = typeof config === 'function' ? config : () => config; + const configCallback = + typeof options === 'function' ? options : () => options; const factory = ( - options: TOpts, + o: TOpts, ): InternalServiceFactory => { - const anyConf = configCallback(options); + const anyConf = configCallback(o); if (anyConf.service.scope === 'root') { - const c = anyConf as RootServiceFactoryConfig; + const c = anyConf as RootServiceFactoryOptions; return { $$type: '@backstage/BackendFeature', version: 'v1', @@ -277,7 +281,7 @@ export function createServiceFactory< factory: async (deps: TDeps) => c.factory(deps), }; } - const c = anyConf as PluginServiceFactoryConfig< + const c = anyConf as PluginServiceFactoryOptions< TService, TContext, TImpl, From 90a7340efd87f1174018093dc671f07bff02e733 Mon Sep 17 00:00:00 2001 From: Ooi Yee Fei Date: Sat, 1 Jun 2024 15:09:02 +0800 Subject: [PATCH 002/212] fix: Prefer accountId for AwsOrganizationCloudAccountProcessor authentication Signed-off-by: Ooi Yee Fei --- .changeset/swift-kings-sparkle.md | 5 +++ .../catalog-backend-module-aws/config.d.ts | 6 +++ .../src/awsOrganization/config.test.ts | 2 + .../src/awsOrganization/config.ts | 7 +++ .../AwsOrganizationCloudAccountProcessor.ts | 44 ++++++++++++------- 5 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 .changeset/swift-kings-sparkle.md diff --git a/.changeset/swift-kings-sparkle.md b/.changeset/swift-kings-sparkle.md new file mode 100644 index 0000000000..18a37b82f1 --- /dev/null +++ b/.changeset/swift-kings-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +AwsOrganizationCloudAccountProcessor configuration field roleArn is deprecated in favor of new field accountId diff --git a/plugins/catalog-backend-module-aws/config.d.ts b/plugins/catalog-backend-module-aws/config.d.ts index 4c9c98512e..0a9d9c97db 100644 --- a/plugins/catalog-backend-module-aws/config.d.ts +++ b/plugins/catalog-backend-module-aws/config.d.ts @@ -26,8 +26,14 @@ export interface Config { provider: { /** * The role to be assumed by this processor + * @deprecated Use `accountId` instead. */ roleArn?: string; + + /** + * The AWS account ID to query for organizational data + */ + accountId?: string; }; }; }; diff --git a/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts b/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts index 7e0dc58559..a84c65abda 100644 --- a/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts +++ b/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts @@ -22,11 +22,13 @@ describe('readAwsOrganizationConfig', () => { const config = { provider: { roleArn: 'aws::arn::foo', + accountId: '111111111111', }, }; const actual = readAwsOrganizationConfig(new ConfigReader(config)); const expected = { roleArn: 'aws::arn::foo', + accountId: '111111111111', }; expect(actual).toEqual(expected); }); diff --git a/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts b/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts index e6096b932b..ff00af48e4 100644 --- a/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts +++ b/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts @@ -24,6 +24,11 @@ export type AwsOrganizationProviderConfig = { * The role to assume for the processor. */ roleArn?: string; + + /** + * The AWS accountId + */ + accountId?: string; }; export function readAwsOrganizationConfig( @@ -32,7 +37,9 @@ export function readAwsOrganizationConfig( const providerConfig = config.getOptionalConfig('provider'); const roleArn = providerConfig?.getOptionalString('roleArn'); + const accountId = providerConfig?.getOptionalString('accountId'); return { roleArn, + accountId, }; } diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index 4a454fce6f..b843503a28 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -59,10 +59,18 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { static async fromConfig(config: Config, options: { logger: LoggerService }) { const c = config.getOptionalConfig('catalog.processors.awsOrganization'); const orgConfig = c ? readAwsOrganizationConfig(c) : undefined; + + if (orgConfig?.roleArn) { + options.logger.warn( + 'The roleArn configuration for AwsOrganizationCloudAccountProcessor ignores the role name, use accountId configuration instead', + ); + } + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config); const credProvider = await awsCredentialsManager.getCredentialProvider({ arn: orgConfig?.roleArn, + accountId: orgConfig?.accountId, }); return new AwsOrganizationCloudAccountProcessor( credProvider, @@ -98,23 +106,27 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { this.logger?.info('Discovering AWS Organization Account objects'); - (await this.getAwsAccounts()) - .map(account => this.mapAccountToComponent(account)) - .filter(entity => { - if (location.target !== '') { - if (entity.metadata.annotations) { - return ( - entity.metadata.annotations[ORGANIZATION_ANNOTATION] === - location.target - ); + try { + (await this.getAwsAccounts()) + .map(account => this.mapAccountToComponent(account)) + .filter(entity => { + if (location.target !== '') { + if (entity.metadata.annotations) { + return ( + entity.metadata.annotations[ORGANIZATION_ANNOTATION] === + location.target + ); + } + return false; } - return false; - } - return true; - }) - .forEach(entity => { - emit(processingResult.entity(location, entity)); - }); + return true; + }) + .forEach(entity => { + emit(processingResult.entity(location, entity)); + }); + } catch (e) { + this.logger?.error(e); + } return true; } From f7bdceae07bf08f9ac80e8285063bb10db0e81bd Mon Sep 17 00:00:00 2001 From: Adam Wallis Date: Wed, 29 May 2024 16:59:04 -0400 Subject: [PATCH 003/212] Adds ProviderTransformer to msgraph catalog plugin for dynamic config Signed-off-by: Adam Wallis --- .changeset/clever-geese-jump.md | 5 +++ docs/integrations/azure/org.md | 34 ++++++++++++++++++- .../api-report-alpha.md | 6 ++++ .../api-report.md | 10 ++++++ .../src/microsoftGraph/index.ts | 1 + .../src/microsoftGraph/types.ts | 14 +++++++- ...ogModuleMicrosoftGraphOrgEntityProvider.ts | 24 +++++++++++++ .../MicrosoftGraphOrgEntityProvider.ts | 23 ++++++++++++- 8 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 .changeset/clever-geese-jump.md diff --git a/.changeset/clever-geese-jump.md b/.changeset/clever-geese-jump.md new file mode 100644 index 0000000000..1c4694fa33 --- /dev/null +++ b/.changeset/clever-geese-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Adds a dynamic provider for the plugin-catalog-backend-module-msgraph. Configuration is now runtime configurable through the ProviderConfigTransformer. diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 89abd4813b..6a72527b94 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -169,6 +169,23 @@ microsoftGraphOrg: select: ['id', 'displayName', 'description'] ``` +### Using Provider Config Transformer + +Dynamic configuration scaling allows the `msgraph` catalog plugin to adjust its settings at runtime without requiring a redeploy. This feature is useful for scenarios where configuration needs to be updated based on real-time events or changing conditions. For example, you can dynamically adjust synchronization schedules, filters, and search parameters to optimize performance and responsiveness. + +:::note +Adjusting fields that are not used on each scheduled ingestion (e.g., `id`, `schedule`) will have no effect. +::: + +:::warning +Dynamically changing configuration on the fly can introduce unintended consequences, such as system instability and configuration errors. Please review your transformer carefully to ensure that it is working as anticipated! +::: + +#### Example Use Cases: + +- **Filter Scaling**: Adjust filters like `userGroupMember` and `groupFilter` dynamically. +- **Search Parameter Adjustment**: Change search parameters such as `groupSearch` and `userSelect` on-the-fly. + ### Using Custom Transformers Transformers can be configured by extending `microsoftGraphOrgEntityProviderTransformExtensionPoint`. Here is an example: @@ -180,6 +197,7 @@ import { myUserTransformer, myGroupTransformer, myOrganizationTransformer, + myProviderConfigTransformer, } from './transformers'; backend.add( @@ -201,6 +219,9 @@ backend.add( microsoftGraphTransformers.setOrganizationTransformer( myOrganizationTransformer, ); + microsoftGraphTransformers.setProviderConfigTransformer( + myProviderConfigTransformer, + ); /* highlight-add-end */ }, }); @@ -209,7 +230,7 @@ backend.add( ); ``` -The `myUserTransformer`, `myGroupTransformer`, and `myOrganizationTransformer` transformer functions are from the examples in the section below. +The `myUserTransformer`, `myGroupTransformer`, `myOrganizationTransformer`, and `myProviderConfigTransformer` transformer functions are from the examples in the section below. ### Transformer Examples @@ -221,6 +242,7 @@ import { defaultGroupTransformer, defaultUserTransformer, defaultOrganizationTransformer, + MicrosoftGraphProviderConfig, } from '@backstage/plugin-catalog-backend-module-msgraph'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; @@ -263,6 +285,16 @@ export async function myOrganizationTransformer( ): Promise { return undefined; } + +// Example config transformer that expands the group filter to also include 'azure-group-a' +export async function myProviderConfigTransformer( + provider: MicrosoftGraphProviderConfig, +): Promise { + if (!provider.groupFilter?.includes('azure-group-a')) { + provider.groupFilter = `${provider.groupFilter} or displayName eq 'azure-group-a'`; + } + return provider; +} ``` ## Troubleshooting diff --git a/plugins/catalog-backend-module-msgraph/api-report-alpha.md b/plugins/catalog-backend-module-msgraph/api-report-alpha.md index b9c96e7798..7b0ced8625 100644 --- a/plugins/catalog-backend-module-msgraph/api-report-alpha.md +++ b/plugins/catalog-backend-module-msgraph/api-report-alpha.md @@ -7,6 +7,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { GroupTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; import { OrganizationTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; +import { ProviderConfigTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; import { UserTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; // @alpha @@ -26,6 +27,11 @@ export interface MicrosoftGraphOrgEntityProviderTransformsExtensionPoint { | OrganizationTransformer | Record, ): void; + setProviderConfigTransformer( + transformer: + | ProviderConfigTransformer + | Record, + ): void; setUserTransformer( transformer: UserTransformer | Record, ): void; diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 86170e2910..9640f3708b 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -126,6 +126,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; + providerConfigTransformer?: ProviderConfigTransformer; }); // (undocumented) connect(connection: EntityProviderConnection): Promise; @@ -145,6 +146,7 @@ export interface MicrosoftGraphOrgEntityProviderLegacyOptions { id: string; logger: LoggerService; organizationTransformer?: OrganizationTransformer; + providerConfigTransformer?: ProviderConfigTransformer; schedule: 'manual' | TaskRunner; target: string; userTransformer?: UserTransformer; @@ -162,6 +164,9 @@ export type MicrosoftGraphOrgEntityProviderOptions = organizationTransformer?: | OrganizationTransformer | Record; + providerConfigTransformer?: + | ProviderConfigTransformer + | Record; }; // @public @deprecated @@ -233,6 +238,11 @@ export type OrganizationTransformer = ( organization: MicrosoftGraph.Organization, ) => Promise; +// @public +export type ProviderConfigTransformer = ( + provider: MicrosoftGraphProviderConfig, +) => Promise; + // @public @deprecated export function readMicrosoftGraphConfig( config: Config, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts index 4db3fb69c6..81f6f21ce2 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts @@ -39,4 +39,5 @@ export type { GroupTransformer, OrganizationTransformer, UserTransformer, + ProviderConfigTransformer, } from './types'; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts index ef60d7b018..bc9270238b 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts @@ -16,7 +16,7 @@ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; - +import { MicrosoftGraphProviderConfig } from './config'; /** * Customize the ingested User entity * @@ -45,3 +45,15 @@ export type GroupTransformer = ( group: MicrosoftGraph.Group, groupPhoto?: string, ) => Promise; + +/** + * Customize the MSGraph Provider Config Dynamically + * + * Transforming fields that are not used for each scheduled ingestion (e.g., id, schedule) + * will have no effect. + * + * @public + */ +export type ProviderConfigTransformer = ( + provider: MicrosoftGraphProviderConfig, +) => Promise; diff --git a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts index 193f07889f..e1a15a3468 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts @@ -23,6 +23,7 @@ import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/ import { GroupTransformer, OrganizationTransformer, + ProviderConfigTransformer, UserTransformer, } from '@backstage/plugin-catalog-backend-module-msgraph'; import { MicrosoftGraphOrgEntityProvider } from '../processors'; @@ -58,6 +59,18 @@ export interface MicrosoftGraphOrgEntityProviderTransformsExtensionPoint { | OrganizationTransformer | Record, ): void; + + /** + * Set the function that transforms provider config dynamically. + * Optionally, you can pass separate transformers per provider ID. + * Note: adjusting fields that are not used on each scheduled ingestion + * (e.g., id, schedule) will have no effect. + */ + setProviderConfigTransformer( + transformer: + | ProviderConfigTransformer + | Record, + ): void; } /** @@ -94,6 +107,10 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( | OrganizationTransformer | Record | undefined; + let providerConfigTransformer: + | ProviderConfigTransformer + | Record + | undefined; env.registerExtensionPoint( microsoftGraphOrgEntityProviderTransformExtensionPoint, @@ -116,6 +133,12 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( } organizationTransformer = transformer; }, + setProviderConfigTransformer(transformer) { + if (providerConfigTransformer) { + throw new Error('Provider transformer may only be set once'); + } + providerConfigTransformer = transformer; + }, }, ); @@ -134,6 +157,7 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( userTransformer: userTransformer, groupTransformer: groupTransformer, organizationTransformer: organizationTransformer, + providerConfigTransformer: providerConfigTransformer, }), ); }, diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 3c8b903e69..21995d2b8d 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -34,6 +34,7 @@ import { MICROSOFT_GRAPH_USER_ID_ANNOTATION, MicrosoftGraphClient, MicrosoftGraphProviderConfig, + ProviderConfigTransformer, OrganizationTransformer, readMicrosoftGraphConfig, readMicrosoftGraphOrg, @@ -94,6 +95,13 @@ export type MicrosoftGraphOrgEntityProviderOptions = organizationTransformer?: | OrganizationTransformer | Record; + + /** + * The function that transforms provider config dynamically. + */ + providerConfigTransformer?: + | ProviderConfigTransformer + | Record; }; /** @@ -152,6 +160,11 @@ export interface MicrosoftGraphOrgEntityProviderLegacyOptions { * The function that transforms an organization entry in msgraph to an entity. */ organizationTransformer?: OrganizationTransformer; + + /** + * The function that transforms provider config dynamically. + */ + providerConfigTransformer?: ProviderConfigTransformer; } /** @@ -216,6 +229,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { providerConfig.id, options.organizationTransformer, ), + providerConfigTransformer: getTransformer( + providerConfig.id, + options.providerConfigTransformer, + ), }); if (taskRunner !== 'manual') { @@ -257,6 +274,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { userTransformer: options.userTransformer, groupTransformer: options.groupTransformer, organizationTransformer: options.organizationTransformer, + providerConfigTransformer: options.providerConfigTransformer, logger, provider, }); @@ -276,6 +294,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; + providerConfigTransformer?: ProviderConfigTransformer; }, ) {} @@ -300,7 +319,9 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { } const logger = options?.logger ?? this.options.logger; - const provider = this.options.provider; + const provider = this.options.providerConfigTransformer + ? await this.options.providerConfigTransformer(this.options.provider) + : this.options.provider; const { markReadComplete } = trackProgress(logger); const client = MicrosoftGraphClient.create(this.options.provider); const { users, groups } = await readMicrosoftGraphOrg( From 771a4c2cf4f77e5f63f8cf82177e35c4b8062c36 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Mon, 27 May 2024 13:50:45 +0200 Subject: [PATCH 004/212] beps/0002: add dynamic FE plugins cli,backend,loading proposals Signed-off-by: Martin Marosi --- beps/0002-dynamic-frontend-plugins/README.md | 754 +++++++++++++----- .../dependency-sharing-schema.json | 43 + .../share-config-sample.json | 17 + 3 files changed, 633 insertions(+), 181 deletions(-) create mode 100644 beps/0002-dynamic-frontend-plugins/dependency-sharing-schema.json create mode 100644 beps/0002-dynamic-frontend-plugins/share-config-sample.json diff --git a/beps/0002-dynamic-frontend-plugins/README.md b/beps/0002-dynamic-frontend-plugins/README.md index f26a3b488d..34c6e2d435 100644 --- a/beps/0002-dynamic-frontend-plugins/README.md +++ b/beps/0002-dynamic-frontend-plugins/README.md @@ -156,12 +156,247 @@ Each plugin is required to provide manifest file (metadata) in predefined format Plugin discovery is a pre-requisite for Plugin registry. This should be responsible for scanning for available plugins and generating/modifying the plugin registry to always keep it up to date. +### CLI + +The following sections contains proposal of new CLI scripts to support the dynamic frontend plugins. + +#### Static and Dynamic Plugin Build Differences + +The major difference between static and dynamic plugins lies in their dependencies and how they are "installed" into a Backstage instance and whether they are bundled into final JS output during the build. + +While static plugins can define their dependencies using the dependencies field inside `package.json`, dynamic (module federation-based) plugins can't. + +A dynamic plugin requires all dependencies to be present in the build output. Module sharing is then used to optimize the "output size" at runtime. If a shared package is not found in the browser scope, or if the version range does not match the requirement, it falls back and loads additional JS assets from the "private" build and adds them to the scope. + +Webpack is also not the best tool for building traditional NPM packages. Dependencies can be externalized, but Webpack brings additional code into the bundles. Using Webpack external dependencies also conflicts with module federation shared modules. Externals will not be included even as a shared module fallback. As a result, such shared module can't be used because the dependency will not be defined in the init container. + +If the vision is to still support "static" plugins installed via NPM, it makes sense to keep the static and dynamic plugin builds separate and build two different bundles. Both can still be published as a single NPM package (not recommended), but to achieve optimal performance for both, there cannot be a shared output. + +#### Dynamic export CLI + +```sh +backstage-cli plugin export-dynamic +``` + +Similar to the current build command, instead of producing static plugin output, it builds a dynamic plugin using Webpack and module federation plugins. + +**Internal arguments** + +Some configuration is required, but they can be internal using the script `cwd`. + +- _path_ + - path to the plugin `package.json` +- _export-name_ + - Export name from where the plugin root is accessible + - defaults to `pluginEntry` + +**Shared modules** + +> NOTE: Details of module sharing are described in the [Module sharing](#module-sharing) section. + +Short summary of dependency sharing: + +- Predefined set of singleton packages +- Generated granular config of modules for MUI dependencies +- Share all other dependencies listed in package.json + +To allow additional customization of module sharing, extra configuration will be required. The extra module haring configuration can be embedded inside `package.json`. + +- [Schema proposal draft](./dependency-sharing-schema.json) +- [Config draft](./share-config-sample.json) + +#### Generate module sharing map + +> NOTE: RedHat has similar internal generators that are used in combination with Scalprum. These can be built upon, moved to Scalprum, and exposed. If there is interest, the Backstage maintainers do not have to be responsible for maintaining the source code. + +A simple program to traverse the AST and find exports from the `main` or `module` script of a dependency. Any exposed modules from the dependency entry will be traced to their source files, and a module map can be generated. + +> NOTE: It is important that import paths in source code are also directly targeting the "traced" modules or are transformed at build time. + +**The script should be triggered by the plugin build cli** + +Generates a module map (exported module source file location) for module sharing. + +**Arguments** + +- _path_ + - path to the module root (directory with package.json) + +[module map generator using TS example](#simplistic-minimal-example-of-module-map-generator) + +### Dynamic assets server + +A simple static asset server is sufficient to serve the dynamic assets. The asset server can collect the necessary metadata at boot or runtime. The asset server can be added to the `app-backend` + +It's important to consider old deployments as part of this storage, to make sure that dynamic chunks are still available to users who have not yet refreshed the page to load the latest version of the app. + +#### Plugin separation + +- Each plugin should be separated into a directory. +- Each plugin can be viewed as a `private` workspace package in a npm workspaces environment. + +```sh +path/to/assets/plugin/dynamic-plugins/ +└── dynamic-backstage.plugin-user-settings + ├── dist + ├── package.json + └── plugin-manifest.json +``` + +#### Boot time initialization + +**Dynamically build plugins** + +Only dynamic plugins can be hosted in the backend. The plugins has to provide the [manifest](#plugin-manifest). It the manifest is not found, plugin will not be considered dynamic and will be enabled for loading. + +**File system assets management** + +Once plugins are installed and fully dynamic, the asset server has to "discover" what plugins are loadable. + +This can be done by scanning the "assets" (dynamic-plugins in the dir example) for individual plugin manifests. Simple `glob` lookup for `path/dynamic-plugins/**/plugins-manifest.json` pattern should give required results. + +With this data the assets server should be able to generate a "plugin registry" with necessary metadata for the frontend. See the [Plugin Registry](#plugin-registry-1) for format details. + +The during the collection, the program can also add the `assetHost` prefix to the manifest location attribute described in the [webpack configuration](#sample-webpack-configuration) section. + +**Alternative means of assets management** + +To manage the assets by other means than just using filesystem, the file system should be treated only as an entry point for the plugins. Leveraging other means such as DB/cache/object storage should be also an option. The storage method should be mandated as long as a plugin registry and assets are available. + +**Singleton dependencies verification (optional)** + +With [module sharing](#module-sharing) and its singleton option, it is critical that the singleton dependencies versions are matching. + +The singleton configuration from the shell application can be extracted as well as from all dynamic plugins (included in the generated [manifest](#plugin-manifest)). A semver check can be used to prevent a plugin from initializing if a version mismatch is found. A version check also has to be made between plugins. New plugin singleton dependencies must match both the shell and all previously loaded plugins. + +#### Runtime updates + +It is fully expected that plugins can be added or removed at the asset server runtime. The same tasks can be performed as during boot time initialization. It is critical that the plugin registry is not cached and updated immediately if assets change. + +The server **should not remove older JS/CSS assets** immediately after updates. It is highly likely user can have older version still loaded in their browsers. A retention period is required for the assets. Using proper content hashes for builds will help with keep the duplicate assets to a minimum. + +#### Asset server API + +> NOTE Routes names are purely arbitrary at this point. + +The API can be very simplistic + +_GET /dynamic-plugins/plugin-registry_ + +Returns generated [Plugin Registry](#plugin-registry-1). + +\*GET /dynamic-plugins/assets/\*\* + +Static assets endpoint. Can be handled via the `express.static` middleware. + +### Plugin loading + +> NOTE: Only viable for the new frontend plugin system. + +The feature loader is a perfect place to initialize a new dynamic plugin as it already accepts async loaders. + +An example of [integration with scalprum](https://github.com/backstage/backstage/commit/23085aa8dfbc73d4648c100cf06b6b67e8e31764). + +#### Integration to core + +**Dynamic Feature configuration** + +The dynamic remote loading can be added directly into the [`createApp`](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/wiring/createApp.tsx#L234) function. + +The current `feature` type can be expanded with a `DynamicFrontendFeature` type: + +```ts +// current +type Features: (FrontendFeature | CreateAppFeatureLoader)[]; + +// with dynamic plugins +type DynamicFrontendFeature = { + name: string; + manifestLocation: string // URI with host enhanced with the "DEFAULT_API_PATH" or "assetHost" prefix +} + +type Features: (FrontendFeature | CreateAppFeatureLoader | DynamicFrontendFeature)[]; +``` + +**Dynamic Feature boot loading** + +Scalprum can be initialized with the metadata originating from the dynamic backend plugin. In order to properly load a plugin with `publicPath` set to `auto`, a small transformation of the manifest has to happen before it is loaded. A sample Scalprum config with such transformation: + +```TS +import { initialize } from '@scalprum/core'; +import { ScalprumProviderProps } from '@scalprum/react-core'; + +// get the pluginRegistry from the backend plugin +const pluginRegistry = await loadRemoteConfig() + +const pluginSDKOptions: ScalprumProviderProps['pluginSDKOptions'] = { + pluginLoaderOptions: { + transformPluginManifest: manifest => { + return { + ...manifest, + loadScripts: manifest.loadScripts.map(script => { + // Any additional transformations can happen here in case the manifest format needs to be adjusted further. + // the init scripts URL is enhanced with the asset location to add the host: + return `${manifest.customProperties.assetHost ?? DEFAULT_API_PATH}${script}`; + }), + }; + }, + }, +}; + +// Initialize Scalprum instance +const scalprum = initialize({ + appsConfig: pluginRegistry, + pluginLoaderOptions: pluginSDKOptions.pluginLoaderOptions, +}); +``` + +Because the [`appLoader`](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/wiring/createApp.tsx#L193) is already async, it is a perfect place to load the plugin registry and init the dynamic plugins. + +Initializing the dynamic feature is just a case of mapping the `DynamicFrontendFeature` to `FrontendFeature` via Scalprum: + +```ts +import { processManifest, getModule } from '@scalprum/core'; +// a ID of the module withing module federation container, can be customized, depends on the build +const DEFAULT_MODULE_NAME = 'pluginEntry'; + +async function loadScalprumFeature({ manifestLocation, name }) { + // initialize dynamic remote within scalprum cache + await processManifest(manifestLocation, name, DEFAULT_MODULE_NAME); + // gets the actual JS module from scalprum + const feature = await getModule(name, DEFAULT_MODULE_NAME); + + return feature; +} +const providedFeatures: FrontendFeature[] = []; +for (const entry of options?.features ?? []) { + if ('manifestLocation' in entry && 'name' in entry) { + // some error handling required + const dynamicFeature = loadScalprumFeature(entry); + providedFeatures.push(dynamicFeature); + } else if ('load' in entry) { + // plugin with load function + } else { + // #static plugin + } +} + +const app = createSpecializedApp({ + icons: options?.icons, + config, + features: [...discoveredFeatures, ...providedFeatures], + bindRoutes: options?.bindRoutes, +}).createRoot(); +``` + +The federated modules are now available as if they were static plugins. + ## Design Details -> NOTE The details are based on the Janus implementation of dynamic frontend plugins. The implementation leverages [Scalprum](https://github.com/scalprum/scaffolding) which is a Webpack based dynamic plugin manager for React applications. - ### Module federation implementation experiments +> NOTE Experiments were tested and documented. Decision was mated to leverage [Scalprum](https://github.com/scalprum/scaffolding) and [dynamic-plugins-sdk](https://github.com/openshift/dynamic-plugin-sdk). Instead of native webpack module federation the [@module-federation/enhanced](https://www.npmjs.com/package/@module-federation/enhanced) plugin will be used to ensure future compatibility with other bundlers. + Test should consist of trying to run permutations of webpack/Rspack/vite based shell apps/plugins and discover if we can freely choose any tool, or if we should restrict the tooling to just a subset of the available options. The outcome of initial testing is positive and it is possible to mix and match different build tools and consume different remote modules in a single shell application. @@ -217,70 +452,7 @@ This config ensures that only those modules (from @mui/material) that are used i This can be checked by debugging network traffic and shared scopes: -![notifications system architecture diagram](./scope-sharing.png) - -#### Limitations - -A limitation of module sharing is that each module name can only appear once in each context. This means that if you're trying to build a bundle of shared modules with a deeper dependency tree, you may end up needing to resolve conflicts by renaming some modules that appear multiple times with different versions. This will in turn limit sharing of those modules with other bundles. - -There is also a conflict with the chunk splitting currently used in the Backstage CLI that means that some singleton modules need to be configured to be eagerly loaded. More broadly we may also want to re-evaluate the chunk splitting strategy in the Backstage CLI when adding support for module federation. - -### Plugin manifest - -Each plugin should have a manifest file with important metadata. This metadata is used to load the remote assets to browser. The plugin manifest should be part of a build output. - -A manifest should have: - -- name of plugin -- how can be the init container accessed -- what is the base URL (assets pathname) -- name of the entry script(s) - -#### Scalprum compatible manifest - -```TS -type PluginManifest = { - name: string; - version: string; - dependencies?: Record; - customProperties?: AnyObject; - baseURL: string; - extensions: Extension[]; - loadScripts: string[]; - registrationMethod: 'callback' | 'custom'; - buildHash?: string; -} -``` - -```js -{ - "name": "backstage.plugin-github-actions", - "version": "0.6.6", - "extensions": [], - "registrationMethod": "callback", // where container init is available in browser - "baseURL": "auto", - "loadScripts": [ - "backstage.plugin-github-actions.804b91040fcbca6585ce.js" - ], - "buildHash": "804b91040fcbca6585ce1bcd4b1f8aa2" -} -``` - -##### `registrationMethod` - -Refers to [webpack output.libraryTarget](https://webpack.js.org/configuration/output/#outputlibrarytarget). - -Callback refers to `jsonp` and a the `custom` is used if other available target configuration has been picked. - -It is recommended to use either `global` or `jsonp` as these are environment agnostic (browser VS node). `jsonp` requires additional configurations. The `global` is preferable due to its simplicity. - -##### `baseURL` - -The `baseURL` is derived from [webpack public path](https://webpack.js.org/configuration/output/#outputpublicpath) - -The public path can also be set to `auto` to remove the need to specify origin or pathname and resolve the pathname at runtime. - -In Scalprum, some manifest [post processing](https://github.com/janus-idp/backstage-showcase/blob/main/packages/app/src/components/DynamicRoot/DynamicRoot.tsx#L323) is required to load the initial scripts if the `auto` baseURL is chosen. +![scope sharing sample](./scope-sharing.png) ### Plugin registry @@ -290,6 +462,7 @@ Plugin registry can be fairly simplistic. It can be as simple as JSON file conta type RegistryEntry = { name: string // plugin name manifestLocation: string // path to the manifest resource + assetHost: string // host part of URL to deal with the `auto` publicPath } // object for easy access @@ -308,7 +481,8 @@ Example of such registry { "backstage.plugin-github-actions": { "name": "backstage.plugin-github-actions", - "manifestLocation": "https://foo-bar.com/api/plugin-storage/plugin-manifest.json" + "manifestLocation": "/api/plugin-storage/plugin-manifest.json", + "assetHost": "https://foo-bar.com" }, // ..rest of plugins } @@ -318,6 +492,7 @@ Example of such registry { "name": "backstage.plugin-github-actions", "manifestLocation": "https://foo-bar.com/api/plugin-storage/plugin-manifest.json" + "assetHost": "https://foo-bar.com" }, // ...rest of plugins ] @@ -353,7 +528,6 @@ Example of registry with embedded manifests "pluginManifest": { "name": "backstage.plugin-github-actions", "version": "0.6.6", - "extensions": [], "registrationMethod": "callback", "baseURL": "auto", "loadScripts": [ @@ -372,7 +546,6 @@ Example of registry with embedded manifests "pluginManifest": { "name": "backstage.plugin-github-actions", "version": "0.6.6", - "extensions": [], "registrationMethod": "callback", "baseURL": "auto", "loadScripts": [ @@ -386,155 +559,374 @@ Example of registry with embedded manifests ``` -### Webpack build configuration +### Frontend plugin build configuration -> NOTE This is a sample current configuration in the Janus project. It uses Scalprum webpack based build plugin to generate the output. It does not take the [all options](#Module-federation-implementation-experiments) in consideration. This section will likely change considerably. +#### Build tooling -Part of a Scalprum tooling is also [dynamic-plugins-sdk](https://github.com/openshift/dynamic-plugin-sdk). Right now the package is a part of a different project, but that is about to change. More details in the [Scalprum roadmap](https://github.com/scalprum/scaffolding/blob/main/ROADMAP.md). +- webpack module bundler as a base +- use `@openshift/dynamic-plugin-sdk-webpack` to build dynamic frontend output + - with `@module-federation/enhanced` plugins instead of native webpack -#### Sample plugin configuration using Scalprum SDK +#### Module sharing + +Webpack allows for dependency/module sharing across different remote modules. For performance's sake, all dependencies should be shared. The sharing is version-restricted, and there can be multiple versions of the same package at the same time. This ensures that remote modules will have the necessary dependency version available, either from the shared scope or as a fallback from the respective build. + +**Singleton sharing** + +Singleton sharing is necessary for context sharing between remote modules and between remote modules and shell application. + +Only a minimal set of dependencies should be shared as singletons as it gives access to all packages to the parent context. Also **version checking is disabled for singleton packages!** + +Singleton "must have" list: + +- @scalprum/core +- @scalprum/react-core +- @openshift/dynamic-plugin-sdk +- react +- react-dom +- react-router-dom + +Singleton "might have to" list: + +- @material-ui/core/styles +- @material-ui/styles + +**@mui/_ and @material-ui/_ like packages** + +> NOTE: Can leverage [Generate module sharing map](#generate-module-sharing-map). + +Component libraries are usually large (thousands of svg icons in @mui/icons) and it is inefficient to share them as a whole. **Tree shaking is disabled for shared modules**. That means sharing large packages in multiple versions will result in a bloated JS in browsers. + +Sharing components like these can be done on module level. Instead of sharing the entire package, share its individual components: + +```js +const shared = { + // will result in bloated bundles and degraded performance + '@mui/material': { + requiredVersion: 'x.x.x', // semver + }, + // sharing per module level, only button module is included in the output + '@mui/material/Button': { + version: 'x.x.x', // might have to specifically set the version + requiredVersion: 'x.x.x', // semver + }, +}; +``` + +Sharing dependencies on module level leads to optimal bundle size. Even if all modules from a dependency are listed, Webpack will only bundle modules that are discovered in the source files. That means the list can be generated at build time with no risk of bloating the output with unused modules. + +The module sharing "matching" is based on import paths. If relative import paths are used `import {foo} from 'bar'` it will not be matched to the absolute import path of a shared module `import foo from 'bar/foo'`. Therefore absolute import paths have to enforced or path transformation has to happen at build time. For JS builds, babel can be used, for ts builds a `ts-patch` utility with custom plugins is an option. + +#### Limitations + +A limitation of module sharing is that each module name can only appear once in each context. This means that if you're trying to build a bundle of shared modules with a deeper dependency tree, you may end up needing to resolve conflicts by renaming some modules that appear multiple times with different versions. This will in turn limit sharing of those modules with other bundles. + +There is also a conflict with the chunk splitting currently used in the Backstage CLI that means that some singleton modules need to be configured to be eagerly loaded. More broadly we may also want to re-evaluate the chunk splitting strategy in the Backstage CLI when adding support for module federation. + +#### Webpack chunk optimization + +Custom webpack chunk splitting configuration can be problematic, especially when modifying runtime and vendor chunks. Module federation creates its own chunks. Shared modules that are not set up to be eagerly loaded (using the `eager` configuration) **cannot be included inside the entry script**. With a custom chunk splitting setup, they can potentially be forced into the entry script, causing runtime errors. On the other hand, some critical runtime code that has to be in the entry script, cna be forced out of it. This is particularly problematic for the "shell" application. + +Chunk optimization should be disabled for the initial implementation. + +#### Webpack Public path option + +The `publicPath` output config in webpack is a mandatory attribute for federated modules. However, at build time, it is impossible to guess where the assets are served from. From origin to the pathname, this is specific to each installation. + +We can leverage the [auto](https://webpack.js.org/guides/public-path/#automatic-publicpath) option. However this means that some manifest transformation has to happen at runtime when entry scripts are loaded into the browser. More on that in the [Plugin manifest](#plugin-manifest), [CDN Plugin](#dynamic-assets-server-plugin), and [Plugin loading](#plugin-loading) sections. + +#### Sample webpack configuration ```TS -import { DynamicRemotePlugin } from '@openshift/dynamic-plugin-sdk-webpack'; -const sharedModules = { - /** - * Mandatory singleton packages for sharing - */ - react: { - singleton: true, - requiredVersion: '*', - }, - 'react-dom': { - singleton: true, - requiredVersion: '*', - }, - 'react-router-dom': { - singleton: true, - requiredVersion: '*', - }, - 'react-router': { - singleton: true, - requiredVersion: '*', - }, - ... - /** - * Full list of shared modules in Janus - * https://github.com/janus-idp/backstage-plugins/blob/87a6b045c7b0f301ebed8b8f99dc1741fa2b044b/packages/cli/src/lib/bundler/scalprumConfig.ts#L16 - */ +type DynamicPluginConfig = { + name: string; + version?: string; +}; +// { name: "@backstage/plugin-api-docs", version: "1.0.0" } +// generates "dynamic-backstage.plugin-api-docs" +const getName = (plugin: DynamicPluginConfig) => { + let pluginName: string; + if (plugin.name.includes('/')) { + const fragments = plugin.name.split('/'); + pluginName = `${fragments[0].replace('@', '')}.${fragments[1]}`; + } else { + pluginName = plugin.name; + } + return `dynamic-${pluginName}`; } + +// plugin = { name: "@backstage/plugin-api-docs", version: "1.0.0" } based on package.json +const pluginName = getName(plugin); + +const { ModuleFederationPlugin, ContainerPlugin } = require('@module-federation/enhanced'); + const dynamicPluginPlugin = new DynamicRemotePlugin({ extensions: [], - sharedModules, - entryScriptFilename: `${options.pluginMetadata.name}.[contenthash].js`, - pluginMetadata: { - // version cna be used from the package.json version field - version: '1.0.0', - /** - * Name can be easily derived from the plugin name - * https://github.com/janus-idp/backstage-plugins/blob/87a6b045c7b0f301ebed8b8f99dc1741fa2b044b/packages/cli/src/commands/export-dynamic-plugin/frontend.ts#L40 - */ - name: 'backstage.plugin-github-actions', - /** - * Path to the plugin entry point. - * It can default to the same entry point as in regular build. - * Plugins can expose multiple modules. We have found that one is sufficient from backstage plugins. - */ - exposedModules: { - PluginRoot: './src/index.ts' + sharedModules, // from shared config and generated from `package.json` dependencies + entryScriptFilename: `[name].[contenthash].js`, // using contenthash for cashing purposes + moduleFederationSettings: { + libraryType: 'global', // use the globalThis object rather than jsonp + // instruct the sdk plugin to use @module-federation/enhanced tooling + pluginOverride: { + ModuleFederationPlugin, + ContainerPlugin, } }, -}); -``` - -The `DynamicRemotePlugin` webpack plugin takes care of the rest, including the manifest generation. - -#### Sample plugin raw webpack configuration - -The Scalprum config translates to a following base webpack plugin configuration: - -```JS -import { container } from 'webpack'; - -const dynamicPlugin = new container.ModuleFederationPlugin({ - name: 'backstage.plugin-github-actions', - library: { - type: 'global', - /** - * Some library.type has name limitation - * for example, if "type": "var" is used, the library.name can contain the "-" character - */ - name: 'backstage.plugin-github-actions' + pluginMetadata: { + name: pluginName, + version: plugin.version || '0.0.0', + exposedModules: { + // path to the default export of the frontend plugin entry point + // the path should be sourced from the "main" attribute withing package.json + pluginEntry: './src/index.ts', + }, }, - filename: 'backstage.plugin-github-actions.[contenthash].js', - exposes: { - PluginRoot: './src/index.ts' - }, - // list of shared modules like "react", "react-dom", etc - shared: sharedModules, }); + + +const config: Configuration = { + context: pluginRoot, + output: { + chunkFilename: `${pluginName}.[contenthash].js`, + path: path.resolve(pluginRoot, 'dist'), + publicPath: 'auto', + }, + entry: {}, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + // extend existing webpack plugins that are relevant for dynamic plugins + plugins: [dynamicPluginPlugin, ...], + module: { + rules: [ + { + // needs TS config as well + test: /\.js$/, + exclude: /(node_modules)/, + use: { + loader: 'swc-loader', + }, + }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', + }, + // additional loaders for CSS if necessary, can use the same as the current backstage shell app + ], + }, + }; + return config; ``` -Additional plugin would have to be written from scratch to generate the plugin manifest file. +**Webpack plugins** -### Sample shell application webpack configuration +Existing webpack plugins can be used. Subset of shared plugins should be identified from current configurations and shared between the dynamic plugins and the shell application. -Shell application (backstage) has very simple module federation configuration. It does not require anything special to be able to inject plugins. Main requirement is to provide core shared packages like react and react-dom. +### Plugin manifest -```JS -import { container } from 'webpack'; +Plugin manifest is a simple JSON file containing critical metadata to initialize a remote plugin. -const scalprumPlugin = new container.ModuleFederationPlugin({ - name: 'backstageHost', - filename: 'backstageHost.[contenthash].js', - // same share modules list as with plugin config - shared: [sharedModules], -}); +The `@openshift/dynamic-plugin-sdk-webpack` plugin will output manifest file in the following format: + +```ts +type PluginManifest = { + name: string; + version: string; + dependencies?: Record; + customProperties?: AnyObject; + baseURL: string; + loadScripts: string[]; + registrationMethod: 'callback' | 'custom'; + buildHash?: string; +}; ``` -Nothing else is required. +This manifest is directly usable in Scalprum at runtime. -### Plugin storage +**baseURL** -Where will the dynamic plugin assets be hosted? Module federation does not strictly require the remote assets to be all hosted on the same origin. +This attribute is populated by the `output.publicPath` configuration. In this case, it will be a value of `auto`. That is not usable by the Scalprum loader. It has to be a URL. -Theoretically plugins can be hosted on some "public CDN" which is detached from individual backstage instances. +Because the origin can't be reliably "guessed" at build time, the value has to be filled by the CDN server. The attribute can be either overridden or, preferably, added to the `customProperties` attribute. -Assets can be also be hosted in the same way as they have always been. +A default`DEFAULT_API_PATH` should be used. Covering majority of the use cases. -It's important to consider old deployments as part of this storage, to make sure that dynamic chunks are still available to users who have not yet refreshed the page to load the latest version of the app. +```ts +type CustomProperties = { + assetHost?: string; // for local development should be http://localhost:PORT + // for deployment, should match the api host https://backstage.company.org +}; +``` -### Plugin discovery +Any additional metadata about plugins, can be also stored to the `customProperties`. Either at build time or runtime. That depends on the use case. The values can be read at runtime during plugin initialization. -How to notify/send data to browser +```ts +type CustomProperties = { + backstage: Record; +}; +``` -### Plugin initialization +**Exposing singleton packages config** -Currently all plugins have to be initialized at UI bootstrap (page refresh). The new UI async API can be used to initialize the remote assets. +Singleton shared packages do not follow the version matching strategy. It is a weak point in the dependency sharing strategy. Exposing the data in the manifest files provides the option to check the singleton configuration at runtime and prevent runtime crashes of plugins. -The `CreateAppFeatureLoader` can be leveraged to initialize the remote container. +```ts +type SingletonShareConfig = { + singleton: true; + version: string; + packageName: string; +}; -```TS -const allPluginManifests = {...} // get this from the config +type SingletonShareObject = { + [packageName: string]: SingletonShareConfig; +}; +``` -const asyncFeatureLoaders: CreateAppFeatureLoader[] = Object.values(allPluginManifests).map(({ manifest }) => { - return { - getLoaderName: () => manifest.name, - load: (options) => { - // initialize the remote container, depends on tooling - const plugin = initDynamicPlugin(manifest) - } +**Backstage plugin manifest** + +The plugin manifest for backstage with the additional attributes: + +```ts +type SingletonShareConfig = { + singleton: true; + version: string; + packageName: string; +}; + +type SingletonShareObject = { + [packageName: string]: SingletonShareConfig; +}; + +type CustomProperties = { + assetHost?: string; + backstage: Record; + singletonPackages: SingletonShareObject; +}; + +type PluginManifest = { + name: string; + version: string; + dependencies?: Record; + customProperties: CustomProperties; + baseURL: 'auto'; + loadScripts: string[]; + registrationMethod: 'custom'; + buildHash?: string; +}; +``` + +### Simplistic minimal example of module map generator + +```js +const path = require('path'); +const typescript = require('typescript'); +const fs = require('fs'); + +/** @type {typescript.CompilerOptions} */ +const defaultCompilerOptions = { + targe: 'es2015', + module: 'es2015', + esModuleInterop: true, + allowJs: true, + strict: false, + skipLibCheck: true, + noEmit: true, + // needs to be configured to the root directory dependency + rootDir: path.resolve(__dirname, 'dist'), + baseUrl: path.resolve(__dirname, 'dist'), +}; + +// these need to be configured, usually getting metadata from the lib package.json +const rootDit = path.resolve(__dirname, 'dist'); +const base = path.resolve(__dirname, 'dist', 'index.js'); + +function getDynamicModuleMap(libName) { + const compiler = typescript.createCompilerHost(defaultCompilerOptions, [ + rootDit, + ]); + const program = typescript.createProgram( + [base], + defaultCompilerOptions, + compiler, + ); + + const moduleResolutionCache = typescript.createModuleResolutionCache( + rootDit, + x => x, + defaultCompilerOptions, + ); + const errorDiagnostics = typescript + .getPreEmitDiagnostics(program) + .filter(d => d.category === typescript.DiagnosticCategory.Error); + if (errorDiagnostics.length > 0) { + const { getCanonicalFileName, getCurrentDirectory, getNewLine } = compiler; + + console.error( + typescript.formatDiagnostics(errorDiagnostics, { + getCanonicalFileName, + getCurrentDirectory, + getNewLine, + }), + ); + + throw new Error(`Detected TypeScript errors while parsing modules`); } -}) + const typeChecker = program.getTypeChecker(); -const app = createApp({ - features: [ - ...asyncFeatureLoaders, - // rest of classic features - ] -}) + /** @param {typescript.SourceFile} sourceFile */ + const getExportNames = sourceFile => + typeChecker + .getExportsOfModule(typeChecker.getSymbolAtLocation(sourceFile)) + .map(symbol => symbol.getName()); + + const baseExports = getExportNames(program.getSourceFile(base)); + const compilerDir = compiler.getCurrentDirectory(); + const resolvedModules = baseExports + .map(name => { + const res = typescript.resolveModuleName( + name, + base, + defaultCompilerOptions, + compiler, + moduleResolutionCache, + ); + if (res.resolvedModule?.resolvedFileName) { + return { + name, + file: + libName + + res.resolvedModule.resolvedFileName + .replace(compilerDir, '') + .replace(/\.js$/, ''), + }; + } + return undefined; + }) + .filter(x => x !== undefined); + + const moduleMapFileName = path.resolve(__dirname, 'module-map.json'); + fs.writeFileSync(moduleMapFileName, JSON.stringify(resolvedModules, null, 2)); +} + +const libName = '@mui/foo'; + +getDynamicModuleMap(libName); + +/** + * [ + * { + * "name": "foo", + * "file": "@mui/foo/dist/foo" + * }, + * { + * "name": "baz", + * "file": "@mui/foo/dist/baz" + * } + * ] + */ ``` ### Plugin declarative configuration diff --git a/beps/0002-dynamic-frontend-plugins/dependency-sharing-schema.json b/beps/0002-dynamic-frontend-plugins/dependency-sharing-schema.json new file mode 100644 index 0000000000..4777f81f78 --- /dev/null +++ b/beps/0002-dynamic-frontend-plugins/dependency-sharing-schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "additionalProperties": false, + "properties": { + "shareModules": { + "type": "object", + "patternProperties": { + "^(@[^/]+/)?[^/]+$": { + "$ref": "#/definitions/sharedModule" + } + }, + "additionalProperties": false + }, + "excludeModules": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "definitions": { + "sharedModule": { + "type": "object", + "required": ["requiredVersion"], + "properties": { + "requiredVersion": { + "$ref": "#/definitions/semverString" + }, + "version": { + "$ref": "#/definitions/semverString" + }, + "singleton": { + "type": "boolean" + } + } + }, + "semverString": { + "type": "string", + "pattern": "^(\\^|\\~)?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } +} diff --git a/beps/0002-dynamic-frontend-plugins/share-config-sample.json b/beps/0002-dynamic-frontend-plugins/share-config-sample.json new file mode 100644 index 0000000000..fe5c9943ad --- /dev/null +++ b/beps/0002-dynamic-frontend-plugins/share-config-sample.json @@ -0,0 +1,17 @@ +{ + "$schema": "./dependency-sharing-schema.json", + "shareModules": { + "foo-bar": { + "requiredVersion": "1.0.0" + }, + "@bax/foo-bar": { + "requiredVersion": "1.0.0", + "singleton": true + }, + "@bax-baz/foo-bar2": { + "requiredVersion": "^1.2.0", + "version": "1.4.0" + } + }, + "excludeModules": ["foo-bar-baz"] +} From a0fd89c3ef77677404cfb46e973000ef4061d2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20St=C3=B6rkle?= Date: Thu, 15 Feb 2024 14:54:48 +0100 Subject: [PATCH 005/212] fix: use correct odata type in TypeScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Florian Störkle --- .../src/microsoftGraph/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 1233b1e2f9..c190e2a959 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -64,8 +64,8 @@ export type ODataQuery = { * @public */ export type GroupMember = - | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.user' }) - | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.group' }); + | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.group' }) + | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.user' }); /** * A HTTP Client that communicates with Microsoft Graph API. From da7651605a6c78e8e4a89ab89fc2d0e30291d06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20St=C3=B6rkle?= Date: Fri, 16 Feb 2024 13:15:50 +0100 Subject: [PATCH 006/212] feat(catalog): ingest member groups if configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Florian Störkle --- .../config.d.ts | 5 + .../src/microsoftGraph/config.test.ts | 2 + .../src/microsoftGraph/config.ts | 10 ++ .../src/microsoftGraph/read.test.ts | 114 ++++++++++++++++++ .../src/microsoftGraph/read.ts | 28 +++++ .../MicrosoftGraphOrgEntityProvider.ts | 1 + 6 files changed, 160 insertions(+) diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index 5a1f56a03f..a470c075f0 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -192,6 +192,11 @@ export interface Config { * E.g. ["id", "displayName", "description"] */ select?: string[]; + /** + * Whether to ingest groups that are members of the found/filtered/searched groups. + * Default value is `false`. + */ + includeSubGroups?: boolean; }; userGroupMember?: { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index b02894dfd9..e1c81662a3 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -175,6 +175,7 @@ describe('readProviderConfigs', () => { expand: 'member', filter: 'securityEnabled eq false', select: ['id', 'displayName', 'description'], + includeSubGroups: true, }, schedule: { frequency: 'PT30M', @@ -203,6 +204,7 @@ describe('readProviderConfigs', () => { groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', + groupIncludeSubGroups: true, schedule: { frequency: Duration.fromISO('PT30M'), timeout: { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 1545114e45..257a7d5736 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -116,6 +116,12 @@ export type MicrosoftGraphProviderConfig = { */ groupSelect?: string[]; + /** + * Whether to ingest groups that are members of the found/filtered/searched groups. + * Default value is `false`. + */ + groupIncludeSubGroups?: boolean; + /** * By default, the Microsoft Graph API only provides the basic feature set * for querying. Certain features are limited to advanced query capabilities @@ -292,6 +298,9 @@ export function readProviderConfig( const groupFilter = config.getOptionalString('group.filter'); const groupSearch = config.getOptionalString('group.search'); const groupSelect = config.getOptionalStringArray('group.select'); + const groupIncludeSubGroups = config.getOptionalBoolean( + 'group.includeSubGroups', + ); const queryMode = config.getOptionalString('queryMode'); if ( @@ -347,6 +356,7 @@ export function readProviderConfig( groupFilter, groupSearch, groupSelect, + groupIncludeSubGroups, queryMode, userGroupMemberFilter, userGroupMemberSearch, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 8821ffd2db..b83597653f 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -90,6 +90,9 @@ describe('read microsoft graph', () => { yield { '@odata.type': '#microsoft.graph.group', id: 'childgroupid', + displayName: 'Child Group Name', + description: 'Child Group Description', + mail: 'childgroup@example.com', }; yield { '@odata.type': '#microsoft.graph.user', @@ -770,6 +773,117 @@ describe('read microsoft graph', () => { top: 999, }); }); + + it('should read groups and their sub groups', async () => { + async function* getExampleGroupMembersForSubGroup(): AsyncIterable { + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid2', + }; + } + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementationOnce(getExampleGroupMembers); + client.getGroupMembers.mockImplementationOnce( + getExampleGroupMembersForSubGroup, + ); + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { groups, groupMember, groupMemberOf, rootGroup } = + await readMicrosoftGraphGroups(client, 'tenantid', { + groupIncludeSubGroups: true, + }); + + const expectedRootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }); + expect(groups).toEqual([ + expectedRootGroup, + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'childgroupid', + }, + name: 'child_group_name', + description: 'Child Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Child Group Name', + email: 'childgroup@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'groupid', + }, + name: 'group_name', + description: 'Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Group Name', + email: 'group@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + ]); + expect(rootGroup).toEqual(expectedRootGroup); + expect(groupMember.get('groupid')).toEqual(new Set(['childgroupid'])); + expect(groupMemberOf.get('userid')).toEqual(new Set(['groupid'])); + expect(groupMemberOf.get('userid2')).toEqual(new Set(['childgroupid'])); + expect(groupMember.get('organization_name')).toEqual(new Set()); + + expect(client.getGroups).toHaveBeenCalledTimes(1); + expect(client.getGroups).toHaveBeenCalledWith( + { + top: 999, + }, + undefined, + ); + expect(client.getGroupMembers).toHaveBeenCalledTimes(2); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); + expect(client.getGroupMembers).toHaveBeenCalledWith('childgroupid', { + top: 999, + }); + // TODO: Loading groups photos doesn't work right now as Microsoft Graph + // doesn't allows this yet + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); + }); }); describe('resolveRelations', () => { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 2ca21fca9c..60e5c3d79b 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -178,6 +178,7 @@ export async function readMicrosoftGraphGroups( groupFilter?: string; groupSearch?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; }, @@ -244,6 +245,31 @@ export async function readMicrosoftGraphGroups( if (member['@odata.type'] === '#microsoft.graph.group') { ensureItem(groupMember, group.id!, member.id); + + if (options?.groupIncludeSubGroups) { + const groupMemberEntity = await transformer(member); + + if (groupMemberEntity) { + groups.push(groupMemberEntity); + + for await (const subMember of client.getGroupMembers( + member.id!, + { top: PAGE_SIZE }, + )) { + if (!subMember.id) { + continue; + } + + if (subMember['@odata.type'] === '#microsoft.graph.user') { + ensureItem(groupMemberOf, subMember.id, member.id!); + } + + if (subMember['@odata.type'] === '#microsoft.graph.group') { + ensureItem(groupMember, member.id!, subMember.id); + } + } + } + } } } @@ -377,6 +403,7 @@ export async function readMicrosoftGraphOrg( groupSearch?: string; groupFilter?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; queryMode?: 'basic' | 'advanced'; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; @@ -421,6 +448,7 @@ export async function readMicrosoftGraphOrg( groupFilter: options.groupFilter, groupSearch: options.groupSearch, groupSelect: options.groupSelect, + groupIncludeSubGroups: options.groupIncludeSubGroups, groupTransformer: options.groupTransformer, organizationTransformer: options.organizationTransformer, }); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 3c8b903e69..c48ee18ffe 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -317,6 +317,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { groupFilter: provider.groupFilter, groupSearch: provider.groupSearch, groupSelect: provider.groupSelect, + groupIncludeSubGroups: provider.groupIncludeSubGroups, queryMode: provider.queryMode, groupTransformer: this.options.groupTransformer, userTransformer: this.options.userTransformer, From 24b3a3141b2eec8f89e4f76aa3cfc7328d28ea6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20St=C3=B6rkle?= Date: Mon, 19 Feb 2024 08:35:18 +0100 Subject: [PATCH 007/212] docs: document `includeSubGroups` config option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Florian Störkle --- docs/integrations/azure/org.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 89abd4813b..b1719a63da 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -112,6 +112,17 @@ microsoftGraphOrg: search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' ``` +If you don't want to only ingest groups matching the `search` and/or `filter` query, but also the groups which are members of the matched groups, you can use the `includeSubGroups` configuration: + +```yaml +microsoftGraphOrg: + providerId: + group: + filter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') + search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' + includeSubGroups: true +``` + In addition to these groups, one additional group will be created for your organization. All imported groups will be a child of this group. From d9cf697afda7543d72610e59f656ca05e2c9c3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20St=C3=B6rkle?= Date: Mon, 19 Feb 2024 08:40:38 +0100 Subject: [PATCH 008/212] chore: generate API report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Florian Störkle --- plugins/catalog-backend-module-msgraph/api-report.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 86170e2910..87402eedb1 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -39,10 +39,10 @@ export function defaultUserTransformer( // @public export type GroupMember = | (MicrosoftGraph.Group & { - '@odata.type': '#microsoft.graph.user'; + '@odata.type': '#microsoft.graph.group'; }) | (MicrosoftGraph.User & { - '@odata.type': '#microsoft.graph.group'; + '@odata.type': '#microsoft.graph.user'; }); // @public @@ -210,6 +210,7 @@ export type MicrosoftGraphProviderConfig = { groupFilter?: string; groupSearch?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; queryMode?: 'basic' | 'advanced'; loadUserPhotos?: boolean; schedule?: TaskScheduleDefinition; @@ -253,6 +254,7 @@ export function readMicrosoftGraphOrg( groupSearch?: string; groupFilter?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; queryMode?: 'basic' | 'advanced'; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; From 58dff4d5e574428090ec09a4fbc7de3e08224c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20St=C3=B6rkle?= Date: Tue, 16 Apr 2024 10:01:13 +0200 Subject: [PATCH 009/212] chore: add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Florian Störkle --- .changeset/little-suns-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/little-suns-fly.md diff --git a/.changeset/little-suns-fly.md b/.changeset/little-suns-fly.md new file mode 100644 index 0000000000..887df4be6d --- /dev/null +++ b/.changeset/little-suns-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Added option to ingest groups based on their group membership in Azure Entra ID From 4e4ef2ba78a8ead2fdf3bd674ac1ef5dcf313eb0 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Wed, 5 Jun 2024 13:33:31 +0300 Subject: [PATCH 010/212] chore: move notification processor filters to common package Signed-off-by: Heikki Hellgren --- .changeset/weak-emus-doubt.md | 7 +++ .../processor/NotificationsEmailProcessor.ts | 30 ++---------- plugins/notifications-common/api-report.md | 14 ++++++ plugins/notifications-common/package.json | 1 + plugins/notifications-common/src/filters.ts | 47 +++++++++++++++++++ plugins/notifications-common/src/index.ts | 1 + plugins/notifications-common/src/types.ts | 9 ++++ plugins/notifications-node/api-report.md | 10 ++-- plugins/notifications-node/src/extensions.ts | 9 ++-- yarn.lock | 1 + 10 files changed, 89 insertions(+), 40 deletions(-) create mode 100644 .changeset/weak-emus-doubt.md create mode 100644 plugins/notifications-common/src/filters.ts diff --git a/.changeset/weak-emus-doubt.md b/.changeset/weak-emus-doubt.md new file mode 100644 index 0000000000..cd3b4a836b --- /dev/null +++ b/.changeset/weak-emus-doubt.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +'@backstage/plugin-notifications-common': patch +'@backstage/plugin-notifications-node': patch +--- + +Move notification processor filter parsing to common package diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index a78ebd3260..8689afc065 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -15,7 +15,6 @@ */ import { NotificationProcessor, - NotificationProcessorFilters, NotificationSendOptions, } from '@backstage/plugin-notifications-node'; import { @@ -30,9 +29,9 @@ import { CatalogClient, } from '@backstage/catalog-client'; import { + getProcessorFiltersFromConfig, Notification, - notificationSeverities, - NotificationSeverity, + NotificationProcessorFilters, } from '@backstage/plugin-notifications-common'; import { createSendmailTransport, @@ -86,30 +85,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ? durationToMilliseconds(readDurationFromConfig(cacheConfig)) : 3_600_000; this.frontendBaseUrl = config.getString('app.baseUrl'); - this.filter = {}; - const minSeverity = emailProcessorConfig.getOptionalString( - 'filter.minSeverity', - ) as NotificationSeverity; - if (minSeverity) { - if (notificationSeverities.includes(minSeverity)) { - this.filter.minSeverity = minSeverity; - } else { - throw new Error(`Invalid minSeverity: ${minSeverity}`); - } - } - const maxSeverity = emailProcessorConfig.getOptionalString( - 'filter.maxSeverity', - ) as NotificationSeverity; - if (maxSeverity) { - if (notificationSeverities.includes(maxSeverity)) { - this.filter.maxSeverity = maxSeverity; - } else { - throw new Error(`Invalid maxSeverity: ${maxSeverity}`); - } - } - this.filter.excludedTopics = emailProcessorConfig.getOptionalStringArray( - 'filter.excludedTopics', - ); + this.filter = getProcessorFiltersFromConfig(emailProcessorConfig); } private async getTransporter() { diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md index 3ddce24393..0f59a85849 100644 --- a/plugins/notifications-common/api-report.md +++ b/plugins/notifications-common/api-report.md @@ -3,6 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Config } from '@backstage/config'; + +// @public (undocumented) +export const getProcessorFiltersFromConfig: ( + config: Config, +) => NotificationProcessorFilters; + // @public (undocumented) export type NewNotificationSignal = { action: 'new_notification'; @@ -33,6 +40,13 @@ export type NotificationPayload = { icon?: string; }; +// @public (undocumented) +export type NotificationProcessorFilters = { + minSeverity?: NotificationSeverity; + maxSeverity?: NotificationSeverity; + excludedTopics?: string[]; +}; + // @public (undocumented) export type NotificationReadSignal = { action: 'notification_read' | 'notification_unread'; diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json index 2745230a4a..eaec75cb5e 100644 --- a/plugins/notifications-common/package.json +++ b/plugins/notifications-common/package.json @@ -32,6 +32,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/config": "workspace:^", "@material-ui/icons": "^4.9.1" }, "devDependencies": { diff --git a/plugins/notifications-common/src/filters.ts b/plugins/notifications-common/src/filters.ts new file mode 100644 index 0000000000..725cfcf714 --- /dev/null +++ b/plugins/notifications-common/src/filters.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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. + */ +import { Config } from '@backstage/config'; +import { NotificationProcessorFilters, NotificationSeverity } from './types'; +import { notificationSeverities } from './constants'; + +/** @public */ +export const getProcessorFiltersFromConfig = (config: Config) => { + const filter: NotificationProcessorFilters = {}; + const minSeverity = config.getOptionalString( + 'filter.minSeverity', + ) as NotificationSeverity; + if (minSeverity) { + if (notificationSeverities.includes(minSeverity)) { + filter.minSeverity = minSeverity; + } else { + throw new Error(`Invalid minSeverity: ${minSeverity}`); + } + } + const maxSeverity = config.getOptionalString( + 'filter.maxSeverity', + ) as NotificationSeverity; + if (maxSeverity) { + if (notificationSeverities.includes(maxSeverity)) { + filter.maxSeverity = maxSeverity; + } else { + throw new Error(`Invalid maxSeverity: ${maxSeverity}`); + } + } + filter.excludedTopics = config.getOptionalStringArray( + 'filter.excludedTopics', + ); + return filter; +}; diff --git a/plugins/notifications-common/src/index.ts b/plugins/notifications-common/src/index.ts index 7d74c75be8..9fc7bde2b8 100644 --- a/plugins/notifications-common/src/index.ts +++ b/plugins/notifications-common/src/index.ts @@ -22,3 +22,4 @@ export * from './types'; export * from './constants'; +export * from './filters'; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index b9c9f63489..a4be3e4255 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -116,3 +116,12 @@ export type NotificationReadSignal = { /** @public */ export type NotificationSignal = NewNotificationSignal | NotificationReadSignal; + +/** + * @public + */ +export type NotificationProcessorFilters = { + minSeverity?: NotificationSeverity; + maxSeverity?: NotificationSeverity; + excludedTopics?: string[]; +}; diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 27e6422f8a..37f28fec35 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -8,7 +8,7 @@ import { DiscoveryService } from '@backstage/backend-plugin-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { Notification as Notification_2 } from '@backstage/plugin-notifications-common'; import { NotificationPayload } from '@backstage/plugin-notifications-common'; -import { NotificationSeverity } from '@backstage/plugin-notifications-common'; +import { NotificationProcessorFilters as NotificationProcessorFilters_2 } from '@backstage/plugin-notifications-common'; import { ServiceRef } from '@backstage/backend-plugin-api'; // @public (undocumented) @@ -38,12 +38,8 @@ export interface NotificationProcessor { ): Promise; } -// @public (undocumented) -export type NotificationProcessorFilters = { - minSeverity?: NotificationSeverity; - maxSeverity?: NotificationSeverity; - excludedTopics?: string[]; -}; +// @public @deprecated (undocumented) +export type NotificationProcessorFilters = NotificationProcessorFilters_2; // @public (undocumented) export type NotificationRecipients = diff --git a/plugins/notifications-node/src/extensions.ts b/plugins/notifications-node/src/extensions.ts index 7adb6caf50..03ce47fea6 100644 --- a/plugins/notifications-node/src/extensions.ts +++ b/plugins/notifications-node/src/extensions.ts @@ -16,7 +16,7 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { Notification, - NotificationSeverity, + NotificationProcessorFilters as NotificationProcessorFiltersCommon, } from '@backstage/plugin-notifications-common'; import { NotificationSendOptions } from './service'; @@ -119,9 +119,6 @@ export const notificationsProcessingExtensionPoint = /** * @public + * @deprecated Please import from `@backstage/plugin-notifications-common` instead */ -export type NotificationProcessorFilters = { - minSeverity?: NotificationSeverity; - maxSeverity?: NotificationSeverity; - excludedTopics?: string[]; -}; +export type NotificationProcessorFilters = NotificationProcessorFiltersCommon; diff --git a/yarn.lock b/yarn.lock index 2ad2995d52..7c2b1ac7fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6240,6 +6240,7 @@ __metadata: resolution: "@backstage/plugin-notifications-common@workspace:plugins/notifications-common" dependencies: "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@material-ui/icons": ^4.9.1 languageName: unknown linkType: soft From 2b8b4f115a1f3052dc5db9ff14c33b8d0afd8d20 Mon Sep 17 00:00:00 2001 From: Niall Thomson Date: Tue, 11 Jun 2024 22:19:44 -0400 Subject: [PATCH 011/212] Removed code that catches error and formatted changeset markdown Signed-off-by: Niall Thomson --- .changeset/swift-kings-sparkle.md | 2 +- .../AwsOrganizationCloudAccountProcessor.ts | 36 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/.changeset/swift-kings-sparkle.md b/.changeset/swift-kings-sparkle.md index 18a37b82f1..84ceae962b 100644 --- a/.changeset/swift-kings-sparkle.md +++ b/.changeset/swift-kings-sparkle.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-aws': patch --- -AwsOrganizationCloudAccountProcessor configuration field roleArn is deprecated in favor of new field accountId +`AwsOrganizationCloudAccountProcessor` configuration field `roleArn` is deprecated in favor of new field `accountId` diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index b843503a28..73d7a63e2b 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -106,27 +106,23 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { this.logger?.info('Discovering AWS Organization Account objects'); - try { - (await this.getAwsAccounts()) - .map(account => this.mapAccountToComponent(account)) - .filter(entity => { - if (location.target !== '') { - if (entity.metadata.annotations) { - return ( - entity.metadata.annotations[ORGANIZATION_ANNOTATION] === - location.target - ); - } - return false; + (await this.getAwsAccounts()) + .map(account => this.mapAccountToComponent(account)) + .filter(entity => { + if (location.target !== '') { + if (entity.metadata.annotations) { + return ( + entity.metadata.annotations[ORGANIZATION_ANNOTATION] === + location.target + ); } - return true; - }) - .forEach(entity => { - emit(processingResult.entity(location, entity)); - }); - } catch (e) { - this.logger?.error(e); - } + return false; + } + return true; + }) + .forEach(entity => { + emit(processingResult.entity(location, entity)); + }); return true; } From 605b6916cf153ea41106e3a09ef4b33fd42abc4f Mon Sep 17 00:00:00 2001 From: "Stephen A. Wilson" Date: Thu, 13 Jun 2024 10:58:22 -0400 Subject: [PATCH 012/212] Allow for searching TechDocs by Title Signed-off-by: Stephen A. Wilson --- .changeset/fuzzy-socks-do.md | 6 + app-config.yaml | 3 + .../catalog-info.yaml | 12 ++ .../docs/code/code-sample.md | 31 +++++ .../docs/extensions.md | 112 ++++++++++++++++++ .../docs/images/backstage-logo-cncf.svg | 1 + .../docs/index.md | 55 +++++++++ .../docs/sub-page.md | 12 ++ .../mkdocs.yml | 12 ++ .../src/home/components/Tables/DocsTable.tsx | 1 + .../src/home/components/Tables/columns.tsx | 34 +++++- 11 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 .changeset/fuzzy-socks-do.md create mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml create mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md create mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md create mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg create mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md create mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md create mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml diff --git a/.changeset/fuzzy-socks-do.md b/.changeset/fuzzy-socks-do.md new file mode 100644 index 0000000000..02d2e4dca9 --- /dev/null +++ b/.changeset/fuzzy-socks-do.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs': patch +--- + +Allow for searching TechDocs by entity title diff --git a/app-config.yaml b/app-config.yaml index cb70170541..69145cd896 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -297,6 +297,9 @@ catalog: # Example component for TechDocs - type: file target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml + # Example component for TechDocs with a title + - type: file + target: ../../plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml # Example component for ADRs - type: file target: ../../plugins/adr/examples/component/catalog-info.yaml diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml new file mode 100644 index 0000000000..1a1d65ba39 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: documented-with-title + title: Fizzbuzz Docs + description: A TechDocs with a title different than it's name + annotations: + backstage.io/techdocs-ref: dir:. +spec: + type: service + lifecycle: experimental + owner: user:guest diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md new file mode 100644 index 0000000000..f41ab1d506 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md @@ -0,0 +1,31 @@ +# Sample Code + +This page provides some sample code which may be used in your example component. + +This code uses TypeScript, and the Markdown code fence to wrap the code. + +```typescript +const serviceEntityPage = ( + + + + + + + + + + + + +); +``` + +Here is an example of Python code: + +```python +def getUsersInGroup(targetGroup, secure=False): + + if __debug__: + print('targetGroup=[' + targetGroup + ']') +``` diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md new file mode 100644 index 0000000000..eb9abca74a --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md @@ -0,0 +1,112 @@ +# Plugins & Extensions + +Just by including the TechDocs Core Plugin to your MkDocs site included with Backstage, +you gain the immediate use of a variety of popular plugins and extensions to MkDocs. + +For more information and full details of the available features, see the +[`mkdocs-techdocs-core` repository](https://github.com/backstage/mkdocs-techdocs-core#mkdocs-plugins-and-extensions). + +This page provides a demonstration of some of the available features. + +## Admonitions + +Admonitions are call outs that help catch a users attention. + +To define an admonition simply put the following Markdown into your content: + +``` +!!! warn + Defining admonitions can be addicting. +``` + +And they end up looking like this: + + +!!! warn + Defining admonitions can be addicting. + + +!!! note + You can learn a lot about TechDocs by just visiting the Backstage web site at + https://backstage.io/docs. + + +!!! info + TechDocs is the core feature that supports documentation as code in Backstage. + + +!!! tip + Don't forget to spell check your documentation. + +## PlantUML + +You can create dynamic UML diagrams on the fly by just specifying flow via text, +using [PlantUML](https://pypi.org/project/plantuml-markdown/). + +```plantuml format="svg" classes="uml myDiagram" alt="Backstage sample PlantUML" title="Backstage sample PlantUML" width="500px" height="250px" +User -> SCMProvider: stores +TechDocs -> SCMProvider: prepares +TechDocs -> TechDocs: generates +TechDocs -> CloudStorage: publishes +CloudStorage -> Backstage: displays +``` + +## Pymdownx Extensions + +Pymdownx (Python Markdown extensions) are a variety of smaller additions. + +### Details + + +??? note "What is the answer to life, the universe, and everything? (click me for the answer)" + The answer is 42. + + +??? note "What is 4 plus 4?" + The answer is 8. + + +???+ note "How do I get support?" + You can get support by opening an issue in this repository. This detail is open by default + so it's more easily visible without requiring the user to click to open it. + +### Task Lists + +Automatic rendering of Markdown task lists. + +- [x] Phase 1 +- [x] Phase 2 +- [ ] Phase 3 + +### Emojis + +Very nice job on documentation! :thumbsup: + +I've read a lot of documentation, but I love :heart: this document. + +Weather: :sunny: :umbrella: :cloud: :snowflake: + +Animals: :tiger: :horse: :turtle: :wolf: :frog: + +### Attributes + +[A Download Link](./images/backstage-logo-cncf.svg){: download } + +![A Scaled Image](./images/backstage-logo-cncf.svg){: style="width: 100px" } + +### MDX truly sane lists + +- `attributes` + +- `customer` + - `first_name` + - `test` + - `family_name` + - `email` +- `person` + - `first_name` + - `family_name` + - `birth_date` +- `subscription_id` + +- `request` diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg b/plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg new file mode 100644 index 0000000000..b5ff591d1b --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg @@ -0,0 +1 @@ +05 Logo_Black \ No newline at end of file diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md new file mode 100644 index 0000000000..6295a69406 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md @@ -0,0 +1,55 @@ +# Welcome! + +This is a basic example of documentation. It is intended as a showcase of some of the +features that TechDocs provides out of the box. + +You can see also: + +- [A sub page](sub-page.md) +- [Inline code examples](code/code-sample.md) +- [Plugin & Extension examples](extensions.md) - Diagrams, emojis, visual formatting. + +## Basic Markdown + +Headings: + +# h1 + +## h2 + +### h3 + +#### h4 + +##### h5 + +###### h6 + +Here is a bulleted list: + +- Item one +- Item two +- Item Three + +Check out the [Markdown Guide](https://www.markdownguide.org/) to learn more about how to +simply create documentation. + +You can also learn more about how to configure and setup this documentation in Backstage, +[read up on the TechDocs Overview](https://backstage.io/docs/features/techdocs/). + +## Image Example + +This documentation is powered by Backstage's TechDocs feature: + +![Backstage Logo](images/backstage-logo-cncf.svg) + +## Table Example + +While this documentation isn't comprehensive, in the future it should cover the following +topics outlined in this example table: + +| Topic | Description | +| ------- | ------------------------------------------------------------ | +| Topic 1 | An introductory topic to help you learn about the component. | +| Topic 2 | A more detailed topic that explains more information. | +| Topic 3 | A final topic that provides conclusions and lessons learned. | diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md new file mode 100644 index 0000000000..987f5e8a95 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md @@ -0,0 +1,12 @@ +# Another page in our documentation + +This sub-page can be used to elaborate on a specific part of the component. + +## Details + +It is linked off the main page, and due to its inclusion in the `mkdocs.yml` file, +becomes part of the auto-generated site navigation. + +## MkDocs + +Visit https://www.mkdocs.org for more information about the MkDocs tool. diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml new file mode 100644 index 0000000000..1a159a4d12 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml @@ -0,0 +1,12 @@ +site_name: 'Example Documentation' +repo_url: https://github.com/backstage/backstage +edit_uri: edit/master/plugins/techdocs-backend/examples/documented-component/docs + +nav: + - Home: index.md + - Subpage: sub-page.md + - 'Code Sample': code/code-sample.md + - Extensions: extensions.md + +plugins: + - techdocs-core diff --git a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx index c0b39878cd..59abd9b278 100644 --- a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx @@ -85,6 +85,7 @@ export const DocsTable = (props: DocsTableProps) => { }); const defaultColumns: TableColumn[] = [ + columnFactories.createTitleColumn({ hidden: true }), columnFactories.createNameColumn(), columnFactories.createOwnerColumn(), columnFactories.createKindColumn(), diff --git a/plugins/techdocs/src/home/components/Tables/columns.tsx b/plugins/techdocs/src/home/components/Tables/columns.tsx index bc85bb7a63..3deb4d9b33 100644 --- a/plugins/techdocs/src/home/components/Tables/columns.tsx +++ b/plugins/techdocs/src/home/components/Tables/columns.tsx @@ -24,17 +24,20 @@ function customTitle(entity: Entity): string { return entity.metadata.title || entity.metadata.name; } +type ColumnOptions = Partial>; + /** * Not directly exported, but through DocsTable.columns and EntityListDocsTable.columns * * @public */ export const columnFactories = { - createNameColumn(): TableColumn { + createTitleColumn(overrides: ColumnOptions = {}): TableColumn { return { title: 'Document', - field: 'entity.metadata.name', + field: 'entity.metadata.title', highlight: true, + searchable: true, render: (row: DocsTableRow) => ( ), + ...overrides, }; }, - createOwnerColumn(): TableColumn { + createNameColumn(overrides: ColumnOptions = {}): TableColumn { + return { + title: 'Document', + field: 'entity.metadata.name', + highlight: true, + searchable: true, + render: (row: DocsTableRow) => ( + {customTitle(row.entity)} + } + subvalue={row.entity.metadata.description} + /> + ), + ...overrides, + }; + }, + createOwnerColumn(overrides: ColumnOptions = {}): TableColumn { return { title: 'Owner', field: 'resolved.ownedByRelationsTitle', @@ -55,18 +76,21 @@ export const columnFactories = { defaultKind="group" /> ), + ...overrides, }; }, - createKindColumn(): TableColumn { + createKindColumn(overrides: ColumnOptions = {}): TableColumn { return { title: 'Kind', field: 'entity.kind', + ...overrides, }; }, - createTypeColumn(): TableColumn { + createTypeColumn(overrides: ColumnOptions = {}): TableColumn { return { title: 'Type', field: 'entity.spec.type', + ...overrides, }; }, }; From edcd757d14708cb8de75278dd28a7d9c68a6dbe0 Mon Sep 17 00:00:00 2001 From: "Stephen A. Wilson" Date: Thu, 13 Jun 2024 11:13:36 -0400 Subject: [PATCH 013/212] remove techdocs-backend from changeset Signed-off-by: Stephen A. Wilson --- .changeset/fuzzy-socks-do.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/fuzzy-socks-do.md b/.changeset/fuzzy-socks-do.md index 02d2e4dca9..aabc10f0cd 100644 --- a/.changeset/fuzzy-socks-do.md +++ b/.changeset/fuzzy-socks-do.md @@ -1,5 +1,4 @@ --- -'@backstage/plugin-techdocs-backend': patch '@backstage/plugin-techdocs': patch --- From e9551a759824d57c4c8a62dd832de4e65e27d253 Mon Sep 17 00:00:00 2001 From: "Stephen A. Wilson" Date: Thu, 13 Jun 2024 13:05:09 -0400 Subject: [PATCH 014/212] revert changes to match prior work in CatalogTable Signed-off-by: Stephen A. Wilson --- plugins/techdocs/api-report.md | 14 ++++++++++++++ .../src/home/components/Tables/columns.tsx | 18 ++++++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index ebd6adcd3c..90834c6fcb 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -78,6 +78,13 @@ export type DocsGroupConfig = { export const DocsTable: { (props: DocsTableProps): React_2.JSX.Element | null; columns: { + createTitleColumn( + options?: + | { + hidden?: boolean | undefined; + } + | undefined, + ): TableColumn; createNameColumn(): TableColumn; createOwnerColumn(): TableColumn; createKindColumn(): TableColumn; @@ -142,6 +149,13 @@ export type EntityListDocsGridPageProps = { export const EntityListDocsTable: { (props: EntityListDocsTableProps): React_2.JSX.Element; columns: { + createTitleColumn( + options?: + | { + hidden?: boolean | undefined; + } + | undefined, + ): TableColumn; createNameColumn(): TableColumn; createOwnerColumn(): TableColumn; createKindColumn(): TableColumn; diff --git a/plugins/techdocs/src/home/components/Tables/columns.tsx b/plugins/techdocs/src/home/components/Tables/columns.tsx index 3deb4d9b33..b9d22705d3 100644 --- a/plugins/techdocs/src/home/components/Tables/columns.tsx +++ b/plugins/techdocs/src/home/components/Tables/columns.tsx @@ -24,18 +24,17 @@ function customTitle(entity: Entity): string { return entity.metadata.title || entity.metadata.name; } -type ColumnOptions = Partial>; - /** * Not directly exported, but through DocsTable.columns and EntityListDocsTable.columns * * @public */ export const columnFactories = { - createTitleColumn(overrides: ColumnOptions = {}): TableColumn { + createTitleColumn(options?: { hidden?: boolean }): TableColumn { return { title: 'Document', field: 'entity.metadata.title', + hidden: options?.hidden, highlight: true, searchable: true, render: (row: DocsTableRow) => ( @@ -46,10 +45,9 @@ export const columnFactories = { subvalue={row.entity.metadata.description} /> ), - ...overrides, }; }, - createNameColumn(overrides: ColumnOptions = {}): TableColumn { + createNameColumn(): TableColumn { return { title: 'Document', field: 'entity.metadata.name', @@ -63,10 +61,9 @@ export const columnFactories = { subvalue={row.entity.metadata.description} /> ), - ...overrides, }; }, - createOwnerColumn(overrides: ColumnOptions = {}): TableColumn { + createOwnerColumn(): TableColumn { return { title: 'Owner', field: 'resolved.ownedByRelationsTitle', @@ -76,21 +73,18 @@ export const columnFactories = { defaultKind="group" /> ), - ...overrides, }; }, - createKindColumn(overrides: ColumnOptions = {}): TableColumn { + createKindColumn(): TableColumn { return { title: 'Kind', field: 'entity.kind', - ...overrides, }; }, - createTypeColumn(overrides: ColumnOptions = {}): TableColumn { + createTypeColumn(): TableColumn { return { title: 'Type', field: 'entity.spec.type', - ...overrides, }; }, }; From d42b410050645dcac54a3bbefa6f20e219b7a807 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 18 Jun 2024 11:53:20 +0200 Subject: [PATCH 015/212] chore: initial pass at implementation from mob with simple test Signed-off-by: blam --- .../src/wiring/createExtensionKind.test.tsx | 44 +++++ .../src/wiring/createExtensionKind.ts | 164 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx create mode 100644 packages/frontend-plugin-api/src/wiring/createExtensionKind.ts diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx new file mode 100644 index 0000000000..d841ea2c62 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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. + */ +import React from 'react'; +import { coreExtensionData } from './coreExtensionData'; +import { createExtensionKind } from './createExtensionKind'; + +describe('createExtensionKind', () => { + it('should allow creation of extension kinds', () => { + const TestExtension = createExtensionKind({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(_, props: { text: string }) { + return { + element:

{props.text}

, + }; + }, + }); + + const extension = TestExtension.new({ + name: 'my-extension', + props: { + text: 'Hello, world!', + }, + }); + + expect(extension).toBeDefined(); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts new file mode 100644 index 0000000000..31a8e5da64 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -0,0 +1,164 @@ +import { AppNode } from '../apis'; +import { PortableSchema } from '../schema'; +import { Expand } from '../types'; +import { + AnyExtensionDataMap, + AnyExtensionInputMap, + ExtensionDataValues, + ResolvedExtensionInputs, + createExtension, +} from './createExtension'; + +/* + * Copyright 2024 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. + */ +interface ExtensionKindOptions< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + kind: string; + namespace?: string; + name?: string; + attachTo: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output: TOutput; + configSchema?: PortableSchema; + factory( + options: { + node: AppNode; + config: TConfig; + inputs: Expand>; + }, + props: TProps, + ): Expand>; +} + +class ExtensionKind< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + static create< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, + >( + options: ExtensionKindOptions, + ): ExtensionKind { + return new ExtensionKind(options); + } + + private constructor( + private readonly options: ExtensionKindOptions< + TProps, + TInputs, + TOutput, + TConfig + >, + ) {} + + public new(options: { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema; + props: TProps; + factory?( + options: { + node: AppNode; + config: TConfig; + inputs: Expand>; + orignalFactory( + options?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand>; + }, + props?: TProps, + ): Expand>; + }, + props: TProps, + ): Expand>; + }) { + return createExtension({ + kind: this.options.kind, + namespace: options.namespace ?? this.options.namespace, + name: options.name ?? this.options.name, + attachTo: options.attachTo ?? this.options.attachTo, + disabled: options.disabled ?? this.options.disabled, + inputs: options.inputs ?? this.options.inputs, + output: options.output ?? this.options.output, + configSchema: options.configSchema ?? this.options.configSchema, // TODO: some config merging or smth + factory: ({ node, config, inputs }) => { + if (options.factory) { + return options.factory( + { + node, + config, + inputs, + orignalFactory: ( + innerOptions?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand>; + }, + innerProps?: TProps, + ) => + this.options.factory( + { + node: innerOptions?.node ?? node, + config: innerOptions?.config ?? config, + inputs: innerOptions?.inputs ?? inputs, + }, + innerProps ?? options.props, + ), + }, + options.props, + ); + } + + return this.options.factory( + { + node, + config, + inputs, + }, + options.props, + ); + }, + }); + } +} + +/** + * A simpler replacement for wrapping up `createExtension` inside a kind or type. This allows for a cleaner API for creating + * types and instances of those types. + */ +export function createExtensionKind< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +>(options: ExtensionKindOptions) { + return ExtensionKind.create(options); +} From d6c9f12ab3bc54f077e1d2baac8396c61c1c9cbf Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 18 Jun 2024 13:27:31 +0200 Subject: [PATCH 016/212] chore: add some simple tests Signed-off-by: blam --- .../src/wiring/createExtensionKind.test.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx index d841ea2c62..ea9459f3f6 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { coreExtensionData } from './coreExtensionData'; import { createExtensionKind } from './createExtensionKind'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; describe('createExtensionKind', () => { it('should allow creation of extension kinds', () => { @@ -40,5 +41,40 @@ describe('createExtensionKind', () => { }); expect(extension).toBeDefined(); + + const { container } = createExtensionTester(extension).render(); + expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); + }); + + it('should allow overriding of the default factory', () => { + const TestExtension = createExtensionKind({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(_, props: { text: string }) { + return { + element:

{props.text}

, + }; + }, + }); + + const extension = TestExtension.new({ + name: 'my-extension', + props: { + text: 'Hello, world!', + }, + factory(_, props: { text: string }) { + return { + element:

{props.text}

, + }; + }, + }); + + expect(extension).toBeDefined(); + + const { container } = createExtensionTester(extension).render(); + expect(container.querySelector('h2')).toHaveTextContent('Hello, world!'); }); }); From ad4591af6615ac1e6281e424ea3b7ec90e9ca4f9 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 18 Jun 2024 13:29:57 +0200 Subject: [PATCH 017/212] chore: test creted extension Signed-off-by: blam --- .../src/wiring/createExtensionKind.test.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx index ea9459f3f6..2d5b218082 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx @@ -40,7 +40,31 @@ describe('createExtensionKind', () => { }, }); - expect(extension).toBeDefined(); + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + attachTo: { + id: 'test', + input: 'default', + }, + configSchema: undefined, + disabled: false, + inputs: {}, + kind: 'test-extension', + name: 'my-extension', + namespace: undefined, + output: { + element: { + $$type: '@backstage/ExtensionDataRef', + config: {}, + id: 'core.reactElement', + optional: expect.any(Function), + toString: expect.any(Function), + }, + }, + factory: expect.any(Function), + toString: expect.any(Function), + version: 'v1', + }); const { container } = createExtensionTester(extension).render(); expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); From 71172a4caf3044b5379bd956c64d3535e1bc6528 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 18 Jun 2024 14:13:34 +0200 Subject: [PATCH 018/212] chore: added api-reports Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 95 +++++++++++++++++++ .../src/wiring/createExtensionKind.ts | 13 ++- .../frontend-plugin-api/src/wiring/index.ts | 5 + 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 3e19573d95..b4c761ec05 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -522,6 +522,16 @@ export function createExtensionInput< } >; +// @public +export function createExtensionKind< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +>( + options: ExtensionKindOptions, +): ExtensionKind; + // @public (undocumented) export interface CreateExtensionOptions< TOutput extends AnyExtensionDataMap, @@ -913,6 +923,91 @@ export interface ExtensionInput< extensionData: TExtensionData; } +// @public (undocumented) +export class ExtensionKind< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + static create< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, + >( + options: ExtensionKindOptions, + ): ExtensionKind; + // (undocumented) + new(options: { + namespace?: string; + name?: string; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema; + props: TProps; + factory?( + options: { + node: AppNode; + config: TConfig; + inputs: Expand>; + orignalFactory( + options?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand>; + }, + props?: TProps, + ): Expand>; + }, + props: TProps, + ): Expand>; + }): ExtensionDefinition; +} + +// @public (undocumented) +export interface ExtensionKindOptions< + TProps, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + attachTo: { + id: string; + input: string; + }; + // (undocumented) + configSchema?: PortableSchema; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory( + options: { + node: AppNode; + config: TConfig; + inputs: Expand>; + }, + props: TProps, + ): Expand>; + // (undocumented) + inputs?: TInputs; + // (undocumented) + kind: string; + // (undocumented) + name?: string; + // (undocumented) + namespace?: string; + // (undocumented) + output: TOutput; +} + // @public (undocumented) export interface ExtensionOverrides { // (undocumented) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts index 31a8e5da64..2067d18c04 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -24,7 +24,11 @@ import { * See the License for the specific language governing permissions and * limitations under the License. */ -interface ExtensionKindOptions< + +/** + * @public + */ +export interface ExtensionKindOptions< TProps, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, @@ -48,7 +52,10 @@ interface ExtensionKindOptions< ): Expand>; } -class ExtensionKind< +/** + * @public + */ +export class ExtensionKind< TProps, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, @@ -153,6 +160,8 @@ class ExtensionKind< /** * A simpler replacement for wrapping up `createExtension` inside a kind or type. This allows for a cleaner API for creating * types and instances of those types. + * + * @public */ export function createExtensionKind< TProps, diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 61f821c7ce..e8c9afbaae 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -48,3 +48,8 @@ export { type FeatureFlagConfig, type FrontendFeature, } from './types'; +export { + type ExtensionKindOptions, + ExtensionKind, + createExtensionKind, +} from './createExtensionKind'; From 4e53ad666d3c39d53ad4b81d7b6869a23dc1d5fa Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 18 Jun 2024 14:15:39 +0200 Subject: [PATCH 019/212] chore: add changeset Signed-off-by: blam --- .changeset/dry-squids-tap.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .changeset/dry-squids-tap.md diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md new file mode 100644 index 0000000000..a9eb7efb3b --- /dev/null +++ b/.changeset/dry-squids-tap.md @@ -0,0 +1,26 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Introduce a new way to create extension types and kinds, with `createExtensionKind`. + +This allows the creation of extension with the following pattern: + +```tsx +// create the extension kind +const TestExtension = createExtensionKind({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(_, props: { text: string }) { + return { + element:

{props.text}

, + }; + }, +}); + +// create an instance of the extension kind with props +const testExtension = TestExtension.create({ text: 'Hello World' }); +``` From 8839381d6a0980df52604bc6fd8cca5a44931afc Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Fri, 21 Jun 2024 00:41:13 -0400 Subject: [PATCH 020/212] Add scaffolder option to display object items in separate rows Signed-off-by: Stephen Glass --- .changeset/late-games-protect.md | 5 + .../ReviewState/ReviewState.test.tsx | 173 ++++++++++++++++++ .../components/ReviewState/ReviewState.tsx | 63 ++++++- 3 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 .changeset/late-games-protect.md diff --git a/.changeset/late-games-protect.md b/.changeset/late-games-protect.md new file mode 100644 index 0000000000..584780f836 --- /dev/null +++ b/.changeset/late-games-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +Add scaffolder option to display object items in separate rows on review page diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx index e9277b2230..dfa1c9c097 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx @@ -202,4 +202,177 @@ describe('ReviewState', () => { expect(queryByRole('row', { name: 'Name type4' })).toBeInTheDocument(); }); + + it('should display exploded object in separate rows', async () => { + const formState = { + name: { + foo: 'type3', + bar: 'type4', + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: { + foo: { + type: 'string', + default: 'type1', + }, + bar: { + type: 'string', + default: 'type2', + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + description: 'asd', + }, + ]; + + const { queryByRole } = render( + , + ); + + expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); + }); + + it('should display exploded nested objects', async () => { + const formState = { + name: { + foo: 'type3', + bar: 'type4', + example: { + test: 'type6', + }, + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: { + foo: { + type: 'string', + default: 'type1', + }, + bar: { + type: 'string', + default: 'type2', + }, + example: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: { + test: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + description: 'asd', + }, + ]; + + const { queryByRole } = render( + , + ); + + expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Test type6' })).toBeInTheDocument(); + }); + + it('should display partially exploded nested objects', async () => { + const formState = { + name: { + foo: 'type3', + bar: 'type4', + example: { + test: 'type6', + }, + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: { + foo: { + type: 'string', + default: 'type1', + }, + bar: { + type: 'string', + default: 'type2', + }, + example: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + description: 'asd', + }, + ]; + + const { queryByRole } = render( + , + ); + + expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Test type6' })).not.toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index e3c7bc303e..ac7ac989e3 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { StructuredMetadataTable } from '@backstage/core-components'; -import { JsonObject } from '@backstage/types'; +import { JsonObject, JsonValue } from '@backstage/types'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; @@ -28,6 +28,39 @@ export type ReviewStateProps = { formState: JsonObject; }; +function flattenObject( + obj: JsonObject, + prefix: string, + schema: JSONSchema, + formState: JsonObject, +): [string, JsonValue | undefined][] { + return Object.entries(obj).flatMap(([key, value]) => { + const prefixedKey = prefix ? `${prefix}/${key}` : key; + + const definitionInSchema = schema.getSchema({ + pointer: `#/${prefixedKey}`, + data: formState, + }); + + if (definitionInSchema) { + const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; + + // Recurse into nested objects + if ( + backstageReviewOptions && + backstageReviewOptions.explode && + typeof value === 'object' && + value !== null && + !Array.isArray(value) + ) { + return flattenObject(value, prefixedKey, schema, formState); + } + } + + return [[key, value]]; + }); +} + /** * The component used by the {@link Stepper} to render the review step. * @alpha @@ -35,7 +68,7 @@ export type ReviewStateProps = { export const ReviewState = (props: ReviewStateProps) => { const reviewData = Object.fromEntries( Object.entries(props.formState) - .map(([key, value]) => { + .flatMap(([key, value]) => { for (const step of props.schemas) { const parsedSchema = new JSONSchema(step.mergedSchema); const definitionInSchema = parsedSchema.getSchema({ @@ -49,30 +82,42 @@ export const ReviewState = (props: ReviewStateProps) => { if (backstageReviewOptions) { if (backstageReviewOptions.mask) { - return [key, backstageReviewOptions.mask]; + return [[key, backstageReviewOptions.mask]]; } if (backstageReviewOptions.show === false) { return []; } + if ( + backstageReviewOptions.explode && + typeof value === 'object' && + value !== null && + !Array.isArray(value) + ) { + return flattenObject(value, key, parsedSchema, props.formState); + } } if (definitionInSchema['ui:widget'] === 'password') { - return [key, '******']; + return [[key, '******']]; } if (definitionInSchema.enum && definitionInSchema.enumNames) { return [ - key, - definitionInSchema.enumNames[ - definitionInSchema.enum.indexOf(value) - ] || value, + [ + key, + definitionInSchema.enumNames[ + definitionInSchema.enum.indexOf(value) + ] || value, + ], ]; } } } - return [key, value]; + + return [[key, value]]; }) .filter(prop => prop.length > 0), ); + return ; }; From 6f8e1176d858545ade3340a656bd098f25113c10 Mon Sep 17 00:00:00 2001 From: Tavi Nolan Date: Fri, 21 Jun 2024 12:30:17 +0100 Subject: [PATCH 021/212] Updating documents Signed-off-by: Tavi Nolan --- docs/features/software-catalog/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 61366a91f7..d0206172e5 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -47,7 +47,7 @@ action is logged for further investigation. ### Local File (`type: file`) Configurations In addition to url locations, you can use the `file` location type to bring in content from the local file system. You should only use this for local development, test setups, and example data, not for production data. -You are also not able to use placeholders in them like `$text`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/tree/master/packages/catalog-model/examples) for an extensive example. +You are also not able to use placeholders in them like `$text`. `$json` or `$yaml`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs) for an extensive example. Here is an example pulling in the `all.yaml` file from the examples folder. Note the use of `../../` to go up two levels from the current execution path of the backend. This is typically `packages/backend/`. From e10c75aebfdc030ea3dac888ac165d93cd75d032 Mon Sep 17 00:00:00 2001 From: "Stephen A. Wilson" Date: Sat, 22 Jun 2024 15:52:58 -0400 Subject: [PATCH 022/212] Remove duplicated docs Signed-off-by: Stephen A. Wilson --- .../catalog-info.yaml | 12 -- .../docs/code/code-sample.md | 31 ----- .../docs/extensions.md | 112 ------------------ .../docs/images/backstage-logo-cncf.svg | 1 - .../docs/index.md | 55 --------- .../docs/sub-page.md | 12 -- .../mkdocs.yml | 12 -- .../documented-component/catalog-info.yaml | 2 +- 8 files changed, 1 insertion(+), 236 deletions(-) delete mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml delete mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md delete mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md delete mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg delete mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md delete mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md delete mode 100644 plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml deleted file mode 100644 index 1a1d65ba39..0000000000 --- a/plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: documented-with-title - title: Fizzbuzz Docs - description: A TechDocs with a title different than it's name - annotations: - backstage.io/techdocs-ref: dir:. -spec: - type: service - lifecycle: experimental - owner: user:guest diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md deleted file mode 100644 index f41ab1d506..0000000000 --- a/plugins/techdocs-backend/examples/documented-component-with-title/docs/code/code-sample.md +++ /dev/null @@ -1,31 +0,0 @@ -# Sample Code - -This page provides some sample code which may be used in your example component. - -This code uses TypeScript, and the Markdown code fence to wrap the code. - -```typescript -const serviceEntityPage = ( - - - - - - - - - - - - -); -``` - -Here is an example of Python code: - -```python -def getUsersInGroup(targetGroup, secure=False): - - if __debug__: - print('targetGroup=[' + targetGroup + ']') -``` diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md deleted file mode 100644 index eb9abca74a..0000000000 --- a/plugins/techdocs-backend/examples/documented-component-with-title/docs/extensions.md +++ /dev/null @@ -1,112 +0,0 @@ -# Plugins & Extensions - -Just by including the TechDocs Core Plugin to your MkDocs site included with Backstage, -you gain the immediate use of a variety of popular plugins and extensions to MkDocs. - -For more information and full details of the available features, see the -[`mkdocs-techdocs-core` repository](https://github.com/backstage/mkdocs-techdocs-core#mkdocs-plugins-and-extensions). - -This page provides a demonstration of some of the available features. - -## Admonitions - -Admonitions are call outs that help catch a users attention. - -To define an admonition simply put the following Markdown into your content: - -``` -!!! warn - Defining admonitions can be addicting. -``` - -And they end up looking like this: - - -!!! warn - Defining admonitions can be addicting. - - -!!! note - You can learn a lot about TechDocs by just visiting the Backstage web site at - https://backstage.io/docs. - - -!!! info - TechDocs is the core feature that supports documentation as code in Backstage. - - -!!! tip - Don't forget to spell check your documentation. - -## PlantUML - -You can create dynamic UML diagrams on the fly by just specifying flow via text, -using [PlantUML](https://pypi.org/project/plantuml-markdown/). - -```plantuml format="svg" classes="uml myDiagram" alt="Backstage sample PlantUML" title="Backstage sample PlantUML" width="500px" height="250px" -User -> SCMProvider: stores -TechDocs -> SCMProvider: prepares -TechDocs -> TechDocs: generates -TechDocs -> CloudStorage: publishes -CloudStorage -> Backstage: displays -``` - -## Pymdownx Extensions - -Pymdownx (Python Markdown extensions) are a variety of smaller additions. - -### Details - - -??? note "What is the answer to life, the universe, and everything? (click me for the answer)" - The answer is 42. - - -??? note "What is 4 plus 4?" - The answer is 8. - - -???+ note "How do I get support?" - You can get support by opening an issue in this repository. This detail is open by default - so it's more easily visible without requiring the user to click to open it. - -### Task Lists - -Automatic rendering of Markdown task lists. - -- [x] Phase 1 -- [x] Phase 2 -- [ ] Phase 3 - -### Emojis - -Very nice job on documentation! :thumbsup: - -I've read a lot of documentation, but I love :heart: this document. - -Weather: :sunny: :umbrella: :cloud: :snowflake: - -Animals: :tiger: :horse: :turtle: :wolf: :frog: - -### Attributes - -[A Download Link](./images/backstage-logo-cncf.svg){: download } - -![A Scaled Image](./images/backstage-logo-cncf.svg){: style="width: 100px" } - -### MDX truly sane lists - -- `attributes` - -- `customer` - - `first_name` - - `test` - - `family_name` - - `email` -- `person` - - `first_name` - - `family_name` - - `birth_date` -- `subscription_id` - -- `request` diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg b/plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg deleted file mode 100644 index b5ff591d1b..0000000000 --- a/plugins/techdocs-backend/examples/documented-component-with-title/docs/images/backstage-logo-cncf.svg +++ /dev/null @@ -1 +0,0 @@ -05 Logo_Black \ No newline at end of file diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md deleted file mode 100644 index 6295a69406..0000000000 --- a/plugins/techdocs-backend/examples/documented-component-with-title/docs/index.md +++ /dev/null @@ -1,55 +0,0 @@ -# Welcome! - -This is a basic example of documentation. It is intended as a showcase of some of the -features that TechDocs provides out of the box. - -You can see also: - -- [A sub page](sub-page.md) -- [Inline code examples](code/code-sample.md) -- [Plugin & Extension examples](extensions.md) - Diagrams, emojis, visual formatting. - -## Basic Markdown - -Headings: - -# h1 - -## h2 - -### h3 - -#### h4 - -##### h5 - -###### h6 - -Here is a bulleted list: - -- Item one -- Item two -- Item Three - -Check out the [Markdown Guide](https://www.markdownguide.org/) to learn more about how to -simply create documentation. - -You can also learn more about how to configure and setup this documentation in Backstage, -[read up on the TechDocs Overview](https://backstage.io/docs/features/techdocs/). - -## Image Example - -This documentation is powered by Backstage's TechDocs feature: - -![Backstage Logo](images/backstage-logo-cncf.svg) - -## Table Example - -While this documentation isn't comprehensive, in the future it should cover the following -topics outlined in this example table: - -| Topic | Description | -| ------- | ------------------------------------------------------------ | -| Topic 1 | An introductory topic to help you learn about the component. | -| Topic 2 | A more detailed topic that explains more information. | -| Topic 3 | A final topic that provides conclusions and lessons learned. | diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md b/plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md deleted file mode 100644 index 987f5e8a95..0000000000 --- a/plugins/techdocs-backend/examples/documented-component-with-title/docs/sub-page.md +++ /dev/null @@ -1,12 +0,0 @@ -# Another page in our documentation - -This sub-page can be used to elaborate on a specific part of the component. - -## Details - -It is linked off the main page, and due to its inclusion in the `mkdocs.yml` file, -becomes part of the auto-generated site navigation. - -## MkDocs - -Visit https://www.mkdocs.org for more information about the MkDocs tool. diff --git a/plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml deleted file mode 100644 index 1a159a4d12..0000000000 --- a/plugins/techdocs-backend/examples/documented-component-with-title/mkdocs.yml +++ /dev/null @@ -1,12 +0,0 @@ -site_name: 'Example Documentation' -repo_url: https://github.com/backstage/backstage -edit_uri: edit/master/plugins/techdocs-backend/examples/documented-component/docs - -nav: - - Home: index.md - - Subpage: sub-page.md - - 'Code Sample': code/code-sample.md - - Extensions: extensions.md - -plugins: - - techdocs-core diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index e6d14025e1..6b30213a60 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -2,7 +2,7 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: documented-component - title: Documented Component + title: Example Docs description: A Service with TechDocs documentation annotations: backstage.io/techdocs-ref: dir:. From f229d94b8abd8a79f651266705740e7b8ec8cd17 Mon Sep 17 00:00:00 2001 From: Tavi Nolan Date: Mon, 24 Jun 2024 13:29:53 +0100 Subject: [PATCH 023/212] changing period to comma Signed-off-by: Tavi Nolan --- docs/features/software-catalog/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index d0206172e5..9524886a49 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -47,7 +47,7 @@ action is logged for further investigation. ### Local File (`type: file`) Configurations In addition to url locations, you can use the `file` location type to bring in content from the local file system. You should only use this for local development, test setups, and example data, not for production data. -You are also not able to use placeholders in them like `$text`. `$json` or `$yaml`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs) for an extensive example. +You are also not able to use placeholders in them like `$text`, `$json` or `$yaml`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs) for an extensive example. Here is an example pulling in the `all.yaml` file from the examples folder. Note the use of `../../` to go up two levels from the current execution path of the backend. This is typically `packages/backend/`. From d1488caaa309df834095bde5a48bf9f99ad797af Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 24 Jun 2024 08:52:29 -0400 Subject: [PATCH 024/212] Revert change to app-config.yaml Signed-off-by: Stephen --- app-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 69145cd896..cb70170541 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -297,9 +297,6 @@ catalog: # Example component for TechDocs - type: file target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml - # Example component for TechDocs with a title - - type: file - target: ../../plugins/techdocs-backend/examples/documented-component-with-title/catalog-info.yaml # Example component for ADRs - type: file target: ../../plugins/adr/examples/component/catalog-info.yaml From 546700b0c1acd565c089c6b8a9a89228d3f97909 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 26 Jun 2024 14:15:45 +0200 Subject: [PATCH 025/212] chore: some PR feedback suggestions Signed-off-by: blam --- .changeset/dry-squids-tap.md | 9 +- packages/frontend-plugin-api/api-report.md | 36 +++---- .../src/wiring/createExtension.ts | 4 +- .../src/wiring/createExtensionKind.test.tsx | 17 ++-- .../src/wiring/createExtensionKind.ts | 93 ++++++++++--------- .../frontend-plugin-api/src/wiring/index.ts | 2 +- 6 files changed, 84 insertions(+), 77 deletions(-) diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md index a9eb7efb3b..5193e1f204 100644 --- a/.changeset/dry-squids-tap.md +++ b/.changeset/dry-squids-tap.md @@ -8,7 +8,7 @@ This allows the creation of extension with the following pattern: ```tsx // create the extension kind -const TestExtension = createExtensionKind({ +const TestExtensionKind = createExtensionKind({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, output: { @@ -22,5 +22,10 @@ const TestExtension = createExtensionKind({ }); // create an instance of the extension kind with props -const testExtension = TestExtension.create({ text: 'Hello World' }); +const testExtension = TestExtensionKind.new({ + name: 'foo', + options: { + text: 'Hello World', + }, +}); ``` diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index b4c761ec05..48718f5375 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -524,13 +524,13 @@ export function createExtensionInput< // @public export function createExtensionKind< - TProps, + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, >( - options: ExtensionKindOptions, -): ExtensionKind; + options: ExtensionKindOptions, +): ExtensionKind; // @public (undocumented) export interface CreateExtensionOptions< @@ -548,7 +548,7 @@ export interface CreateExtensionOptions< // (undocumented) disabled?: boolean; // (undocumented) - factory(options: { + factory(context: { node: AppNode; config: TConfig; inputs: Expand>; @@ -923,24 +923,24 @@ export interface ExtensionInput< extensionData: TExtensionData; } -// @public (undocumented) +// @public export class ExtensionKind< - TProps, + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, > { // (undocumented) static create< - TProps, + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, >( - options: ExtensionKindOptions, - ): ExtensionKind; + options: ExtensionKindOptions, + ): ExtensionKind; // (undocumented) - new(options: { + new(args: { namespace?: string; name?: string; attachTo?: { @@ -951,29 +951,29 @@ export class ExtensionKind< inputs?: TInputs; output?: TOutput; configSchema?: PortableSchema; - props: TProps; + options: TOptions; factory?( - options: { + context: { node: AppNode; config: TConfig; inputs: Expand>; orignalFactory( - options?: { + context?: { node?: AppNode; config?: TConfig; inputs?: Expand>; }, - props?: TProps, + options?: TOptions, ): Expand>; }, - props: TProps, + options: TOptions, ): Expand>; }): ExtensionDefinition; } // @public (undocumented) export interface ExtensionKindOptions< - TProps, + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, @@ -989,12 +989,12 @@ export interface ExtensionKindOptions< disabled?: boolean; // (undocumented) factory( - options: { + context: { node: AppNode; config: TConfig; inputs: Expand>; }, - props: TProps, + options: TOptions, ): Expand>; // (undocumented) inputs?: TInputs; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index be76fbc069..28dc6a4e29 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -91,7 +91,7 @@ export interface CreateExtensionOptions< inputs?: TInputs; output: TOutput; configSchema?: PortableSchema; - factory(options: { + factory(context: { node: AppNode; config: TConfig; inputs: Expand>; @@ -115,7 +115,7 @@ export interface InternalExtensionDefinition readonly version: 'v1'; readonly inputs: AnyExtensionInputMap; readonly output: AnyExtensionDataMap; - factory(options: { + factory(context: { node: AppNode; config: TConfig; inputs: ResolvedExtensionInputs; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx index 2d5b218082..3bdcd3a7cc 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { coreExtensionData } from './coreExtensionData'; import { createExtensionKind } from './createExtensionKind'; @@ -26,16 +27,16 @@ describe('createExtensionKind', () => { output: { element: coreExtensionData.reactElement, }, - factory(_, props: { text: string }) { + factory(_, options: { text: string }) { return { - element:

{props.text}

, + element:

{options.text}

, }; }, }); const extension = TestExtension.new({ name: 'my-extension', - props: { + options: { text: 'Hello, world!', }, }); @@ -77,21 +78,21 @@ describe('createExtensionKind', () => { output: { element: coreExtensionData.reactElement, }, - factory(_, props: { text: string }) { + factory(_, options: { text: string }) { return { - element:

{props.text}

, + element:

{options.text}

, }; }, }); const extension = TestExtension.new({ name: 'my-extension', - props: { + options: { text: 'Hello, world!', }, - factory(_, props: { text: string }) { + factory(_, options: { text: string }) { return { - element:

{props.text}

, + element:

{options.text}

, }; }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts index 2067d18c04..52bf711822 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -1,14 +1,3 @@ -import { AppNode } from '../apis'; -import { PortableSchema } from '../schema'; -import { Expand } from '../types'; -import { - AnyExtensionDataMap, - AnyExtensionInputMap, - ExtensionDataValues, - ResolvedExtensionInputs, - createExtension, -} from './createExtension'; - /* * Copyright 2024 The Backstage Authors * @@ -25,11 +14,22 @@ import { * limitations under the License. */ +import { AppNode } from '../apis'; +import { PortableSchema } from '../schema'; +import { Expand } from '../types'; +import { + AnyExtensionDataMap, + AnyExtensionInputMap, + ExtensionDataValues, + ResolvedExtensionInputs, + createExtension, +} from './createExtension'; + /** * @public */ -export interface ExtensionKindOptions< - TProps, +export interface CreateExtensionKindOptions< + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, @@ -43,45 +43,46 @@ export interface ExtensionKindOptions< output: TOutput; configSchema?: PortableSchema; factory( - options: { + context: { node: AppNode; config: TConfig; inputs: Expand>; }, - props: TProps, + options: TOptions, ): Expand>; } /** + * TODO: should we export an interface instead of a concrete class? * @public */ export class ExtensionKind< - TProps, + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, > { static create< - TProps, + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, >( - options: ExtensionKindOptions, - ): ExtensionKind { + options: CreateExtensionKindOptions, + ): ExtensionKind { return new ExtensionKind(options); } private constructor( - private readonly options: ExtensionKindOptions< - TProps, + private readonly options: CreateExtensionKindOptions< + TOptions, TInputs, TOutput, TConfig >, ) {} - public new(options: { + public new(args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; @@ -89,58 +90,58 @@ export class ExtensionKind< inputs?: TInputs; output?: TOutput; configSchema?: PortableSchema; - props: TProps; + options: TOptions; factory?( - options: { + context: { node: AppNode; config: TConfig; inputs: Expand>; orignalFactory( - options?: { + context?: { node?: AppNode; config?: TConfig; inputs?: Expand>; }, - props?: TProps, + options?: TOptions, ): Expand>; }, - props: TProps, + options: TOptions, ): Expand>; }) { return createExtension({ kind: this.options.kind, - namespace: options.namespace ?? this.options.namespace, - name: options.name ?? this.options.name, - attachTo: options.attachTo ?? this.options.attachTo, - disabled: options.disabled ?? this.options.disabled, - inputs: options.inputs ?? this.options.inputs, - output: options.output ?? this.options.output, - configSchema: options.configSchema ?? this.options.configSchema, // TODO: some config merging or smth + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, + attachTo: args.attachTo ?? this.options.attachTo, + disabled: args.disabled ?? this.options.disabled, + inputs: args.inputs ?? this.options.inputs, + output: args.output ?? this.options.output, + configSchema: args.configSchema ?? this.options.configSchema, // TODO: some config merging or smth factory: ({ node, config, inputs }) => { - if (options.factory) { - return options.factory( + if (args.factory) { + return args.factory( { node, config, inputs, orignalFactory: ( - innerOptions?: { + innerContext?: { node?: AppNode; config?: TConfig; inputs?: Expand>; }, - innerProps?: TProps, + innerOptions?: TOptions, ) => this.options.factory( { - node: innerOptions?.node ?? node, - config: innerOptions?.config ?? config, - inputs: innerOptions?.inputs ?? inputs, + node: innerContext?.node ?? node, + config: innerContext?.config ?? config, + inputs: innerContext?.inputs ?? inputs, }, - innerProps ?? options.props, + innerOptions ?? args.options, ), }, - options.props, + args.options, ); } @@ -150,7 +151,7 @@ export class ExtensionKind< config, inputs, }, - options.props, + args.options, ); }, }); @@ -164,10 +165,10 @@ export class ExtensionKind< * @public */ export function createExtensionKind< - TProps, + TOptions, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, ->(options: ExtensionKindOptions) { +>(options: CreateExtensionKindOptions) { return ExtensionKind.create(options); } diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index e8c9afbaae..8fe9dc3a54 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -49,7 +49,7 @@ export { type FrontendFeature, } from './types'; export { - type ExtensionKindOptions, + type CreateExtensionKindOptions as ExtensionKindOptions, ExtensionKind, createExtensionKind, } from './createExtensionKind'; From a629fb22914852cd5f2c2e431b372833773c5156 Mon Sep 17 00:00:00 2001 From: 1337 <19777147+the-serious-programmer@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:26:29 +0000 Subject: [PATCH 026/212] Added setAllowedLocationTypes with new CatalogLocationsExtensionPoint Signed-off-by: The serious programmer <19777147+the-serious-programmer@users.noreply.github.com> Signed-off-by: 1337 <19777147+the-serious-programmer@users.noreply.github.com> --- .changeset/hip-hairs-exist.md | 6 +++++ .../src/service/CatalogPlugin.ts | 26 +++++++++++++++++++ plugins/catalog-node/api-report-alpha.md | 8 ++++++ plugins/catalog-node/src/alpha.ts | 2 ++ plugins/catalog-node/src/extensions.ts | 19 ++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 .changeset/hip-hairs-exist.md diff --git a/.changeset/hip-hairs-exist.md b/.changeset/hip-hairs-exist.md new file mode 100644 index 0000000000..d8cb939dfb --- /dev/null +++ b/.changeset/hip-hairs-exist.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-node': patch +--- + +Added setAllowedLocationTypes while introducing a new extension point called CatalogLocationsExtensionPoint diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index aebd969701..2f5fd9103d 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -28,6 +28,8 @@ import { catalogPermissionExtensionPoint, CatalogProcessingExtensionPoint, catalogProcessingExtensionPoint, + CatalogLocationsExtensionPoint, + catalogLocationsExtensionPoint, } from '@backstage/plugin-catalog-node/alpha'; import { CatalogProcessor, @@ -41,6 +43,20 @@ import { merge } from 'lodash'; import { Permission } from '@backstage/plugin-permission-common'; import { ForwardedError } from '@backstage/errors'; +class CatalogLocationsExtensionPointImpl + implements CatalogLocationsExtensionPoint +{ + #locationTypes = new Array(); + + setAllowedLocationTypes(locationTypes: Array) { + this.#locationTypes = locationTypes; + } + + get allowedLocationTypes() { + return this.#locationTypes; + } +} + class CatalogProcessingExtensionPointImpl implements CatalogProcessingExtensionPoint { @@ -199,6 +215,12 @@ export const catalogPlugin = createBackendPlugin({ const modelExtensions = new CatalogModelExtensionPointImpl(); env.registerExtensionPoint(catalogModelExtensionPoint, modelExtensions); + const locationTypeExtensions = new CatalogLocationsExtensionPointImpl(); + env.registerExtensionPoint( + catalogLocationsExtensionPoint, + locationTypeExtensions, + ); + env.registerInit({ deps: { logger: coreServices.logger, @@ -271,6 +293,10 @@ export const catalogPlugin = createBackendPlugin({ builder.addPermissionRules(...permissionExtensions.permissionRules); builder.setFieldFormatValidators(modelExtensions.fieldValidators); + builder.setAllowedLocationTypes( + locationTypeExtensions.allowedLocationTypes, + ); + const { processingEngine, router } = await builder.build(); lifecycle.addStartupHook(async () => { diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index b61d1b1d27..e14f55c196 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -34,6 +34,14 @@ export interface CatalogAnalysisExtensionPoint { // @alpha (undocumented) export const catalogAnalysisExtensionPoint: ExtensionPoint; +// @alpha (undocumented) +export interface CatalogLocationsExtensionPoint { + setAllowedLocationTypes(locationTypes: Array): void; +} + +// @alpha (undocumented) +export const catalogLocationsExtensionPoint: ExtensionPoint; + // @alpha (undocumented) export interface CatalogModelExtensionPoint { setEntityDataParser(parser: CatalogProcessorParser): void; diff --git a/plugins/catalog-node/src/alpha.ts b/plugins/catalog-node/src/alpha.ts index 91b5bb99b6..b75ec48a7f 100644 --- a/plugins/catalog-node/src/alpha.ts +++ b/plugins/catalog-node/src/alpha.ts @@ -15,6 +15,8 @@ */ export { catalogServiceRef } from './catalogService'; +export type { CatalogLocationsExtensionPoint } from './extensions'; +export { catalogLocationsExtensionPoint } from './extensions'; export type { CatalogProcessingExtensionPoint } from './extensions'; export { catalogProcessingExtensionPoint } from './extensions'; export type { CatalogAnalysisExtensionPoint } from './extensions'; diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 7c1fe6c30d..a971e1588d 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -31,6 +31,25 @@ import { } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; +/** + * @alpha + */ +export interface CatalogLocationsExtensionPoint { + /** + * Allows setting custom location types, such as showcased in: https://backstage.io/docs/features/software-catalog/external-integrations/#creating-a-catalog-data-reader-processor + * @param locationTypes - List of location types to allow, default is "url" and "file" + */ + setAllowedLocationTypes(locationTypes: Array): void; +} + +/** + * @alpha + */ +export const catalogLocationsExtensionPoint = + createExtensionPoint({ + id: 'catalog.locations', + }); + /** * @alpha */ From 64e7e9409f6bc84fed0241adca226d09ac396561 Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Thu, 27 Jun 2024 21:44:08 +0200 Subject: [PATCH 027/212] feat: add oauth token option for fetch action Signed-off-by: ElaineDeMattosSilvaB --- .../urlReader/lib/GitlabUrlReader.ts | 26 +++++++-------- packages/integration/src/gitlab/core.test.ts | 32 ++++++++++++++++++- packages/integration/src/gitlab/core.ts | 17 +++++++--- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts index 615eeb20a2..35123da851 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts @@ -23,21 +23,21 @@ import { UrlReaderServiceSearchOptions, UrlReaderServiceSearchResponse, } from '@backstage/backend-plugin-api'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { + GitLabIntegration, + ScmIntegrations, getGitLabFileFetchUrl, getGitLabIntegrationRelativePath, getGitLabRequestOptions, - GitLabIntegration, - ScmIntegrations, } from '@backstage/integration'; -import fetch, { Response } from 'node-fetch'; import parseGitUrl from 'git-url-parse'; -import { Minimatch } from 'minimatch'; -import { Readable } from 'stream'; -import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { ReadTreeResponseFactory, ReaderFactory } from './types'; import { trimEnd, trimStart } from 'lodash'; +import { Minimatch } from 'minimatch'; +import fetch, { Response } from 'node-fetch'; +import { Readable } from 'stream'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; +import { ReadTreeResponseFactory, ReaderFactory } from './types'; import { parseLastModified } from './util'; /** @@ -71,14 +71,14 @@ export class GitlabUrlReader implements UrlReaderService { url: string, options?: UrlReaderServiceReadUrlOptions, ): Promise { - const { etag, lastModifiedAfter, signal } = options ?? {}; + const { etag, lastModifiedAfter, signal, token } = options ?? {}; const builtUrl = await this.getGitlabFetchUrl(url); let response: Response; try { response = await fetch(builtUrl, { headers: { - ...getGitLabRequestOptions(this.integration.config).headers, + ...getGitLabRequestOptions(this.integration.config, token).headers, ...(etag && { 'If-None-Match': etag }), ...(lastModifiedAfter && { 'If-Modified-Since': lastModifiedAfter.toUTCString(), @@ -120,7 +120,7 @@ export class GitlabUrlReader implements UrlReaderService { url: string, options?: UrlReaderServiceReadTreeOptions, ): Promise { - const { etag, signal } = options ?? {}; + const { etag, signal, token } = options ?? {}; const { ref, full_name, filepath } = parseGitUrl(url); let repoFullName = full_name; @@ -147,7 +147,7 @@ export class GitlabUrlReader implements UrlReaderService { repoFullName, )}`, ).toString(), - getGitLabRequestOptions(this.integration.config), + getGitLabRequestOptions(this.integration.config, token), ); if (!projectGitlabResponse.ok) { const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`; @@ -175,7 +175,7 @@ export class GitlabUrlReader implements UrlReaderService { )}/repository/commits?${commitsReqParams.toString()}`, ).toString(), { - ...getGitLabRequestOptions(this.integration.config), + ...getGitLabRequestOptions(this.integration.config, token), // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can @@ -209,7 +209,7 @@ export class GitlabUrlReader implements UrlReaderService { repoFullName, )}/repository/archive?${archiveReqParams.toString()}`, { - ...getGitLabRequestOptions(this.integration.config), + ...getGitLabRequestOptions(this.integration.config, token), // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 98fd9b7c13..fd374ea56f 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -17,7 +17,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { GitLabIntegrationConfig } from './config'; -import { getGitLabFileFetchUrl } from './core'; +import { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core'; const worker = setupServer(); @@ -186,4 +186,34 @@ describe('gitlab core', () => { }); }); }); + + describe('getGitLabRequestOptions', () => { + it('should return Authorization header when oauthToken is provided', () => { + const oauthToken = 'mock-oauth-token'; + const result = getGitLabRequestOptions( + configSelfHosteWithRelativePath, + oauthToken, + ); + + expect(result).toEqual({ + headers: { + Authorization: `Bearer ${oauthToken}`, + }, + }); + }); + + it('should return private-token header when oauthToken is undefined', () => { + const oauthToken = undefined; + const result = getGitLabRequestOptions( + configSelfHosteWithRelativePath, + oauthToken, + ); + + expect(result).toEqual({ + headers: { + 'PRIVATE-TOKEN': configSelfHosteWithRelativePath.token, + }, + }); + }); + }); }); diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index 040ca202d6..e940440ea4 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -14,11 +14,11 @@ * limitations under the License. */ +import fetch from 'cross-fetch'; import { getGitLabIntegrationRelativePath, GitLabIntegrationConfig, } from './config'; -import fetch from 'cross-fetch'; /** * Given a URL pointing to a file on a provider, returns a URL that is suitable @@ -51,9 +51,18 @@ export async function getGitLabFileFetchUrl( * @param config - The relevant provider config * @public */ -export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { - headers: Record; -} { +export function getGitLabRequestOptions( + config: GitLabIntegrationConfig, + oauthToken?: string, +): { headers: Record } { + if (oauthToken) { + return { + headers: { + Authorization: `Bearer ${oauthToken}`, + }, + }; + } + const { token = '' } = config; return { headers: { From 2f7a7df46b557ddb747e2952cc657aa9f46be133 Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Thu, 27 Jun 2024 22:10:40 +0200 Subject: [PATCH 028/212] feat: add possibility of a user provided glpat Signed-off-by: ElaineDeMattosSilvaB --- packages/integration/src/gitlab/core.test.ts | 22 ++++++++++++++++---- packages/integration/src/gitlab/core.ts | 18 ++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index fd374ea56f..c6e7410932 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -189,15 +189,30 @@ describe('gitlab core', () => { describe('getGitLabRequestOptions', () => { it('should return Authorization header when oauthToken is provided', () => { - const oauthToken = 'mock-oauth-token'; + const token = + 'de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54'; const result = getGitLabRequestOptions( configSelfHosteWithRelativePath, - oauthToken, + token, ); expect(result).toEqual({ headers: { - Authorization: `Bearer ${oauthToken}`, + Authorization: `Bearer ${token}`, + }, + }); + }); + + it('should return private-token header when gl-token is provided', () => { + const token = 'glpat-1234566'; + const result = getGitLabRequestOptions( + configSelfHosteWithRelativePath, + token, + ); + + expect(result).toEqual({ + headers: { + 'PRIVATE-TOKEN': token, }, }); }); @@ -208,7 +223,6 @@ describe('gitlab core', () => { configSelfHosteWithRelativePath, oauthToken, ); - expect(result).toEqual({ headers: { 'PRIVATE-TOKEN': configSelfHosteWithRelativePath.token, diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index e940440ea4..94f4b65d9f 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -53,21 +53,21 @@ export async function getGitLabFileFetchUrl( */ export function getGitLabRequestOptions( config: GitLabIntegrationConfig, - oauthToken?: string, + token?: string, ): { headers: Record } { - if (oauthToken) { + if (token) { + // If token comes from the user and starts with "gl", it's a private token (see https://docs.gitlab.com/ee/security/token_overview.html#token-prefixes) return { - headers: { - Authorization: `Bearer ${oauthToken}`, - }, + headers: token.startsWith('gl') + ? { 'PRIVATE-TOKEN': token } + : { Authorization: `Bearer ${token}` }, // Otherwise, it's a bearer token }; } - const { token = '' } = config; + // If token not provided, fetch the integration token + const { token: configToken = '' } = config; return { - headers: { - 'PRIVATE-TOKEN': token, - }, + headers: { 'PRIVATE-TOKEN': configToken }, }; } From 78c13290e470387e4e490bece3362fe86f115bc6 Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Thu, 27 Jun 2024 22:21:59 +0200 Subject: [PATCH 029/212] chore: add changeset and api-report Signed-off-by: ElaineDeMattosSilvaB --- .changeset/nice-peas-retire.md | 6 ++++++ packages/integration/api-report.md | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/nice-peas-retire.md diff --git a/.changeset/nice-peas-retire.md b/.changeset/nice-peas-retire.md new file mode 100644 index 0000000000..7b7b13706d --- /dev/null +++ b/.changeset/nice-peas-retire.md @@ -0,0 +1,6 @@ +--- +'@backstage/integration': minor +'@backstage/backend-defaults': patch +--- + +Updated `GitlabUrlReader.readUrl` and `GitlabUrlReader.readTree` to accept a user-provided token, supporting both bearer and private tokens. diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 4c8e8accf2..cf4b380ad9 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -508,7 +508,10 @@ export function getGitLabIntegrationRelativePath( ): string; // @public -export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { +export function getGitLabRequestOptions( + config: GitLabIntegrationConfig, + token?: string, +): { headers: Record; }; From 28d3cf8c9db6f72f1d7f14c0c5d34d53a0599d80 Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Thu, 27 Jun 2024 22:42:29 +0200 Subject: [PATCH 030/212] fix: change mocked bearer token format Signed-off-by: ElaineDeMattosSilvaB --- packages/integration/src/gitlab/core.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index c6e7410932..81adc29701 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -189,8 +189,7 @@ describe('gitlab core', () => { describe('getGitLabRequestOptions', () => { it('should return Authorization header when oauthToken is provided', () => { - const token = - 'de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54'; + const token = '1234567890'; const result = getGitLabRequestOptions( configSelfHosteWithRelativePath, token, From 19d495939d23984987ac1097e61faadb3b0fd2aa Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Thu, 27 Jun 2024 23:20:51 -0400 Subject: [PATCH 031/212] Extract condition check to function Signed-off-by: Stephen Glass --- .../components/ReviewState/ReviewState.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index ac7ac989e3..202e0c0fbf 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -47,11 +47,8 @@ function flattenObject( // Recurse into nested objects if ( - backstageReviewOptions && - backstageReviewOptions.explode && - typeof value === 'object' && - value !== null && - !Array.isArray(value) + backstageReviewOptions?.explode && + isJsonObject(value) ) { return flattenObject(value, prefixedKey, schema, formState); } @@ -61,6 +58,14 @@ function flattenObject( }); } +function isJsonObject(value?: JsonValue): value is JsonObject { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) + ); +} + /** * The component used by the {@link Stepper} to render the review step. * @alpha @@ -89,9 +94,7 @@ export const ReviewState = (props: ReviewStateProps) => { } if ( backstageReviewOptions.explode && - typeof value === 'object' && - value !== null && - !Array.isArray(value) + isJsonObject(value) ) { return flattenObject(value, key, parsedSchema, props.formState); } @@ -106,7 +109,7 @@ export const ReviewState = (props: ReviewStateProps) => { [ key, definitionInSchema.enumNames[ - definitionInSchema.enum.indexOf(value) + definitionInSchema.enum.indexOf(value) ] || value, ], ]; From 7193d02b32b27eed9519875a03456b687e738936 Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Fri, 28 Jun 2024 01:18:20 -0400 Subject: [PATCH 032/212] Fix formatting Signed-off-by: Stephen Glass --- .../components/ReviewState/ReviewState.tsx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index 202e0c0fbf..22ec54ac26 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -46,10 +46,7 @@ function flattenObject( const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; // Recurse into nested objects - if ( - backstageReviewOptions?.explode && - isJsonObject(value) - ) { + if (backstageReviewOptions?.explode && isJsonObject(value)) { return flattenObject(value, prefixedKey, schema, formState); } } @@ -59,11 +56,7 @@ function flattenObject( } function isJsonObject(value?: JsonValue): value is JsonObject { - return ( - typeof value === 'object' && - value !== null && - !Array.isArray(value) - ); + return typeof value === 'object' && value !== null && !Array.isArray(value); } /** @@ -92,10 +85,7 @@ export const ReviewState = (props: ReviewStateProps) => { if (backstageReviewOptions.show === false) { return []; } - if ( - backstageReviewOptions.explode && - isJsonObject(value) - ) { + if (backstageReviewOptions.explode && isJsonObject(value)) { return flattenObject(value, key, parsedSchema, props.formState); } } @@ -109,7 +99,7 @@ export const ReviewState = (props: ReviewStateProps) => { [ key, definitionInSchema.enumNames[ - definitionInSchema.enum.indexOf(value) + definitionInSchema.enum.indexOf(value) ] || value, ], ]; From c59167016378de2b5206737ab283b43d5b77887a Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Wed, 26 Jun 2024 16:07:06 -0600 Subject: [PATCH 033/212] fix: enable support for account and org level urls Signed-off-by: Calvin Lee --- .changeset/witty-bears-behave.md | 5 ++ packages/integration/src/harness/core.ts | 71 +++++++++++++++++------- 2 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 .changeset/witty-bears-behave.md diff --git a/.changeset/witty-bears-behave.md b/.changeset/witty-bears-behave.md new file mode 100644 index 0000000000..33b4c0b187 --- /dev/null +++ b/.changeset/witty-bears-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Updated function for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls diff --git a/packages/integration/src/harness/core.ts b/packages/integration/src/harness/core.ts index 81281eaa6e..1887a5c0d8 100644 --- a/packages/integration/src/harness/core.ts +++ b/packages/integration/src/harness/core.ts @@ -34,7 +34,12 @@ export function getHarnessEditContentsUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/ng/account/${parsedUrl.accountId}/module/code/orgs/${parsedUrl.orgName}/projects/${parsedUrl.projectName}/repos/${parsedUrl.repoName}/files/${parsedUrl.branch}/~/${parsedUrl.path}`; + + return `${parsedUrl.baseUrl}/ng/account/${parsedUrl.accountId}/module/code${ + parsedUrl.orgName !== '' ? `/orgs/${parsedUrl.orgName}` : '' + }${ + parsedUrl.projectName !== '' ? `/projects/${parsedUrl.projectName}` : '' + }/repos/${parsedUrl.repoName}/files/${parsedUrl.branch}/~/${parsedUrl.path}`; } /** @@ -44,6 +49,7 @@ export function getHarnessEditContentsUrl( * * Converts * from: https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml + * https://qa.harness.io/ng/account/bDCAuAjFSJCLFj_0ug3lCg/module/code/orgs/HiteshTest/repos/impoorter/files/main/~/catalog.yaml * to: https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/content/all-apis.yaml?routingId=accountId&include_commit=false&ref=refMain * * @param url - A URL pointing to a file @@ -55,7 +61,14 @@ export function getHarnessFileContentsUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/raw/${parsedUrl.path}?routingId=${parsedUrl.accountId}&git_ref=refs/heads/${parsedUrl.refString}`; + + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/raw/${parsedUrl.path}?routingId=${ + parsedUrl.accountId + }&git_ref=refs/heads/${parsedUrl.refString}`; } /** @@ -77,7 +90,13 @@ export function getHarnessArchiveUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/archive/${parsedUrl.branch}.zip?routingId=${parsedUrl.accountId}`; + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/archive/${parsedUrl.branch}.zip?routingId=${ + parsedUrl.accountId + }`; } /** @@ -99,7 +118,13 @@ export function getHarnessLatestCommitUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/content?routingId=${parsedUrl.accountId}&include_commit=true&git_ref=refs/heads/${parsedUrl.branch}`; + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/content?routingId=${ + parsedUrl.accountId + }&include_commit=true&git_ref=refs/heads/${parsedUrl.branch}`; } /** @@ -153,27 +178,30 @@ export function parseHarnessUrl( .split('/') .filter(segment => segment !== ''); const urlParts = pathUrl.pathname.split('/'); - const [ - _ng, - _account, - accountId, - _module, - _moduleName, - _org, - orgName, - _projects, - projectName, - _repos, - repoName, - _files, - _ref, - _branch, - ..._path - ] = pathSegments; + + const accountIdIndex = + pathSegments.findIndex(segment => segment === 'account') + 1; + const accountId = pathSegments[accountIdIndex]; + + const orgNameIndex = pathSegments.findIndex(segment => segment === 'orgs'); + const orgName = orgNameIndex !== -1 ? pathSegments[orgNameIndex + 1] : ''; + + const projectNameIndex = pathSegments.findIndex( + segment => segment === 'projects', + ); + + const projectName = + projectNameIndex !== -1 ? pathSegments[projectNameIndex + 1] : ''; + + const repoNameIndex = + pathSegments.findIndex(segment => segment === 'repos') + 1; + const repoName = pathSegments[repoNameIndex]; + const refAndPath = urlParts.slice( urlParts.findIndex(i => i === 'files' || i === 'edit') + 1, ); const refIndex = refAndPath.findIndex(item => item === '~'); + const refString = refAndPath.slice(0, refIndex).join('/'); const pathWithoutSlash = refIndex !== -1 @@ -182,6 +210,7 @@ export function parseHarnessUrl( .join('/') .replace(/^\//, '') : ''; + return { baseUrl: baseUrl, accountId: accountId, From a840355a10f9ad1b47975b65b30e89a4e7181876 Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Fri, 28 Jun 2024 11:41:43 -0600 Subject: [PATCH 034/212] fix: update changeset Signed-off-by: Calvin Lee --- .changeset/witty-bears-behave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/witty-bears-behave.md b/.changeset/witty-bears-behave.md index 33b4c0b187..7514759189 100644 --- a/.changeset/witty-bears-behave.md +++ b/.changeset/witty-bears-behave.md @@ -2,4 +2,4 @@ '@backstage/integration': patch --- -Updated function for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls +Updated functions for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls From b141c20b53082ad16af942c2574040d4b83921c6 Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Sun, 30 Jun 2024 00:53:24 -0400 Subject: [PATCH 035/212] Move review object explode function into utils Signed-off-by: Stephen Glass --- .../components/ReviewState/ReviewState.tsx | 34 +--- .../next/components/ReviewState/util.test.ts | 187 ++++++++++++++++++ .../src/next/components/ReviewState/util.ts | 49 +++++ 3 files changed, 238 insertions(+), 32 deletions(-) create mode 100644 plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts create mode 100644 plugins/scaffolder-react/src/next/components/ReviewState/util.ts diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index 22ec54ac26..95f5d76212 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -15,9 +15,10 @@ */ import React from 'react'; import { StructuredMetadataTable } from '@backstage/core-components'; -import { JsonObject, JsonValue } from '@backstage/types'; +import { JsonObject } from '@backstage/types'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; +import { flattenObject, isJsonObject } from './util'; /** * The props for the {@link ReviewState} component. @@ -28,37 +29,6 @@ export type ReviewStateProps = { formState: JsonObject; }; -function flattenObject( - obj: JsonObject, - prefix: string, - schema: JSONSchema, - formState: JsonObject, -): [string, JsonValue | undefined][] { - return Object.entries(obj).flatMap(([key, value]) => { - const prefixedKey = prefix ? `${prefix}/${key}` : key; - - const definitionInSchema = schema.getSchema({ - pointer: `#/${prefixedKey}`, - data: formState, - }); - - if (definitionInSchema) { - const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; - - // Recurse into nested objects - if (backstageReviewOptions?.explode && isJsonObject(value)) { - return flattenObject(value, prefixedKey, schema, formState); - } - } - - return [[key, value]]; - }); -} - -function isJsonObject(value?: JsonValue): value is JsonObject { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - /** * The component used by the {@link Stepper} to render the review step. * @alpha diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts new file mode 100644 index 0000000000..c468c1a055 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts @@ -0,0 +1,187 @@ +/* eslint-disable no-console */ +/* + * Copyright 2022 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. + */ + +import { flattenObject, isJsonObject } from './util'; +import { Draft07 as JSONSchema } from 'json-schema-library'; +import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; + +describe('isJsonObject', () => { + it('should return true for non-null objects', () => { + expect(isJsonObject({})).toBe(true); + expect(isJsonObject({ key: 'value' })).toBe(true); + }); + + it('should return false for null', () => { + expect(isJsonObject(null)).toBe(false); + }); + + it('should return false for arrays', () => { + expect(isJsonObject([])).toBe(false); + expect(isJsonObject([1, 2, 3])).toBe(false); + }); + + it('should return false for non-objects', () => { + expect(isJsonObject('string')).toBe(false); + expect(isJsonObject(123)).toBe(false); + expect(isJsonObject(true)).toBe(false); + expect(isJsonObject(undefined)).toBe(false); + }); +}); + +describe('flattenObject', () => { + it('should handle an empty object', () => { + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: {}, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + }, + ]; + + const parsedSchema = new JSONSchema(schemas[0].mergedSchema); + + const result = flattenObject({}, '', parsedSchema, {}); + + expect(result).toEqual([]); + }); + + it('should flatten a simple object', () => { + const formState = { + name: { + foo: 'value1', + bar: 'value2', + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: { + foo: { + type: 'string', + }, + bar: { + type: 'string', + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + }, + ]; + + const [key, value] = Object.entries(formState)[0]; + const parsedSchema = new JSONSchema(schemas[0].mergedSchema); + + const result = flattenObject(value, key, parsedSchema, formState); + + expect(result).toEqual([ + ['foo', 'value1'], + ['bar', 'value2'], + ]); + }); + + it('should recurse into a nested object', () => { + const formState = { + name: { + foo: 'value1', + bar: 'value2', + example: { + test: 'value3', + }, + }, + }; + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: { + foo: { + type: 'string', + }, + bar: { + type: 'string', + }, + example: { + type: 'object', + 'ui:backstage': { + review: { + explode: true, + }, + }, + properties: { + test: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + }, + ]; + + const [key, value] = Object.entries(formState)[0]; + const parsedSchema = new JSONSchema(schemas[0].mergedSchema); + + const result = flattenObject(value, key, parsedSchema, formState); + + expect(result).toEqual([ + ['foo', 'value1'], + ['bar', 'value2'], + ['test', 'value3'], + ]); + }); +}); diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts new file mode 100644 index 0000000000..0bb6fc08d2 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2022 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. + */ + +import { JsonObject, JsonValue } from '@backstage/types'; +import { Draft07 as JSONSchema } from 'json-schema-library'; + +export function flattenObject( + obj: JsonObject, + prefix: string, + schema: JSONSchema, + formState: JsonObject, +): [string, JsonValue | undefined][] { + return Object.entries(obj).flatMap(([key, value]) => { + const prefixedKey = prefix ? `${prefix}/${key}` : key; + + const definitionInSchema = schema.getSchema({ + pointer: `#/${prefixedKey}`, + data: formState, + }); + + if (definitionInSchema) { + const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; + + // Recurse into nested objects + if (backstageReviewOptions?.explode && isJsonObject(value)) { + return flattenObject(value, prefixedKey, schema, formState); + } + } + + return [[key, value]]; + }); +} + +export function isJsonObject(value?: JsonValue): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} From 08b7c3e1802702b456d4b51c69ddfa5d327e1f13 Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Sun, 30 Jun 2024 01:29:30 -0400 Subject: [PATCH 036/212] Remove debug code Signed-off-by: Stephen Glass --- .../src/next/components/ReviewState/util.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts index c468c1a055..e2a10587dc 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ /* * Copyright 2022 The Backstage Authors * From 72143aa7d65a743ec521536a64b1b1164c254136 Mon Sep 17 00:00:00 2001 From: Tavi Nolan Date: Wed, 3 Jul 2024 11:03:11 +0100 Subject: [PATCH 037/212] Changing to original link Signed-off-by: Tavi Nolan --- docs/features/software-catalog/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 9524886a49..3c875d90c4 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -47,7 +47,7 @@ action is logged for further investigation. ### Local File (`type: file`) Configurations In addition to url locations, you can use the `file` location type to bring in content from the local file system. You should only use this for local development, test setups, and example data, not for production data. -You are also not able to use placeholders in them like `$text`, `$json` or `$yaml`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs) for an extensive example. +You are also not able to use placeholders in them like `$text`, `$json` or `$yaml`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/tree/master/packages/catalog-model/examples) for an extensive example. Here is an example pulling in the `all.yaml` file from the examples folder. Note the use of `../../` to go up two levels from the current execution path of the backend. This is typically `packages/backend/`. From fad1b903a8e166492837c003916adb1f59be788e Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Wed, 3 Jul 2024 19:47:37 +0200 Subject: [PATCH 038/212] feat: allow the oauth token to be used in the gitlab:projectVariableAction:create action Signed-off-by: ElaineDeMattosSilvaB --- .changeset/grumpy-owls-suffer.md | 5 ++ ...tlabProjectVariableCreate.examples.test.ts | 61 ++++++++++--------- .../actions/gitlabProjectVariableCreate.ts | 27 ++++---- 3 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 .changeset/grumpy-owls-suffer.md diff --git a/.changeset/grumpy-owls-suffer.md b/.changeset/grumpy-owls-suffer.md new file mode 100644 index 0000000000..977055eb33 --- /dev/null +++ b/.changeset/grumpy-owls-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Allow the `createGitlabProjectVariableAction` to use oauth tokens diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts index 1eeb01ca91..99b26682d8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts @@ -26,7 +26,7 @@ const mockGitlabClient = { create: jest.fn(), }, }; -jest.mock('@gitbeaker/node', () => ({ +jest.mock('@gitbeaker/rest', () => ({ Gitlab: class { constructor() { return mockGitlabClient; @@ -41,7 +41,7 @@ describe('gitlab:projectVariableAction: create examples', () => { { host: 'gitlab.com', token: 'tokenlols', - apiBaseUrl: 'https://api.gitlab.com', + apiBaseUrl: 'https://api.gitlab.com/api/v4', }, { host: 'hosted.gitlab.com', @@ -79,17 +79,18 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', - variable_type: 'env_var', - environment_scope: '*', + variableType: 'env_var', // Correctly using variableType + environmentScope: '*', masked: false, protected: false, raw: false, }, ); }); + it(`Should ${examples[1].description}`, async () => { mockGitlabClient.ProjectVariables.create.mockResolvedValue({ token: 'TOKEN', @@ -102,14 +103,14 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my-file-content', { - key: 'MY_VARIABLE', - value: 'my-file-content', + variableType: 'file', // Correctly using variableType protected: false, masked: false, raw: false, - environment_scope: '*', - variable_type: 'file', + environmentScope: '*', }, ); }); @@ -126,13 +127,13 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '456', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', masked: false, raw: false, - environment_scope: '*', - variable_type: 'env_var', + environmentScope: '*', + variableType: 'env_var', // Correctly using variableType protected: true, }, ); @@ -150,13 +151,13 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '789', + 'DB_PASSWORD', + 'password123', { - key: 'DB_PASSWORD', - value: 'password123', protected: false, raw: false, - environment_scope: '*', - variable_type: 'env_var', + environmentScope: '*', + variableType: 'env_var', // Correctly using variableType masked: true, }, ); @@ -174,12 +175,12 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', protected: false, - environment_scope: '*', - variable_type: 'env_var', + environmentScope: '*', + variableType: 'env_var', // Correctly using variableType masked: false, raw: true, }, @@ -198,14 +199,14 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', protected: false, - variable_type: 'env_var', + variableType: 'env_var', // Correctly using variableType masked: false, raw: false, - environment_scope: 'production', + environmentScope: 'production', }, ); }); @@ -222,14 +223,14 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', protected: false, - variable_type: 'env_var', + variableType: 'env_var', // Correctly using variableType masked: false, raw: false, - environment_scope: '*', + environmentScope: '*', }, ); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts index a07ec747fa..3a9eeec0a1 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts @@ -16,10 +16,10 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; -import { Gitlab } from '@gitbeaker/node'; +import { VariableType } from '@gitbeaker/rest'; import { z } from 'zod'; import commonGitlabConfig from '../commonGitlabConfig'; -import { getToken } from '../util'; +import { getClient, parseRepoUrl } from '../util'; import { examples } from './gitlabProjectVariableCreate.examples'; /** @@ -72,6 +72,7 @@ export const createGitlabProjectVariableAction = (options: { }, async handler(ctx) { const { + repoUrl, projectId, key, value, @@ -80,21 +81,19 @@ export const createGitlabProjectVariableAction = (options: { masked = false, raw = false, environmentScope = '*', + token, } = ctx.input; - const { token, integrationConfig } = getToken(ctx.input, integrations); - const api = new Gitlab({ - host: integrationConfig.config.baseUrl, - token: token, - }); - await api.ProjectVariables.create(projectId, { - key: key, - value: value, - variable_type: variableType, + const { host } = parseRepoUrl(repoUrl, integrations); + + const api = getClient({ host, integrations, token }); + + await api.ProjectVariables.create(projectId, key, value, { + variableType: variableType as VariableType, protected: variableProtected, - masked: masked, - raw: raw, - environment_scope: environmentScope, + masked, + raw, + environmentScope, }); }, }); From 1ba55321bfea5911cec426452868f8d7e50edb2c Mon Sep 17 00:00:00 2001 From: "Stephen A. Wilson" Date: Wed, 3 Jul 2024 20:17:30 -0400 Subject: [PATCH 039/212] Fix linting errors Signed-off-by: Stephen A. Wilson --- plugins/techdocs/src/home/components/Tables/DocsTable.tsx | 2 +- plugins/techdocs/src/home/components/Tables/columns.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx index e2904374a0..b1537affdb 100644 --- a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx @@ -58,7 +58,7 @@ const defaultColumns: TableColumn[] = [ columnFactories.createKindColumn(), columnFactories.createTypeColumn(), ]; - + /** * Component which renders a table documents * diff --git a/plugins/techdocs/src/home/components/Tables/columns.tsx b/plugins/techdocs/src/home/components/Tables/columns.tsx index 08c190c26b..ef22f6d288 100644 --- a/plugins/techdocs/src/home/components/Tables/columns.tsx +++ b/plugins/techdocs/src/home/components/Tables/columns.tsx @@ -31,12 +31,12 @@ function customTitle(entity: Entity): string { */ export const columnFactories = { createTitleColumn(options?: { hidden?: boolean }): TableColumn { - const nameCol = columnFactories.createNameColumn() + const nameCol = columnFactories.createNameColumn(); return { ...nameCol, field: 'entity.metadata.title', hidden: options?.hidden, - } + }; }, createNameColumn(): TableColumn { return { From 0976bbd15238533f72038a66d0ec2696adb2daf2 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 5 Jul 2024 12:07:22 -0500 Subject: [PATCH 040/212] Added backend install steps for various auth providers Signed-off-by: Andre Wanlin --- docs/auth/atlassian/provider.md | 17 +++ docs/auth/aws-alb/provider.md | 67 +++++----- docs/auth/bitbucket/provider.md | 77 ++++++------ docs/auth/cloudflare/access.md | 174 -------------------------- docs/auth/cloudflare/provider.md | 82 ++++++++++++ docs/auth/github/provider.md | 33 +++-- docs/auth/gitlab/provider.md | 17 +++ docs/auth/google/gcp-iap-auth.md | 95 ++------------ docs/auth/google/provider.md | 21 +++- docs/auth/identity-resolver.md | 2 + docs/auth/microsoft/azure-easyauth.md | 137 +++++++++----------- docs/auth/microsoft/provider.md | 41 ++++-- docs/auth/oauth2-proxy/provider.md | 38 +++--- docs/auth/okta/provider.md | 17 +++ docs/auth/onelogin/provider.md | 39 ++++++ docs/auth/vmware-cloud/provider.md | 133 +++++--------------- 16 files changed, 439 insertions(+), 551 deletions(-) delete mode 100644 docs/auth/cloudflare/access.md create mode 100644 docs/auth/cloudflare/provider.md diff --git a/docs/auth/atlassian/provider.md b/docs/auth/atlassian/provider.md index c8db009805..44f6d097b9 100644 --- a/docs/auth/atlassian/provider.md +++ b/docs/auth/atlassian/provider.md @@ -79,6 +79,23 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-atlassian-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-atlassian-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `atlassianAuthApi` reference and diff --git a/docs/auth/aws-alb/provider.md b/docs/auth/aws-alb/provider.md index dc878e8971..8f21b7dbf0 100644 --- a/docs/auth/aws-alb/provider.md +++ b/docs/auth/aws-alb/provider.md @@ -8,37 +8,6 @@ description: Adding AWS ALB as an authentication provider in Backstage Backstage can de deployed behind [AWS Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) and get the user seamlessly authenticated. -## Installation - -### Backend - -:::note -These instructions are written for the [new backend system](../../backend-system/index.md). -::: - -Add the `@backstage/plugin-auth-backend-module-aws-alb-provider` to your backend installation. - -```sh -# From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-aws-alb-provider -``` - -Then, add it to your backend's source, - -```ts title="packages/backend/src/index.ts" -const backend = createBackend(); - -backend.add(import('@backstage/plugin-auth-backend')); -// highlight-add-next-line -backend.add(import('@backstage/plugin-auth-backend-module-aws-alb-provider')); - -await backend.start(); -``` - -### Frontend - -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. - ## Configuration The provider configuration can be added to your `app-config.yaml` under the root @@ -57,4 +26,40 @@ auth: - resolver: emailLocalPartMatchingUserEntityName ``` +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-aws-alb-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-aws-alb-provider')); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `awsalb` as the provider name. + If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index b03201893c..163647c790 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -37,6 +37,13 @@ auth: development: clientId: ${AUTH_BITBUCKET_CLIENT_ID} clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET} + signIn: + resolvers: + # typically you would pick one of these + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName + - resolver: userIdMatchingUserEntityAnnotation + - resolver: usernameMatchingUserEntityAnnotation ``` The Bitbucket provider is a structure with two configuration keys: @@ -45,44 +52,42 @@ The Bitbucket provider is a structure with two configuration keys: `b59241722e3c3b4816e2` - `clientSecret`: The Secret tied to the generated Key. +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. +- `userIdMatchingUserEntityAnnotation`: Matches the `userId` from the auth provider with the User entity that has a matching `bitbucket.org/user-id` annotation. If no match is found it will throw a `NotFoundError`. +- `usernameMatchingUserEntityAnnotation`: Matches the `username` from the auth provider with the User entity that has a matching `bitbucket.org/username` annotation. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-bitbucket-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-bitbucket-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `bitbucketAuthApi` reference and `SignInPage` component as shown in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). - -## Using Bitbucket for sign-in - -In order to use the Bitbucket provider for sign-in, you must configure it with a -`signIn.resolver`. See the -[Sign-In Resolver documentation](../identity-resolver.md) for more details on -how this is done. Note that for the Bitbucket provider, you'll want to use -`bitbucket` as the provider ID, and `providers.bitbucket.create` for the provider -factory. - -The `@backstage/plugin-auth-backend` plugin also comes with two built-in -resolvers that can be used if desired. The first one is the -`bitbucketUsernameSignInResolver`, which identifies users by matching their -Bitbucket username to `bitbucket.org/username` annotations of `User` entities in -the catalog. Note that you must populate your catalog with matching entities or -users will not be able to sign in. - -The second resolver is the `bitbucketUserIdSignInResolver`, which works the -same way, but uses the Bitbucket user ID instead, and matches on the -`bitbucket.org/user-id` annotation. - -The following is an example of how to use one of the built-in resolvers: - -```ts -import { providers } from '@backstage/plugin-auth-backend'; - -// ... - providerFactories: { - bitbucket: providers.bitbucket.create({ - signIn: { - resolver: - providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation(), - }, - }), - }, -``` diff --git a/docs/auth/cloudflare/access.md b/docs/auth/cloudflare/access.md deleted file mode 100644 index 52d9fdfc21..0000000000 --- a/docs/auth/cloudflare/access.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -id: provider -title: Cloudflare Access Provider -sidebar_label: Cloudflare Access -description: Adding Cloudflare Access as an authentication provider in Backstage ---- - -Similar to GCP IAP Proxy Provider or AWS ALB provider, developers can offload authentication -support to Cloudflare Access. - -This tutorial shows how to use authentication on Cloudflare Access sitting in -front of Backstage. - -It is assumed a Cloudflare tunnel is already serving traffic in front of a -Backstage instance configured to serve the frontend app from the backend and is -already gated using Cloudflare Access. - -## Configuration - -Let's start by adding the following `auth` configuration in your -`app-config.yaml` or `app-config.production.yaml` or similar: - -```yaml -auth: - providers: - cfaccess: - # You can find the team name in the Cloudflare Zero Trust dashboard. - teamName: - # This service tokens section is optional -- you only need it if you have - # some Cloudflare Service Tokens that you want to be able to log in to your - # Backstage instance. - serviceTokens: - - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' - subject: 'bot-user@your-company.com' - # This picks what sign in resolver(s) you want to use. - signIn: - resolvers: - - resolver: emailMatchingUserEntityProfileEmail -``` - -This config section must be in place for the provider to load at all. - -The `signIn` section picks what sign-in resolver(s) to use for sign-in attempts. -It is responsible for matching the upstream provider's sign-in result to a -corresponding Backstage identity, or to throw an error if the attempt should be -rejected for any reason. The `emailMatchingUserEntityProfileEmail` is a common -choice: it tries to match the email of the signed-in user to a `User` kind -entity in the catalog whose profile email matches that. - -If the builtin sign in resolvers do not match your needs, you can skip the -`signIn` section and instead [provide a custom resolver](#advanced-custom-sign-in-resolver). - -## Backend Changes - -We need to add the provider package as a dependency to our backend: - -```bash title="from your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-cloudflare-access-provider -``` - -And to tell the backend to load it: - -```ts title="in packages/backend/src/index.ts" -backend.add(import('@backstage/plugin-auth-backend')); -/* highlight-add-start */ -backend.add( - import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), -); -/* highlight-add-end */ -``` - -Now the backend is ready to serve auth requests on the -`/api/auth/cfaccess/refresh` endpoint. All that's left is to update the frontend -sign-in mechanism to poll that endpoint through Cloudflare Access, on the user's -behalf. - -## Frontend Changes - -It is recommended to use the `ProxiedSignInPage` for this provider, which is -installed in your app like this: - -```tsx title="in packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => , - }, - /* highlight-add-end */ - // ... -}); -``` - -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for -pointers on how to set up the sign-in page to also work smoothly for local -development. - -## Advanced: Custom Sign-in Resolver - -If none of the built-in sign in resolvers fit your needs, you need to provide a -customized version of the module. Now you should _not_ -`backend.add(import(...))`, instead you will do the following. - -```ts title="in packages/backend/plugin/auth.ts" -/* highlight-add-start */ -import { createCloudflareAccessAuthenticator } from '@backstage/plugin-auth-backend-module-cloudflare-access-provider'; -import { - coreServices, - createBackendModule, -} from '@backstage/backend-plugin-api'; -import { - authProvidersExtensionPoint, - createProxyAuthProviderFactory, -} from '@backstage/plugin-auth-node'; - -const customAuth = createBackendModule({ - // This ID must be exactly "auth" because that's the plugin it targets - pluginId: 'auth', - // This ID must be unique, but can be anything - moduleId: 'custom-auth-provider', - register(reg) { - reg.registerInit({ - deps: { - providers: authProvidersExtensionPoint, - cache: coreServices.cache, - }, - async init({ providers, cache }) { - providers.registerProvider({ - // This ID must match the actual provider config, e.g. addressing - // auth.providers.github means that this must be "github". - providerId: 'cfaccess', - // Use createProxyAuthProviderFactory instead if it's one of the proxy - // based providers rather than an OAuth based one - factory: createProxyAuthProviderFactory({ - authenticator: createCloudflareAccessAuthenticator({ cache }), - async signInResolver(info, ctx) { - // This is where the body of the sign-in resolver goes! - const { profile } = info; - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } - return ctx.signInWithCatalogUser({ - filter: { - 'spec.profile.email': profile.email, - }, - }); - }, - }), - }); - }, - }); - }, -}); -/* highlight-add-end */ - -backend.add(import('@backstage/plugin-auth-backend')); -/* highlight-remove-start */ -backend.add( - import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), -); -/* highlight-remove-end */ -/* highlight-add-next-line */ -backend.add(customAuth); -``` - -The body of the sign-in resolver is up to you to write! The example code above -is just a copy of what `emailMatchingUserEntityProfileEmail` does. The `info` -parameter contains all of the results of the sign-in attempt so far. The `ctx` -context [has several useful functions](https://backstage.io/docs/reference/plugin-auth-node.authresolvercontext/) -for issuing tokens in various ways. diff --git a/docs/auth/cloudflare/provider.md b/docs/auth/cloudflare/provider.md new file mode 100644 index 0000000000..516ea9d647 --- /dev/null +++ b/docs/auth/cloudflare/provider.md @@ -0,0 +1,82 @@ +--- +id: provider +title: Cloudflare Access Provider +sidebar_label: Cloudflare Access +description: Adding Cloudflare Access as an authentication provider in Backstage +--- + +Similar to GCP IAP Proxy Provider or AWS ALB provider, developers can offload authentication +support to Cloudflare Access. + +This tutorial shows how to use authentication on Cloudflare Access sitting in +front of Backstage. + +It is assumed a Cloudflare tunnel is already serving traffic in front of a +Backstage instance configured to serve the frontend app from the backend and is +already gated using Cloudflare Access. + +## Configuration + +Let's start by adding the following `auth` configuration in your +`app-config.yaml` or `app-config.production.yaml` or similar: + +```yaml +auth: + providers: + cfaccess: + # You can find the team name in the Cloudflare Zero Trust dashboard. + teamName: + # This service tokens section is optional -- you only need it if you have + # some Cloudflare Service Tokens that you want to be able to log in to your + # Backstage instance. + serviceTokens: + - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' + subject: 'bot-user@your-company.com' + # This picks what sign in resolver(s) you want to use. + signIn: + resolvers: + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName +``` + +This config section must be in place for the provider to load at all. + +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-cloudflare-access-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `cfaccess` as the provider name. + +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 1873730bd6..801320299b 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -26,6 +26,14 @@ Settings for local development: - Homepage URL: `http://localhost:3000` - Authorization callback URL: `http://localhost:7007/api/auth/github/handler/frame` +### Difference between GitHub Apps and GitHub OAuth Apps + +GitHub Apps handle OAuth scope at the app installation level, meaning that the +`scope` parameter for the call to `getAccessToken` in the frontend has no +effect. When calling `getAccessToken` in open source plugins, one should still +include the appropriate scope, but also document in the plugin README what +scopes are required for GitHub Apps. + ## Configuration The provider configuration can then be added to your `app-config.yaml` under the @@ -79,16 +87,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-github-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `githubAuthApi` reference and `SignInPage` component as shown in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). - -## Difference between GitHub Apps and GitHub OAuth Apps - -GitHub Apps handle OAuth scope at the app installation level, meaning that the -`scope` parameter for the call to `getAccessToken` in the frontend has no -effect. When calling `getAccessToken` in open source plugins, one should still -include the appropriate scope, but also document in the plugin README what -scopes are required for GitHub Apps. diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index f5613582fd..e70953e11d 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -78,6 +78,23 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gitlab-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-gitlab-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `gitlabAuthApi` reference and diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index 7e6178154c..6479ac7268 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -58,102 +58,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. -## Backend Changes +## Backend Installation -There is a module for this provider that you will need to add to your backend. +To add the provider to the backend we will first need to install the package by running this command: -First you'll want to run this command to add the module: - -```sh - yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gcp-iap-provider +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gcp-iap-provider ``` -Then you will need to add this to your backend: - -```ts title="packages/backend/src/index.ts" -const backend = createBackend(); +Then we will need to add this line: +```ts title="in packages/backend/src/index.ts" backend.add(import('@backstage/plugin-auth-backend')); /* highlight-add-start */ backend.add(import('@backstage/plugin-auth-backend-module-gcp-iap-provider')); /* highlight-add-end */ ``` -### Legacy Backend Changes +## Adding the provider to the Backstage frontend -If you are still using the legacy backend you will need to make the changes outlined here. +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `gcp-iap` as the provider name. -This provider is not enabled by default in the auth backend code, because besides the config section above, it also needs to be given one or more callbacks in actual code as well as described below. - -Add a `providerFactories` entry to the router in -`packages/backend/src/plugins/auth.ts`. - -```ts title="packages/backend/src/plugins/auth.ts" -import { providers } from '@backstage/plugin-auth-backend'; -import { stringifyEntityRef } from '@backstage/catalog-model'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - providerFactories: { - 'gcp-iap': providers.gcpIap.create({ - // Replace the auth handler if you want to customize the returned user - // profile info (can be left out; the default implementation is shown - // below which only returns the email). You may want to amend this code - // with something that loads additional user profile data out of e.g. - // GSuite or LDAP or similar. - async authHandler({ iapToken }) { - return { profile: { email: iapToken.email } }; - }, - signIn: { - // You need to supply an identity resolver, that takes the profile - // and the IAP token and produces the Backstage token with the - // relevant user info. - async resolver({ profile, result: { iapToken } }, ctx) { - // Somehow compute the Backstage token claims. Just some sample code - // shown here, but you may want to query your LDAP server, or - // GSuite or similar, based on the IAP token sub/email claims - const id = iapToken.email.split('@')[0]; - const sub = stringifyEntityRef({ kind: 'User', name: id }); - const ent = [ - sub, - stringifyEntityRef({ kind: 'Group', name: 'team-name' }), - ]; - return ctx.issueToken({ claims: { sub, ent } }); - }, - }, - }), - }, - }); -} -``` - -Now the backend is ready to serve auth requests on the -`/api/auth/gcp-iap/refresh` endpoint. All that's left is to update the frontend -sign-in mechanism to poll that endpoint through the IAP, on the user's behalf. - -## Frontend Changes - -It is recommended to use the `ProxiedSignInPage` for this provider, which is -installed in `packages/app/src/App.tsx` like this: - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => , - }, - /* highlight-add-end */ - // .. -}); -``` - -See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information. +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/google/provider.md b/docs/auth/google/provider.md index f7802d43f7..e2efacfed2 100644 --- a/docs/auth/google/provider.md +++ b/docs/auth/google/provider.md @@ -24,9 +24,9 @@ To support Google authentication, you must create OAuth credentials: - Add yourself as a test user, if using External user type 6. Set **Application Type** to `Web Application` with these settings: - `Name`: Backstage (or your custom app name) - - `Authorized JavaScript origins`: http://localhost:3000 + - `Authorized JavaScript origins`: - `Authorized Redirect URIs`: - http://localhost:7007/api/auth/google/handler/frame + 7. Click Create ## Configuration @@ -72,6 +72,23 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-google-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-google-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `googleAuthApi` reference and diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 4140276df8..5efb592270 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -124,6 +124,8 @@ The list of available resolvers is different for each provider, since they often depend on the information model returned from the upstream provider service. Consult the documentation of the respective provider to find the list. +In the example above `emailMatchingUserEntityProfileEmail` and `emailLocalPartMatchingUserEntityName` are common to all auth providers and `usernameMatchingUserEntityName` is specific to GitHub. + ### Building Custom Resolvers If the builtins don't work for you, you can also provide a completely custom diff --git a/docs/auth/microsoft/azure-easyauth.md b/docs/auth/microsoft/azure-easyauth.md index 5fec3354d7..e8d6f2f105 100644 --- a/docs/auth/microsoft/azure-easyauth.md +++ b/docs/auth/microsoft/azure-easyauth.md @@ -7,87 +7,6 @@ description: Adding Azure's EasyAuth Proxy as an authentication provider in Back The Backstage `core-plugin-api` package comes with a Microsoft authentication provider that can authenticate users using Microsoft Entra ID (formerly Azure Active Directory) for PaaS service hosted in Azure that support Easy Auth, such as Azure App Services. -## Backend Changes - -Add the following into your `app-config.yaml` under the root `auth` configuration: - -```yaml title="app-config.yaml" -auth: - providers: - azureEasyAuth: - signIn: - resolvers: - - resolver: idMatchingUserEntityAnnotation - - resolver: emailMatchingUserEntityProfileEmail - - resolver: emailLocalPartMatchingUserEntityName -``` - -The `idMatchingUserEntityAnnotation` is -[a builtin sign-in resolver](../identity-resolver.md#using-builtin-resolvers) from `azureEasyAuth` provider. -It tries to find a user entity with [a `graph.microsoft.com/user-id` annotation](../../features/software-catalog/well-known-annotations.md#graphmicrosoftcomtenant-id-graphmicrosoftcomgroup-id-graphmicrosoftcomuser-id) -which matches the object ID of the user attempting to sign in. -If you want to provide your own sign-in resolver, -see [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers). - -Add the `@backstage/plugin-auth-backend-module-azure-easyauth-provider` to your backend installation. - -```sh -# From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-azure-easyauth-provider -``` - -Then, add it to your backend's source, - -```ts title="packages/backend/src/index.ts" -const backend = createBackend(); - -backend.add(import('@backstage/plugin-auth-backend')); -// highlight-add-next-line -backend.add( - import('@backstage/plugin-auth-backend-module-azure-easyauth-provider'), -); - -await backend.start(); -``` - -Now the backend is ready to serve auth requests on the -`/api/auth/azureEasyAuth/refresh` endpoint. All that's left is to update the frontend -sign-in mechanism to poll that endpoint through the Easy Auth proxy, on the user's behalf. - -## Frontend Changes - -To use this component, you'll need to configure the app's `SignInPage`. -It is recommended to use the `ProxiedSignInPage` for this provider when running in Azure, However for local development (or any other scenario running outside of Azure), you'll want to set up something different. -For the closest experience to Easy Auth, you could set up the `microsoft` provider locally, but that will requires setting up App Registrations & secrets which may be locked down by your organisation, in which case it may be easier to use guest login locally. -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for more details. - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => { - const configApi = useApi(configApiRef); - if (configApi.getString('auth.environment') !== 'development') { - return ; - } - return ( - - ); - }, - }, - /* highlight-add-end */ - // .. -}); -``` - ## Azure Configuration How to configure azure depends on the Azure service you're using to host Backstage. @@ -136,3 +55,59 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { } } ``` + +## Configuration + +Add the following into your `app-config.yaml` under the root `auth` configuration: + +```yaml title="app-config.yaml" +auth: + providers: + azureEasyAuth: + signIn: + resolvers: + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName + - resolver: idMatchingUserEntityAnnotation +``` + +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. +- `idMatchingUserEntityAnnotation`: Matches the user Id from the auth provider with the User entity that has a matching `graph.microsoft.com/user-id` annotation. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-azure-easyauth-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-azure-easyauth-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `azureEasyAuth` as the provider name. + +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index b96f175183..aeec71213f 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -40,6 +40,18 @@ If you're using an existing app registration, and backstage already has a client If not, go to the **Certificates & Secrets** page, then the **Client secrets** tab and create a new client secret. Make a note of this value as you'll need it in the next section. +## Outbound Network Access + +If your environment has restrictions on outgoing access (e.g. through +firewall rules), make sure your Backstage backend has access to the following +hosts: + +- `login.microsoftonline.com`, to get and exchange authorization codes and access + tokens +- `graph.microsoft.com`, to fetch user profile information (as seen + in [this source code](https://github.com/seanfisher/passport-microsoft/blob/0456aa9bce05579c18e77f51330176eb26373658/lib/strategy.js#L93-L95)). + If this host is unreachable, users may see an `Authentication failed, failed to fetch user profile` error when they attempt to log in. + ## Configuration The provider configuration can then be added to your `app-config.yaml` under the @@ -92,20 +104,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-microsoft-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `microsoftAuthApiRef` reference and `SignInPage` component as shown in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). - -## Outbound Network Access - -If your environment has restrictions on outgoing access (e.g. through -firewall rules), make sure your Backstage backend has access to the following -hosts: - -- `login.microsoftonline.com`, to get and exchange authorization codes and access - tokens -- `graph.microsoft.com`, to fetch user profile information (as seen - in [this source code](https://github.com/seanfisher/passport-microsoft/blob/0456aa9bce05579c18e77f51330176eb26373658/lib/strategy.js#L93-L95)). - If this host is unreachable, users may see an `Authentication failed, failed to fetch user profile` error when they attempt to log in. diff --git a/docs/auth/oauth2-proxy/provider.md b/docs/auth/oauth2-proxy/provider.md index dca074cc7d..0901685b2f 100644 --- a/docs/auth/oauth2-proxy/provider.md +++ b/docs/auth/oauth2-proxy/provider.md @@ -47,25 +47,27 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. -## Adding the provider to the Backstage frontend +## Backend Installation -It is recommended to use the `ProxiedSignInPage` for this provider, which is -installed in `packages/app/src/App.tsx` like this: +To add the provider to the backend we will first need to install the package by running this command: -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), - }, - /* highlight-add-end */ - // .. -}); +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-oauth2-proxy-provider ``` -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development. +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-oauth2-proxy-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `oauth2Proxy` as the provider name. + +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index 6fe7a8b992..77dace2289 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -83,6 +83,23 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-okta-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-okta-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `oktaAuthApi` reference and diff --git a/docs/auth/onelogin/provider.md b/docs/auth/onelogin/provider.md index c3572367d2..9330a20332 100644 --- a/docs/auth/onelogin/provider.md +++ b/docs/auth/onelogin/provider.md @@ -38,6 +38,12 @@ auth: clientId: ${AUTH_ONELOGIN_CLIENT_ID} clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET} issuer: https://.onelogin.com/oidc/2 + signIn: + resolvers: + # typically you would pick one of these + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName + - resolver: usernameMatchingUserEntityName ``` The OneLogin provider is a structure with three configuration keys; **these are @@ -47,6 +53,39 @@ found on the SSO tab** for the OneLogin Application: - `clientSecret`: The client secret - `issuer`: The issuer URL +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. +- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-onelogin-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-onelogin-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `oneloginAuthApi` reference and diff --git a/docs/auth/vmware-cloud/provider.md b/docs/auth/vmware-cloud/provider.md index dd7c32f700..d79c84534a 100644 --- a/docs/auth/vmware-cloud/provider.md +++ b/docs/auth/vmware-cloud/provider.md @@ -33,108 +33,6 @@ Cloud Console and within a Backstage app required to enable this capability. 1. Take note of the `App ID` in the resulting modal; this is the client ID to be used by Backstage. -## Install the provider in the backend - -### New backend system - -Apps using the [new backend system](../../backend-system/index.md), -can enable the VMware Cloud provider with a small modification like: - -```ts title="packages/backend/src/index.ts" -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); -backend.add(import('@backstage/plugin-auth-backend')); -/* highlight-add-start */ -backend.add( - import('@backstage/plugin-auth-backend-module-vmware-cloud-provider'), -); -/* highlight-add-end */ -backend.start(); -``` - -### Old backend system - -This provider was added after the migration of the auth-backend plugin to the -new backend system, so no default provider factory was added. Because of this, -the installation procedure for old-style backends is slightly more involved: - -```ts title="packages/backend/src/plugins/auth.ts" -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { - createRouter, - providers, - defaultAuthProviderFactories, -} from '@backstage/plugin-auth-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; -/* highlight-add-start */ -import { - commonSignInResolvers, - createOAuthProviderFactory, -} from '@backstage/plugin-auth-node'; -import { - vmwareCloudAuthenticator, -} from '@backstage/plugin-auth-backend-module-vmware-cloud-provider'; -/* highlight-add-end */ - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - tokenManager: env.tokenManager, - providerFactories: { - ...defaultAuthProviderFactories, - /* highlight-add-start */ - vmwareCloudServices: createOAuthProviderFactory({ - authenticator: vmwareCloudAuthenticator, - signInResolver: - commonSignInResolvers.emailLocalPartMatchingUserEntityName(), - }), - /* highlight-add-end */ -``` - -In the above, `commonSignInResolvers.emailLocalPartMatchingUserEntityName()` -can be replaced with a more suitable resolver for the app in question. - -## Add to Sign-in Page - -See the [Sign-In Configuration](../index.md#sign-in-configuration) docs for -general guidance, but as an example: - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-start */ -import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api'; -import { SignInPage } from '@backstage/core-components'; -/* highlight-add-end */ - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), - }, - /* highlight-add-end */ - // .. -}); -``` - ## Configuration Add the following to your `app-config.yaml` under the root `auth` configuration: @@ -161,13 +59,17 @@ Where `APP_ID` refers to the ID retrieved when creating the OAuth App, and `ORG_ID` is the [long ID of the Organization](https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-CF9E9318-B811-48CF-8499-9419997DC1F8.html#view-the-organization-id-1) in VMware Cloud for which you wish to enable sign-in. -Note that VMware Cloud requires OAuth Apps to use +:::note Note + +VMware Cloud requires OAuth Apps to use [PKCE](https://oauth.net/2/pkce/) when performing authorization code flows; the library used by this provider requires the use of Express session middleware to do this. Therefore the value `your session secret` under `auth.session.secret` should be replaced with a long, complex and unique string which will act as a key for signing session cookies set by Backstage. +::: + ### Resolvers This provider includes several resolvers out of the box that you can use: @@ -183,3 +85,28 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` ::: If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-vmware-cloud-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-vmware-cloud-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +To add the provider to the frontend, add the `vmwareCloudAuthApiRef` reference and +`SignInPage` component as shown in +[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). From b4e401fd9ed99f379c6a2a5a3277cad3cab1d568 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 5 Jul 2024 13:32:46 -0500 Subject: [PATCH 041/212] Fixed links and MkDocs config Signed-off-by: Andre Wanlin --- docs/auth/atlassian/provider.md | 2 +- docs/auth/auth0/provider.md | 2 +- docs/auth/bitbucket/provider.md | 2 +- docs/auth/bitbucketServer/provider.md | 2 +- docs/auth/github/provider.md | 2 +- docs/auth/gitlab/provider.md | 2 +- docs/auth/google/provider.md | 2 +- docs/auth/microsoft/provider.md | 2 +- docs/auth/okta/provider.md | 2 +- docs/auth/onelogin/provider.md | 2 +- docs/auth/vmware-cloud/provider.md | 2 +- mkdocs.yml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/auth/atlassian/provider.md b/docs/auth/atlassian/provider.md index 44f6d097b9..96271f04a0 100644 --- a/docs/auth/atlassian/provider.md +++ b/docs/auth/atlassian/provider.md @@ -100,4 +100,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-atlassian-provider')); To add the provider to the frontend, add the `atlassianAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/auth0/provider.md b/docs/auth/auth0/provider.md index dcd02d226f..ae44114906 100644 --- a/docs/auth/auth0/provider.md +++ b/docs/auth/auth0/provider.md @@ -64,4 +64,4 @@ Auth0 requires a session, so you need to give the session a secret key. To add the provider to the frontend, add the `auth0AuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index 163647c790..1ca35437ba 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -90,4 +90,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-bitbucket-provider')); To add the provider to the frontend, add the `bitbucketAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md index eef934ca60..82aebc20b2 100644 --- a/docs/auth/bitbucketServer/provider.md +++ b/docs/auth/bitbucketServer/provider.md @@ -37,7 +37,7 @@ The Bitbucket Server provider is a structure with two configuration keys: ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `bitbucketServerAuthApi` reference and `SignInPage` component as shown -in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +in [Adding the provider to the sign-in page](../index.md#sign-in-configuration). ## Using Bitbucket Server for sign-in diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 801320299b..76b63c6617 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -108,4 +108,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); To add the provider to the frontend, add the `githubAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index e70953e11d..73546411bf 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -99,4 +99,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-gitlab-provider')); To add the provider to the frontend, add the `gitlabAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/google/provider.md b/docs/auth/google/provider.md index e2efacfed2..b19d21d63f 100644 --- a/docs/auth/google/provider.md +++ b/docs/auth/google/provider.md @@ -93,4 +93,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-google-provider')); To add the provider to the frontend, add the `googleAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index aeec71213f..f799eeb627 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -125,4 +125,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider')); To add the provider to the frontend, add the `microsoftAuthApiRef` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index 77dace2289..83352aaa0d 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -104,4 +104,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-okta-provider')); To add the provider to the frontend, add the `oktaAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/onelogin/provider.md b/docs/auth/onelogin/provider.md index 9330a20332..002445a9d1 100644 --- a/docs/auth/onelogin/provider.md +++ b/docs/auth/onelogin/provider.md @@ -90,4 +90,4 @@ backend.add(import('@backstage/plugin-auth-backend-module-onelogin-provider')); To add the provider to the frontend, add the `oneloginAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/vmware-cloud/provider.md b/docs/auth/vmware-cloud/provider.md index d79c84534a..ac2cf3ee92 100644 --- a/docs/auth/vmware-cloud/provider.md +++ b/docs/auth/vmware-cloud/provider.md @@ -109,4 +109,4 @@ backend.add( To add the provider to the frontend, add the `vmwareCloudAuthApiRef` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/mkdocs.yml b/mkdocs.yml index b9f6d41d46..f163005377 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -158,7 +158,7 @@ nav: - Azure EasyAuth: 'auth/microsoft/azure-easyauth.md' - Bitbucket: 'auth/bitbucket/provider.md' - Bitbucket Server: 'auth/bitbucketServer/provider.md' - - Cloudflare Access: 'auth/cloudflare/access.md' + - Cloudflare Access: 'auth/cloudflare/provider.md' - GitHub: 'auth/github/provider.md' - GitLab: 'auth/gitlab/provider.md' - Google: 'auth/google/provider.md' From 401e892bafc3927acd87213b69712144dbef1603 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 5 Jul 2024 13:49:45 -0500 Subject: [PATCH 042/212] Fixed more links Signed-off-by: Andre Wanlin --- docs/auth/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/auth/index.md b/docs/auth/index.md index f925e52917..4a0e56a36e 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -31,7 +31,7 @@ Backstage comes with many common authentication providers in the core library: - [Azure Easy Auth](microsoft/azure-easyauth.md) - [Bitbucket](bitbucket/provider.md) - [Bitbucket Server](bitbucketServer/provider.md) -- [Cloudflare Access](cloudflare/access.md) +- [Cloudflare Access](cloudflare/provider.md) - [GitHub](github/provider.md) - [GitLab](gitlab/provider.md) - [Google](google/provider.md) @@ -158,7 +158,7 @@ Some auth providers are so-called "proxy" providers, meaning they're meant to be behind an authentication proxy. Examples of these are [Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), [Azure EasyAuth](./microsoft/azure-easyauth.md), -[Cloudflare Access](./cloudflare/access.md), +[Cloudflare Access](./cloudflare/provider.md), [Google Identity-Aware Proxy](./google/gcp-iap-auth.md) and [OAuth2 Proxy](./oauth2-proxy/provider.md). From 583715dbbe375fe54d9feb8fee3153e5876ecd33 Mon Sep 17 00:00:00 2001 From: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> Date: Fri, 5 Jul 2024 23:33:03 -0400 Subject: [PATCH 043/212] chore: Fix comment typo Signed-off-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- packages/repo-tools/src/commands/api-reports/api-extractor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index 6a523af6a9..c3054dffe9 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -145,7 +145,7 @@ ApiReportGenerator.generateReviewFileContent = if (!localName) { // Sometimes the symbol name is suffixed with a number to disambiguate, // e.g. "Props_14" instead of "Props" if there are multiple Props interfaces - // so we tyry to strip that suffix and look up the symbol again. + // so we try to strip that suffix and look up the symbol again. const [, trimmedSymbolName] = symbolName.match(/(.*)_\d+/) || []; localName = (sourceFile as any).identifiers?.get(trimmedSymbolName); } @@ -1101,6 +1101,7 @@ export async function buildDocs({ switch (hierarchyItem.kind) { case ApiItemKind.Model: case ApiItemKind.EntryPoint: + case ApiItemKind.None: // We don't show the model as part of the breadcrumb because it is the root-level container. // We don't show the entry point because today API Extractor doesn't support multiple entry points; // this may change in the future. From c3837fb3fca523d296b9024c5dd8c80a5f207e1f Mon Sep 17 00:00:00 2001 From: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> Date: Fri, 5 Jul 2024 23:34:33 -0400 Subject: [PATCH 044/212] chore: Fix invalid API docs cross-reference Signed-off-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- plugins/permission-node/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 36e30714f6..0a7e06ed91 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -23,7 +23,7 @@ import { NoInfer } from './integration/util'; /** * A conditional rule that can be provided in an - * {@link @backstage/permission-common#AuthorizeDecision} response to an authorization request. + * {@link @backstage/plugin-permission-common#AuthorizeDecision} response to an authorization request. * * @remarks * From 09086e8d0ae82ca18f1e2a476bff60278af872f6 Mon Sep 17 00:00:00 2001 From: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> Date: Fri, 5 Jul 2024 23:34:53 -0400 Subject: [PATCH 045/212] chore: Add packageDocumentation for API Docs Index Signed-off-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- plugins/permission-react/api-report.md | 2 -- plugins/permission-react/src/index.ts | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/permission-react/api-report.md b/plugins/permission-react/api-report.md index 3c1961c92c..4c51d16b80 100644 --- a/plugins/permission-react/api-report.md +++ b/plugins/permission-react/api-report.md @@ -100,6 +100,4 @@ export function usePermission( resourceRef: string | undefined; }, ): AsyncPermissionResult; - -// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/permission-react/src/index.ts b/plugins/permission-react/src/index.ts index 57352c5cdd..de6958b3e5 100644 --- a/plugins/permission-react/src/index.ts +++ b/plugins/permission-react/src/index.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * Components and hooks to help implement permissions in Backstage frontend plugins. + * + * @packageDocumentation + */ export * from './components'; export * from './hooks'; export * from './apis'; From 28b2cfb08cf1bc88107ef61d344b687febfd2bed Mon Sep 17 00:00:00 2001 From: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> Date: Fri, 5 Jul 2024 23:37:30 -0400 Subject: [PATCH 046/212] chore: Add changesets Signed-off-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- .changeset/famous-clocks-visit.md | 5 +++++ .changeset/tender-turkeys-begin.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/famous-clocks-visit.md create mode 100644 .changeset/tender-turkeys-begin.md diff --git a/.changeset/famous-clocks-visit.md b/.changeset/famous-clocks-visit.md new file mode 100644 index 0000000000..66c2048872 --- /dev/null +++ b/.changeset/famous-clocks-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Fix invalid cross-reference in API Reference docs diff --git a/.changeset/tender-turkeys-begin.md b/.changeset/tender-turkeys-begin.md new file mode 100644 index 0000000000..26e376cecf --- /dev/null +++ b/.changeset/tender-turkeys-begin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-react': patch +--- + +Improve API Reference documentation From 518616d4c9f13eeaef816a9dfa8ce9b95a07e773 Mon Sep 17 00:00:00 2001 From: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:14:44 -0400 Subject: [PATCH 047/212] chore: Remove testing Signed-off-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- packages/repo-tools/src/commands/api-reports/api-extractor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index c3054dffe9..122f04cc7e 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -1101,7 +1101,6 @@ export async function buildDocs({ switch (hierarchyItem.kind) { case ApiItemKind.Model: case ApiItemKind.EntryPoint: - case ApiItemKind.None: // We don't show the model as part of the breadcrumb because it is the root-level container. // We don't show the entry point because today API Extractor doesn't support multiple entry points; // this may change in the future. From ad8f60b1f74761558f65b777f593db6ba2d88c47 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 8 Jul 2024 10:59:17 +0200 Subject: [PATCH 048/212] chore: add DPG Media as an adopter Signed-off-by: Benjamin Janssens --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 3ff0c5e1b5..c1b1686ccc 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -272,3 +272,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Senora.dev](https://senora.dev) | [Shaked Braimok Yosef](https://github.com/ShakedBraimok) | We are using Backstage as a service catalog for our costumers. | | [Covestro](https://www.covestro.com) | [Julien Gedeon](https://github.com/cvvcv1) | We use Backstage as our Internal developer portal for the Digital R&D department. | | [Grupo OLX](https://www.olx.com.br/) | [@marciorgb](https://github.com/marciorgb), [@jeffbraga](https://github.com/jeffbraga), [@rogerfernandes](https://github.com/rogerfernandes), [@stockrt](https://github.com/stockrt) | At Grupo OLX, we are using Backstage to reduce friction and enhance developer autonomy by creating Golden Paths, which automate previously manual processes. Additionally, Backstage's catalog integrates various views of our organizational structure and workflows, offering a comprehensive overview of how different components interconnect. | +| [DPG Media](https://www.dpgmediagroup.com) | [@benjidotsh](https://github.com/benjidotsh), [@lvstb](https://github.com/lvstb), [@ssliman](https://github.com/ssliman), [@edewel-beep](https://github.com/edewel-beep) | Access to infrastructure, documentation and internal tooling | From 9ddbd8c729bf96a36d6642459e3a326e7f8bb6d5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Jul 2024 17:09:53 +0200 Subject: [PATCH 049/212] search: add missing createApiFactory Signed-off-by: Patrik Oldsberg --- plugins/search/src/alpha.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index c202d58712..838fb95046 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -31,10 +31,9 @@ import { } from '@backstage/core-components'; import { useApi, - DiscoveryApi, discoveryApiRef, fetchApiRef, - FetchApi, + createApiFactory, } from '@backstage/core-plugin-api'; import { @@ -77,17 +76,12 @@ import { /** @alpha */ export const searchApi = createApiExtension({ - factory: { + factory: createApiFactory({ api: searchApiRef, deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, - factory: ({ - fetchApi, - discoveryApi, - }: { - fetchApi: FetchApi; - discoveryApi: DiscoveryApi; - }) => new SearchClient({ discoveryApi, fetchApi }), - }, + factory: ({ discoveryApi, fetchApi }) => + new SearchClient({ discoveryApi, fetchApi }), + }), }); const useSearchPageStyles = makeStyles((theme: Theme) => ({ From 0b6d31a56b530d92a6881174b91ed7036e4ea169 Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 8 Jul 2024 18:55:40 +0200 Subject: [PATCH 050/212] docs: add Baloise as adaptor Signed-off-by: David Weber Signed-off-by: David Weber --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index c1b1686ccc..870b4c93d4 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -273,3 +273,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Covestro](https://www.covestro.com) | [Julien Gedeon](https://github.com/cvvcv1) | We use Backstage as our Internal developer portal for the Digital R&D department. | | [Grupo OLX](https://www.olx.com.br/) | [@marciorgb](https://github.com/marciorgb), [@jeffbraga](https://github.com/jeffbraga), [@rogerfernandes](https://github.com/rogerfernandes), [@stockrt](https://github.com/stockrt) | At Grupo OLX, we are using Backstage to reduce friction and enhance developer autonomy by creating Golden Paths, which automate previously manual processes. Additionally, Backstage's catalog integrates various views of our organizational structure and workflows, offering a comprehensive overview of how different components interconnect. | | [DPG Media](https://www.dpgmediagroup.com) | [@benjidotsh](https://github.com/benjidotsh), [@lvstb](https://github.com/lvstb), [@ssliman](https://github.com/ssliman), [@edewel-beep](https://github.com/edewel-beep) | Access to infrastructure, documentation and internal tooling | +| [Baloise](https://www.baloise.ch) | [@dweber019](https://github.com/dweber019), [@mikevader](https://github.com/mikevader) | We are using Backstage as internal developer protal for use cases like software catalog, documentation, infrastructure. | From b9832aee3d57c40de8e77e35275eff5acfe8d1de Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 09:55:58 +0200 Subject: [PATCH 051/212] Fix issues described in #25551 Signed-off-by: Olivier Liechti --- .changeset/chilly-rabbits-sneeze.md | 5 +++ .../src/authenticator.test.ts | 40 +++++++++++++++++ .../src/authenticator.ts | 44 +++++++++++++++---- .../src/types.d.ts | 3 +- 4 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 .changeset/chilly-rabbits-sneeze.md create mode 100644 plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts diff --git a/.changeset/chilly-rabbits-sneeze.md b/.changeset/chilly-rabbits-sneeze.md new file mode 100644 index 0000000000..cbce80ebbc --- /dev/null +++ b/.changeset/chilly-rabbits-sneeze.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-atlassian-provider': patch +--- + +Fix several issues with the Atlassian auth provider (type definition, profile url, profile transformation, scopes) diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..b8e9e1ad75 --- /dev/null +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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. + */ +import Strategy from 'passport-atlassian-oauth2'; + +describe('Strategy', () => { + it('should be an instance of', () => { + try { + console.log(Strategy); + const strategy = new Strategy( + { + authorizationURL: 'https://auth.atlassian.com/authorize', + tokenURL: 'https://auth.atlassian.com/oauth/token', + clientID: 'my-client-id', + clientSecret: 'my-client-secret', + scope: ['offline_access', 'read:jira-work', 'read:jira-user'], + }, + () => {}, + ); + console.log(strategy.name); + expect(strategy).toBeInstanceOf(Strategy); + + expect((strategy as any).name).toBe('atlassian'); + } catch (e) { + console.error(e); + } + }); +}); diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index 0ced9cf831..ab2cd3f94a 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -20,14 +20,35 @@ import { PassportOAuthDoneCallback, PassportProfile, } from '@backstage/plugin-auth-node'; -import { Strategy as AtlassianStrategy } from 'passport-atlassian-oauth2'; +import AtlassianStrategy from 'passport-atlassian-oauth2'; + +export type AtlassianPassportProfile = { + id: string; + displayName: string; + email: string; + photo: string; + provider: string; + _json: any; +}; /** @public */ -export const atlassianAuthenticator = createOAuthAuthenticator({ - defaultProfileTransform: - PassportOAuthAuthenticatorHelper.defaultProfileTransform, +export const atlassianAuthenticator = createOAuthAuthenticator< + any, + AtlassianPassportProfile +>({ + defaultProfileTransform: async (input, context) => { + // const result = await PassportOAuthAuthenticatorHelper.defaultProfileTransform(input, context); + + return { + profile: { + displayName: input.fullProfile.displayName, + email: input.fullProfile.email, + picture: input.fullProfile.photo, + }, + }; + }, scopes: { - required: ['offline_access', 'read:jira-work', 'read:jira-user'], + required: ['offline_access', 'read:me', 'read:jira-work', 'read:jira-user'], }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); @@ -49,18 +70,25 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ baseURL: baseUrl, authorizationURL: `${baseUrl}/authorize`, tokenURL: `${baseUrl}/oauth/token`, - profileURL: `${baseUrl}/api/v4/user`, + // profileURL: `${baseUrl}/api/v4/user`, + profileURL: 'https://api.atlassian.com/me', + scope: config.getOptionalString('additionalScopes')?.split(' ') || [], }, ( accessToken: string, refreshToken: string, params: any, - fullProfile: PassportProfile, + fullProfile: PassportProfile & { email: string; photo: string }, done: PassportOAuthDoneCallback, ) => { + const fullProfileWithEmails = { + ...fullProfile, + // avatarUrl: fullProfile.photo, + // emails: [{ value: fullProfile.email }], + }; done( undefined, - { fullProfile, params, accessToken }, + { fullProfile: fullProfileWithEmails, params, accessToken }, { refreshToken }, ); }, diff --git a/plugins/auth-backend-module-atlassian-provider/src/types.d.ts b/plugins/auth-backend-module-atlassian-provider/src/types.d.ts index 5fd47bf885..aa945f3705 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/types.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/types.d.ts @@ -18,8 +18,9 @@ declare module 'passport-atlassian-oauth2' { import { Request } from 'express'; import { StrategyCreated } from 'passport'; - export class Strategy { + export default class Strategy { constructor(options: any, verify: any); + name?: string; authenticate(this: StrategyCreated, req: Request, options?: any): any; } } From be5f08f3a8474cb428ddaf0f6b2fe6b61b27fdaa Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 10:20:17 +0200 Subject: [PATCH 052/212] Cleaning up details Signed-off-by: Olivier Liechti --- .../src/authenticator.test.ts | 3 --- .../src/authenticator.ts | 8 +------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts index b8e9e1ad75..cb723ecece 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts @@ -29,9 +29,6 @@ describe('Strategy', () => { }, () => {}, ); - console.log(strategy.name); - expect(strategy).toBeInstanceOf(Strategy); - expect((strategy as any).name).toBe('atlassian'); } catch (e) { console.error(e); diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index ab2cd3f94a..ff020f131d 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -70,7 +70,6 @@ export const atlassianAuthenticator = createOAuthAuthenticator< baseURL: baseUrl, authorizationURL: `${baseUrl}/authorize`, tokenURL: `${baseUrl}/oauth/token`, - // profileURL: `${baseUrl}/api/v4/user`, profileURL: 'https://api.atlassian.com/me', scope: config.getOptionalString('additionalScopes')?.split(' ') || [], }, @@ -81,14 +80,9 @@ export const atlassianAuthenticator = createOAuthAuthenticator< fullProfile: PassportProfile & { email: string; photo: string }, done: PassportOAuthDoneCallback, ) => { - const fullProfileWithEmails = { - ...fullProfile, - // avatarUrl: fullProfile.photo, - // emails: [{ value: fullProfile.email }], - }; done( undefined, - { fullProfile: fullProfileWithEmails, params, accessToken }, + { fullProfile, params, accessToken }, { refreshToken }, ); }, From f7cf3a2a875f833c63dbab687b77b28eeb991f67 Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 10:30:26 +0200 Subject: [PATCH 053/212] Cleaning up details Signed-off-by: Olivier Liechti --- .../auth-backend-module-atlassian-provider/src/authenticator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index ff020f131d..1e7b631493 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -77,7 +77,7 @@ export const atlassianAuthenticator = createOAuthAuthenticator< accessToken: string, refreshToken: string, params: any, - fullProfile: PassportProfile & { email: string; photo: string }, + fullProfile: AtlassianPassportProfile, done: PassportOAuthDoneCallback, ) => { done( From 3cdea7aa1e38893c239d71b2ac073457ffe059ff Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 12:12:58 +0200 Subject: [PATCH 054/212] Fix typescript typing issues Signed-off-by: Olivier Liechti --- .../src/authenticator.ts | 32 +++++++++++-------- .../src/module.ts | 7 ++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index 1e7b631493..8a1b952d15 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -16,9 +16,10 @@ import { createOAuthAuthenticator, + OAuthAuthenticatorResult, PassportOAuthAuthenticatorHelper, PassportOAuthDoneCallback, - PassportProfile, + ProfileInfo, } from '@backstage/plugin-auth-node'; import AtlassianStrategy from 'passport-atlassian-oauth2'; @@ -32,19 +33,20 @@ export type AtlassianPassportProfile = { }; /** @public */ -export const atlassianAuthenticator = createOAuthAuthenticator< - any, - AtlassianPassportProfile ->({ - defaultProfileTransform: async (input, context) => { - // const result = await PassportOAuthAuthenticatorHelper.defaultProfileTransform(input, context); +export const atlassianAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: async ( + input: OAuthAuthenticatorResult, + ) => { + const atlassianProfile = input.fullProfile as AtlassianPassportProfile; + + const profile: ProfileInfo = { + displayName: atlassianProfile.displayName, + email: atlassianProfile.email, + picture: atlassianProfile.photo, + }; return { - profile: { - displayName: input.fullProfile.displayName, - email: input.fullProfile.email, - picture: input.fullProfile.photo, - }, + profile, }; }, scopes: { @@ -98,10 +100,12 @@ export const atlassianAuthenticator = createOAuthAuthenticator< }, async authenticate(input, helper) { - return helper.authenticate(input); + const result = await helper.authenticate(input); + return result as OAuthAuthenticatorResult; }, async refresh(input, helper) { - return helper.refresh(input); + const result = await helper.refresh(input); + return result as OAuthAuthenticatorResult; }, }); diff --git a/plugins/auth-backend-module-atlassian-provider/src/module.ts b/plugins/auth-backend-module-atlassian-provider/src/module.ts index 9acb665ede..2080da3065 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/module.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/module.ts @@ -19,7 +19,10 @@ import { commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; -import { atlassianAuthenticator } from './authenticator'; +import { + AtlassianPassportProfile, + atlassianAuthenticator, +} from './authenticator'; import { atlassianSignInResolvers } from './resolvers'; /** @public */ @@ -34,7 +37,7 @@ export const authModuleAtlassianProvider = createBackendModule({ async init({ providers }) { providers.registerProvider({ providerId: 'atlassian', - factory: createOAuthProviderFactory({ + factory: createOAuthProviderFactory({ authenticator: atlassianAuthenticator, signInResolverFactories: { ...atlassianSignInResolvers, From a6acc4c2ea28965e0d695972b0eecb9f78be4373 Mon Sep 17 00:00:00 2001 From: Lorenzo Davis <118796878+lorrrrrrrenzo@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:24:35 +0400 Subject: [PATCH 055/212] Export `defaultEksClusterEntityTransformer` from `catalog-backend-module-aws` Signed-off-by: Lorenzo Davis <118796878+lorrrrrrrenzo@users.noreply.github.com> --- plugins/catalog-backend-module-aws/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend-module-aws/src/index.ts b/plugins/catalog-backend-module-aws/src/index.ts index 72ddc6386a..de8f9fd7b2 100644 --- a/plugins/catalog-backend-module-aws/src/index.ts +++ b/plugins/catalog-backend-module-aws/src/index.ts @@ -24,3 +24,4 @@ export * from './processors'; export * from './providers'; export * from './types'; export * from './constants'; +export { defaultEksClusterEntityTransformer } from './lib'; From 4afa050758b60b81ad978b6a293dbb94fcc05c4e Mon Sep 17 00:00:00 2001 From: Lorenzo Davis <118796878+lorrrrrrrenzo@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:31:17 +0400 Subject: [PATCH 056/212] Add changeset Signed-off-by: Lorenzo Davis <118796878+lorrrrrrrenzo@users.noreply.github.com> --- .changeset/mighty-keys-bow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-keys-bow.md diff --git a/.changeset/mighty-keys-bow.md b/.changeset/mighty-keys-bow.md new file mode 100644 index 0000000000..5f1bb408c9 --- /dev/null +++ b/.changeset/mighty-keys-bow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +Export `defaultEksClusterEntityTransformer` to allow library consumers to layer additional changes on top of the default transformer. From baaf9977730807c67602324c02941000eb759fd6 Mon Sep 17 00:00:00 2001 From: Lorenzo Davis <118796878+lorrrrrrrenzo@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:17:40 +0400 Subject: [PATCH 057/212] Update API Report in light of the change Signed-off-by: Lorenzo Davis <118796878+lorrrrrrrenzo@users.noreply.github.com> --- plugins/catalog-backend-module-aws/api-report.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 5098ab5c32..428a39d934 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -106,6 +106,9 @@ export class AwsS3EntityProvider implements EntityProvider { refresh(logger: LoggerService): Promise; } +// @public +export const defaultEksClusterEntityTransformer: EksClusterEntityTransformer; + // @public export type EksClusterEntityTransformer = ( cluster: Cluster, From 9d9a391cfb92796faf192a0f60fdba01bb7ce5f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Jul 2024 14:21:33 +0200 Subject: [PATCH 058/212] beps/0009: move to implemented Signed-off-by: Patrik Oldsberg --- beps/0009-plugin-metadata/README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/beps/0009-plugin-metadata/README.md b/beps/0009-plugin-metadata/README.md index c0ae487daa..59101f121c 100644 --- a/beps/0009-plugin-metadata/README.md +++ b/beps/0009-plugin-metadata/README.md @@ -1,6 +1,6 @@ --- title: Plugin Metadata -status: implementable +status: implemented authors: - '@Rugvip' owners: @@ -10,16 +10,8 @@ project-areas: creation-date: yyyy-mm-dd --- - - # BEP: Plugin Metadata - - [**Discussion Issue**](https://github.com/backstage/backstage/issues/25076) - [Summary](#summary) From e583b476870481d92c16e951ad0ab357cb8ad093 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Jul 2024 12:58:58 +0000 Subject: [PATCH 059/212] Version Packages (next) --- .changeset/pre.json | 6 + docs/releases/v1.29.0-next.2-changelog.md | 446 ++++++++++++++++++ package.json | 2 +- packages/app-defaults/CHANGELOG.md | 7 + packages/app-defaults/package.json | 2 +- packages/app-next-example-plugin/CHANGELOG.md | 8 + packages/app-next-example-plugin/package.json | 2 +- packages/app-next/CHANGELOG.md | 35 ++ packages/app-next/package.json | 2 +- packages/app/CHANGELOG.md | 33 ++ packages/app/package.json | 2 +- packages/backend-legacy/CHANGELOG.md | 8 + packages/backend-legacy/package.json | 2 +- packages/backend/CHANGELOG.md | 7 + packages/backend/package.json | 2 +- packages/core-compat-api/CHANGELOG.md | 7 + packages/core-compat-api/package.json | 2 +- packages/core-components/CHANGELOG.md | 6 + packages/core-components/package.json | 2 +- packages/create-app/CHANGELOG.md | 6 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 10 + packages/dev-utils/package.json | 2 +- packages/e2e-test/CHANGELOG.md | 7 + packages/e2e-test/package.json | 2 +- packages/frontend-app-api/CHANGELOG.md | 8 + packages/frontend-app-api/package.json | 2 +- packages/frontend-plugin-api/CHANGELOG.md | 7 + packages/frontend-plugin-api/package.json | 2 +- packages/frontend-test-utils/CHANGELOG.md | 8 + packages/frontend-test-utils/package.json | 2 +- .../techdocs-cli-embedded-app/CHANGELOG.md | 13 + .../techdocs-cli-embedded-app/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 11 + plugins/api-docs/package.json | 2 +- plugins/app-visualizer/CHANGELOG.md | 8 + plugins/app-visualizer/package.json | 2 +- plugins/auth-react/CHANGELOG.md | 7 + plugins/auth-react/package.json | 2 +- plugins/catalog-graph/CHANGELOG.md | 10 + plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/CHANGELOG.md | 11 + plugins/catalog-import/package.json | 2 +- plugins/catalog-react/CHANGELOG.md | 9 + plugins/catalog-react/package.json | 2 +- .../catalog-unprocessed-entities/CHANGELOG.md | 7 + .../catalog-unprocessed-entities/package.json | 2 +- plugins/catalog/CHANGELOG.md | 12 + plugins/catalog/package.json | 2 +- plugins/config-schema/CHANGELOG.md | 7 + plugins/config-schema/package.json | 2 +- plugins/devtools/CHANGELOG.md | 9 + plugins/devtools/package.json | 2 +- plugins/example-todo-list/CHANGELOG.md | 7 + plugins/example-todo-list/package.json | 2 +- plugins/home-react/CHANGELOG.md | 7 + plugins/home-react/package.json | 2 +- plugins/home/CHANGELOG.md | 11 + plugins/home/package.json | 2 +- plugins/kubernetes-cluster/CHANGELOG.md | 9 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes-react/CHANGELOG.md | 8 + plugins/kubernetes-react/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 9 + plugins/kubernetes/package.json | 2 +- plugins/notifications/CHANGELOG.md | 7 + plugins/notifications/package.json | 2 +- plugins/org-react/CHANGELOG.md | 8 + plugins/org-react/package.json | 2 +- plugins/org/CHANGELOG.md | 10 + plugins/org/package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 6 + plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-react/CHANGELOG.md | 8 + plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 12 + plugins/scaffolder/package.json | 2 +- plugins/search-react/CHANGELOG.md | 8 + plugins/search-react/package.json | 2 +- plugins/search/CHANGELOG.md | 12 + plugins/search/package.json | 2 +- plugins/signals/CHANGELOG.md | 7 + plugins/signals/package.json | 2 +- .../techdocs-addons-test-utils/CHANGELOG.md | 12 + .../techdocs-addons-test-utils/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- plugins/techdocs-react/CHANGELOG.md | 7 + plugins/techdocs-react/package.json | 2 +- plugins/techdocs/CHANGELOG.md | 15 + plugins/techdocs/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 10 + plugins/user-settings/package.json | 2 +- 93 files changed, 941 insertions(+), 46 deletions(-) create mode 100644 docs/releases/v1.29.0-next.2-changelog.md diff --git a/.changeset/pre.json b/.changeset/pre.json index ebf0c6bf22..acd37c2559 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -197,9 +197,11 @@ "create-app-1719320674", "curvy-teachers-smell", "eighty-games-wink", + "eleven-radios-begin", "fair-fans-flow", "fair-pillows-know", "fair-rockets-leave", + "fair-spoons-work", "famous-dodos-crash", "fast-pens-smell", "fifty-pumpkins-smell", @@ -214,6 +216,7 @@ "heavy-moose-pull", "honest-pears-run", "khaki-rivers-obey", + "kind-dancers-boil", "light-avocados-worry", "little-games-fail", "metal-jokes-add", @@ -221,6 +224,7 @@ "metal-suits-drum", "moody-llamas-breathe", "neat-dingos-tickle", + "new-sheep-arrive", "poor-cheetahs-raise", "rare-peas-dream", "renovate-7b61228", @@ -228,6 +232,7 @@ "rich-bears-march", "selfish-turtles-jog", "serious-kings-trade", + "sharp-rice-tickle", "short-flowers-cry", "shy-hounds-battle", "silent-experts-move", @@ -241,6 +246,7 @@ "ten-nails-hear", "ten-pots-walk", "thick-lizards-divide", + "thick-moose-decide", "three-carpets-smoke", "tough-lies-mate", "weak-boats-float", diff --git a/docs/releases/v1.29.0-next.2-changelog.md b/docs/releases/v1.29.0-next.2-changelog.md new file mode 100644 index 0000000000..f6269407cf --- /dev/null +++ b/docs/releases/v1.29.0-next.2-changelog.md @@ -0,0 +1,446 @@ +# Release v1.29.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.29.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.29.0-next.2) + +## @backstage/app-defaults@1.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/core-compat-api@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/core-components@0.14.9-next.1 + +### Patch Changes + +- 99d672d: Modified the `Select` component to take in a `data-testid` parameter ensuring backwards compatibility with default value corresponding to previously hardcoded `data-testid` of "select". + +## @backstage/create-app@0.5.17-next.2 + +### Patch Changes + +- e90a2cd: Added the Catalog logs module to the `create-app` template + +## @backstage/dev-utils@1.0.35-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/frontend-app-api@0.7.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/frontend-plugin-api@0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/frontend-test-utils@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/plugin-api-docs@0.11.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-app-visualizer@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/plugin-auth-react@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-catalog@1.21.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-catalog-graph@0.4.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-catalog-import@0.12.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-catalog-react@1.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-config-schema@0.1.57-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-devtools@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-home@0.7.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-home-react@0.1.15-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-home-react@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-kubernetes@0.11.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-kubernetes-react@0.4.1-next.1 + +### Patch Changes + +- e3cb6ab: Add a namespace label to RolloutDrawer +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-notifications@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-org@0.6.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-org-react@0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-scaffolder@1.22.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-scaffolder-backend@1.23.0-next.2 + +### Patch Changes + +- ff1bb4c: Added a documentation how to use checkpoints + +## @backstage/plugin-scaffolder-react@1.10.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-search@1.4.14-next.2 + +### Patch Changes + +- 1117aba: Update deps in search api extension to include fetch api +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-search-react@1.7.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/plugin-signals@0.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-techdocs@1.10.7-next.2 + +### Patch Changes + +- 6fa652c: Improve default sorting of docs table +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.35-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + +## @backstage/plugin-techdocs-react@1.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-user-settings@0.8.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## example-app@0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-devtools@0.1.16-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + +## example-app-next@0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-app-visualizer@0.1.8-next.1 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## app-next-example-plugin@0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## example-backend@0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + +## example-backend-legacy@0.2.100-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + - example-app@0.2.99-next.2 + +## e2e-test@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.2 + +## techdocs-cli-embedded-app@0.2.98-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + +## @internal/plugin-todo-list@1.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 diff --git a/package.json b/package.json index 7de757ff9b..d3d042c2f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.29.0-next.1", + "version": "1.29.0-next.2", "private": true, "repository": { "type": "git", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 588c59c3e4..21fe959edb 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/app-defaults +## 1.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 1.5.8-next.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 8bb0a707b5..4393164fcd 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.5.8-next.1", + "version": "1.5.8-next.2", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index e6788c8566..5456da237c 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # app-next-example-plugin +## 0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + ## 0.0.13-next.0 ### Patch Changes diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index 9d2e76c418..c3faec66d6 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.13-next.0", + "version": "0.0.13-next.1", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 6c3d48601f..22d53e4c78 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,40 @@ # example-app-next +## 0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-app-visualizer@0.1.8-next.1 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.0.13-next.1 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index a0f0174753..c38eacfcd5 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.13-next.1", + "version": "0.0.13-next.2", "private": true, "repository": { "type": "git", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 3a06dd99fd..4977cc1d6d 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,38 @@ # example-app +## 0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-devtools@0.1.16-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + ## 0.2.99-next.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index a30c8e15d7..f15dbac9db 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.99-next.1", + "version": "0.2.99-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/backend-legacy/CHANGELOG.md b/packages/backend-legacy/CHANGELOG.md index 7734d12d4f..35ed7a6c93 100644 --- a/packages/backend-legacy/CHANGELOG.md +++ b/packages/backend-legacy/CHANGELOG.md @@ -1,5 +1,13 @@ # example-backend-legacy +## 0.2.100-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + - example-app@0.2.99-next.2 + ## 0.2.100-next.1 ### Patch Changes diff --git a/packages/backend-legacy/package.json b/packages/backend-legacy/package.json index 384ce61973..a537361746 100644 --- a/packages/backend-legacy/package.json +++ b/packages/backend-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-legacy", - "version": "0.2.100-next.1", + "version": "0.2.100-next.2", "backstage": { "role": "backend" }, diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 0c91daa13d..4ceb786f2a 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,12 @@ # example-backend +## 0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + ## 0.0.28-next.1 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 8e6918bfc5..459db3c317 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.28-next.1", + "version": "0.0.28-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 8bcedca9c4..3ad2bd654e 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-compat-api +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7-next.1 + ## 0.2.7-next.0 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 954cd6deb3..0f164a6187 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.2.7-next.0", + "version": "0.2.7-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index f42b6396ce..163406bbca 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core-components +## 0.14.9-next.1 + +### Patch Changes + +- 99d672d: Modified the `Select` component to take in a `data-testid` parameter ensuring backwards compatibility with default value corresponding to previously hardcoded `data-testid` of "select". + ## 0.14.9-next.0 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 72f733e1a9..21c4a19dbf 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.14.9-next.0", + "version": "0.14.9-next.1", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 3f102c17da..708a9b33f8 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/create-app +## 0.5.17-next.2 + +### Patch Changes + +- e90a2cd: Added the Catalog logs module to the `create-app` template + ## 0.5.17-next.1 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 96be1f941c..c1ca92f87b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.5.17-next.1", + "version": "0.5.17-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 62a5c2adc2..ee9f56dec7 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/dev-utils +## 1.0.35-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + ## 1.0.35-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 3e05873d6c..50220c36e1 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.0.35-next.1", + "version": "1.0.35-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index a11001a2aa..5144d9fd86 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,12 @@ # e2e-test +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.2 + ## 0.2.18-next.1 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index d1f42a1d8f..1d65314e83 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.18-next.1", + "version": "0.2.18-next.2", "private": true, "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index fc39807e5b..dde282a831 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/frontend-app-api +## 0.7.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + ## 0.7.3-next.1 ### Patch Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index a6c2583fbc..68c2fd8fab 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.7.3-next.1", + "version": "0.7.3-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 15e3c06bd6..dda46968a4 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/frontend-plugin-api +## 0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.6.7-next.0 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index be603e2611..b62fe36059 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.6.7-next.0", + "version": "0.6.7-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 5ece8edb86..34d9b28143 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/frontend-test-utils +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + ## 0.1.10-next.1 ### Patch Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 9856a09367..2a366fa152 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.1.10-next.1", + "version": "0.1.10-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 343ecf5ccb..123e82edf7 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,18 @@ # techdocs-cli-embedded-app +## 0.2.98-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + ## 0.2.98-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 3b234c8f4c..979d18620f 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.98-next.1", + "version": "0.2.98-next.2", "private": true, "backstage": { "role": "frontend" diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index f41b4534f1..58d69f516d 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-api-docs +## 0.11.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.11.7-next.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 84e2891bcc..5a6d97b7ce 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.11.7-next.1", + "version": "0.11.7-next.2", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index d4a23d79f2..cdb8bab69e 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-visualizer +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + ## 0.1.8-next.0 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index 31c7dfd87e..95df128040 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.8-next.0", + "version": "0.1.8-next.1", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index 668f751352..84bfadc98c 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-auth-react +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 114ecd1d55..8af97a0b04 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.4-next.0", + "version": "0.1.4-next.1", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index fd2b0477e0..f397955e1c 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-graph +## 0.4.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.4.7-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 22e30e3406..eb27725d45 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.7-next.1", + "version": "0.4.7-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index bebc0c5240..455f094a86 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-import +## 0.12.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.12.1-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 704fdae2e6..f41d239792 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.12.1-next.1", + "version": "0.12.1-next.2", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 68febf6b12..b29a49e47d 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-react +## 1.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + ## 1.12.2-next.1 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 99eb8af013..f522a4f111 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.12.2-next.1", + "version": "1.12.2-next.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index ab22634257..fefeffe9d2 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.2.6-next.0 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index f32ff293df..142b850bd3 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.2.6-next.0", + "version": "0.2.6-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-unprocessed-entities", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index d003afccf1..08a70d4956 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog +## 1.21.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + ## 1.21.1-next.1 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 5cf63e86ab..0d101af3df 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.21.1-next.1", + "version": "1.21.1-next.2", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 76472b69b4..8458a50aa3 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-config-schema +## 0.1.57-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.1.57-next.0 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 38db6a21d5..9a1971d80b 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-config-schema", - "version": "0.1.57-next.0", + "version": "0.1.57-next.1", "description": "A Backstage plugin that lets you browse the configuration schema of your app", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index 977e9cd73f..5bc6220aa3 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-devtools +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.1.16-next.0 ### Patch Changes diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index 2749bbeef2..90d724692c 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.16-next.0", + "version": "0.1.16-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 2f392e6c33..f61f8b8d0a 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list +## 1.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 1.0.29-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index c0ef80b3de..d008b73538 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.29-next.0", + "version": "1.0.29-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "todo-list", diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index 8353571dd8..1f1c9a0e2e 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-home-react +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index b5dcaae83e..ec05852818 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-react", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "description": "A Backstage plugin that contains react components helps you build a home page", "backstage": { "role": "web-library", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index b475b7f764..e65b3dec1c 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-home +## 0.7.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-home-react@0.1.15-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.7.7-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 928417145b..01dd27a295 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.7.7-next.1", + "version": "0.7.7-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 0d8596888a..8932454b3c 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + ## 0.0.13-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 40a6d33dd2..3c6e6c8f1b 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.13-next.1", + "version": "0.0.13-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 81b5c62fad..3755d259a8 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-react +## 0.4.1-next.1 + +### Patch Changes + +- e3cb6ab: Add a namespace label to RolloutDrawer +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.4.1-next.0 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 0cc34da7db..fc6f382d3d 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.4.1-next.0", + "version": "0.4.1-next.1", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 2d92f6b026..a6d07dc706 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes +## 0.11.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + ## 0.11.12-next.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index eef4da08bc..d0ee97a882 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.11.12-next.1", + "version": "0.11.12-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 9f532b3698..97e6c50b44 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-notifications +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 2e2c67e891..4ef28f9508 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.2.3-next.1", + "version": "0.2.3-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 1ce749fede..d13578fd32 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-org-react +## 0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + ## 0.1.26-next.1 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 327e27662f..e6494eaa28 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.26-next.1", + "version": "0.1.26-next.2", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 5ca788557f..aa4bd9a93f 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-org +## 0.6.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.6.27-next.1 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 5c62ffa99f..c2d43bc5ed 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.27-next.1", + "version": "0.6.27-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 97cd859856..a4df9e72e9 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-scaffolder-backend +## 1.23.0-next.2 + +### Patch Changes + +- ff1bb4c: Added a documentation how to use checkpoints + ## 1.23.0-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 71857e245b..10d827762e 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "1.23.0-next.1", + "version": "1.23.0-next.2", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 6cbdb781f3..a1ed542bd5 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-react +## 1.10.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + ## 1.10.0-next.1 ### Minor Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 6bdc263b3d..36ee0870fc 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.10.0-next.1", + "version": "1.10.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 566411b9bd..3c4cfe54ab 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder +## 1.22.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 1.22.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 0db796359a..95dd5bff08 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.22.1-next.1", + "version": "1.22.1-next.2", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 19ace29f0c..494ce07772 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-react +## 1.7.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + ## 1.7.13-next.0 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 25bc946bd4..a0701e1f3b 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.7.13-next.0", + "version": "1.7.13-next.1", "backstage": { "role": "web-library", "pluginId": "search", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index dac2074373..a6cab671b5 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search +## 1.4.14-next.2 + +### Patch Changes + +- 1117aba: Update deps in search api extension to include fetch api +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + ## 1.4.13-next.1 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 3b08790026..ad8210df4c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.13-next.1", + "version": "1.4.14-next.2", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index f5992789c5..9ccdff8b97 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-signals +## 0.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 0.0.8-next.0 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 18345295b5..58b82fab7a 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.8-next.0", + "version": "0.0.8-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "signals", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 0695f7e1f4..5b247addab 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.35-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + ## 1.0.35-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index fd1c806414..966308ee94 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.35-next.1", + "version": "1.0.35-next.2", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index da76210708..2cc99a8158 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + ## 1.1.12-next.0 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index f0b2301dfb..f68df2a883 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", - "version": "1.1.12-next.0", + "version": "1.1.12-next.1", "description": "Plugin module for contributed TechDocs Addons", "backstage": { "role": "frontend-plugin-module", diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index d07ed27eb0..97677352df 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-techdocs-react +## 1.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + ## 1.2.6-next.0 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index d002db68d1..1686b176ca 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.2.6-next.0", + "version": "1.2.6-next.1", "description": "Shared frontend utilities for TechDocs and Addons", "backstage": { "role": "web-library", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index a14559de1e..34dc9a310f 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs +## 1.10.7-next.2 + +### Patch Changes + +- 6fa652c: Improve default sorting of docs table +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + ## 1.10.7-next.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 2a404551be..cbf1f828ce 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.10.7-next.1", + "version": "1.10.7-next.2", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 5b86011ba8..8d886aee80 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-user-settings +## 0.8.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + ## 0.8.9-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index a549c7cdd2..34c719d9df 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.9-next.1", + "version": "0.8.9-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", From efda5a301dbe9a4c40e9194559d32cc606734aeb Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 15:08:24 +0200 Subject: [PATCH 060/212] Export Atlassian OAuth Profile type Signed-off-by: Olivier Liechti --- .changeset/nasty-balloons-attack.md | 5 +++++ plugins/auth-backend-module-atlassian-provider/src/index.ts | 3 +-- plugins/auth-backend/src/providers/atlassian/provider.ts | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/nasty-balloons-attack.md diff --git a/.changeset/nasty-balloons-attack.md b/.changeset/nasty-balloons-attack.md new file mode 100644 index 0000000000..60229b4a25 --- /dev/null +++ b/.changeset/nasty-balloons-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Use new Atlassian Passport profile in type definition diff --git a/plugins/auth-backend-module-atlassian-provider/src/index.ts b/plugins/auth-backend-module-atlassian-provider/src/index.ts index cc71c9b7e3..224037b868 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/index.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/index.ts @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /** * The atlassian-provider backend module for the auth plugin. * * @packageDocumentation */ - export { atlassianAuthenticator } from './authenticator'; +export type { AtlassianPassportProfile } from './authenticator'; export { authModuleAtlassianProvider as default } from './module'; export { atlassianSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 22db26fd85..d30164bd6e 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -26,6 +26,7 @@ import { import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { AuthHandler } from '../types'; +import { AtlassianPassportProfile } from '@backstage/plugin-auth-backend-module-atlassian-provider'; /** * Auth provider integration for Atlassian auth @@ -47,7 +48,7 @@ export const atlassian = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return createOAuthProviderFactory({ + return createOAuthProviderFactory({ authenticator: atlassianAuthenticator, profileTransform: adaptLegacyOAuthHandler(options?.authHandler), signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), From 55c1a729ac4cdd84afd9ac4bb081b9b1c2ad7274 Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 16:29:50 +0200 Subject: [PATCH 061/212] Simplify the bug fix, by implementing logic in the PassportHelper Signed-off-by: Olivier Liechti --- .changeset/stale-beds-sneeze.md | 6 +++ .../src/authenticator.ts | 37 +++---------------- .../src/index.ts | 1 - .../src/providers/atlassian/provider.ts | 3 +- .../auth-node/src/passport/PassportHelpers.ts | 7 ++++ plugins/auth-node/src/passport/types.ts | 2 + 6 files changed, 22 insertions(+), 34 deletions(-) create mode 100644 .changeset/stale-beds-sneeze.md diff --git a/.changeset/stale-beds-sneeze.md b/.changeset/stale-beds-sneeze.md new file mode 100644 index 0000000000..e2fc5acdf2 --- /dev/null +++ b/.changeset/stale-beds-sneeze.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend-module-atlassian-provider': patch +'@backstage/plugin-auth-node': patch +--- + +Fix issues with Atlassian OAuth provider diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index 8a1b952d15..23a4a52197 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -16,39 +16,16 @@ import { createOAuthAuthenticator, - OAuthAuthenticatorResult, PassportOAuthAuthenticatorHelper, PassportOAuthDoneCallback, - ProfileInfo, + PassportProfile, } from '@backstage/plugin-auth-node'; import AtlassianStrategy from 'passport-atlassian-oauth2'; -export type AtlassianPassportProfile = { - id: string; - displayName: string; - email: string; - photo: string; - provider: string; - _json: any; -}; - /** @public */ export const atlassianAuthenticator = createOAuthAuthenticator({ - defaultProfileTransform: async ( - input: OAuthAuthenticatorResult, - ) => { - const atlassianProfile = input.fullProfile as AtlassianPassportProfile; - - const profile: ProfileInfo = { - displayName: atlassianProfile.displayName, - email: atlassianProfile.email, - picture: atlassianProfile.photo, - }; - - return { - profile, - }; - }, + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, scopes: { required: ['offline_access', 'read:me', 'read:jira-work', 'read:jira-user'], }, @@ -79,7 +56,7 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ accessToken: string, refreshToken: string, params: any, - fullProfile: AtlassianPassportProfile, + fullProfile: PassportProfile, done: PassportOAuthDoneCallback, ) => { done( @@ -100,12 +77,10 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ }, async authenticate(input, helper) { - const result = await helper.authenticate(input); - return result as OAuthAuthenticatorResult; + return helper.authenticate(input); }, async refresh(input, helper) { - const result = await helper.refresh(input); - return result as OAuthAuthenticatorResult; + return helper.refresh(input); }, }); diff --git a/plugins/auth-backend-module-atlassian-provider/src/index.ts b/plugins/auth-backend-module-atlassian-provider/src/index.ts index 224037b868..8f090b76d4 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/index.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/index.ts @@ -19,6 +19,5 @@ * @packageDocumentation */ export { atlassianAuthenticator } from './authenticator'; -export type { AtlassianPassportProfile } from './authenticator'; export { authModuleAtlassianProvider as default } from './module'; export { atlassianSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index d30164bd6e..22db26fd85 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -26,7 +26,6 @@ import { import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { AuthHandler } from '../types'; -import { AtlassianPassportProfile } from '@backstage/plugin-auth-backend-module-atlassian-provider'; /** * Auth provider integration for Atlassian auth @@ -48,7 +47,7 @@ export const atlassian = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return createOAuthProviderFactory({ + return createOAuthProviderFactory({ authenticator: atlassianAuthenticator, profileTransform: adaptLegacyOAuthHandler(options?.authHandler), signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), diff --git a/plugins/auth-node/src/passport/PassportHelpers.ts b/plugins/auth-node/src/passport/PassportHelpers.ts index d7b554d56a..8531309917 100644 --- a/plugins/auth-node/src/passport/PassportHelpers.ts +++ b/plugins/auth-node/src/passport/PassportHelpers.ts @@ -42,12 +42,19 @@ export class PassportHelpers { email = firstEmail.value; } + // This is the case for Atlassian + if (profile.email) { + email = profile.email; + } + let picture: string | undefined = undefined; if (profile.avatarUrl) { picture = profile.avatarUrl; } else if (profile.photos && profile.photos.length > 0) { const [firstPhoto] = profile.photos; picture = firstPhoto.value; + } else if (profile.photo) { + picture = profile.photo; // This is the case for Atlassian } let displayName: string | undefined = diff --git a/plugins/auth-node/src/passport/types.ts b/plugins/auth-node/src/passport/types.ts index 4a3f41b6b2..d5681fa9e3 100644 --- a/plugins/auth-node/src/passport/types.ts +++ b/plugins/auth-node/src/passport/types.ts @@ -19,6 +19,8 @@ import { Profile } from 'passport'; /** @public */ export type PassportProfile = Profile & { avatarUrl?: string; + email?: string; + photo?: string; }; /** @public */ From 487c100100365db39a89814f9dd127f8c17ec40e Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 16:41:18 +0200 Subject: [PATCH 062/212] Simplify the bug fix, by implementing logic in the PassportHelper Signed-off-by: Olivier Liechti --- .../auth-backend-module-atlassian-provider/src/module.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend-module-atlassian-provider/src/module.ts b/plugins/auth-backend-module-atlassian-provider/src/module.ts index 2080da3065..9acb665ede 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/module.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/module.ts @@ -19,10 +19,7 @@ import { commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; -import { - AtlassianPassportProfile, - atlassianAuthenticator, -} from './authenticator'; +import { atlassianAuthenticator } from './authenticator'; import { atlassianSignInResolvers } from './resolvers'; /** @public */ @@ -37,7 +34,7 @@ export const authModuleAtlassianProvider = createBackendModule({ async init({ providers }) { providers.registerProvider({ providerId: 'atlassian', - factory: createOAuthProviderFactory({ + factory: createOAuthProviderFactory({ authenticator: atlassianAuthenticator, signInResolverFactories: { ...atlassianSignInResolvers, From e1eda89df32a9d2b6987c87e8351288379332210 Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 18:58:55 +0200 Subject: [PATCH 063/212] Add api-report.md Signed-off-by: Olivier Liechti --- plugins/auth-node/api-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index dc1d86308a..a3750819f9 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -552,6 +552,8 @@ export type PassportOAuthResult = { // @public (undocumented) export type PassportProfile = Profile & { avatarUrl?: string; + email?: string; + photo?: string; }; // @public From 9076843ffd67153f71b1ff9df0eb9af68ba96236 Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Tue, 9 Jul 2024 19:48:38 +0200 Subject: [PATCH 064/212] Adatp test to the payload returned by the actual Atlassian strategy (vs its parent) Signed-off-by: Olivier Liechti --- .../src/module.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend-module-atlassian-provider/src/module.test.ts b/plugins/auth-backend-module-atlassian-provider/src/module.test.ts index 7b92218d56..7e83aed179 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/module.test.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/module.test.ts @@ -64,10 +64,12 @@ describe('authModuleAtlassianProvider', () => { expect(startUrl.pathname).toBe('/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', + audience: 'api.atlassian.com', client_id: 'my-client-id', + prompt: 'consent', redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`, state: expect.any(String), - scope: 'offline_access read:jira-work read:jira-user', + scope: 'offline_access read:me read:jira-work read:jira-user', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ @@ -92,10 +94,7 @@ describe('authModuleAtlassianProvider', () => { development: { clientId: 'my-client-id', clientSecret: 'my-client-secret', - additionalScopes: [ - 'read:filter:jira', - 'read:jira-work', // already required - ], + additionalScopes: 'read:filter:jira read:jira-work', // 2nd is already required }, }, }, @@ -123,11 +122,14 @@ describe('authModuleAtlassianProvider', () => { expect(startUrl.origin).toBe('https://auth.atlassian.com'); expect(startUrl.pathname).toBe('/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + audience: 'api.atlassian.com', response_type: 'code', client_id: 'my-client-id', + prompt: 'consent', redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`, state: expect.any(String), - scope: 'offline_access read:jira-work read:jira-user read:filter:jira', + scope: + 'offline_access read:me read:jira-work read:jira-user read:filter:jira', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ From aca86a6a007fa8c9c431f92b685f58c591f25c73 Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Wed, 10 Jul 2024 08:11:02 +0200 Subject: [PATCH 065/212] Address review comments Signed-off-by: Olivier Liechti --- .changeset/nasty-balloons-attack.md | 5 ----- .changeset/stale-beds-sneeze.md | 3 +-- plugins/auth-node/src/passport/PassportHelpers.ts | 9 ++++----- 3 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 .changeset/nasty-balloons-attack.md diff --git a/.changeset/nasty-balloons-attack.md b/.changeset/nasty-balloons-attack.md deleted file mode 100644 index 60229b4a25..0000000000 --- a/.changeset/nasty-balloons-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Use new Atlassian Passport profile in type definition diff --git a/.changeset/stale-beds-sneeze.md b/.changeset/stale-beds-sneeze.md index e2fc5acdf2..695e09a624 100644 --- a/.changeset/stale-beds-sneeze.md +++ b/.changeset/stale-beds-sneeze.md @@ -1,6 +1,5 @@ --- -'@backstage/plugin-auth-backend-module-atlassian-provider': patch '@backstage/plugin-auth-node': patch --- -Fix issues with Atlassian OAuth provider +Fix issues with Atlassian OAuth provider: retrieve the email and photo that were not in arrays but rather in single props. diff --git a/plugins/auth-node/src/passport/PassportHelpers.ts b/plugins/auth-node/src/passport/PassportHelpers.ts index 8531309917..d1ddee20a7 100644 --- a/plugins/auth-node/src/passport/PassportHelpers.ts +++ b/plugins/auth-node/src/passport/PassportHelpers.ts @@ -40,10 +40,8 @@ export class PassportHelpers { if (profile.emails && profile.emails.length > 0) { const [firstEmail] = profile.emails; email = firstEmail.value; - } - - // This is the case for Atlassian - if (profile.email) { + } else if (profile.email) { + // This is the case for Atlassian email = profile.email; } @@ -54,7 +52,8 @@ export class PassportHelpers { const [firstPhoto] = profile.photos; picture = firstPhoto.value; } else if (profile.photo) { - picture = profile.photo; // This is the case for Atlassian + // This is the case for Atlassian + picture = profile.photo; } let displayName: string | undefined = From c48f2cc68df4065d1d19fbbf09e167fe89492600 Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Wed, 10 Jul 2024 10:42:16 +0200 Subject: [PATCH 066/212] Cleanup test Signed-off-by: Olivier Liechti --- .../src/authenticator.test.ts | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts index cb723ecece..ab0a4840f9 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts @@ -17,21 +17,16 @@ import Strategy from 'passport-atlassian-oauth2'; describe('Strategy', () => { it('should be an instance of', () => { - try { - console.log(Strategy); - const strategy = new Strategy( - { - authorizationURL: 'https://auth.atlassian.com/authorize', - tokenURL: 'https://auth.atlassian.com/oauth/token', - clientID: 'my-client-id', - clientSecret: 'my-client-secret', - scope: ['offline_access', 'read:jira-work', 'read:jira-user'], - }, - () => {}, - ); - expect((strategy as any).name).toBe('atlassian'); - } catch (e) { - console.error(e); - } + const strategy = new Strategy( + { + authorizationURL: 'https://auth.atlassian.com/authorize', + tokenURL: 'https://auth.atlassian.com/oauth/token', + clientID: 'my-client-id', + clientSecret: 'my-client-secret', + scope: ['offline_access', 'read:jira-work', 'read:jira-user'], + }, + () => {}, + ); + expect((strategy as any).name).toBe('atlassian'); }); }); From 3d9abab684c528f9547814713bf1a93531059417 Mon Sep 17 00:00:00 2001 From: Olivier Liechti Date: Wed, 10 Jul 2024 11:10:56 +0200 Subject: [PATCH 067/212] Pass an empty array to the 'scope' prop required by Atlassian strategy, but which value is not actually used Signed-off-by: Olivier Liechti --- .../auth-backend-module-atlassian-provider/src/authenticator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index 23a4a52197..d4f46b5a8c 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -50,7 +50,7 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ authorizationURL: `${baseUrl}/authorize`, tokenURL: `${baseUrl}/oauth/token`, profileURL: 'https://api.atlassian.com/me', - scope: config.getOptionalString('additionalScopes')?.split(' ') || [], + scope: [], // the Atlassian strategy requires a scope, but Backstage passes the right set of scopes when calling OAuth2Strategy.prototype.authenticate }, ( accessToken: string, From 1cb84d7085cf9be7b5255d26e2a7c4317e9d8da7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jul 2024 11:19:10 +0200 Subject: [PATCH 068/212] backend-defaults: removed the deprecated getPath option from httpRouterServiceFactory Signed-off-by: Patrik Oldsberg --- .changeset/cold-jars-punch.md | 6 ++ packages/backend-app-api/api-report.md | 12 +-- .../httpRouter/httpRouterServiceFactory.ts | 11 +-- .../implementations/httpRouter/index.ts | 1 - .../backend-defaults/api-report-httpRouter.md | 12 +-- .../httpRouterServiceFactory.test.ts | 26 ------ .../httpRouter/httpRouterServiceFactory.ts | 92 +++++++------------ .../src/entrypoints/httpRouter/index.ts | 1 - packages/backend-test-utils/api-report.md | 5 +- 9 files changed, 48 insertions(+), 118 deletions(-) create mode 100644 .changeset/cold-jars-punch.md diff --git a/.changeset/cold-jars-punch.md b/.changeset/cold-jars-punch.md new file mode 100644 index 0000000000..8708e1f670 --- /dev/null +++ b/.changeset/cold-jars-punch.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': minor +'@backstage/backend-app-api': minor +--- + +**BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 7518f8e2cc..c577084a58 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -146,15 +146,11 @@ export const httpAuthServiceFactory: () => ServiceFactory< 'plugin' >; -// Warning: (ae-forgotten-export) The symbol "HttpRouterFactoryOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type HttpRouterFactoryOptions = HttpRouterFactoryOptions_2; - // @public @deprecated -export const httpRouterServiceFactory: ( - options?: HttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory; +export const httpRouterServiceFactory: () => ServiceFactory< + HttpRouterService, + 'plugin' +>; // Warning: (ae-forgotten-export) The symbol "HttpServerCertificateOptions_2" needs to be exported by the entry point index.d.ts // diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts index 9d2b74fb43..0088e53f98 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts @@ -15,16 +15,7 @@ */ // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - httpRouterServiceFactory as _httpRouterServiceFactory, - type HttpRouterFactoryOptions as _HttpRouterFactoryOptions, -} from '../../../../../backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead. - */ -export type HttpRouterFactoryOptions = _HttpRouterFactoryOptions; +import { httpRouterServiceFactory as _httpRouterServiceFactory } from '../../../../../backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory'; /** * HTTP route registration for plugins. diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts index 928a232131..bc12480ae8 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts @@ -15,6 +15,5 @@ */ export { httpRouterServiceFactory } from './httpRouterServiceFactory'; -export type { HttpRouterFactoryOptions } from './httpRouterServiceFactory'; export { createLifecycleMiddleware } from './createLifecycleMiddleware'; export type { LifecycleMiddlewareOptions } from './createLifecycleMiddleware'; diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md index a7ca2c8dd3..f191a5a847 100644 --- a/packages/backend-defaults/api-report-httpRouter.md +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -14,15 +14,11 @@ export function createLifecycleMiddleware( options: LifecycleMiddlewareOptions, ): RequestHandler; -// @public (undocumented) -export interface HttpRouterFactoryOptions { - getPath?(pluginId: string): string; -} - // @public -export const httpRouterServiceFactory: ( - options?: HttpRouterFactoryOptions | undefined, -) => ServiceFactory; +export const httpRouterServiceFactory: () => ServiceFactory< + HttpRouterService, + 'plugin' +>; // @public export interface LifecycleMiddlewareOptions { diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts index 58c074f985..17665a176a 100644 --- a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts @@ -52,32 +52,6 @@ describe('httpRouterFactory', () => { ); }); - it('should use custom path generator', async () => { - const rootHttpRouter = mockServices.rootHttpRouter.mock(); - const tester = ServiceFactoryTester.from( - httpRouterServiceFactory({ - getPath: id => `/some/${id}/path`, - }), - { dependencies: [rootHttpRouter.factory] }, - ); - - const router1 = await tester.get('test1'); - router1.use(() => {}); - expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); - expect(rootHttpRouter.use).toHaveBeenCalledWith( - '/some/test1/path', - expect.any(Function), - ); - - const router2 = await tester.get('test2'); - router2.use(() => {}); - expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); - expect(rootHttpRouter.use).toHaveBeenCalledWith( - '/some/test2/path', - expect.any(Function), - ); - }); - describe('auth services', () => { const pluginSubject = createBackendPlugin({ pluginId: 'test', diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts index 4a0dec49e6..fb2646580b 100644 --- a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts @@ -26,16 +26,6 @@ import { createCredentialsBarrier } from './createCredentialsBarrier'; import { createAuthIntegrationRouter } from './createAuthIntegrationRouter'; import { createCookieAuthRefreshMiddleware } from './createCookieAuthRefreshMiddleware'; -/** - * @public - */ -export interface HttpRouterFactoryOptions { - /** - * A callback used to generate the path for each plugin, defaults to `/api/{pluginId}`. - */ - getPath?(pluginId: string): string; -} - /** * HTTP route registration for plugins. * @@ -45,57 +35,39 @@ export interface HttpRouterFactoryOptions { * * @public */ -export const httpRouterServiceFactory = createServiceFactory( - (options?: HttpRouterFactoryOptions) => ({ - service: coreServices.httpRouter, - initialization: 'always', - deps: { - plugin: coreServices.pluginMetadata, - config: coreServices.rootConfig, - logger: coreServices.logger, - lifecycle: coreServices.lifecycle, - rootHttpRouter: coreServices.rootHttpRouter, - auth: coreServices.auth, - httpAuth: coreServices.httpAuth, - }, - async factory({ - auth, +export const httpRouterServiceFactory = createServiceFactory({ + service: coreServices.httpRouter, + initialization: 'always', + deps: { + plugin: coreServices.pluginMetadata, + config: coreServices.rootConfig, + lifecycle: coreServices.lifecycle, + rootHttpRouter: coreServices.rootHttpRouter, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + }, + async factory({ auth, httpAuth, config, plugin, rootHttpRouter, lifecycle }) { + const router = PromiseRouter(); + + rootHttpRouter.use(`/api/${plugin.getId()}`, router); + + const credentialsBarrier = createCredentialsBarrier({ httpAuth, config, - logger, - plugin, - rootHttpRouter, - lifecycle, - }) { - if (options?.getPath) { - logger.warn( - `DEPRECATION WARNING: The 'getPath' option for HttpRouterService is deprecated. The ability to reconfigure the '/api/' path prefix for plugins will be removed in the future.`, - ); - } - const getPath = options?.getPath ?? (id => `/api/${id}`); - const path = getPath(plugin.getId()); + }); - const router = PromiseRouter(); - rootHttpRouter.use(path, router); + router.use(createAuthIntegrationRouter({ auth })); + router.use(createLifecycleMiddleware({ lifecycle })); + router.use(credentialsBarrier.middleware); + router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth })); - const credentialsBarrier = createCredentialsBarrier({ - httpAuth, - config, - }); - - router.use(createAuthIntegrationRouter({ auth })); - router.use(createLifecycleMiddleware({ lifecycle })); - router.use(credentialsBarrier.middleware); - router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth })); - - return { - use(handler: Handler): void { - router.use(handler); - }, - addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void { - credentialsBarrier.addAuthPolicy(policy); - }, - }; - }, - }), -); + return { + use(handler: Handler): void { + router.use(handler); + }, + addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void { + credentialsBarrier.addAuthPolicy(policy); + }, + }; + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/index.ts b/packages/backend-defaults/src/entrypoints/httpRouter/index.ts index 928a232131..bc12480ae8 100644 --- a/packages/backend-defaults/src/entrypoints/httpRouter/index.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/index.ts @@ -15,6 +15,5 @@ */ export { httpRouterServiceFactory } from './httpRouterServiceFactory'; -export type { HttpRouterFactoryOptions } from './httpRouterServiceFactory'; export { createLifecycleMiddleware } from './createLifecycleMiddleware'; export type { LifecycleMiddlewareOptions } from './createLifecycleMiddleware'; diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index a2fb4b7674..0a44fa5ed3 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -22,7 +22,6 @@ import { EventsService } from '@backstage/plugin-events-node'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HttpAuthService } from '@backstage/backend-plugin-api'; -import { HttpRouterFactoryOptions } from '@backstage/backend-defaults/httpRouter'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { IdentityService } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; @@ -221,9 +220,7 @@ export namespace mockServices { // (undocumented) export namespace httpRouter { const // (undocumented) - factory: ( - options?: HttpRouterFactoryOptions | undefined, - ) => ServiceFactory; + factory: () => ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, From 2f991786a3f9bfb227e7ea6ee97b2f9a8b6dfa28 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 10 Jul 2024 11:50:52 +0200 Subject: [PATCH 069/212] refactor(test-utils): rename `ServiceFactoryTester.get` to `ServiceFactoryTestest.getSubject` Signed-off-by: Camila Belo --- .changeset/chilly-coats-report.md | 7 +++ .../scheduler/schedulerServiceFactory.test.ts | 2 +- .../tokenManagerServiceFactory.test.ts | 2 +- .../auth/authServiceFactory.test.ts | 24 +++++----- .../httpRouterServiceFactory.test.ts | 4 +- .../scheduler/schedulerServiceFactory.test.ts | 2 +- packages/backend-test-utils/api-report.md | 4 ++ .../next/wiring/ServiceFactoryTester.test.ts | 46 +++++++++---------- .../src/next/wiring/ServiceFactoryTester.ts | 13 +++++- 9 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 .changeset/chilly-coats-report.md diff --git a/.changeset/chilly-coats-report.md b/.changeset/chilly-coats-report.md new file mode 100644 index 0000000000..2fd044b57c --- /dev/null +++ b/.changeset/chilly-coats-report.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +--- + +The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts index 7b889b3a0e..b16509cf96 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts @@ -22,7 +22,7 @@ describe('schedulerFactory', () => { it('creates sidecar database features', async () => { const tester = ServiceFactoryTester.from(schedulerServiceFactory()); - const scheduler = await tester.get(); + const scheduler = await tester.getSubject(); await scheduler.scheduleTask({ id: 'task1', timeout: { seconds: 1 }, diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts index f603c6295e..dc52130ec6 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts @@ -21,7 +21,7 @@ describe('tokenManagerFactory', () => { it('should create a disabled manager without configuration', async () => { const tokenManager = await ServiceFactoryTester.from( tokenManagerServiceFactory, - ).get(); + ).getSubject(); await expect(tokenManager.authenticate('abc')).rejects.toThrow( 'no legacy keys are configured', diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index 22357d0f49..de89efa351 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -86,8 +86,8 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const searchAuth = await tester.get('search'); - const catalogAuth = await tester.get('catalog'); + const searchAuth = await tester.getSubject('search'); + const catalogAuth = await tester.getSubject('catalog'); const { token: searchToken } = await searchAuth.getPluginRequestToken({ onBehalfOf: await searchAuth.getOwnServiceCredentials(), @@ -117,8 +117,8 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const searchAuth = await tester.get('search'); - const catalogAuth = await tester.get('catalog'); + const searchAuth = await tester.getSubject('search'); + const catalogAuth = await tester.getSubject('catalog'); server.use( rest.get( @@ -163,7 +163,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); await expect( catalogAuth.getPluginRequestToken({ @@ -194,7 +194,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); const { token } = await catalogAuth.getPluginRequestToken({ onBehalfOf: { @@ -261,7 +261,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); const fullToken = 'eyJ0eXAiOiJ2bmQuYmFja3N0YWdlLnVzZXIiLCJhbGciOiJFUzI1NiIsImtpZCI6IjhkMDFjM2RiLTU2ZjktNDVmMC04NmRkLTA1YjNjODM1YjNkMyJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjcwMDcvYXBpL2F1dGgiLCJzdWIiOiJ1c2VyOmRldmVsb3BtZW50L2d1ZXN0IiwiZW50IjpbInVzZXI6ZGV2ZWxvcG1lbnQvZ3Vlc3QiLCJncm91cDpkZWZhdWx0L3RlYW0tYSJdLCJhdWQiOiJiYWNrc3RhZ2UiLCJpYXQiOjE3MTIwNzE3MTQsImV4cCI6MTcxMjA3NTMxNCwidWlwIjoiSmwxVEpycG9VUjR1NENjUE9nalJMeHpEMi1FMGZPR3ptSm81UWI2eS1aN19meG5oVVBEdWVWRE1CS0l6WF9pc0lvSDhlZm9EUFA5bG9aQnpPblB5Z2cifQ.1gVMq1ofO8PzRctu72D6c4IMqXuIabT79WdGEhW6vIrBRs_qhuWAa94Wvz_KYKpBTb2nxgzXJ5OeddeoYApMyQ'; @@ -318,9 +318,9 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const searchAuth = await tester.get('search'); - const catalogAuth = await tester.get('catalog'); - const permissionAuth = await tester.get('permission'); + const searchAuth = await tester.getSubject('search'); + const catalogAuth = await tester.getSubject('catalog'); + const permissionAuth = await tester.getSubject('permission'); /* Corresponding private key in case this test needs to be updated in the future: { @@ -431,7 +431,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); await expect( catalogAuth.authenticate('limited-static-token'), @@ -450,7 +450,7 @@ describe('authServiceFactory', () => { }, }); - const scaffolderAuth = await tester.get('scaffolder'); + const scaffolderAuth = await tester.getSubject('scaffolder'); await expect( scaffolderAuth.authenticate('limited-static-token'), diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts index 17665a176a..a8b749939b 100644 --- a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts @@ -35,7 +35,7 @@ describe('httpRouterFactory', () => { dependencies: [rootHttpRouter.factory], }); - const router1 = await tester.get('test1'); + const router1 = await tester.getSubject('test1'); router1.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); expect(rootHttpRouter.use).toHaveBeenCalledWith( @@ -43,7 +43,7 @@ describe('httpRouterFactory', () => { expect.any(Function), ); - const router2 = await tester.get('test2'); + const router2 = await tester.getSubject('test2'); router2.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); expect(rootHttpRouter.use).toHaveBeenCalledWith( diff --git a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts index 7b889b3a0e..b16509cf96 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts @@ -22,7 +22,7 @@ describe('schedulerFactory', () => { it('creates sidecar database features', async () => { const tester = ServiceFactoryTester.from(schedulerServiceFactory()); - const scheduler = await tester.get(); + const scheduler = await tester.getSubject(); await scheduler.scheduleTask({ id: 'task1', timeout: { seconds: 1 }, diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 0a44fa5ed3..a61d1ccb58 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -373,6 +373,7 @@ export class ServiceFactoryTester { | (() => ServiceFactory), options?: ServiceFactoryTesterOptions, ): ServiceFactoryTester; + // @deprecated get( ...args: 'root' extends TScope ? [] : [pluginId?: string] ): Promise; @@ -380,6 +381,9 @@ export class ServiceFactoryTester { service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] ): Promise; + getSubject( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise; } // @public diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts index 14dd427ecf..5970f6ea52 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts @@ -56,34 +56,34 @@ describe('ServiceFactoryTester', () => { it('should test a root service factory', async () => { const tester = ServiceFactoryTester.from(rootFactory); - await expect(tester.get()).resolves.toBe('root'); + await expect(tester.getSubject()).resolves.toBe('root'); }); it('should test a plugin service factory', async () => { const tester = ServiceFactoryTester.from(pluginFactory); - await expect(tester.get('x')).resolves.toBe('x-plugin'); - await expect(tester.get('y')).resolves.toBe('y-plugin'); - await expect(tester.get('z')).resolves.toBe('z-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('x-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-plugin'); + await expect(tester.getSubject('z')).resolves.toBe('z-plugin'); }); it('should test a plugin service factory with root context', async () => { const tester = ServiceFactoryTester.from(sharedPluginFactory); - await expect(tester.get('x')).resolves.toBe('x-1-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester.get('z')).resolves.toBe('z-3-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('x-1-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester.getSubject('z')).resolves.toBe('z-3-plugin'); const tester2 = ServiceFactoryTester.from(sharedPluginFactory); - await expect(tester2.get('z')).resolves.toBe('z-1-plugin'); - await expect(tester2.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester2.get('x')).resolves.toBe('x-3-plugin'); - await expect(tester2.get('x')).resolves.toBe('x-3-plugin'); - await expect(tester2.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester2.get('z')).resolves.toBe('z-1-plugin'); + await expect(tester2.getSubject('z')).resolves.toBe('z-1-plugin'); + await expect(tester2.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester2.getSubject('x')).resolves.toBe('x-3-plugin'); + await expect(tester2.getSubject('x')).resolves.toBe('x-3-plugin'); + await expect(tester2.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester2.getSubject('z')).resolves.toBe('z-1-plugin'); }); it('should use dependencies', async () => { @@ -96,7 +96,7 @@ describe('ServiceFactoryTester', () => { { dependencies: [rootFactory, pluginFactory()] }, ); - await expect(tester.get('x')).resolves.toBe('root, x-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('root, x-plugin'); }); it('should use dependencies with root context', async () => { @@ -109,11 +109,11 @@ describe('ServiceFactoryTester', () => { { dependencies: [sharedPluginFactory(), pluginFactory] }, ); - await expect(tester.get('x')).resolves.toBe('x-1-plugin, x-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(tester.get('z')).resolves.toBe('z-3-plugin, z-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('x-1-plugin, x-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.getSubject('z')).resolves.toBe('z-3-plugin, z-plugin'); }); it('should prioritize the subject implementation', async () => { @@ -126,7 +126,7 @@ describe('ServiceFactoryTester', () => { { dependencies: [rootFactory] }, ); - await expect(tester.get()).resolves.toBe('other-root'); + await expect(tester.getSubject()).resolves.toBe('other-root'); }); it('should throw on missing dependencies', async () => { @@ -138,7 +138,7 @@ describe('ServiceFactoryTester', () => { }), ); - await expect(tester.get('x')).rejects.toThrow( + await expect(tester.getSubject('x')).rejects.toThrow( "Failed to instantiate service 'b' for 'x' because the following dependent services are missing: 'a'", ); }); diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index 0555ab10a4..40b473663b 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -79,6 +79,17 @@ export class ServiceFactoryTester { this.#registry = registry; } + /** + * Returns the service instance for the subject. + * + * @deprecated Use `getSubject` instead. + */ + async get( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise { + return this.getSubject(...args); + } + /** * Returns the service instance for the subject. * @@ -89,7 +100,7 @@ export class ServiceFactoryTester { * * By default the plugin ID 'test' is used. */ - async get( + async getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] ): Promise { const [pluginId] = args; From 062c01c7c411aa13eb3c28d3726bf757c80d74a6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jul 2024 12:12:22 +0200 Subject: [PATCH 070/212] backend-plugin-api: deprecate service factories with callback form Signed-off-by: Patrik Oldsberg --- .changeset/spotty-apricots-tan.md | 55 ++++++++++ packages/backend-app-api/api-report-alpha.md | 7 +- packages/backend-app-api/api-report.md | 103 +++++++++++------- packages/backend-defaults/api-report-auth.md | 8 +- packages/backend-defaults/api-report-cache.md | 8 +- .../backend-defaults/api-report-database.md | 7 +- .../backend-defaults/api-report-discovery.md | 7 +- .../backend-defaults/api-report-httpAuth.md | 7 +- .../backend-defaults/api-report-httpRouter.md | 7 +- .../backend-defaults/api-report-lifecycle.md | 7 +- .../backend-defaults/api-report-logger.md | 7 +- .../api-report-permissions.md | 7 +- .../backend-defaults/api-report-rootConfig.md | 10 +- .../backend-defaults/api-report-rootHealth.md | 7 +- .../api-report-rootHttpRouter.md | 10 +- .../api-report-rootLifecycle.md | 7 +- .../backend-defaults/api-report-rootLogger.md | 7 +- .../backend-defaults/api-report-scheduler.md | 7 +- .../backend-defaults/api-report-urlReader.md | 7 +- .../backend-defaults/api-report-userInfo.md | 7 +- .../api-report.md | 28 +++-- packages/backend-plugin-api/api-report.md | 34 ++++-- .../src/services/system/index.ts | 1 + .../src/services/system/types.ts | 59 ++++++++-- packages/backend-test-utils/api-report.md | 51 +++++---- plugins/events-node/api-report.md | 7 +- 26 files changed, 321 insertions(+), 151 deletions(-) create mode 100644 .changeset/spotty-apricots-tan.md diff --git a/.changeset/spotty-apricots-tan.md b/.changeset/spotty-apricots-tan.md new file mode 100644 index 0000000000..bc1b26e2f4 --- /dev/null +++ b/.changeset/spotty-apricots-tan.md @@ -0,0 +1,55 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Deprecated the ability to define options for service factories through `createServiceFactory`. In the future all service factories will return a plain `ServiceFactory` object, rather than allowing users to pass options to the factory. To allow for customization of a service implementation one can instead export one or a few building blocks that allows for simple re-implementation of the service instead. + +For example, instead of: + +```ts +export const fooServiceFactory = createServiceFactory( + (options?: { bar: string }) => ({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }), +); +``` + +We instead encourage service implementations to provide an easy to use API for re-implementing the service for advanced use-cases: + +```ts +/** @public */ +export class DefaultFooService implements FooService { + static create(options: { bar: string; logger: LoggerService }) { + return new DefaultFooService(options.logger, options.bar ?? 'default'); + } + + private constructor( + private readonly logger: string, + private readonly bar: string, + ) {} + + // The rest of the implementation +} +``` + +A user that wishes to customize the service can then easily do so by defining their own factory: + +```ts +export const fooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return DefaultFooService.create({ logger, bar: 'baz' }); + }, +}); +``` + +This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. + +This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. diff --git a/packages/backend-app-api/api-report-alpha.md b/packages/backend-app-api/api-report-alpha.md index 488feedf2c..e4769f4e72 100644 --- a/packages/backend-app-api/api-report-alpha.md +++ b/packages/backend-app-api/api-report-alpha.md @@ -4,12 +4,13 @@ ```ts import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @alpha (undocumented) -export const featureDiscoveryServiceFactory: () => ServiceFactory< +export const featureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, - 'root' + 'root', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index c577084a58..51839b88be 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -38,7 +38,7 @@ import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import type { Server } from 'node:http'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { transport } from 'winston'; @@ -46,7 +46,11 @@ import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserInfoService } from '@backstage/backend-plugin-api'; // @public @deprecated (undocumented) -export const authServiceFactory: () => ServiceFactory; +export const authServiceFactory: ServiceFactoryCompat< + AuthService, + 'plugin', + undefined +>; // @public (undocumented) export interface Backend { @@ -66,7 +70,11 @@ export interface Backend { } // @public @deprecated (undocumented) -export const cacheServiceFactory: () => ServiceFactory; +export const cacheServiceFactory: ServiceFactoryCompat< + CacheService, + 'plugin', + undefined +>; // Warning: (ae-forgotten-export) The symbol "createConfigSecretEnumerator_2" needs to be exported by the entry point index.d.ts // @@ -95,9 +103,10 @@ export interface CreateSpecializedBackendOptions { } // @public @deprecated (undocumented) -export const databaseServiceFactory: () => ServiceFactory< +export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated @@ -116,9 +125,10 @@ export class DefaultRootHttpRouter implements RootHttpRouterService { export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2; // @public @deprecated (undocumented) -export const discoveryServiceFactory: () => ServiceFactory< +export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, - 'plugin' + 'plugin', + undefined >; // Warning: (ae-forgotten-export) The symbol "ExtendedHttpServer_2" needs to be exported by the entry point index.d.ts @@ -141,15 +151,17 @@ export class HostDiscovery implements DiscoveryService { } // @public @deprecated (undocumented) -export const httpAuthServiceFactory: () => ServiceFactory< +export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated -export const httpRouterServiceFactory: () => ServiceFactory< +export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, - 'plugin' + 'plugin', + undefined >; // Warning: (ae-forgotten-export) The symbol "HttpServerCertificateOptions_2" needs to be exported by the entry point index.d.ts @@ -169,9 +181,11 @@ export type IdentityFactoryOptions = { }; // @public @deprecated (undocumented) -export const identityServiceFactory: ( - options?: IdentityFactoryOptions | undefined, -) => ServiceFactory; +export const identityServiceFactory: ServiceFactoryCompat< + IdentityService, + 'plugin', + IdentityFactoryOptions +>; // Warning: (ae-forgotten-export) The symbol "LifecycleMiddlewareOptions_2" needs to be exported by the entry point index.d.ts // @@ -179,9 +193,10 @@ export const identityServiceFactory: ( export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2; // @public @deprecated -export const lifecycleServiceFactory: () => ServiceFactory< +export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated @@ -195,9 +210,10 @@ export function loadBackendConfig(options: { }>; // @public @deprecated -export const loggerServiceFactory: () => ServiceFactory< +export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated (undocumented) @@ -222,9 +238,10 @@ export type MiddlewareFactoryErrorOptions = MiddlewareFactoryErrorOptions_2; export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2; // @public @deprecated (undocumented) -export const permissionsServiceFactory: () => ServiceFactory< +export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, - 'plugin' + 'plugin', + undefined >; // Warning: (ae-forgotten-export) The symbol "readCorsOptions_2" needs to be exported by the entry point index.d.ts @@ -251,9 +268,11 @@ export interface RootConfigFactoryOptions { } // @public @deprecated (undocumented) -export const rootConfigServiceFactory: ( - options?: RootConfigFactoryOptions | undefined, -) => ServiceFactory; +export const rootConfigServiceFactory: ServiceFactoryCompat< + RootConfigService, + 'root', + RootConfigFactoryOptions +>; // Warning: (ae-forgotten-export) The symbol "RootHttpRouterConfigureContext_2" needs to be exported by the entry point index.d.ts // @@ -266,44 +285,52 @@ export type RootHttpRouterConfigureContext = RootHttpRouterConfigureContext_2; export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; // @public @deprecated (undocumented) -export const rootHttpRouterServiceFactory: ( - options?: RootHttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory; +export const rootHttpRouterServiceFactory: ServiceFactoryCompat< + RootHttpRouterService, + 'root', + RootHttpRouterFactoryOptions_2 +>; // @public @deprecated -export const rootLifecycleServiceFactory: () => ServiceFactory< +export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, - 'root' + 'root', + undefined >; // @public @deprecated -export const rootLoggerServiceFactory: () => ServiceFactory< +export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, - 'root' + 'root', + undefined >; // @public @deprecated (undocumented) -export const schedulerServiceFactory: () => ServiceFactory< +export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated (undocumented) -export const tokenManagerServiceFactory: () => ServiceFactory< +export const tokenManagerServiceFactory: ServiceFactoryCompat< TokenManagerService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated (undocumented) -export const urlReaderServiceFactory: () => ServiceFactory< +export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated (undocumented) -export const userInfoServiceFactory: () => ServiceFactory< +export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated diff --git a/packages/backend-defaults/api-report-auth.md b/packages/backend-defaults/api-report-auth.md index d64ce5bc2a..7df0b394e3 100644 --- a/packages/backend-defaults/api-report-auth.md +++ b/packages/backend-defaults/api-report-auth.md @@ -4,10 +4,14 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const authServiceFactory: () => ServiceFactory; +export const authServiceFactory: ServiceFactoryCompat< + AuthService, + 'plugin', + undefined +>; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-cache.md b/packages/backend-defaults/api-report-cache.md index 0bfd7eb463..0b2d8706df 100644 --- a/packages/backend-defaults/api-report-cache.md +++ b/packages/backend-defaults/api-report-cache.md @@ -7,7 +7,7 @@ import { CacheService } from '@backstage/backend-plugin-api'; import { CacheServiceOptions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export class CacheManager { @@ -25,7 +25,11 @@ export type CacheManagerOptions = { }; // @public -export const cacheServiceFactory: () => ServiceFactory; +export const cacheServiceFactory: ServiceFactoryCompat< + CacheService, + 'plugin', + undefined +>; // @public (undocumented) export interface PluginCacheManager { diff --git a/packages/backend-defaults/api-report-database.md b/packages/backend-defaults/api-report-database.md index 116830343a..0d54b43777 100644 --- a/packages/backend-defaults/api-report-database.md +++ b/packages/backend-defaults/api-report-database.md @@ -8,7 +8,7 @@ import { DatabaseService } from '@backstage/backend-plugin-api'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export class DatabaseManager implements LegacyRootDatabaseService { @@ -32,9 +32,10 @@ export type DatabaseManagerOptions = { }; // @public -export const databaseServiceFactory: () => ServiceFactory< +export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, - 'plugin' + 'plugin', + undefined >; // @public @deprecated diff --git a/packages/backend-defaults/api-report-discovery.md b/packages/backend-defaults/api-report-discovery.md index b434dab6fc..dbb469f758 100644 --- a/packages/backend-defaults/api-report-discovery.md +++ b/packages/backend-defaults/api-report-discovery.md @@ -5,12 +5,13 @@ ```ts import { Config } from '@backstage/config'; import { DiscoveryService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const discoveryServiceFactory: () => ServiceFactory< +export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, - 'plugin' + 'plugin', + undefined >; // @public diff --git a/packages/backend-defaults/api-report-httpAuth.md b/packages/backend-defaults/api-report-httpAuth.md index 6ffe3d6539..613e6a4452 100644 --- a/packages/backend-defaults/api-report-httpAuth.md +++ b/packages/backend-defaults/api-report-httpAuth.md @@ -4,12 +4,13 @@ ```ts import { HttpAuthService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const httpAuthServiceFactory: () => ServiceFactory< +export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, - 'plugin' + 'plugin', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md index f191a5a847..a2bf1cd39b 100644 --- a/packages/backend-defaults/api-report-httpRouter.md +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -7,7 +7,7 @@ import { HttpRouterService } from '@backstage/backend-plugin-api'; import { HumanDuration } from '@backstage/types'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export function createLifecycleMiddleware( @@ -15,9 +15,10 @@ export function createLifecycleMiddleware( ): RequestHandler; // @public -export const httpRouterServiceFactory: () => ServiceFactory< +export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, - 'plugin' + 'plugin', + undefined >; // @public diff --git a/packages/backend-defaults/api-report-lifecycle.md b/packages/backend-defaults/api-report-lifecycle.md index 4233584664..36adbbf6e7 100644 --- a/packages/backend-defaults/api-report-lifecycle.md +++ b/packages/backend-defaults/api-report-lifecycle.md @@ -4,12 +4,13 @@ ```ts import { LifecycleService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const lifecycleServiceFactory: () => ServiceFactory< +export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, - 'plugin' + 'plugin', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-logger.md b/packages/backend-defaults/api-report-logger.md index fa9018a40c..c66f886bf4 100644 --- a/packages/backend-defaults/api-report-logger.md +++ b/packages/backend-defaults/api-report-logger.md @@ -4,12 +4,13 @@ ```ts import { LoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const loggerServiceFactory: () => ServiceFactory< +export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, - 'plugin' + 'plugin', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-permissions.md b/packages/backend-defaults/api-report-permissions.md index c436443ff1..86b9605606 100644 --- a/packages/backend-defaults/api-report-permissions.md +++ b/packages/backend-defaults/api-report-permissions.md @@ -4,12 +4,13 @@ ```ts import { PermissionsService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const permissionsServiceFactory: () => ServiceFactory< +export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, - 'plugin' + 'plugin', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 317c451e1b..24e827f520 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -8,7 +8,7 @@ import { ConfigSchema } from '@backstage/config-loader'; import { LoggerService } from '@backstage/backend-plugin-api'; import { RemoteConfigSourceOptions } from '@backstage/config-loader'; import { RootConfigService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public (undocumented) export function createConfigSecretEnumerator(options: { @@ -26,9 +26,11 @@ export interface RootConfigFactoryOptions { } // @public (undocumented) -export const rootConfigServiceFactory: ( - options?: RootConfigFactoryOptions | undefined, -) => ServiceFactory; +export const rootConfigServiceFactory: ServiceFactoryCompat< + RootConfigService, + 'root', + RootConfigFactoryOptions +>; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootHealth.md b/packages/backend-defaults/api-report-rootHealth.md index ffa067605a..c69f66e747 100644 --- a/packages/backend-defaults/api-report-rootHealth.md +++ b/packages/backend-defaults/api-report-rootHealth.md @@ -4,12 +4,13 @@ ```ts import { RootHealthService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public (undocumented) -export const rootHealthServiceFactory: () => ServiceFactory< +export const rootHealthServiceFactory: ServiceFactoryCompat< RootHealthService, - 'root' + 'root', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index d9727fd36d..9ea4cd7ebb 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -19,7 +19,7 @@ import { RequestListener } from 'http'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; import type { Server } from 'node:http'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export function createHttpServer( @@ -141,9 +141,11 @@ export type RootHttpRouterFactoryOptions = { }; // @public (undocumented) -export const rootHttpRouterServiceFactory: ( - options?: RootHttpRouterFactoryOptions | undefined, -) => ServiceFactory; +export const rootHttpRouterServiceFactory: ServiceFactoryCompat< + RootHttpRouterService, + 'root', + RootHttpRouterFactoryOptions +>; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootLifecycle.md b/packages/backend-defaults/api-report-rootLifecycle.md index 00eb9c1ebb..faa83706a7 100644 --- a/packages/backend-defaults/api-report-rootLifecycle.md +++ b/packages/backend-defaults/api-report-rootLifecycle.md @@ -4,12 +4,13 @@ ```ts import { RootLifecycleService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const rootLifecycleServiceFactory: () => ServiceFactory< +export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, - 'root' + 'root', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-rootLogger.md b/packages/backend-defaults/api-report-rootLogger.md index 70dc0deee6..075a8b4aa9 100644 --- a/packages/backend-defaults/api-report-rootLogger.md +++ b/packages/backend-defaults/api-report-rootLogger.md @@ -7,13 +7,14 @@ import { Format } from 'logform'; import { JsonObject } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { transport } from 'winston'; // @public -export const rootLoggerServiceFactory: () => ServiceFactory< +export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, - 'root' + 'root', + undefined >; // @public diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md index 58991ddca3..a3467b97de 100644 --- a/packages/backend-defaults/api-report-scheduler.md +++ b/packages/backend-defaults/api-report-scheduler.md @@ -6,7 +6,7 @@ import { DatabaseService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export class DefaultSchedulerService { @@ -18,9 +18,10 @@ export class DefaultSchedulerService { } // @public -export const schedulerServiceFactory: () => ServiceFactory< +export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, - 'plugin' + 'plugin', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md index c56cdba69b..0980e3cd67 100644 --- a/packages/backend-defaults/api-report-urlReader.md +++ b/packages/backend-defaults/api-report-urlReader.md @@ -21,7 +21,7 @@ import { GitLabIntegration } from '@backstage/integration'; import { HarnessIntegration } from '@backstage/integration'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Readable } from 'stream'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeOptions } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeResponse } from '@backstage/backend-plugin-api'; @@ -426,9 +426,10 @@ export class UrlReaders { } // @public -export const urlReaderServiceFactory: () => ServiceFactory< +export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, - 'plugin' + 'plugin', + undefined >; // @public diff --git a/packages/backend-defaults/api-report-userInfo.md b/packages/backend-defaults/api-report-userInfo.md index d11fb69718..b77878ff34 100644 --- a/packages/backend-defaults/api-report-userInfo.md +++ b/packages/backend-defaults/api-report-userInfo.md @@ -3,13 +3,14 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { UserInfoService } from '@backstage/backend-plugin-api'; // @public -export const userInfoServiceFactory: () => ServiceFactory< +export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, - 'plugin' + 'plugin', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 20db7db71b..39aa60b09b 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -28,7 +28,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { Router } from 'express'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TaskRunner } from '@backstage/backend-tasks'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; @@ -114,18 +114,20 @@ export interface DynamicPluginsFactoryOptions { } // @public (undocumented) -export const dynamicPluginsFeatureDiscoveryServiceFactory: () => ServiceFactory< +export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, - 'root' + 'root', + undefined >; // @public (undocumented) export const dynamicPluginsFrontendSchemas: BackendFeatureCompat; // @public (undocumented) -export const dynamicPluginsRootLoggerServiceFactory: () => ServiceFactory< +export const dynamicPluginsRootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, - 'root' + 'root', + undefined >; // @public (undocumented) @@ -142,14 +144,18 @@ export interface DynamicPluginsSchemasService { } // @public (undocumented) -export const dynamicPluginsSchemasServiceFactory: ( - options?: DynamicPluginsSchemasOptions | undefined, -) => ServiceFactory; +export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< + DynamicPluginsSchemasService, + 'root', + DynamicPluginsSchemasOptions +>; // @public (undocumented) -export const dynamicPluginsServiceFactory: ( - options?: DynamicPluginsFactoryOptions | undefined, -) => ServiceFactory; +export const dynamicPluginsServiceFactory: ServiceFactoryCompat< + DynamicPluginProvider, + 'root', + DynamicPluginsFactoryOptions +>; // @public (undocumented) export const dynamicPluginsServiceRef: ServiceRef< diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 7b1e6ef2d3..0a01878148 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -257,9 +257,9 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( options: RootServiceFactoryOptions, -): () => ServiceFactory; +): ServiceFactoryCompat; -// @public +// @public @deprecated export function createServiceFactory< TService, TImpl extends TService, @@ -271,7 +271,7 @@ export function createServiceFactory< options: ( options?: TOpts, ) => RootServiceFactoryOptions, -): (options?: TOpts) => ServiceFactory; +): ServiceFactoryCompat; // @public export function createServiceFactory< @@ -284,9 +284,9 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( options: PluginServiceFactoryOptions, -): () => ServiceFactory; +): ServiceFactoryCompat; -// @public +// @public @deprecated export function createServiceFactory< TService, TImpl extends TService, @@ -299,7 +299,7 @@ export function createServiceFactory< options: ( options?: TOpts, ) => PluginServiceFactoryOptions, -): (options?: TOpts) => ServiceFactory; +): ServiceFactoryCompat; // @public export function createServiceRef( @@ -645,7 +645,19 @@ export interface ServiceFactory< service: ServiceRef; } -// @public +// @public @deprecated (undocumented) +export interface ServiceFactoryCompat< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TOpts extends object | undefined = undefined, +> extends ServiceFactory { + // @deprecated (undocumented) + ( + ...options: undefined extends TOpts ? [] : [options?: TOpts] + ): ServiceFactory; +} + +// @public @deprecated export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); // @public @@ -668,9 +680,13 @@ export type ServiceRefConfig< // @public (undocumented) export interface ServiceRefOptions { // (undocumented) - defaultFactory?: ( + defaultFactory?( service: ServiceRef, - ) => Promise; + ): Promise; + // @deprecated (undocumented) + defaultFactory?( + service: ServiceRef, + ): Promise<() => ServiceFactory>; // (undocumented) id: string; // (undocumented) diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index 9f62968ecd..c2d3b9c6fc 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -21,5 +21,6 @@ export type { PluginServiceFactoryOptions, RootServiceFactoryOptions, ServiceFactoryOrFunction, + ServiceFactoryCompat, } from './types'; export { createServiceRef, createServiceFactory } from './types'; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 2d6951ffea..9545408ff2 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -55,6 +55,23 @@ export interface ServiceFactory< service: ServiceRef; } +/** + * @public + * @deprecated This type exists only as a helper for old code that relied on `createServiceFactory` to return `() => ServiceFactory` instead of `ServiceFactory`. You should remove the `()` parentheses at the end of your usages. This type will be removed in a future release. + */ +export interface ServiceFactoryCompat< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TOpts extends object | undefined = undefined, +> extends ServiceFactory { + /** + * @deprecated You do not need to use this call signature; use the type directly instead by removing the `()` parentheses at the end. This call signature will be removed in a future release. + */ + ( + ...options: undefined extends TOpts ? [] : [options?: TOpts] + ): ServiceFactory; +} + /** @internal */ export interface InternalServiceFactory< TService = unknown, @@ -73,6 +90,7 @@ export interface InternalServiceFactory< /** * Represents either a {@link ServiceFactory} or a function that returns one. * + * @deprecated The support for service factory functions is deprecated and will be removed. * @public */ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); @@ -81,9 +99,15 @@ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); export interface ServiceRefOptions { id: string; scope?: TScope; - defaultFactory?: ( + defaultFactory?( service: ServiceRef, - ) => Promise; + ): Promise; + /** + * @deprecated The defaultFactory must return a plain `ServiceFactory` object, support for returning a function will be removed. + */ + defaultFactory?( + service: ServiceRef, + ): Promise<() => ServiceFactory>; } /** @@ -198,10 +222,14 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( options: RootServiceFactoryOptions, -): () => ServiceFactory; +): ServiceFactoryCompat; /** * Creates a root scoped service factory with optional options. * + * @deprecated The ability to define options for service factories is deprecated + * and will be removed. Please use the non-callback form of createServiceFactory + * and provide an API that allows for a simple re-implementation of the service + * factory instead. * @public * @param options - The service factory configuration. */ @@ -214,7 +242,7 @@ export function createServiceFactory< options: ( options?: TOpts, ) => RootServiceFactoryOptions, -): (options?: TOpts) => ServiceFactory; +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory without options. * @@ -229,10 +257,14 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( options: PluginServiceFactoryOptions, -): () => ServiceFactory; +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory with optional options. * + * @deprecated The ability to define options for service factories is deprecated + * and will be removed. Please use the non-callback form of createServiceFactory + * and provide an API that allows for a simple re-implementation of the service + * factory instead. * @public * @param options - The service factory configuration. */ @@ -246,7 +278,7 @@ export function createServiceFactory< options: ( options?: TOpts, ) => PluginServiceFactoryOptions, -): (options?: TOpts) => ServiceFactory; +): ServiceFactoryCompat; export function createServiceFactory< TService, TImpl extends TService, @@ -263,13 +295,13 @@ export function createServiceFactory< ) => PluginServiceFactoryOptions) | (() => RootServiceFactoryOptions) | (() => PluginServiceFactoryOptions), -): (options: TOpts) => ServiceFactory { +): ServiceFactoryCompat { const configCallback = typeof options === 'function' ? options : () => options; const factory = ( - o: TOpts, + o?: TOpts, ): InternalServiceFactory => { - const anyConf = configCallback(o); + const anyConf = configCallback(o!); if (anyConf.service.scope === 'root') { const c = anyConf as RootServiceFactoryOptions; return { @@ -303,7 +335,10 @@ export function createServiceFactory< }; }; - factory.$$type = '@backstage/BackendFeatureFactory'; - - return factory; + // This constructs the `ServiceFactoryCompat` type, which is both a plain + // factory object as well as a function that can be called to construct a + // factory, potentially with options. In the future only the plain factory + // form will be supported, but for now we need to allow callers to call the + // factory too. + return Object.assign(factory, factory(undefined as TOpts)); } diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 0a44fa5ed3..566ac5b888 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -38,6 +38,7 @@ import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; @@ -155,7 +156,7 @@ export namespace mockServices { // (undocumented) export namespace auth { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -164,7 +165,7 @@ export namespace mockServices { // (undocumented) export namespace cache { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -173,7 +174,7 @@ export namespace mockServices { // (undocumented) export namespace database { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -184,7 +185,7 @@ export namespace mockServices { // (undocumented) export namespace discovery { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -193,7 +194,7 @@ export namespace mockServices { // (undocumented) export namespace events { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -205,13 +206,13 @@ export namespace mockServices { }): HttpAuthService; // (undocumented) export namespace httpAuth { - const factory: ( - options?: - | { - defaultCredentials?: BackstageCredentials | undefined; - } - | undefined, - ) => ServiceFactory; + const factory: ServiceFactoryCompat< + HttpAuthService, + 'plugin', + { + defaultCredentials?: BackstageCredentials | undefined; + } + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -220,7 +221,7 @@ export namespace mockServices { // (undocumented) export namespace httpRouter { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -240,7 +241,7 @@ export namespace mockServices { // (undocumented) export namespace lifecycle { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -249,7 +250,7 @@ export namespace mockServices { // (undocumented) export namespace logger { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -258,7 +259,7 @@ export namespace mockServices { // (undocumented) export namespace permissions { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -280,7 +281,7 @@ export namespace mockServices { // (undocumented) export namespace rootHealth { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -289,9 +290,11 @@ export namespace mockServices { // (undocumented) export namespace rootHttpRouter { const // (undocumented) - factory: ( - options?: RootHttpRouterFactoryOptions | undefined, - ) => ServiceFactory; + factory: ServiceFactoryCompat< + RootHttpRouterService, + 'root', + RootHttpRouterFactoryOptions + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -300,7 +303,7 @@ export namespace mockServices { // (undocumented) export namespace rootLifecycle { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -326,7 +329,7 @@ export namespace mockServices { // (undocumented) export namespace scheduler { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -346,7 +349,7 @@ export namespace mockServices { // (undocumented) export namespace urlReader { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -357,7 +360,7 @@ export namespace mockServices { ): UserInfoService; // (undocumented) export namespace userInfo { - const factory: () => ServiceFactory; + const factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 46646d2ce8..5b45b4419e 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -4,7 +4,7 @@ ```ts import { LoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; // @public @@ -63,9 +63,10 @@ export interface EventsService { export type EventsServiceEventHandler = (params: EventParams) => Promise; // @public (undocumented) -export const eventsServiceFactory: () => ServiceFactory< +export const eventsServiceFactory: ServiceFactoryCompat< EventsService, - 'plugin' + 'plugin', + undefined >; // @public From edf5cc349ef822619e19534690ec7a659b9ce0ad Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 10 Jul 2024 12:53:32 +0200 Subject: [PATCH 071/212] refactor(test-utils): deprecate the `isDockerDisabledForTests` function Signed-off-by: Camila Belo --- .changeset/forty-buttons-cover.md | 5 +++++ packages/backend-test-utils/api-report.md | 2 +- packages/backend-test-utils/src/deprecated.ts | 9 +++++++++ packages/backend-test-utils/src/index.ts | 1 - .../src/util/isDockerDisabledForTests.ts | 1 - 5 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 .changeset/forty-buttons-cover.md diff --git a/.changeset/forty-buttons-cover.md b/.changeset/forty-buttons-cover.md new file mode 100644 index 0000000000..16e66df061 --- /dev/null +++ b/.changeset/forty-buttons-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +The function `isDockerDisabledForTests` is deprecated and will no longer be exported in the near future as it should only be used internally. diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index a61d1ccb58..d3a618d214 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -54,7 +54,7 @@ export interface CreateMockDirectoryOptions { mockOsTmpDir?: boolean; } -// @public (undocumented) +// @public @deprecated (undocumented) export function isDockerDisabledForTests(): boolean; // @public (undocumented) diff --git a/packages/backend-test-utils/src/deprecated.ts b/packages/backend-test-utils/src/deprecated.ts index fad7523367..5fe74e7ef3 100644 --- a/packages/backend-test-utils/src/deprecated.ts +++ b/packages/backend-test-utils/src/deprecated.ts @@ -15,9 +15,18 @@ */ import { CreateMockDirectoryOptions } from './filesystem'; +import { isDockerDisabledForTests as _isDockerDisabledForTests } from './util'; /** * @public * @deprecated Use `CreateMockDirectoryOptions` from `@backstage/backend-test-utils` instead. */ export type MockDirectoryOptions = CreateMockDirectoryOptions; + +/** + * @public + * @deprecated This is an internal function and will no longer be exported from this package. + */ +export function isDockerDisabledForTests(): boolean { + return _isDockerDisabledForTests(); +} diff --git a/packages/backend-test-utils/src/index.ts b/packages/backend-test-utils/src/index.ts index a9feacbacd..4124a861f4 100644 --- a/packages/backend-test-utils/src/index.ts +++ b/packages/backend-test-utils/src/index.ts @@ -26,4 +26,3 @@ export * from './database'; export * from './msw'; export * from './filesystem'; export * from './next'; -export * from './util'; diff --git a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts index 05eeabd91a..ca7cc95425 100644 --- a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts +++ b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -/** @public */ export function isDockerDisabledForTests() { // If we are not running in continuous integration, the default is to skip // the (relatively heavy, long running) docker based tests. If you want to From 2414d8666862d00cbed6b6b4292d7e3a7e777d1e Mon Sep 17 00:00:00 2001 From: Seba <17096352+sebalaini@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:28:50 +0200 Subject: [PATCH 072/212] Kubernetes react -> Warning: validateDOMNesting(...):
    cannot appear as a descendant of

    (#25559) * fix html markup in FixDialog Signed-off-by: sebalaini * Update .changeset/short-impalas-suffer.md Co-authored-by: Camila Belo Signed-off-by: Seba <17096352+sebalaini@users.noreply.github.com> --------- Signed-off-by: sebalaini Signed-off-by: Seba <17096352+sebalaini@users.noreply.github.com> Co-authored-by: Camila Belo --- .changeset/short-impalas-suffer.md | 5 +++++ .../components/Pods/FixDialog/FixDialog.tsx | 22 +++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changeset/short-impalas-suffer.md diff --git a/.changeset/short-impalas-suffer.md b/.changeset/short-impalas-suffer.md new file mode 100644 index 0000000000..34bc31e85d --- /dev/null +++ b/.changeset/short-impalas-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +Fix the `HTML` markup of the `FixDialog` component, `ul` and `li` are not allowed inside a `p` tag. diff --git a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx index fd7440870f..99386b3d40 100644 --- a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx +++ b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx @@ -33,6 +33,8 @@ import { DetectedError } from '@backstage/plugin-kubernetes-common'; import { PodLogs } from '../PodLogs'; import { Events } from '../Events'; import { LinkButton } from '@backstage/core-components'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -96,17 +98,15 @@ export const FixDialog: React.FC = ({ Fix: - -

      - {(error.proposedFix?.actions ?? []).map((fix, i) => { - return ( -
    • - {fix} -
    • - ); - })} -
    - + + {(error.proposedFix?.actions ?? []).map((fix, i) => { + return ( + + {fix} + + ); + })} + {pf && pf.type === 'logs' && ( From 6e4d937728c66d55e2ffde3986d1386cd12bf3f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jul 2024 15:19:03 +0200 Subject: [PATCH 073/212] docs/backend-system: update service factory options section Signed-off-by: Patrik Oldsberg --- .../architecture/03-services.md | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 777eddd3c4..7d6824c41e 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -190,43 +190,36 @@ If a service defines a default factory, that factory will be used if there is no When defining a default factory for a service, it is possible for it to end up with duplicate implementations at runtime. This applies both to any shared root context in your factory, as well as plugin specific instances of your service. This is because package dependency version ranges may not line up perfectly, causing duplicate installations of the same package. This can happen both for two different plugins using the same service, but also across a plugin and its modules. If your service would break in this scenario, you should not define a default factory for it, but instead require that users of your service explicitly install a factory in their backend instance. -## Service Factory Options +## Service Factory Customization -:::note Note - -This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead. - -::: - -When declaring a service factory it's possible to include an options callback. This allows you to customize the factory through code when installing it in the backend. For example, this is how you install an explicit factory instance in the backend without any options: +When declaring a service factory you may also want to make the export the building blocks of the implementation itself. This is to allow for further customization of the service implementation through code, beyond what is possible with static configuration, without the need to re-implement the entire service from scratch. For example, we might export our example `DefaultFooService` class, while moving construction to a static factory method to make it easier to evolve: ```ts -const backend = createBackend(); +export class DefaultFooService { + static create(options: { transform: (foo: string) => string }) { + return new DefaultFooService(options.transform ?? (foo) => foo); + } -backend.add(fooServiceFactory()); -``` + private constructor(private readonly transform: (foo: string) => string) {} -Note that we call `fooServiceFactory` to create the service factory instance. This is because `createServiceFactory` always returns a factory function that creates the actual service factory. To add options to your service factory, you wrap the object passed to `createServiceFactory` in a callback that accepts the desired options. Note that the options must always be optional. For example: - -```ts -export interface FooFactoryOptions { - transform: (foo: string) => string; + foo(foo: string): string { + return this.transform(foo); + } } - -export const fooServiceFactory = createServiceFactory( - (options?: FooFactoryOptions) => ({ - service: fooServiceRef, - factory() { - return new DefaultFooService(options?.transform); - }, - }), -); ``` -This lets us use the options to customize the factory implementation in any way we want. From the outside the service factory looks just like before, except that we're now also able to pass options when installing the factory: +By exporting `DefaultFooService` we now make it relatively simple for advanced users of our service to customize the implementation. To do so, they can define their own service factory that uses our provided implementation: ```ts -const backend = createBackend(); - -backend.add(fooServiceFactory({ transform: foo => foo.toUpperCase() })); +export const customFooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: {}, + factory() { + return DefaultFooService.create({ + transform: foo => foo.toUpperCase(), + }); + }, +}); ``` + +This allows lets you provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. From b9451dd573a1dd3d095c49bfc2d96bf5009a03fc Mon Sep 17 00:00:00 2001 From: CasperJ Date: Wed, 10 Jul 2024 15:22:56 +0200 Subject: [PATCH 074/212] Fixed log message to output the correct file name being written to Signed-off-by: CasperJ --- .changeset/dirty-wasps-do.md | 5 +++++ .../src/scaffolder/actions/builtin/catalog/write.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/dirty-wasps-do.md diff --git a/.changeset/dirty-wasps-do.md b/.changeset/dirty-wasps-do.md new file mode 100644 index 0000000000..fbf6799baf --- /dev/null +++ b/.changeset/dirty-wasps-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Catalog:Write scaffolder action shows incorrect log message diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index 9feaa14c07..a957adb71a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -49,10 +49,10 @@ export function createCatalogWriteAction() { examples, supportsDryRun: true, async handler(ctx) { - ctx.logger.info(`Writing catalog-info.yaml`); const { filePath, entity } = ctx.input; const entityRef = ctx.templateInfo?.entityRef; const path = filePath ?? 'catalog-info.yaml'; + ctx.logger.info(`Writing ${path}`); await fs.writeFile( resolveSafeChildPath(ctx.workspacePath, path), From c6c6e2d33427eff8fd9d8f8044ccaee18d0fc63a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jul 2024 15:38:28 +0200 Subject: [PATCH 075/212] changesets: add node about TS improvement when removing service factory options Signed-off-by: Patrik Oldsberg --- .changeset/spotty-apricots-tan.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/spotty-apricots-tan.md b/.changeset/spotty-apricots-tan.md index bc1b26e2f4..2b88e3a7f5 100644 --- a/.changeset/spotty-apricots-tan.md +++ b/.changeset/spotty-apricots-tan.md @@ -53,3 +53,5 @@ export const fooServiceFactory = createServiceFactory({ This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. + +A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. From b05e1e115789f44a65bd13b4ddb60299acf9a5c0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jul 2024 15:41:02 +0200 Subject: [PATCH 076/212] changesets: add changeset for packages with updated service factory exports Signed-off-by: Patrik Oldsberg --- .changeset/hot-items-kick.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/hot-items-kick.md diff --git a/.changeset/hot-items-kick.md b/.changeset/hot-items-kick.md new file mode 100644 index 0000000000..a3a9f129c1 --- /dev/null +++ b/.changeset/hot-items-kick.md @@ -0,0 +1,9 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +'@backstage/backend-test-utils': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +'@backstage/plugin-events-node': patch +--- + +Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. From 95a3a0b91c5403f35dc5d9520a8e0b3ce5e5c8ae Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 10 Jul 2024 13:28:48 +0200 Subject: [PATCH 077/212] refactor(test-utils): rename `setupRequestMockHandlers` to `setupMswHandler` Signed-off-by: Camila Belo --- .changeset/tiny-schools-draw.md | 53 +++++++++++++++++++ .../auth/authServiceFactory.test.ts | 4 +- .../external/ExternalTokenHandler.test.ts | 7 +-- .../entrypoints/auth/external/jwks.test.ts | 4 +- .../auth/user/UserTokenHandler.test.ts | 7 +-- .../urlReader/lib/AzureUrlReader.test.ts | 4 +- .../lib/BitbucketCloudUrlReader.test.ts | 4 +- .../lib/BitbucketServerUrlReader.test.ts | 4 +- .../urlReader/lib/BitbucketUrlReader.test.ts | 4 +- .../urlReader/lib/FetchUrlReader.test.ts | 7 +-- .../urlReader/lib/GerritUrlReader.test.ts | 4 +- .../urlReader/lib/GiteaUrlReader.test.ts | 7 +-- .../urlReader/lib/GithubUrlReader.test.ts | 4 +- .../urlReader/lib/GitlabUrlReader.test.ts | 4 +- .../urlReader/lib/HarnessUrlReader.test.ts | 7 +-- .../userInfo/DefaultUserInfoService.test.ts | 7 +-- packages/backend-test-utils/api-report.md | 7 +++ packages/backend-test-utils/src/deprecated.ts | 13 +++++ packages/backend-test-utils/src/msw/index.ts | 2 +- .../src/msw/setupRequestMockHandlers.ts | 2 +- .../cli/src/commands/versions/bump.test.ts | 7 +-- .../src/sources/RemoteConfigSource.test.ts | 4 +- .../auth/microsoft/MicrosoftAuth.test.ts | 4 +- .../DefaultAuthConnector.test.ts | 4 +- .../ProxiedSignInIdentity.test.ts | 4 +- .../ProxiedSignInPage.test.tsx | 4 +- packages/create-app/src/lib/tasks.test.ts | 4 +- packages/frontend-test-utils/api-report.md | 2 +- packages/frontend-test-utils/src/index.ts | 2 +- .../release-manifests/src/manifest.test.ts | 4 +- packages/test-utils/api-report.md | 7 +++ packages/test-utils/src/deprecated.ts | 29 ++++++++++ packages/test-utils/src/index.ts | 1 + .../apis/FetchApi/MockFetchApi.test.ts | 4 +- .../test-utils/src/testUtils/msw/index.ts | 2 +- ...estMockHandlers.ts => setupMswHandlers.ts} | 2 +- .../src/helpers.test.ts | 4 +- .../src/helpers.test.ts | 7 +-- .../src/authenticator.test.ts | 4 +- .../src/authenticator.test.ts | 4 +- .../src/module.test.ts | 4 +- .../src/authenticator.test.ts | 4 +- .../src/module.test.ts | 4 +- .../src/authenticator.test.ts | 4 +- .../bitbucketServer/provider.test.ts | 6 +-- .../src/providers/oidc/provider.test.ts | 7 +-- .../src/identity/IdentityClient.test.ts | 4 +- .../src/lib/azure.test.ts | 4 +- .../BitbucketCloudEntityProvider.test.ts | 7 +-- .../src/lib/BitbucketServerClient.test.ts | 4 +- .../BitbucketServerEntityProvider.test.ts | 7 +-- .../providers/GerritEntityProvider.test.ts | 7 +-- .../analyzers/GithubLocationAnalyzer.test.ts | 7 +-- .../src/lib/github.test.ts | 4 +- .../src/GitLabDiscoveryProcessor.test.ts | 7 +-- .../src/lib/client.test.ts | 7 +-- .../GitlabDiscoveryEntityProvider.test.ts | 7 +-- .../GitlabOrgDiscoveryEntityProvider.test.ts | 7 +-- .../src/microsoftGraph/client.test.ts | 4 +- .../src/puppet/read.test.ts | 4 +- .../modules/core/UrlReaderProcessor.test.ts | 7 +-- .../src/search/DefaultCatalogCollator.test.ts | 4 +- .../src/api/AzureRepoApiClient.test.ts | 4 +- .../src/api/CatalogImportClient.test.ts | 4 +- .../src/service/KubernetesBuilder.test.ts | 4 +- .../service/KubernetesFanOutHandler.test.ts | 7 +-- .../src/service/KubernetesFetcher.test.ts | 4 +- .../src/service/KubernetesProxy.test.ts | 4 +- .../src/auth/PinnipedHelper.test.ts | 4 +- .../src/api/KubernetesBackendClient.test.ts | 4 +- .../DefaultNotificationService.test.ts | 4 +- .../src/api/NotificationsClient.test.ts | 4 +- .../src/ServerPermissionClient.test.ts | 4 +- .../src/service/router.config.test.ts | 4 +- .../src/service/router.credentials.test.ts | 4 +- .../actions/bitbucketCloud.examples.test.ts | 4 +- .../src/actions/bitbucketCloud.test.ts | 4 +- ...itbucketCloudPipelinesRun.examples.test.ts | 4 +- .../bitbucketCloudPipelinesRun.test.ts | 4 +- .../actions/bitbucketServer.examples.test.ts | 4 +- .../src/actions/bitbucketServer.test.ts | 4 +- ...itbucketServerPullRequest.examples.test.ts | 4 +- .../src/actions/bitbucket.examples.test.ts | 4 +- .../src/actions/bitbucket.test.ts | 4 +- .../confluenceToMarkdown.examples.test.ts | 4 +- .../confluence/confluenceToMarkdown.test.ts | 4 +- .../src/actions/gerrit.test.ts | 4 +- .../src/actions/gitea.test.ts | 4 +- .../src/actions/createProject.test.ts | 4 +- plugins/scaffolder/src/api.test.ts | 4 +- .../DefaultCatalogCollatorFactory.test.ts | 4 +- .../ToolDocumentCollatorFactory.test.ts | 7 +-- ...ckOverflowQuestionsCollatorFactory.test.ts | 7 +-- .../DefaultTechDocsCollatorFactory.test.ts | 7 +-- .../search/DefaultTechDocsCollator.test.ts | 7 +-- .../StorageApi/UserSettingsStorage.test.ts | 4 +- 96 files changed, 285 insertions(+), 244 deletions(-) create mode 100644 .changeset/tiny-schools-draw.md create mode 100644 packages/test-utils/src/deprecated.ts rename packages/test-utils/src/testUtils/msw/{setupRequestMockHandlers.ts => setupMswHandlers.ts} (94%) diff --git a/.changeset/tiny-schools-draw.md b/.changeset/tiny-schools-draw.md new file mode 100644 index 0000000000..02f57308fb --- /dev/null +++ b/.changeset/tiny-schools-draw.md @@ -0,0 +1,53 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-auth-backend-module-pinniped-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-auth-backend-module-oidc-provider': patch +'@backstage/plugin-scaffolder-backend-module-gerrit': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-scaffolder-backend-module-gitea': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-search-backend-module-catalog': patch +'@backstage/plugin-search-backend-module-explore': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/frontend-test-utils': patch +'@backstage/backend-test-utils': patch +'@backstage/release-manifests': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-notifications-node': patch +'@backstage/backend-defaults': patch +'@backstage/core-components': patch +'@backstage/plugin-kubernetes-react': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-kubernetes-node': patch +'@backstage/plugin-permission-node': patch +'@backstage/config-loader': patch +'@backstage/plugin-catalog-import': patch +'@backstage/core-app-api': patch +'@backstage/plugin-notifications': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-auth-backend': patch +'@backstage/create-app': patch +'@backstage/test-utils': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-auth-node': patch +'@backstage/cli': patch +--- + +Rename frontend and backend `setupRequestMockHandlers` methods to `setupMswHandlers`. diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index de89efa351..385bc17a36 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -17,7 +17,7 @@ import { ServiceFactoryTester, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import { tokenManagerServiceFactory } from '@backstage/backend-app-api'; import { authServiceFactory } from './authServiceFactory'; @@ -68,7 +68,7 @@ const mockDeps = [ ]; describe('authServiceFactory', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); afterEach(() => { jest.useRealTimers(); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts index 559107c59c..2e413c73d6 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts @@ -17,10 +17,7 @@ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { ExternalTokenHandler } from './ExternalTokenHandler'; import { TokenHandler } from './types'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { randomBytes } from 'crypto'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { DateTime } from 'luxon'; @@ -81,7 +78,7 @@ class FakeTokenFactory { describe('ExternalTokenHandler', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { const handler1: TokenHandler = { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts index 56930e4480..02abd1e889 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -81,7 +81,7 @@ describe('JWKSHandler', () => { let mockSubject: string; const keyDurationSeconds = 5; - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { mockSubject = 'test_subject'; diff --git a/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts index 76bafb2a8f..9078be81e0 100644 --- a/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts @@ -16,10 +16,7 @@ import { JsonObject } from '@backstage/types'; import { UserTokenHandler } from './UserTokenHandler'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { AuthenticationError } from '@backstage/errors'; @@ -64,7 +61,7 @@ async function createToken(options: { describe('UserTokenHandler', () => { let userTokenHandler: UserTokenHandler; - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.useRealTimers(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts index ce2385507b..9e2aec1b56 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts @@ -26,7 +26,7 @@ import { import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -76,7 +76,7 @@ describe('AzureUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts index dc182af619..43be9b1a92 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -56,7 +56,7 @@ describe('BitbucketCloudUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); describe('readUrl', () => { it('should be able to readUrl via buffer without ETag', async () => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts index 2870138535..b63e557f3e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -51,7 +51,7 @@ const reader = new BitbucketServerUrlReader( describe('BitbucketServerUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); describe('readTree', () => { const repoBuffer = fs.readFileSync( diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts index edd8d6d3fc..67953cd020 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts @@ -22,7 +22,7 @@ import { import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -104,7 +104,7 @@ describe('BitbucketUrlReader', () => { ); const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); describe('readUrl', () => { it('should be able to readUrl via buffer without ETag', async () => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts index 38936806c6..31ad87178e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts @@ -16,10 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { FetchUrlReader } from './FetchUrlReader'; @@ -30,7 +27,7 @@ const fetchUrlReader = new FetchUrlReader(); describe('FetchUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts index 94627e074f..61ae9eba6a 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts @@ -17,7 +17,7 @@ import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { NotModifiedError, NotFoundError } from '@backstage/errors'; @@ -92,7 +92,7 @@ const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { // eslint-disable-next-line jest/no-disabled-tests describe.skip('GerritUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(() => { mockDir.clear(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts index 0b5983eefd..c3b744a792 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { GiteaIntegration, readGiteaConfig } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; @@ -64,7 +61,7 @@ const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { describe('GiteaUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); afterAll(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts index f07ada0cc3..b0734a0f0e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -74,7 +74,7 @@ const gheProcessor = new GithubUrlReader( describe('GithubUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(mockDir.clear); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts index 46276fcdda..62f7d84c84 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts @@ -18,7 +18,7 @@ import { ConfigReader } from '@backstage/config'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -72,7 +72,7 @@ describe('GitlabUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts index f1efbca108..a95cf7f479 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { HarnessIntegration, readHarnessConfig } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; @@ -120,7 +117,7 @@ const handlers = [ describe('HarnessUrlReader', () => { const worker = setupServer(...handlers); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeAll(() => worker.listen({ onUnhandledRequest: 'bypass' })); afterAll(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts b/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts index b63b24fb18..bba0c5145a 100644 --- a/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts +++ b/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts @@ -15,10 +15,7 @@ */ import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; import { SignJWT, base64url, importJWK } from 'jose'; import { rest } from 'msw'; @@ -28,7 +25,7 @@ import { DefaultUserInfoService } from './DefaultUserInfoService'; describe('DefaultUserInfoService', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const mockPublicKey = { kty: 'EC', diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index d3a618d214..ac14bae321 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -403,6 +403,13 @@ export type ServiceMock = { }; // @public +export function setupMswHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + +// @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { listen: (t: any) => void; close: () => void; diff --git a/packages/backend-test-utils/src/deprecated.ts b/packages/backend-test-utils/src/deprecated.ts index 5fe74e7ef3..a349030b12 100644 --- a/packages/backend-test-utils/src/deprecated.ts +++ b/packages/backend-test-utils/src/deprecated.ts @@ -16,6 +16,7 @@ import { CreateMockDirectoryOptions } from './filesystem'; import { isDockerDisabledForTests as _isDockerDisabledForTests } from './util'; +import { setupMswHandlers } from './msw'; /** * @public @@ -30,3 +31,15 @@ export type MockDirectoryOptions = CreateMockDirectoryOptions; export function isDockerDisabledForTests(): boolean { return _isDockerDisabledForTests(); } + +/** + * @public + * @deprecated Use `setupMswHandlers` from `@backstage/backend-test-utils` instead. + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void { + setupMswHandlers(worker); +} diff --git a/packages/backend-test-utils/src/msw/index.ts b/packages/backend-test-utils/src/msw/index.ts index 29cbdf854a..a339721ba5 100644 --- a/packages/backend-test-utils/src/msw/index.ts +++ b/packages/backend-test-utils/src/msw/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { setupRequestMockHandlers } from './setupRequestMockHandlers'; +export { setupMswHandlers } from './setupRequestMockHandlers'; diff --git a/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts b/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts index 625ef0e0fc..e123bc91c0 100644 --- a/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts +++ b/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts @@ -19,7 +19,7 @@ * @public * @param worker - service worker */ -export function setupRequestMockHandlers(worker: { +export function setupMswHandlers(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 0dfe2f61c3..c7f8b7ed0c 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -18,10 +18,7 @@ import fs from 'fs-extra'; import { Command } from 'commander'; import * as runObj from '../../lib/run'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; -import { - setupRequestMockHandlers, - withLogCollector, -} from '@backstage/test-utils'; +import { setupMswHandlers, withLogCollector } from '@backstage/test-utils'; import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; @@ -156,7 +153,7 @@ describe('bump', () => { }); const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('should bump backstage dependencies', async () => { mockDir.setContent({ diff --git a/packages/config-loader/src/sources/RemoteConfigSource.test.ts b/packages/config-loader/src/sources/RemoteConfigSource.test.ts index 6082c05e65..5fb16bf0c8 100644 --- a/packages/config-loader/src/sources/RemoteConfigSource.test.ts +++ b/packages/config-loader/src/sources/RemoteConfigSource.test.ts @@ -15,14 +15,14 @@ */ import { rest } from 'msw'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { RemoteConfigSource } from './RemoteConfigSource'; import { readN } from './__testUtils__/testUtils'; describe('RemoteConfigSource', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('should load config from a remote URL', async () => { worker.use( diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts index b6d59bd3f2..698c9930b7 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -19,13 +19,13 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import { ConfigReader } from '@backstage/config'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupMswHandlers } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; describe('MicrosoftAuth', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const microsoftAuth = MicrosoftAuth.create({ configApi: new ConfigReader(undefined), oauthRequestApi: new MockOAuthApi(), diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 4273ea99ee..d4bd136201 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -18,7 +18,7 @@ import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; import { UrlPatternDiscovery } from '../../apis'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupMswHandlers } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -53,7 +53,7 @@ const defaultOptions = { describe('DefaultAuthConnector', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); afterEach(() => { jest.resetAllMocks(); diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts index b8c2b84085..4780b174e0 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupMswHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -60,7 +60,7 @@ describe('ProxiedSignInIdentity', () => { afterEach(() => jest.useRealTimers()); const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('runs the happy path', async () => { const getBaseUrl = jest.fn(); diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index 05960cd213..3cfde3d0f0 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -20,7 +20,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { TestApiProvider, - setupRequestMockHandlers, + setupMswHandlers, wrapInTestApp, } from '@backstage/test-utils'; import { ProxiedSignInPage } from './ProxiedSignInPage'; @@ -28,7 +28,7 @@ import { discoveryApiRef } from '@backstage/core-plugin-api'; describe('ProxiedSignInPage', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); const Subject = wrapInTestApp(
    authenticated
    , { components: { diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 1f507b95ed..f26c3306f1 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -31,7 +31,7 @@ import { } from './tasks'; import { createMockDirectory, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -423,7 +423,7 @@ describe('tasks', () => { describe('fetchYarnLockSeedTask', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('should fetch the yarn.lock seed file', async () => { worker.use( diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 2605499a96..7c8f5a5837 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -20,7 +20,7 @@ import { MockStorageApi } from '@backstage/test-utils'; import { MockStorageBucket } from '@backstage/test-utils'; import { RenderResult } from '@testing-library/react'; import { RouteRef } from '@backstage/frontend-plugin-api'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupMswHandlers as setupRequestMockHandlers } from '@backstage/test-utils'; import { TestApiProvider } from '@backstage/test-utils'; import { TestApiProviderProps } from '@backstage/test-utils'; import { TestApiRegistry } from '@backstage/test-utils'; diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts index 0dab8e75b7..9d41699073 100644 --- a/packages/frontend-test-utils/src/index.ts +++ b/packages/frontend-test-utils/src/index.ts @@ -28,4 +28,4 @@ export type { TestApiProviderProps } from '@backstage/test-utils'; export { withLogCollector } from '@backstage/test-utils'; -export { setupRequestMockHandlers } from '@backstage/test-utils'; +export { setupMswHandlers as setupRequestMockHandlers } from '@backstage/test-utils'; diff --git a/packages/release-manifests/src/manifest.test.ts b/packages/release-manifests/src/manifest.test.ts index 6a53f993c8..8192bf867e 100644 --- a/packages/release-manifests/src/manifest.test.ts +++ b/packages/release-manifests/src/manifest.test.ts @@ -16,7 +16,7 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupMswHandlers } from '@backstage/test-utils'; import { getManifestByReleaseLine, getManifestByVersion, @@ -25,7 +25,7 @@ import { describe('Release Manifests', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); describe('getManifestByVersion', () => { it('should return a list of packages in a release', async () => { diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index e31a820a1c..82816d931a 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -210,6 +210,13 @@ export function renderWithEffects( ): Promise; // @public +export function setupMswHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + +// @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { listen: (t: any) => void; close: () => void; diff --git a/packages/test-utils/src/deprecated.ts b/packages/test-utils/src/deprecated.ts new file mode 100644 index 0000000000..6825f9d0da --- /dev/null +++ b/packages/test-utils/src/deprecated.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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. + */ + +import { setupMswHandlers } from './testUtils/msw'; + +/** + * @public + * @deprecated Use `CreateMockDirectoryOptions` from `@backstage/backend-test-utils` instead. + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void { + setupMswHandlers(worker); +} diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 83a955baa7..b971825f66 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ +export * from './deprecated'; export * from './testUtils'; diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts index 37b9d874f9..d51f4c7aee 100644 --- a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts @@ -16,12 +16,12 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../../msw'; +import { setupMswHandlers } from '../../msw'; import { MockFetchApi } from './MockFetchApi'; describe('MockFetchApi', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('works with default constructor', async () => { worker.use( diff --git a/packages/test-utils/src/testUtils/msw/index.ts b/packages/test-utils/src/testUtils/msw/index.ts index 29cbdf854a..a924676d06 100644 --- a/packages/test-utils/src/testUtils/msw/index.ts +++ b/packages/test-utils/src/testUtils/msw/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { setupRequestMockHandlers } from './setupRequestMockHandlers'; +export { setupMswHandlers } from './setupMswHandlers'; diff --git a/packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts b/packages/test-utils/src/testUtils/msw/setupMswHandlers.ts similarity index 94% rename from packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts rename to packages/test-utils/src/testUtils/msw/setupMswHandlers.ts index 625ef0e0fc..e123bc91c0 100644 --- a/packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts +++ b/packages/test-utils/src/testUtils/msw/setupMswHandlers.ts @@ -19,7 +19,7 @@ * @public * @param worker - service worker */ -export function setupRequestMockHandlers(worker: { +export function setupMswHandlers(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts index 0bbcf9b5c1..1b902246fb 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts @@ -19,7 +19,7 @@ import { JWTHeaderParameters, UnsecuredJWT } from 'jose'; import NodeCache from 'node-cache'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { PassportProfile } from '@backstage/plugin-auth-node'; import { makeProfileInfo, provisionKeyCache } from './helpers'; @@ -34,7 +34,7 @@ describe.each([ ], ])('helpers', (region, url) => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const nodeCache = jest.fn() as unknown as NodeCache; nodeCache.set = jest.fn(); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts index 8043f4e214..a0b3335874 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { HttpResponse, http } from 'msw'; @@ -89,7 +86,7 @@ describe('helpers', () => { const tokenFactory = new MockTokenFactory(); const cache = mockServices.cache.mock(); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts index 21a60a229b..fc392f68c2 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { encodeOAuthState, @@ -43,7 +43,7 @@ describe('microsoftAuthenticator', () => { const microsoftApi = new FakeMicrosoftAPI(); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); let implementation: { domainHint: string | undefined; diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts index 4203a70153..05ccebdf7d 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-auth-node'; import { oidcAuthenticator } from './authenticator'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -38,7 +38,7 @@ describe('oidcAuthenticator', () => { const revokedTokenMap: Record = {}; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + setupMswHandlers(mswServer); const issuerMetadata = { issuer: 'https://oidc.test', diff --git a/plugins/auth-backend-module-oidc-provider/src/module.test.ts b/plugins/auth-backend-module-oidc-provider/src/module.test.ts index 1359634950..827ff9dd01 100644 --- a/plugins/auth-backend-module-oidc-provider/src/module.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/module.test.ts @@ -20,7 +20,7 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { mockServices, - setupRequestMockHandlers, + setupMswHandlers, startTestBackend, } from '@backstage/backend-test-utils'; import { Server } from 'http'; @@ -35,7 +35,7 @@ describe('authModuleOidcProvider', () => { let publicKey: JWK; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + setupMswHandlers(mswServer); const issuerMetadata = { issuer: 'https://oidc.test', diff --git a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts index e2dedf2129..4cef747416 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-auth-node'; import { pinnipedAuthenticator } from './authenticator'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -37,7 +37,7 @@ describe('pinnipedAuthenticator', () => { let publicKey: JWK; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + setupMswHandlers(mswServer); const issuerMetadata = { issuer: 'https://pinniped.test', diff --git a/plugins/auth-backend-module-pinniped-provider/src/module.test.ts b/plugins/auth-backend-module-pinniped-provider/src/module.test.ts index a5c8fb4599..a7cbcaa0da 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/module.test.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/module.test.ts @@ -15,7 +15,7 @@ */ import { mockServices, - setupRequestMockHandlers, + setupMswHandlers, startTestBackend, } from '@backstage/backend-test-utils'; import { Server } from 'http'; @@ -32,7 +32,7 @@ describe('authModulePinnipedProvider', () => { let publicKey: JWK; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + setupMswHandlers(mswServer); const issuerMetadata = { issuer: 'https://pinniped.test', diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts index c69d4e9ab1..b5b335a112 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { AuthResolverContext, @@ -37,7 +37,7 @@ jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); describe('vmwareCloudAuthenticator', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); let oAuthState: OAuthState = { nonce: 'nonce', diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts index 187c3b09a5..8e71b2e8bb 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -22,7 +22,7 @@ import { BitbucketServerOAuthResult, } from './provider'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { AuthResolverContext } from '@backstage/plugin-auth-node'; @@ -131,7 +131,7 @@ describe('BitbucketServerAuthProvider', () => { describe('when transforming to type OAuthResponse', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); it('should map to a valid response', async () => { server.use(whoAmIHandler(), getUserHandler()); @@ -311,7 +311,7 @@ describe('BitbucketServerAuthProvider', () => { describe('when authenticating', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); it('should forward the refresh token', async () => { server.use(whoAmIHandler(), getUserHandler()); diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index c7e9c68580..01c3e04bc5 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Config, ConfigReader } from '@backstage/config'; import { @@ -38,7 +35,7 @@ describe('oidc.create', () => { nonce: 'foo', }; const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); let publicKey: JWK; let tokenset: object; diff --git a/plugins/auth-node/src/identity/IdentityClient.test.ts b/plugins/auth-node/src/identity/IdentityClient.test.ts index bdc7dd5cd7..beefe43173 100644 --- a/plugins/auth-node/src/identity/IdentityClient.test.ts +++ b/plugins/auth-node/src/identity/IdentityClient.test.ts @@ -15,7 +15,7 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { decodeProtectedHeader, exportJWK, @@ -102,7 +102,7 @@ describe('IdentityClient', () => { let factory: FakeTokenFactory; const keyDurationSeconds = 5; - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { client = IdentityClient.create({ discovery, issuer: mockBaseUrl }); diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index ef00610dc0..9d153e630c 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; @@ -26,7 +26,7 @@ import { ConfigReader } from '@backstage/config'; describe('azure', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const createFixture = (host: string, token: string) => { const azureConfig = { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 55b701e016..1c6128ef9c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -20,10 +20,7 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity, LocationEntity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; @@ -62,7 +59,7 @@ const logger = mockServices.logger.mock(); const server = setupServer(); describe('BitbucketCloudEntityProvider', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); const simpleConfig = new ConfigReader({ catalog: { diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts index a3d4155121..31ee011c23 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -50,7 +50,7 @@ describe('BitbucketServerClient', () => { config: config, }); - setupRequestMockHandlers(server); + setupMswHandlers(server); it('listProjects', async () => { server.use( diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts index 9624e396cd..fce1102aab 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts @@ -19,10 +19,7 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { rest } from 'msw'; @@ -103,7 +100,7 @@ function setupStubs(projects: Project[], baseUrl: string) { } describe('BitbucketServerEntityProvider', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); afterEach(() => { jest.clearAllMocks(); }); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts index 8b1acbdbaa..0a5baba297 100644 --- a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts @@ -19,10 +19,7 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import fs from 'fs-extra'; @@ -57,7 +54,7 @@ class PersistingTaskRunner implements TaskRunner { const logger = mockServices.logger.mock(); describe('GerritEntityProvider', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); afterEach(() => { jest.clearAllMocks(); diff --git a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts index e5c7eb5896..7b496a45c5 100644 --- a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts +++ b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts @@ -34,10 +34,7 @@ jest.mock('@octokit/rest', () => { import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { GithubLocationAnalyzer } from './GithubLocationAnalyzer'; -import { - setupRequestMockHandlers, - mockServices, -} from '@backstage/backend-test-utils'; +import { setupMswHandlers, mockServices } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -63,7 +60,7 @@ describe('GithubLocationAnalyzer', () => { }, }); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { server.use( diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index d83e8031cc..ac88a79bee 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql as graphqlOctokit } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; @@ -39,7 +39,7 @@ describe('github', () => { const graphql = graphqlOctokit.defaults({ request: { fetch } }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); describe('getOrganizationUsers using defaultUserMapper', () => { it('reads members', async () => { diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index c1608cd386..10751ad615 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-node'; import { rest, RestRequest } from 'msw'; @@ -157,7 +154,7 @@ function getProcessor({ } describe('GitlabDiscoveryProcessor', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index 7d40eb1204..212c5cd4a4 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { readGitLabIntegrationConfig } from '@backstage/integration'; import { setupServer } from 'msw/node'; @@ -27,7 +24,7 @@ import { GitLabClient, paginated } from './client'; import { GitLabGroup, GitLabUser } from './types'; const server = setupServer(...handlers); -setupRequestMockHandlers(server); +setupMswHandlers(server); describe('GitLabClient', () => { describe('isSelfManaged', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index e89b378cfe..c23a897df2 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -19,10 +19,7 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { DefaultEventsService } from '@backstage/plugin-events-node'; @@ -32,7 +29,7 @@ import * as mock from '../__testUtils__/mocks'; import { GitlabDiscoveryEntityProvider } from './GitlabDiscoveryEntityProvider'; const server = setupServer(...handlers); -setupRequestMockHandlers(server); +setupMswHandlers(server); afterEach(() => jest.clearAllMocks()); class PersistingTaskRunner implements TaskRunner { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts index ddc0d03570..2fb8a6c839 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts @@ -19,10 +19,7 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { DefaultEventsService } from '@backstage/plugin-events-node'; @@ -33,7 +30,7 @@ import { GroupNameTransformerOptions } from '../lib/types'; import { GitlabOrgDiscoveryEntityProvider } from './GitlabOrgDiscoveryEntityProvider'; const server = setupServer(...handlers); -setupRequestMockHandlers(server); +setupMswHandlers(server); afterEach(() => jest.clearAllMocks()); class PersistingTaskRunner implements TaskRunner { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index 6d63537b54..a58db08bfc 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -15,7 +15,7 @@ */ import { TokenCredential } from '@azure/identity'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { MicrosoftGraphClient } from './client'; @@ -27,7 +27,7 @@ describe('MicrosoftGraphClient', () => { let client: MicrosoftGraphClient; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(() => { tokenCredential.getToken.mockResolvedValue({ diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts index ac28f04503..05ae65f85c 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts @@ -20,14 +20,14 @@ import { PuppetDbEntityProviderConfig, } from '../providers'; import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ANNOTATION_PUPPET_CERTNAME, ENDPOINT_FACTSETS } from './constants'; describe('readPuppetNodes', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index dd80f8f0c4..2d46ce2256 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -15,10 +15,7 @@ */ import { UrlReader, UrlReaders } from '@backstage/backend-common'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -39,7 +36,7 @@ describe('UrlReaderProcessor', () => { set: jest.fn(), }; const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts index 4c1673b785..8dc22f6ea0 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { Entity } from '@backstage/catalog-model'; import { DefaultCatalogCollator } from './DefaultCatalogCollator'; import { setupServer } from 'msw/node'; @@ -62,7 +62,7 @@ describe('DefaultCatalogCollator', () => { let mockTokenManager: jest.Mocked; let collator: DefaultCatalogCollator; - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeAll(() => { mockDiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), diff --git a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts index a8fbe9db76..65ef2c035e 100644 --- a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts +++ b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts @@ -26,7 +26,7 @@ import { RepoApiClient, } from './AzureRepoApiClient'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupMswHandlers } from '@backstage/test-utils'; function mockRepoEndpoint() { return rest.get( @@ -153,7 +153,7 @@ function mockPrEndpoint() { describe('RepoApiClient', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const testToken = new Date().toString(); const sut = new RepoApiClient({ project: 'project', diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 1f16840c33..67a019cc1b 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -57,7 +57,7 @@ import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { ScmAuthApi } from '@backstage/integration-react'; import { CatalogApi } from '@backstage/plugin-catalog-react'; -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; import { Octokit } from '@octokit/rest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -70,7 +70,7 @@ import { describe('CatalogImportClient', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/catalog'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 1ac653bb58..7b61a22009 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -38,7 +38,7 @@ import { ServiceMock, mockCredentials, mockServices, - setupRequestMockHandlers, + setupMswHandlers, startTestBackend, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; @@ -508,7 +508,7 @@ describe('API integration tests', () => { describe('/proxy', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(() => { worker.use( diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index a433374a2c..7a2aba2977 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -26,10 +26,7 @@ import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { FetchResponse, KubernetesRequestAuth, @@ -1128,7 +1125,7 @@ describe('KubernetesFanOutHandler', () => { describe('with a real fetcher', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('fetch error short-circuits requests to a single cluster, recovering across the fleet', async () => { const pods = [{ metadata: { name: 'pod-name' } }]; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index dafb073bb1..e7196d9dc6 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -28,7 +28,7 @@ import { setupServer } from 'msw/node'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import { Config } from '@kubernetes/client-node'; @@ -71,7 +71,7 @@ const POD_METRICS_FIXTURE = [ describe('KubernetesFetcher', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); const labels = (req: MockedRequest): object => { const selectorParam = req.url.searchParams.get('labelSelector'); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 02eeb8c7ed..45a131439b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -20,7 +20,7 @@ import { errorHandler } from '@backstage/backend-common'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import { NotFoundError } from '@backstage/errors'; import { @@ -90,7 +90,7 @@ describe('KubernetesProxy', () => { getExternalBaseUrl: jest.fn(), }; - setupRequestMockHandlers(worker); + setupMswHandlers(worker); const buildMockRequest = (clusterName: any, path: string): Request => { const req = getMockReq({ diff --git a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts index fdfea030a5..f882d2373c 100644 --- a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts +++ b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts @@ -18,7 +18,7 @@ import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { ClusterDetails } from '../types'; import { mockServices, - setupRequestMockHandlers, + setupMswHandlers, startTestBackend, } from '@backstage/backend-test-utils'; import { createBackendModule } from '@backstage/backend-plugin-api'; @@ -43,7 +43,7 @@ describe('Pinniped - tokenCredentialRequest', () => { const logger = loggerToWinstonLogger(mockServices.logger.mock()); let httpsRequest: jest.SpyInstance; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeAll(() => { httpsRequest = jest.spyOn( diff --git a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts index 82ac7c84e2..eecaed20b6 100644 --- a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts +++ b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts @@ -19,7 +19,7 @@ import { KubernetesBackendClient } from './KubernetesBackendClient'; import { rest } from 'msw'; import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { setupServer } from 'msw/node'; -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; import { CustomObjectsByEntityRequest, KubernetesRequestBody, @@ -36,7 +36,7 @@ describe('KubernetesBackendClient', () => { }; let mockResponse: ObjectsByEntityResponse; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); const identityApi = { getCredentials: jest.fn(), diff --git a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts index d5c327e788..843852af21 100644 --- a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts +++ b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupMswHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { NotificationPayload } from '@backstage/plugin-notifications-common'; @@ -32,7 +32,7 @@ const testNotification: NotificationPayload = { describe('DefaultNotificationService', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const discovery = mockServices.discovery.mock({ getBaseUrl: jest.fn().mockResolvedValue('http://example.com'), diff --git a/plugins/notifications/src/api/NotificationsClient.test.ts b/plugins/notifications/src/api/NotificationsClient.test.ts index e9acd04fc9..b4be073738 100644 --- a/plugins/notifications/src/api/NotificationsClient.test.ts +++ b/plugins/notifications/src/api/NotificationsClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { NotificationsClient } from './NotificationsClient'; @@ -33,7 +33,7 @@ const testNotification: Partial = { }; describe('NotificationsClient', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); const mockBaseUrl = 'http://backstage/api/notifications'; const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; const fetchApi = new MockFetchApi(); diff --git a/plugins/permission-node/src/ServerPermissionClient.test.ts b/plugins/permission-node/src/ServerPermissionClient.test.ts index 6e99ff71ab..b09c25eeb7 100644 --- a/plugins/permission-node/src/ServerPermissionClient.test.ts +++ b/plugins/permission-node/src/ServerPermissionClient.test.ts @@ -25,7 +25,7 @@ import { import { mockCredentials, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { setupServer } from 'msw/node'; @@ -54,7 +54,7 @@ const config = new ConfigReader({ }); describe('ServerPermissionClient', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); const discovery = mockServices.discovery.mock(); diff --git a/plugins/proxy-backend/src/service/router.config.test.ts b/plugins/proxy-backend/src/service/router.config.test.ts index ba81553a4f..3f11faea46 100644 --- a/plugins/proxy-backend/src/service/router.config.test.ts +++ b/plugins/proxy-backend/src/service/router.config.test.ts @@ -19,7 +19,7 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { - setupRequestMockHandlers, + setupMswHandlers, startTestBackend, } from '@backstage/backend-test-utils'; import { @@ -36,7 +36,7 @@ import fetch from 'node-fetch'; describe('createRouter reloadable configuration', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); it('should be able to observe the config', async () => { // Grab the subscriber function and use mutable config data to mock a config file change diff --git a/plugins/proxy-backend/src/service/router.credentials.test.ts b/plugins/proxy-backend/src/service/router.credentials.test.ts index 54c865ba11..aede6bb353 100644 --- a/plugins/proxy-backend/src/service/router.credentials.test.ts +++ b/plugins/proxy-backend/src/service/router.credentials.test.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-app-api'; import { mockServices, - setupRequestMockHandlers, + setupMswHandlers, startTestBackend, } from '@backstage/backend-test-utils'; import { ResponseError } from '@backstage/errors'; @@ -34,7 +34,7 @@ import fetch from 'node-fetch'; describe('credentials', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('handles all valid credentials settings', async () => { const config = { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts index 5e95f41751..ef745fbd18 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketCloudAction } from './bitbucketCloud'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -66,7 +66,7 @@ describe('publish:bitbucketCloud', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index 6a079a3f9b..57215ac06d 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketCloudAction } from './bitbucketCloud'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -56,7 +56,7 @@ describe('publish:bitbucketCloud', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index 472c7a6a03..6a4f82066e 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -21,7 +21,7 @@ import yaml from 'yaml'; import { examples } from './bitbucketCloudPipelinesRun.examples'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('bitbucket:pipelines:run', () => { @@ -48,7 +48,7 @@ describe('bitbucket:pipelines:run', () => { build_number: 1, }; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts index 9386246868..be8500895a 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -48,7 +48,7 @@ describe('bitbucket:pipelines:run', () => { build_number: 1, }; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts index eae8385822..8e6356df18 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerAction } from './bitbucketServer'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import yaml from 'yaml'; @@ -68,7 +68,7 @@ describe('publish:bitbucketServer', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts index 1d42e7368c..fb7d17c0ba 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerAction } from './bitbucketServer'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -66,7 +66,7 @@ describe('publish:bitbucketServer', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts index 1d3556fc88..0bc6d1464b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import yaml from 'yaml'; @@ -191,7 +191,7 @@ describe('publish:bitbucketServer:pull-request', () => { }; const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts index 6633246d70..964b2f207c 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -67,7 +67,7 @@ describe('publish:bitbucket', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts index a63d657904..dddbcde828 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts @@ -28,7 +28,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -63,7 +63,7 @@ describe('publish:bitbucket', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index da82849c1e..3aff044b89 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -20,7 +20,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -32,7 +32,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- describe('confluence:transform:markdown examples', () => { const baseUrl = `https://confluence.example.com`; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); const config = new ConfigReader({ confluence: { diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index b19259b3df..5958f1407b 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -21,7 +21,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + setupMswHandlers, } from '@backstage/backend-test-utils'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; @@ -31,7 +31,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- describe('confluence:transform:markdown', () => { const baseUrl = `https://nodomain.confluence.com`; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); const config = new ConfigReader({ confluence: { diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts index 1f069d3835..b91cc2d84d 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts @@ -30,7 +30,7 @@ import path from 'path'; import { createPublishGerritAction } from './gerrit'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -61,7 +61,7 @@ describe('publish:gerrit', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 8a72265df0..ab4d4641d0 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -18,7 +18,7 @@ import { ConfigReader } from '@backstage/config'; import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { setupServer } from 'msw/node'; @@ -62,7 +62,7 @@ describe('publish:gitea', () => { }); const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts index 158b7db4ae..9eef522ddc 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { InputError } from '@backstage/errors'; @@ -27,7 +27,7 @@ import { createSentryCreateProjectAction } from './createProject'; describe('sentry:project:create action', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); const createScaffolderConfig = (configData: JsonObject = {}) => ({ config: new ConfigReader({ diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 488169719d..1da440ab7b 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ScaffolderClient } from './api'; @@ -30,7 +30,7 @@ const mockFetchEventSource = fetchEventSource as jest.MockedFunction< const server = setupServer(); describe('api', () => { - setupRequestMockHandlers(server); + setupMswHandlers(server); const mockBaseUrl = 'http://backstage/api'; const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; diff --git a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts index 7af3a8e9a2..0d9320f3dd 100644 --- a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts +++ b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupMswHandlers } from '@backstage/backend-test-utils'; import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; @@ -74,7 +74,7 @@ describe('DefaultCatalogCollatorFactory', () => { tokenManager: mockTokenManager, }; - setupRequestMockHandlers(server); + setupMswHandlers(server); beforeEach(() => { server.use( diff --git a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts index 82305d0d86..e69b48b1c3 100644 --- a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts +++ b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts @@ -19,10 +19,7 @@ import { } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'stream'; @@ -82,7 +79,7 @@ describe('ToolDocumentCollatorFactory', () => { let collator: Readable; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(async () => { factory = ToolDocumentCollatorFactory.fromConfig(config, options); diff --git a/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts index f4d4a77c3c..3698f7c8f6 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts @@ -17,10 +17,7 @@ import { StackOverflowQuestionsCollatorFactory, StackOverflowQuestionsCollatorFactoryOptions, } from './StackOverflowQuestionsCollatorFactory'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; import { ConfigReader } from '@backstage/config'; import { Readable } from 'stream'; @@ -95,7 +92,7 @@ describe('StackOverflowQuestionsCollatorFactory', () => { describe('getCollator', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); it('returns a readable stream', async () => { const factory = StackOverflowQuestionsCollatorFactory.fromConfig( diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts index 26329e5ed6..a2e8cc9992 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts @@ -20,10 +20,7 @@ import { import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'stream'; @@ -106,7 +103,7 @@ describe('DefaultTechDocsCollatorFactory', () => { let collator: Readable; const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); beforeEach(async () => { factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index a0fc908ee5..47245dc6fc 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -21,10 +21,7 @@ import { } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; -import { - mockServices, - setupRequestMockHandlers, -} from '@backstage/backend-test-utils'; +import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -79,7 +76,7 @@ const expectedEntities: Entity[] = [ describe('TechDocs Collator', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + setupMswHandlers(worker); describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { let mockDiscoveryApi: jest.Mocked; diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index a421c413dc..6ee6a3df2f 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -21,14 +21,14 @@ import { IdentityApi, StorageApi, } from '@backstage/core-plugin-api'; -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { UserSettingsStorage } from './UserSettingsStorage'; describe('Persistent Storage API', () => { const server = setupServer(); - setupRequestMockHandlers(server); + setupMswHandlers(server); const mockBaseUrl = 'http://backstage:9191/api'; const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; From b82aff92aaa3b270e661dcd1a198e512b7a16099 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 10 Jul 2024 12:53:32 +0200 Subject: [PATCH 078/212] refactor(test-utils): deprecate the `isDockerDisabledForTests` function Signed-off-by: Camila Belo --- .changeset/tiny-schools-draw.md | 50 +------------------ .../02-testing.md | 6 +-- .../auth/authServiceFactory.test.ts | 4 +- .../external/ExternalTokenHandler.test.ts | 7 ++- .../entrypoints/auth/external/jwks.test.ts | 4 +- .../auth/user/UserTokenHandler.test.ts | 7 ++- .../urlReader/lib/AzureUrlReader.test.ts | 4 +- .../lib/BitbucketCloudUrlReader.test.ts | 4 +- .../lib/BitbucketServerUrlReader.test.ts | 4 +- .../urlReader/lib/BitbucketUrlReader.test.ts | 4 +- .../urlReader/lib/FetchUrlReader.test.ts | 7 ++- .../urlReader/lib/GerritUrlReader.test.ts | 4 +- .../urlReader/lib/GiteaUrlReader.test.ts | 7 ++- .../urlReader/lib/GithubUrlReader.test.ts | 4 +- .../urlReader/lib/GitlabUrlReader.test.ts | 4 +- .../urlReader/lib/HarnessUrlReader.test.ts | 7 ++- .../userInfo/DefaultUserInfoService.test.ts | 7 ++- packages/backend-test-utils/api-report.md | 14 +++--- packages/backend-test-utils/src/deprecated.ts | 22 ++++---- packages/backend-test-utils/src/msw/index.ts | 2 +- ...ockHandlers.ts => registerMswTestHooks.ts} | 2 +- .../cli/src/commands/versions/bump.test.ts | 4 +- .../ExampleComponent.test.tsx.hbs | 4 +- .../src/sources/RemoteConfigSource.test.ts | 4 +- .../auth/microsoft/MicrosoftAuth.test.ts | 4 +- .../DefaultAuthConnector.test.ts | 4 +- .../ProxiedSignInIdentity.test.ts | 4 +- .../ProxiedSignInPage.test.tsx | 4 +- packages/create-app/src/lib/tasks.test.ts | 4 +- packages/frontend-test-utils/api-report.md | 11 +++- .../frontend-test-utils/src/deprecated.ts | 29 +++++++++++ packages/frontend-test-utils/src/index.ts | 3 +- .../integration/src/bitbucket/core.test.ts | 4 +- .../src/bitbucketCloud/core.test.ts | 4 +- .../src/bitbucketServer/core.test.ts | 4 +- packages/integration/src/gerrit/core.test.ts | 4 +- packages/integration/src/gitea/core.test.ts | 4 +- packages/integration/src/harness/core.test.ts | 4 +- packages/integration/src/helpers.ts | 2 +- .../release-manifests/src/manifest.test.ts | 4 +- packages/test-utils/api-report.md | 14 +++--- packages/test-utils/src/deprecated.ts | 6 +-- .../apis/FetchApi/MockFetchApi.test.ts | 4 +- .../test-utils/src/testUtils/msw/index.ts | 2 +- ...MswHandlers.ts => registerMswTestHooks.ts} | 2 +- .../src/helpers.test.ts | 4 +- .../src/helpers.test.ts | 7 ++- .../src/authenticator.test.ts | 4 +- .../src/authenticator.test.ts | 4 +- .../src/module.test.ts | 4 +- .../src/authenticator.test.ts | 4 +- .../src/module.test.ts | 4 +- .../src/authenticator.test.ts | 4 +- .../bitbucketServer/provider.test.ts | 6 +-- .../src/providers/oidc/provider.test.ts | 7 ++- .../src/identity/IdentityClient.test.ts | 4 +- .../src/lib/azure.test.ts | 4 +- .../BitbucketCloudEntityProvider.test.ts | 7 ++- .../src/lib/BitbucketServerClient.test.ts | 4 +- .../BitbucketServerEntityProvider.test.ts | 7 ++- .../providers/GerritEntityProvider.test.ts | 7 ++- .../analyzers/GithubLocationAnalyzer.test.ts | 7 ++- .../src/lib/github.test.ts | 4 +- .../src/GitLabDiscoveryProcessor.test.ts | 7 ++- .../src/lib/client.test.ts | 7 ++- .../GitlabDiscoveryEntityProvider.test.ts | 7 ++- .../GitlabOrgDiscoveryEntityProvider.test.ts | 7 ++- .../src/microsoftGraph/client.test.ts | 4 +- .../src/puppet/read.test.ts | 4 +- .../modules/core/UrlReaderProcessor.test.ts | 7 ++- .../src/search/DefaultCatalogCollator.test.ts | 4 +- .../src/api/AzureRepoApiClient.test.ts | 4 +- .../src/api/CatalogImportClient.test.ts | 4 +- .../src/service/KubernetesBuilder.test.ts | 4 +- .../service/KubernetesFanOutHandler.test.ts | 7 ++- .../src/service/KubernetesFetcher.test.ts | 4 +- .../src/service/KubernetesProxy.test.ts | 4 +- .../src/auth/PinnipedHelper.test.ts | 4 +- .../src/api/KubernetesBackendClient.test.ts | 4 +- .../DefaultNotificationService.test.ts | 4 +- .../src/api/NotificationsClient.test.ts | 4 +- .../src/ServerPermissionClient.test.ts | 4 +- .../src/service/router.config.test.ts | 4 +- .../src/service/router.credentials.test.ts | 4 +- .../actions/bitbucketCloud.examples.test.ts | 4 +- .../src/actions/bitbucketCloud.test.ts | 4 +- ...itbucketCloudPipelinesRun.examples.test.ts | 4 +- .../bitbucketCloudPipelinesRun.test.ts | 4 +- .../actions/bitbucketServer.examples.test.ts | 4 +- .../src/actions/bitbucketServer.test.ts | 4 +- ...itbucketServerPullRequest.examples.test.ts | 4 +- .../bitbucketServerPullRequest.test.ts | 4 +- .../src/actions/bitbucket.examples.test.ts | 4 +- .../src/actions/bitbucket.test.ts | 4 +- .../confluenceToMarkdown.examples.test.ts | 4 +- .../confluence/confluenceToMarkdown.test.ts | 4 +- .../src/actions/gerrit.test.ts | 4 +- .../src/actions/gitea.test.ts | 4 +- .../src/actions/createProject.test.ts | 4 +- plugins/scaffolder/src/api.test.ts | 4 +- .../DefaultCatalogCollatorFactory.test.ts | 4 +- .../ToolDocumentCollatorFactory.test.ts | 7 ++- ...ckOverflowQuestionsCollatorFactory.test.ts | 7 ++- .../DefaultTechDocsCollatorFactory.test.ts | 7 ++- .../search/DefaultTechDocsCollator.test.ts | 7 ++- .../StorageApi/UserSettingsStorage.test.ts | 4 +- 106 files changed, 329 insertions(+), 272 deletions(-) rename packages/backend-test-utils/src/msw/{setupRequestMockHandlers.ts => registerMswTestHooks.ts} (95%) create mode 100644 packages/frontend-test-utils/src/deprecated.ts rename packages/test-utils/src/testUtils/msw/{setupMswHandlers.ts => registerMswTestHooks.ts} (95%) diff --git a/.changeset/tiny-schools-draw.md b/.changeset/tiny-schools-draw.md index 02f57308fb..2042f13fac 100644 --- a/.changeset/tiny-schools-draw.md +++ b/.changeset/tiny-schools-draw.md @@ -1,53 +1,7 @@ --- -'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch -'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch -'@backstage/plugin-search-backend-module-stack-overflow-collator': patch -'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch -'@backstage/plugin-auth-backend-module-vmware-cloud-provider': patch -'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch -'@backstage/plugin-catalog-backend-module-bitbucket-server': patch -'@backstage/plugin-auth-backend-module-microsoft-provider': patch -'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch -'@backstage/plugin-auth-backend-module-pinniped-provider': patch -'@backstage/plugin-auth-backend-module-aws-alb-provider': patch -'@backstage/plugin-scaffolder-backend-module-bitbucket': patch -'@backstage/plugin-auth-backend-module-oidc-provider': patch -'@backstage/plugin-scaffolder-backend-module-gerrit': patch -'@backstage/plugin-scaffolder-backend-module-sentry': patch -'@backstage/plugin-catalog-backend-module-puppetdb': patch -'@backstage/plugin-scaffolder-backend-module-gitea': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-search-backend-module-techdocs': patch -'@backstage/plugin-catalog-backend-module-gerrit': patch -'@backstage/plugin-catalog-backend-module-github': patch -'@backstage/plugin-catalog-backend-module-gitlab': patch -'@backstage/plugin-search-backend-module-catalog': patch -'@backstage/plugin-search-backend-module-explore': patch -'@backstage/plugin-catalog-backend-module-azure': patch -'@backstage/frontend-test-utils': patch '@backstage/backend-test-utils': patch -'@backstage/release-manifests': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-notifications-node': patch -'@backstage/backend-defaults': patch -'@backstage/core-components': patch -'@backstage/plugin-kubernetes-react': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-kubernetes-node': patch -'@backstage/plugin-permission-node': patch -'@backstage/config-loader': patch -'@backstage/plugin-catalog-import': patch -'@backstage/core-app-api': patch -'@backstage/plugin-notifications': patch -'@backstage/plugin-proxy-backend': patch -'@backstage/plugin-user-settings': patch -'@backstage/plugin-auth-backend': patch -'@backstage/create-app': patch +'@backstage/frontend-test-utils': patch '@backstage/test-utils': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-auth-node': patch -'@backstage/cli': patch --- -Rename frontend and backend `setupRequestMockHandlers` methods to `setupMswHandlers`. +Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 0739ad24aa..addcc72c84 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -61,7 +61,7 @@ requests and return mock responses. This lets you stub out remote services rather than the local clients, leading to more thorough and robust tests. You can read more about how it works [in their documentation](https://mswjs.io/). -The `@backstage/backend-test-utils` package exports a `setupRequestMockHandlers` +The `@backstage/backend-test-utils` package exports a `registerMswTestHooks` function which ensures that the correct `jest` lifecycle hooks are invoked to set up and tear down your `msw` instance, and enables the option that completely rejects requests that don't match one of your mock rules. This ensures that your @@ -70,13 +70,13 @@ tests cannot accidentally leak traffic into production from tests. Example: ```ts -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; describe('read from remote', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('should auth and read successfully', async () => { expect.assertions(1); diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index 385bc17a36..f9ef4c9762 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -17,7 +17,7 @@ import { ServiceFactoryTester, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { tokenManagerServiceFactory } from '@backstage/backend-app-api'; import { authServiceFactory } from './authServiceFactory'; @@ -68,7 +68,7 @@ const mockDeps = [ ]; describe('authServiceFactory', () => { - setupMswHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.useRealTimers(); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts index 2e413c73d6..39b01c46b6 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts @@ -17,7 +17,10 @@ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { ExternalTokenHandler } from './ExternalTokenHandler'; import { TokenHandler } from './types'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { randomBytes } from 'crypto'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { DateTime } from 'luxon'; @@ -78,7 +81,7 @@ class FakeTokenFactory { describe('ExternalTokenHandler', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { const handler1: TokenHandler = { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts index 02abd1e889..321e1823aa 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -81,7 +81,7 @@ describe('JWKSHandler', () => { let mockSubject: string; const keyDurationSeconds = 5; - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { mockSubject = 'test_subject'; diff --git a/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts index 9078be81e0..e85ec8db63 100644 --- a/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts @@ -16,7 +16,10 @@ import { JsonObject } from '@backstage/types'; import { UserTokenHandler } from './UserTokenHandler'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { AuthenticationError } from '@backstage/errors'; @@ -61,7 +64,7 @@ async function createToken(options: { describe('UserTokenHandler', () => { let userTokenHandler: UserTokenHandler; - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.useRealTimers(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts index 9e2aec1b56..f53a7b6c11 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts @@ -26,7 +26,7 @@ import { import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -76,7 +76,7 @@ describe('AzureUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts index 43be9b1a92..f5caade3fa 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -56,7 +56,7 @@ describe('BitbucketCloudUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); describe('readUrl', () => { it('should be able to readUrl via buffer without ETag', async () => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts index b63e557f3e..6af2858cde 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -51,7 +51,7 @@ const reader = new BitbucketServerUrlReader( describe('BitbucketServerUrlReader', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); describe('readTree', () => { const repoBuffer = fs.readFileSync( diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts index 67953cd020..54880aaa75 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts @@ -22,7 +22,7 @@ import { import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -104,7 +104,7 @@ describe('BitbucketUrlReader', () => { ); const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); describe('readUrl', () => { it('should be able to readUrl via buffer without ETag', async () => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts index 31ad87178e..f84422c564 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts @@ -16,7 +16,10 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { FetchUrlReader } from './FetchUrlReader'; @@ -27,7 +30,7 @@ const fetchUrlReader = new FetchUrlReader(); describe('FetchUrlReader', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts index 61ae9eba6a..14ca4c018e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts @@ -17,7 +17,7 @@ import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { NotModifiedError, NotFoundError } from '@backstage/errors'; @@ -92,7 +92,7 @@ const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { // eslint-disable-next-line jest/no-disabled-tests describe.skip('GerritUrlReader', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { mockDir.clear(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts index c3b744a792..77c995c552 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { GiteaIntegration, readGiteaConfig } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; @@ -61,7 +64,7 @@ const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { describe('GiteaUrlReader', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); afterAll(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts index b0734a0f0e..f1dd7c0dc6 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -74,7 +74,7 @@ const gheProcessor = new GithubUrlReader( describe('GithubUrlReader', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(mockDir.clear); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts index 62f7d84c84..dd6a0970d2 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts @@ -18,7 +18,7 @@ import { ConfigReader } from '@backstage/config'; import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -72,7 +72,7 @@ describe('GitlabUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts index a95cf7f479..004e1931d0 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { HarnessIntegration, readHarnessConfig } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; @@ -117,7 +120,7 @@ const handlers = [ describe('HarnessUrlReader', () => { const worker = setupServer(...handlers); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeAll(() => worker.listen({ onUnhandledRequest: 'bypass' })); afterAll(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts b/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts index bba0c5145a..692d179ee2 100644 --- a/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts +++ b/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts @@ -15,7 +15,10 @@ */ import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; import { SignJWT, base64url, importJWK } from 'jose'; import { rest } from 'msw'; @@ -25,7 +28,7 @@ import { DefaultUserInfoService } from './DefaultUserInfoService'; describe('DefaultUserInfoService', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const mockPublicKey = { kty: 'EC', diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index ac14bae321..37dc5d097d 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -365,6 +365,13 @@ export namespace mockServices { } } +// @public +export function registerMswTestHooks(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + // @public export class ServiceFactoryTester { static from( @@ -402,13 +409,6 @@ export type ServiceMock = { : TService[Key]; }; -// @public -export function setupMswHandlers(worker: { - listen: (t: any) => void; - close: () => void; - resetHandlers: () => void; -}): void; - // @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { listen: (t: any) => void; diff --git a/packages/backend-test-utils/src/deprecated.ts b/packages/backend-test-utils/src/deprecated.ts index a349030b12..9790580a9c 100644 --- a/packages/backend-test-utils/src/deprecated.ts +++ b/packages/backend-test-utils/src/deprecated.ts @@ -14,9 +14,9 @@ * limitations under the License. */ +import { registerMswTestHooks } from './msw'; import { CreateMockDirectoryOptions } from './filesystem'; import { isDockerDisabledForTests as _isDockerDisabledForTests } from './util'; -import { setupMswHandlers } from './msw'; /** * @public @@ -26,20 +26,20 @@ export type MockDirectoryOptions = CreateMockDirectoryOptions; /** * @public - * @deprecated This is an internal function and will no longer be exported from this package. - */ -export function isDockerDisabledForTests(): boolean { - return _isDockerDisabledForTests(); -} - -/** - * @public - * @deprecated Use `setupMswHandlers` from `@backstage/backend-test-utils` instead. + * @deprecated Use `registerMswTestHooks` from `@backstage/backend-test-utils` instead. */ export function setupRequestMockHandlers(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; }): void { - setupMswHandlers(worker); + registerMswTestHooks(worker); +} + +/** + * @public + * @deprecated This is an internal function and will no longer be exported from this package. + */ +export function isDockerDisabledForTests(): boolean { + return _isDockerDisabledForTests(); } diff --git a/packages/backend-test-utils/src/msw/index.ts b/packages/backend-test-utils/src/msw/index.ts index a339721ba5..fa6a37445f 100644 --- a/packages/backend-test-utils/src/msw/index.ts +++ b/packages/backend-test-utils/src/msw/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { setupMswHandlers } from './setupRequestMockHandlers'; +export { registerMswTestHooks } from './registerMswTestHooks'; diff --git a/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts b/packages/backend-test-utils/src/msw/registerMswTestHooks.ts similarity index 95% rename from packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts rename to packages/backend-test-utils/src/msw/registerMswTestHooks.ts index e123bc91c0..4e26c38321 100644 --- a/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts +++ b/packages/backend-test-utils/src/msw/registerMswTestHooks.ts @@ -19,7 +19,7 @@ * @public * @param worker - service worker */ -export function setupMswHandlers(worker: { +export function registerMswTestHooks(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index c7f8b7ed0c..b8677a48c5 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { Command } from 'commander'; import * as runObj from '../../lib/run'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; -import { setupMswHandlers, withLogCollector } from '@backstage/test-utils'; +import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; @@ -153,7 +153,7 @@ describe('bump', () => { }); const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('should bump backstage dependencies', async () => { mockDir.setContent({ diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 9db62feebc..c79977f301 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -4,14 +4,14 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { screen } from '@testing-library/react'; import { - setupRequestMockHandlers, + registerMswTestHooks, renderInTestApp, } from '@backstage/test-utils'; describe('ExampleComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - setupRequestMockHandlers(server); + registerMswTestHooks(server); // setup mock response beforeEach(() => { diff --git a/packages/config-loader/src/sources/RemoteConfigSource.test.ts b/packages/config-loader/src/sources/RemoteConfigSource.test.ts index 5fb16bf0c8..e825bf3434 100644 --- a/packages/config-loader/src/sources/RemoteConfigSource.test.ts +++ b/packages/config-loader/src/sources/RemoteConfigSource.test.ts @@ -15,14 +15,14 @@ */ import { rest } from 'msw'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { RemoteConfigSource } from './RemoteConfigSource'; import { readN } from './__testUtils__/testUtils'; describe('RemoteConfigSource', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('should load config from a remote URL', async () => { worker.use( diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts index 698c9930b7..0143aa2ded 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -19,13 +19,13 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import { ConfigReader } from '@backstage/config'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; -import { setupMswHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; describe('MicrosoftAuth', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const microsoftAuth = MicrosoftAuth.create({ configApi: new ConfigReader(undefined), oauthRequestApi: new MockOAuthApi(), diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index d4bd136201..60c4a3b8d9 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -18,7 +18,7 @@ import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; import { UrlPatternDiscovery } from '../../apis'; -import { setupMswHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -53,7 +53,7 @@ const defaultOptions = { describe('DefaultAuthConnector', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.resetAllMocks(); diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts index 4780b174e0..7eebc13d56 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -60,7 +60,7 @@ describe('ProxiedSignInIdentity', () => { afterEach(() => jest.useRealTimers()); const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('runs the happy path', async () => { const getBaseUrl = jest.fn(); diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index 3cfde3d0f0..e4e65b613f 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -20,7 +20,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { TestApiProvider, - setupMswHandlers, + registerMswTestHooks, wrapInTestApp, } from '@backstage/test-utils'; import { ProxiedSignInPage } from './ProxiedSignInPage'; @@ -28,7 +28,7 @@ import { discoveryApiRef } from '@backstage/core-plugin-api'; describe('ProxiedSignInPage', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); const Subject = wrapInTestApp(
    authenticated
    , { components: { diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index f26c3306f1..deba27273c 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -31,7 +31,7 @@ import { } from './tasks'; import { createMockDirectory, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -423,7 +423,7 @@ describe('tasks', () => { describe('fetchYarnLockSeedTask', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('should fetch the yarn.lock seed file', async () => { worker.use( diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 7c8f5a5837..9629e13bfa 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -18,9 +18,9 @@ import { MockFetchApiOptions } from '@backstage/test-utils'; import { MockPermissionApi } from '@backstage/test-utils'; import { MockStorageApi } from '@backstage/test-utils'; import { MockStorageBucket } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { RenderResult } from '@testing-library/react'; import { RouteRef } from '@backstage/frontend-plugin-api'; -import { setupMswHandlers as setupRequestMockHandlers } from '@backstage/test-utils'; import { TestApiProvider } from '@backstage/test-utils'; import { TestApiProviderProps } from '@backstage/test-utils'; import { TestApiRegistry } from '@backstage/test-utils'; @@ -73,13 +73,20 @@ export { MockStorageApi }; export { MockStorageBucket }; +export { registerMswTestHooks }; + // @public export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, ): RenderResult; -export { setupRequestMockHandlers }; +// @public @deprecated (undocumented) +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; export { TestApiProvider }; diff --git a/packages/frontend-test-utils/src/deprecated.ts b/packages/frontend-test-utils/src/deprecated.ts new file mode 100644 index 0000000000..1b67f73e6b --- /dev/null +++ b/packages/frontend-test-utils/src/deprecated.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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. + */ + +import { registerMswTestHooks } from '@backstage/test-utils'; + +/** + * @public + * @deprecated Use `registerMswTestHooks` from `@backstage/frontend-test-utils` instead. + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void { + registerMswTestHooks(worker); +} diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts index 9d41699073..20f393706e 100644 --- a/packages/frontend-test-utils/src/index.ts +++ b/packages/frontend-test-utils/src/index.ts @@ -20,6 +20,7 @@ * Contains utilities that can be used when testing frontend features such as extensions. */ +export * from './deprecated'; export * from './apis'; export * from './app'; @@ -28,4 +29,4 @@ export type { TestApiProviderProps } from '@backstage/test-utils'; export { withLogCollector } from '@backstage/test-utils'; -export { setupMswHandlers as setupRequestMockHandlers } from '@backstage/test-utils'; +export { registerMswTestHooks } from '@backstage/test-utils'; diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts index 6af974723f..f1b4e04f15 100644 --- a/packages/integration/src/bitbucket/core.test.ts +++ b/packages/integration/src/bitbucket/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { BitbucketIntegrationConfig } from './config'; import { getBitbucketDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucket core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getBitbucketRequestOptions', () => { it('inserts a token when needed', () => { diff --git a/packages/integration/src/bitbucketCloud/core.test.ts b/packages/integration/src/bitbucketCloud/core.test.ts index 3a342bae8b..56a53b924d 100644 --- a/packages/integration/src/bitbucketCloud/core.test.ts +++ b/packages/integration/src/bitbucketCloud/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { BitbucketCloudIntegrationConfig } from './config'; import { getBitbucketCloudDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucketCloud core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getBitbucketCloudRequestOptions', () => { it('insert basic auth when needed', () => { diff --git a/packages/integration/src/bitbucketServer/core.test.ts b/packages/integration/src/bitbucketServer/core.test.ts index b03a8dda83..c1c72b38dd 100644 --- a/packages/integration/src/bitbucketServer/core.test.ts +++ b/packages/integration/src/bitbucketServer/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { BitbucketServerIntegrationConfig } from './config'; import { getBitbucketServerDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucketServer core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getBitbucketServerRequestOptions', () => { it('inserts a token when needed', () => { diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index c2bf5d3ed2..35632c654e 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -17,7 +17,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import fetch from 'cross-fetch'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { GerritIntegrationConfig } from './config'; import { buildGerritGitilesArchiveUrl, @@ -32,7 +32,7 @@ import { describe('gerrit core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('buildGerritGitilesArchiveUrl', () => { const config: GerritIntegrationConfig = { diff --git a/packages/integration/src/gitea/core.test.ts b/packages/integration/src/gitea/core.test.ts index ac29c469b6..e9d7c592f0 100644 --- a/packages/integration/src/gitea/core.test.ts +++ b/packages/integration/src/gitea/core.test.ts @@ -15,7 +15,7 @@ */ import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { GiteaIntegrationConfig } from './config'; import { getGiteaArchiveUrl, @@ -28,7 +28,7 @@ import { describe('gitea core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getGiteaFileCatalogInfoCntentsUrl', () => { it('can create an url from arguments', () => { diff --git a/packages/integration/src/harness/core.test.ts b/packages/integration/src/harness/core.test.ts index 78ca68a98a..90f37aaa08 100644 --- a/packages/integration/src/harness/core.test.ts +++ b/packages/integration/src/harness/core.test.ts @@ -15,7 +15,7 @@ */ import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { HarnessIntegrationConfig } from './config'; import { getHarnessArchiveUrl, @@ -28,7 +28,7 @@ import { describe('Harness code core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getHarnessFileContentsUrl', () => { it('can create an url from arguments', () => { diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index b403155cc5..f56d360325 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -114,7 +114,7 @@ export function defaultScmResolveUrl(options: { * * @param worker - service worker */ -export function setupRequestMockHandlers(worker: { +export function registerMswTestHooks(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/packages/release-manifests/src/manifest.test.ts b/packages/release-manifests/src/manifest.test.ts index 8192bf867e..3c7657a83f 100644 --- a/packages/release-manifests/src/manifest.test.ts +++ b/packages/release-manifests/src/manifest.test.ts @@ -16,7 +16,7 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; -import { setupMswHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { getManifestByReleaseLine, getManifestByVersion, @@ -25,7 +25,7 @@ import { describe('Release Manifests', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); describe('getManifestByVersion', () => { it('should return a list of packages in a release', async () => { diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 82816d931a..cc1c645387 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -197,6 +197,13 @@ export type MockStorageBucket = { [key: string]: any; }; +// @public +export function registerMswTestHooks(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + // @public export function renderInTestApp( Component: ComponentType> | ReactNode, @@ -209,13 +216,6 @@ export function renderWithEffects( options?: Pick & LegacyRootOption, ): Promise; -// @public -export function setupMswHandlers(worker: { - listen: (t: any) => void; - close: () => void; - resetHandlers: () => void; -}): void; - // @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { listen: (t: any) => void; diff --git a/packages/test-utils/src/deprecated.ts b/packages/test-utils/src/deprecated.ts index 6825f9d0da..2033bbea2e 100644 --- a/packages/test-utils/src/deprecated.ts +++ b/packages/test-utils/src/deprecated.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { setupMswHandlers } from './testUtils/msw'; +import { registerMswTestHooks } from './testUtils/msw'; /** * @public - * @deprecated Use `CreateMockDirectoryOptions` from `@backstage/backend-test-utils` instead. + * @deprecated Use `registerMswTestHooks` from `@backstage/test-utils` instead. */ export function setupRequestMockHandlers(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; }): void { - setupMswHandlers(worker); + registerMswTestHooks(worker); } diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts index d51f4c7aee..d33036ca8c 100644 --- a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts @@ -16,12 +16,12 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '../../msw'; +import { registerMswTestHooks } from '../../msw'; import { MockFetchApi } from './MockFetchApi'; describe('MockFetchApi', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('works with default constructor', async () => { worker.use( diff --git a/packages/test-utils/src/testUtils/msw/index.ts b/packages/test-utils/src/testUtils/msw/index.ts index a924676d06..fa6a37445f 100644 --- a/packages/test-utils/src/testUtils/msw/index.ts +++ b/packages/test-utils/src/testUtils/msw/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { setupMswHandlers } from './setupMswHandlers'; +export { registerMswTestHooks } from './registerMswTestHooks'; diff --git a/packages/test-utils/src/testUtils/msw/setupMswHandlers.ts b/packages/test-utils/src/testUtils/msw/registerMswTestHooks.ts similarity index 95% rename from packages/test-utils/src/testUtils/msw/setupMswHandlers.ts rename to packages/test-utils/src/testUtils/msw/registerMswTestHooks.ts index e123bc91c0..4e26c38321 100644 --- a/packages/test-utils/src/testUtils/msw/setupMswHandlers.ts +++ b/packages/test-utils/src/testUtils/msw/registerMswTestHooks.ts @@ -19,7 +19,7 @@ * @public * @param worker - service worker */ -export function setupMswHandlers(worker: { +export function registerMswTestHooks(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts index 1b902246fb..81251584e9 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts @@ -19,7 +19,7 @@ import { JWTHeaderParameters, UnsecuredJWT } from 'jose'; import NodeCache from 'node-cache'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { PassportProfile } from '@backstage/plugin-auth-node'; import { makeProfileInfo, provisionKeyCache } from './helpers'; @@ -34,7 +34,7 @@ describe.each([ ], ])('helpers', (region, url) => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const nodeCache = jest.fn() as unknown as NodeCache; nodeCache.set = jest.fn(); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts index a0b3335874..9efab07ba5 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { HttpResponse, http } from 'msw'; @@ -86,7 +89,7 @@ describe('helpers', () => { const tokenFactory = new MockTokenFactory(); const cache = mockServices.cache.mock(); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts index fc392f68c2..74a7ab9974 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { encodeOAuthState, @@ -43,7 +43,7 @@ describe('microsoftAuthenticator', () => { const microsoftApi = new FakeMicrosoftAPI(); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); let implementation: { domainHint: string | undefined; diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts index 05ccebdf7d..38e134bcd0 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-auth-node'; import { oidcAuthenticator } from './authenticator'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -38,7 +38,7 @@ describe('oidcAuthenticator', () => { const revokedTokenMap: Record = {}; const mswServer = setupServer(); - setupMswHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://oidc.test', diff --git a/plugins/auth-backend-module-oidc-provider/src/module.test.ts b/plugins/auth-backend-module-oidc-provider/src/module.test.ts index 827ff9dd01..1bbb5c1cff 100644 --- a/plugins/auth-backend-module-oidc-provider/src/module.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/module.test.ts @@ -20,7 +20,7 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { mockServices, - setupMswHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { Server } from 'http'; @@ -35,7 +35,7 @@ describe('authModuleOidcProvider', () => { let publicKey: JWK; const mswServer = setupServer(); - setupMswHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://oidc.test', diff --git a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts index 4cef747416..359e497e5f 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-auth-node'; import { pinnipedAuthenticator } from './authenticator'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -37,7 +37,7 @@ describe('pinnipedAuthenticator', () => { let publicKey: JWK; const mswServer = setupServer(); - setupMswHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://pinniped.test', diff --git a/plugins/auth-backend-module-pinniped-provider/src/module.test.ts b/plugins/auth-backend-module-pinniped-provider/src/module.test.ts index a7cbcaa0da..e170c8d4ed 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/module.test.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/module.test.ts @@ -15,7 +15,7 @@ */ import { mockServices, - setupMswHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { Server } from 'http'; @@ -32,7 +32,7 @@ describe('authModulePinnipedProvider', () => { let publicKey: JWK; const mswServer = setupServer(); - setupMswHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://pinniped.test', diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts index b5b335a112..0e363d4700 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { AuthResolverContext, @@ -37,7 +37,7 @@ jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); describe('vmwareCloudAuthenticator', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); let oAuthState: OAuthState = { nonce: 'nonce', diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts index 8e71b2e8bb..3756e00b38 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -22,7 +22,7 @@ import { BitbucketServerOAuthResult, } from './provider'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { AuthResolverContext } from '@backstage/plugin-auth-node'; @@ -131,7 +131,7 @@ describe('BitbucketServerAuthProvider', () => { describe('when transforming to type OAuthResponse', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); it('should map to a valid response', async () => { server.use(whoAmIHandler(), getUserHandler()); @@ -311,7 +311,7 @@ describe('BitbucketServerAuthProvider', () => { describe('when authenticating', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); it('should forward the refresh token', async () => { server.use(whoAmIHandler(), getUserHandler()); diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index 01c3e04bc5..89cf9c3b2c 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Config, ConfigReader } from '@backstage/config'; import { @@ -35,7 +38,7 @@ describe('oidc.create', () => { nonce: 'foo', }; const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); let publicKey: JWK; let tokenset: object; diff --git a/plugins/auth-node/src/identity/IdentityClient.test.ts b/plugins/auth-node/src/identity/IdentityClient.test.ts index beefe43173..418eb544ad 100644 --- a/plugins/auth-node/src/identity/IdentityClient.test.ts +++ b/plugins/auth-node/src/identity/IdentityClient.test.ts @@ -15,7 +15,7 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { decodeProtectedHeader, exportJWK, @@ -102,7 +102,7 @@ describe('IdentityClient', () => { let factory: FakeTokenFactory; const keyDurationSeconds = 5; - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { client = IdentityClient.create({ discovery, issuer: mockBaseUrl }); diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index 9d153e630c..4839b625d2 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; @@ -26,7 +26,7 @@ import { ConfigReader } from '@backstage/config'; describe('azure', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const createFixture = (host: string, token: string) => { const azureConfig = { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 1c6128ef9c..80d09f3e2f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -20,7 +20,10 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity, LocationEntity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; @@ -59,7 +62,7 @@ const logger = mockServices.logger.mock(); const server = setupServer(); describe('BitbucketCloudEntityProvider', () => { - setupMswHandlers(server); + registerMswTestHooks(server); const simpleConfig = new ConfigReader({ catalog: { diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts index 31ee011c23..db2a05d0b1 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -50,7 +50,7 @@ describe('BitbucketServerClient', () => { config: config, }); - setupMswHandlers(server); + registerMswTestHooks(server); it('listProjects', async () => { server.use( diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts index fce1102aab..49b69f0fa6 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts @@ -19,7 +19,10 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { rest } from 'msw'; @@ -100,7 +103,7 @@ function setupStubs(projects: Project[], baseUrl: string) { } describe('BitbucketServerEntityProvider', () => { - setupMswHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.clearAllMocks(); }); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts index 0a5baba297..ee3555c7f9 100644 --- a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts @@ -19,7 +19,10 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import fs from 'fs-extra'; @@ -54,7 +57,7 @@ class PersistingTaskRunner implements TaskRunner { const logger = mockServices.logger.mock(); describe('GerritEntityProvider', () => { - setupMswHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.clearAllMocks(); diff --git a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts index 7b496a45c5..29c8cbc66d 100644 --- a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts +++ b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts @@ -34,7 +34,10 @@ jest.mock('@octokit/rest', () => { import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { GithubLocationAnalyzer } from './GithubLocationAnalyzer'; -import { setupMswHandlers, mockServices } from '@backstage/backend-test-utils'; +import { + registerMswTestHooks, + mockServices, +} from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -60,7 +63,7 @@ describe('GithubLocationAnalyzer', () => { }, }); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { server.use( diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index ac88a79bee..9eae9e1161 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql as graphqlOctokit } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; @@ -39,7 +39,7 @@ describe('github', () => { const graphql = graphqlOctokit.defaults({ request: { fetch } }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); describe('getOrganizationUsers using defaultUserMapper', () => { it('reads members', async () => { diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index 10751ad615..d7ce338d6d 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-node'; import { rest, RestRequest } from 'msw'; @@ -154,7 +157,7 @@ function getProcessor({ } describe('GitlabDiscoveryProcessor', () => { - setupMswHandlers(server); + registerMswTestHooks(server); beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index 212c5cd4a4..1eebe7d6e0 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { readGitLabIntegrationConfig } from '@backstage/integration'; import { setupServer } from 'msw/node'; @@ -24,7 +27,7 @@ import { GitLabClient, paginated } from './client'; import { GitLabGroup, GitLabUser } from './types'; const server = setupServer(...handlers); -setupMswHandlers(server); +registerMswTestHooks(server); describe('GitLabClient', () => { describe('isSelfManaged', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index c23a897df2..473e547a57 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -19,7 +19,10 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { DefaultEventsService } from '@backstage/plugin-events-node'; @@ -29,7 +32,7 @@ import * as mock from '../__testUtils__/mocks'; import { GitlabDiscoveryEntityProvider } from './GitlabDiscoveryEntityProvider'; const server = setupServer(...handlers); -setupMswHandlers(server); +registerMswTestHooks(server); afterEach(() => jest.clearAllMocks()); class PersistingTaskRunner implements TaskRunner { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts index 2fb8a6c839..9706384ff0 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts @@ -19,7 +19,10 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { DefaultEventsService } from '@backstage/plugin-events-node'; @@ -30,7 +33,7 @@ import { GroupNameTransformerOptions } from '../lib/types'; import { GitlabOrgDiscoveryEntityProvider } from './GitlabOrgDiscoveryEntityProvider'; const server = setupServer(...handlers); -setupMswHandlers(server); +registerMswTestHooks(server); afterEach(() => jest.clearAllMocks()); class PersistingTaskRunner implements TaskRunner { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index a58db08bfc..624b6e42b0 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -15,7 +15,7 @@ */ import { TokenCredential } from '@azure/identity'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { MicrosoftGraphClient } from './client'; @@ -27,7 +27,7 @@ describe('MicrosoftGraphClient', () => { let client: MicrosoftGraphClient; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { tokenCredential.getToken.mockResolvedValue({ diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts index 05ae65f85c..8ad4c2b850 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts @@ -20,14 +20,14 @@ import { PuppetDbEntityProviderConfig, } from '../providers'; import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ANNOTATION_PUPPET_CERTNAME, ENDPOINT_FACTSETS } from './constants'; describe('readPuppetNodes', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index 2d46ce2256..7d1578db00 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -15,7 +15,10 @@ */ import { UrlReader, UrlReaders } from '@backstage/backend-common'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -36,7 +39,7 @@ describe('UrlReaderProcessor', () => { set: jest.fn(), }; const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts index 8dc22f6ea0..aee65bd2e1 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { Entity } from '@backstage/catalog-model'; import { DefaultCatalogCollator } from './DefaultCatalogCollator'; import { setupServer } from 'msw/node'; @@ -62,7 +62,7 @@ describe('DefaultCatalogCollator', () => { let mockTokenManager: jest.Mocked; let collator: DefaultCatalogCollator; - setupMswHandlers(server); + registerMswTestHooks(server); beforeAll(() => { mockDiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), diff --git a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts index 65ef2c035e..9f058dc744 100644 --- a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts +++ b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts @@ -26,7 +26,7 @@ import { RepoApiClient, } from './AzureRepoApiClient'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; function mockRepoEndpoint() { return rest.get( @@ -153,7 +153,7 @@ function mockPrEndpoint() { describe('RepoApiClient', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const testToken = new Date().toString(); const sut = new RepoApiClient({ project: 'project', diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 67a019cc1b..fa646e3ca4 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -57,7 +57,7 @@ import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { ScmAuthApi } from '@backstage/integration-react'; import { CatalogApi } from '@backstage/plugin-catalog-react'; -import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { Octokit } from '@octokit/rest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -70,7 +70,7 @@ import { describe('CatalogImportClient', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const mockBaseUrl = 'http://backstage:9191/api/catalog'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 7b61a22009..5edb8845d4 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -38,7 +38,7 @@ import { ServiceMock, mockCredentials, mockServices, - setupMswHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; @@ -508,7 +508,7 @@ describe('API integration tests', () => { describe('/proxy', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { worker.use( diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 7a2aba2977..3491268d39 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -26,7 +26,10 @@ import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { FetchResponse, KubernetesRequestAuth, @@ -1125,7 +1128,7 @@ describe('KubernetesFanOutHandler', () => { describe('with a real fetcher', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('fetch error short-circuits requests to a single cluster, recovering across the fleet', async () => { const pods = [{ metadata: { name: 'pod-name' } }]; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index e7196d9dc6..8666e0ef16 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -28,7 +28,7 @@ import { setupServer } from 'msw/node'; import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { Config } from '@kubernetes/client-node'; @@ -71,7 +71,7 @@ const POD_METRICS_FIXTURE = [ describe('KubernetesFetcher', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); const labels = (req: MockedRequest): object => { const selectorParam = req.url.searchParams.get('labelSelector'); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 45a131439b..fe4feaee11 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -20,7 +20,7 @@ import { errorHandler } from '@backstage/backend-common'; import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { NotFoundError } from '@backstage/errors'; import { @@ -90,7 +90,7 @@ describe('KubernetesProxy', () => { getExternalBaseUrl: jest.fn(), }; - setupMswHandlers(worker); + registerMswTestHooks(worker); const buildMockRequest = (clusterName: any, path: string): Request => { const req = getMockReq({ diff --git a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts index f882d2373c..24cd459fde 100644 --- a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts +++ b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts @@ -18,7 +18,7 @@ import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { ClusterDetails } from '../types'; import { mockServices, - setupMswHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { createBackendModule } from '@backstage/backend-plugin-api'; @@ -43,7 +43,7 @@ describe('Pinniped - tokenCredentialRequest', () => { const logger = loggerToWinstonLogger(mockServices.logger.mock()); let httpsRequest: jest.SpyInstance; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeAll(() => { httpsRequest = jest.spyOn( diff --git a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts index eecaed20b6..4698f01dfe 100644 --- a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts +++ b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts @@ -19,7 +19,7 @@ import { KubernetesBackendClient } from './KubernetesBackendClient'; import { rest } from 'msw'; import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { setupServer } from 'msw/node'; -import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { CustomObjectsByEntityRequest, KubernetesRequestBody, @@ -36,7 +36,7 @@ describe('KubernetesBackendClient', () => { }; let mockResponse: ObjectsByEntityResponse; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); const identityApi = { getCredentials: jest.fn(), diff --git a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts index 843852af21..3872f898ee 100644 --- a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts +++ b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { NotificationPayload } from '@backstage/plugin-notifications-common'; @@ -32,7 +32,7 @@ const testNotification: NotificationPayload = { describe('DefaultNotificationService', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const discovery = mockServices.discovery.mock({ getBaseUrl: jest.fn().mockResolvedValue('http://example.com'), diff --git a/plugins/notifications/src/api/NotificationsClient.test.ts b/plugins/notifications/src/api/NotificationsClient.test.ts index b4be073738..f378269e45 100644 --- a/plugins/notifications/src/api/NotificationsClient.test.ts +++ b/plugins/notifications/src/api/NotificationsClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { NotificationsClient } from './NotificationsClient'; @@ -33,7 +33,7 @@ const testNotification: Partial = { }; describe('NotificationsClient', () => { - setupMswHandlers(server); + registerMswTestHooks(server); const mockBaseUrl = 'http://backstage/api/notifications'; const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; const fetchApi = new MockFetchApi(); diff --git a/plugins/permission-node/src/ServerPermissionClient.test.ts b/plugins/permission-node/src/ServerPermissionClient.test.ts index b09c25eeb7..529b0db907 100644 --- a/plugins/permission-node/src/ServerPermissionClient.test.ts +++ b/plugins/permission-node/src/ServerPermissionClient.test.ts @@ -25,7 +25,7 @@ import { import { mockCredentials, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { setupServer } from 'msw/node'; @@ -54,7 +54,7 @@ const config = new ConfigReader({ }); describe('ServerPermissionClient', () => { - setupMswHandlers(server); + registerMswTestHooks(server); const discovery = mockServices.discovery.mock(); diff --git a/plugins/proxy-backend/src/service/router.config.test.ts b/plugins/proxy-backend/src/service/router.config.test.ts index 3f11faea46..be26ddb9bc 100644 --- a/plugins/proxy-backend/src/service/router.config.test.ts +++ b/plugins/proxy-backend/src/service/router.config.test.ts @@ -19,7 +19,7 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { - setupMswHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { @@ -36,7 +36,7 @@ import fetch from 'node-fetch'; describe('createRouter reloadable configuration', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); it('should be able to observe the config', async () => { // Grab the subscriber function and use mutable config data to mock a config file change diff --git a/plugins/proxy-backend/src/service/router.credentials.test.ts b/plugins/proxy-backend/src/service/router.credentials.test.ts index aede6bb353..ac32179ef3 100644 --- a/plugins/proxy-backend/src/service/router.credentials.test.ts +++ b/plugins/proxy-backend/src/service/router.credentials.test.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-app-api'; import { mockServices, - setupMswHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { ResponseError } from '@backstage/errors'; @@ -34,7 +34,7 @@ import fetch from 'node-fetch'; describe('credentials', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('handles all valid credentials settings', async () => { const config = { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts index ef745fbd18..6b4b77879e 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketCloudAction } from './bitbucketCloud'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -66,7 +66,7 @@ describe('publish:bitbucketCloud', () => { }, }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index 57215ac06d..6cac8a0e1c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketCloudAction } from './bitbucketCloud'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -56,7 +56,7 @@ describe('publish:bitbucketCloud', () => { }, }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index 6a4f82066e..3e306c4065 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -21,7 +21,7 @@ import yaml from 'yaml'; import { examples } from './bitbucketCloudPipelinesRun.examples'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('bitbucket:pipelines:run', () => { @@ -48,7 +48,7 @@ describe('bitbucket:pipelines:run', () => { build_number: 1, }; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts index be8500895a..edabe2a7ba 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -48,7 +48,7 @@ describe('bitbucket:pipelines:run', () => { build_number: 1, }; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts index 8e6356df18..101379d1e6 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerAction } from './bitbucketServer'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import yaml from 'yaml'; @@ -68,7 +68,7 @@ describe('publish:bitbucketServer', () => { }, }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts index fb7d17c0ba..250b3f9b54 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerAction } from './bitbucketServer'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -66,7 +66,7 @@ describe('publish:bitbucketServer', () => { }, }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts index 0bc6d1464b..b83ba2be55 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import yaml from 'yaml'; @@ -191,7 +191,7 @@ describe('publish:bitbucketServer:pull-request', () => { }; const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts index 61adf0e3c8..37df535fec 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -221,7 +221,7 @@ describe('publish:bitbucketServer:pull-request', () => { ]; const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts index 964b2f207c..d452f2e11f 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -67,7 +67,7 @@ describe('publish:bitbucket', () => { }, }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts index dddbcde828..654b0fffcb 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts @@ -28,7 +28,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -63,7 +63,7 @@ describe('publish:bitbucket', () => { }, }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index 3aff044b89..36ae750723 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -20,7 +20,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -32,7 +32,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- describe('confluence:transform:markdown examples', () => { const baseUrl = `https://confluence.example.com`; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); const config = new ConfigReader({ confluence: { diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index 5958f1407b..4c3c6b891f 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -21,7 +21,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory, mockServices, - setupMswHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; @@ -31,7 +31,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- describe('confluence:transform:markdown', () => { const baseUrl = `https://nodomain.confluence.com`; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); const config = new ConfigReader({ confluence: { diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts index b91cc2d84d..c102353209 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts @@ -30,7 +30,7 @@ import path from 'path'; import { createPublishGerritAction } from './gerrit'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -61,7 +61,7 @@ describe('publish:gerrit', () => { }, }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index ab4d4641d0..7812ded60a 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -18,7 +18,7 @@ import { ConfigReader } from '@backstage/config'; import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { setupServer } from 'msw/node'; @@ -62,7 +62,7 @@ describe('publish:gitea', () => { }); const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts index 9eef522ddc..48c437d2a4 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { InputError } from '@backstage/errors'; @@ -27,7 +27,7 @@ import { createSentryCreateProjectAction } from './createProject'; describe('sentry:project:create action', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); const createScaffolderConfig = (configData: JsonObject = {}) => ({ config: new ConfigReader({ diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 1da440ab7b..805828a9fe 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; -import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ScaffolderClient } from './api'; @@ -30,7 +30,7 @@ const mockFetchEventSource = fetchEventSource as jest.MockedFunction< const server = setupServer(); describe('api', () => { - setupMswHandlers(server); + registerMswTestHooks(server); const mockBaseUrl = 'http://backstage/api'; const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; diff --git a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts index 0d9320f3dd..4bdb8a5ae9 100644 --- a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts +++ b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { setupMswHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; @@ -74,7 +74,7 @@ describe('DefaultCatalogCollatorFactory', () => { tokenManager: mockTokenManager, }; - setupMswHandlers(server); + registerMswTestHooks(server); beforeEach(() => { server.use( diff --git a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts index e69b48b1c3..82f5cffbc0 100644 --- a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts +++ b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts @@ -19,7 +19,10 @@ import { } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'stream'; @@ -79,7 +82,7 @@ describe('ToolDocumentCollatorFactory', () => { let collator: Readable; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(async () => { factory = ToolDocumentCollatorFactory.fromConfig(config, options); diff --git a/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts index 3698f7c8f6..90b324866f 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts @@ -17,7 +17,10 @@ import { StackOverflowQuestionsCollatorFactory, StackOverflowQuestionsCollatorFactoryOptions, } from './StackOverflowQuestionsCollatorFactory'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; import { ConfigReader } from '@backstage/config'; import { Readable } from 'stream'; @@ -92,7 +95,7 @@ describe('StackOverflowQuestionsCollatorFactory', () => { describe('getCollator', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); it('returns a readable stream', async () => { const factory = StackOverflowQuestionsCollatorFactory.fromConfig( diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts index a2e8cc9992..476c07ff48 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts @@ -20,7 +20,10 @@ import { import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'stream'; @@ -103,7 +106,7 @@ describe('DefaultTechDocsCollatorFactory', () => { let collator: Readable; const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); beforeEach(async () => { factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 47245dc6fc..64b0917202 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -21,7 +21,10 @@ import { } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; -import { mockServices, setupMswHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -76,7 +79,7 @@ const expectedEntities: Entity[] = [ describe('TechDocs Collator', () => { const worker = setupServer(); - setupMswHandlers(worker); + registerMswTestHooks(worker); describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { let mockDiscoveryApi: jest.Mocked; diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index 6ee6a3df2f..6bb0035b83 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -21,14 +21,14 @@ import { IdentityApi, StorageApi, } from '@backstage/core-plugin-api'; -import { MockFetchApi, setupMswHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { UserSettingsStorage } from './UserSettingsStorage'; describe('Persistent Storage API', () => { const server = setupServer(); - setupMswHandlers(server); + registerMswTestHooks(server); const mockBaseUrl = 'http://backstage:9191/api'; const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; From ccfc9d12c11db4d5a64d551a56bda686cd036346 Mon Sep 17 00:00:00 2001 From: Mccaskey Skye Date: Wed, 10 Jul 2024 12:57:23 -0500 Subject: [PATCH 079/212] fix: Added owner and repo args to getEnvironmentPublicKey Signed-off-by: Mccaskey Skye --- .changeset/early-deers-beg.md | 16 ++++++++++++++++ .../src/actions/githubEnvironment.test.ts | 11 +++++++++++ .../src/actions/githubEnvironment.ts | 2 ++ 3 files changed, 29 insertions(+) create mode 100644 .changeset/early-deers-beg.md diff --git a/.changeset/early-deers-beg.md b/.changeset/early-deers-beg.md new file mode 100644 index 0000000000..2814273afa --- /dev/null +++ b/.changeset/early-deers-beg.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Fixed bug resulting from missing required owner and repo arguments in `octokit getEnvironmentPublicKey in action `github:environment:create`. + +Adding environment secrets now works as expected. + +```diff + const publicKeyResponse = await client.rest.actions.getEnvironmentPublicKey({ + repository_id: repository.data.id, ++ owner, ++ repo, + environment_name: name + }); +``` diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts index c4eae41029..139d8028eb 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts @@ -304,5 +304,16 @@ describe('github:environment:create', () => { key_id: 'keyid', encrypted_value: expect.any(String), }); + expect( + mockOctokit.rest.actions.getEnvironmentPublicKey, + ).toHaveBeenCalledTimes(1); + expect( + mockOctokit.rest.actions.getEnvironmentPublicKey, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + }); }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 98e7cea1a9..d3e8d0e5d2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -199,6 +199,8 @@ export function createGithubEnvironmentAction(options: { const publicKeyResponse = await client.rest.actions.getEnvironmentPublicKey({ repository_id: repository.data.id, + owner: owner, + repo: repo, environment_name: name, }); From a7d9216e505d7ee8c4b397bbd3810633ab174a14 Mon Sep 17 00:00:00 2001 From: Mccaskey Skye Date: Wed, 10 Jul 2024 13:38:35 -0500 Subject: [PATCH 080/212] Update early-deers-beg.md Signed-off-by: Mccaskey Skye --- .changeset/early-deers-beg.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/early-deers-beg.md b/.changeset/early-deers-beg.md index 2814273afa..4bac074d8c 100644 --- a/.changeset/early-deers-beg.md +++ b/.changeset/early-deers-beg.md @@ -2,7 +2,7 @@ '@backstage/plugin-scaffolder-backend-module-github': patch --- -Fixed bug resulting from missing required owner and repo arguments in `octokit getEnvironmentPublicKey in action `github:environment:create`. +Fixed bug resulting from missing required owner and repo arguments in `getEnvironmentPublicKey` in action `github:environment:create`. Adding environment secrets now works as expected. From e149572b1a5a2dbe49225a5b5589b52f4172d32d Mon Sep 17 00:00:00 2001 From: CasperJ Date: Wed, 10 Jul 2024 21:03:43 +0200 Subject: [PATCH 081/212] Update .changeset/dirty-wasps-do.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: CasperJ --- .changeset/dirty-wasps-do.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dirty-wasps-do.md b/.changeset/dirty-wasps-do.md index fbf6799baf..f8b4e614eb 100644 --- a/.changeset/dirty-wasps-do.md +++ b/.changeset/dirty-wasps-do.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': patch --- -Catalog:Write scaffolder action shows incorrect log message +Updated `catalog:write` scaffolder action to show correct file path location in log message From bebd569c565516fb411c276bf47b1aef80c5bc9e Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Wed, 10 Jul 2024 20:44:25 -0400 Subject: [PATCH 082/212] Fix extra divider displayed on user list picker component Signed-off-by: Stephen Glass --- .changeset/pink-gorillas-brake.md | 5 +++++ .../src/components/UserListPicker/UserListPicker.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/pink-gorillas-brake.md diff --git a/.changeset/pink-gorillas-brake.md b/.changeset/pink-gorillas-brake.md new file mode 100644 index 0000000000..188884f13c --- /dev/null +++ b/.changeset/pink-gorillas-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fix extra divider displayed on user list picker component diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 7f0ff8be74..c6b515aa67 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -247,11 +247,11 @@ export const UserListPicker = (props: UserListPickerProps) => { - {group.items.map(item => ( + {group.items.map((item, index) => ( setSelectedUserFilter(item.id)} selected={item.id === filters.user?.value} className={classes.menuItem} From 707eb3989dfd3ddf32dc14564ba06a4649935905 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Jul 2024 10:16:25 +0200 Subject: [PATCH 083/212] Apply suggestions from code review Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .changeset/spotty-apricots-tan.md | 2 +- docs/backend-system/architecture/03-services.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/spotty-apricots-tan.md b/.changeset/spotty-apricots-tan.md index 2b88e3a7f5..8eea2b1dce 100644 --- a/.changeset/spotty-apricots-tan.md +++ b/.changeset/spotty-apricots-tan.md @@ -41,7 +41,7 @@ export class DefaultFooService implements FooService { A user that wishes to customize the service can then easily do so by defining their own factory: ```ts -export const fooServiceFactory = createServiceFactory({ +export const customFooServiceFactory = createServiceFactory({ service: fooServiceRef, deps: { logger: coreServices.logger }, factory({ logger }) { diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 7d6824c41e..e1d86164c3 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -192,7 +192,7 @@ When defining a default factory for a service, it is possible for it to end up w ## Service Factory Customization -When declaring a service factory you may also want to make the export the building blocks of the implementation itself. This is to allow for further customization of the service implementation through code, beyond what is possible with static configuration, without the need to re-implement the entire service from scratch. For example, we might export our example `DefaultFooService` class, while moving construction to a static factory method to make it easier to evolve: +When declaring a service factory you may also want to make the export the building blocks of the implementation itself. This is to allow for further customization of the service implementation through code, beyond what is possible with static configuration, without the need to re-implement the entire service from scratch. For example, we might export our example `DefaultFooService` class, while moving construction to a static `create` factory method to make it easier to evolve: ```ts export class DefaultFooService { @@ -222,4 +222,4 @@ export const customFooServiceFactory = createServiceFactory({ }); ``` -This allows lets you provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. +This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. From 87d3f665f820c3520716a3a0415e6d86552d28a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Jul 2024 10:45:18 +0200 Subject: [PATCH 084/212] backend-plugin-api: update deprecation message for ServiceFactoryCompat Signed-off-by: Patrik Oldsberg --- packages/backend-plugin-api/src/services/system/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 9545408ff2..d31350806d 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -65,7 +65,7 @@ export interface ServiceFactoryCompat< TOpts extends object | undefined = undefined, > extends ServiceFactory { /** - * @deprecated You do not need to use this call signature; use the type directly instead by removing the `()` parentheses at the end. This call signature will be removed in a future release. + * @deprecated Callable service factories will be removed in a future release, please re-implement the service factory using the available APIs instead. If no options are being passed, you can simply remove the trailing `()`. */ ( ...options: undefined extends TOpts ? [] : [options?: TOpts] From 617a7d29c242679b9659ab15944856b85a27a2ca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Jul 2024 11:18:18 +0200 Subject: [PATCH 085/212] backend-app-api: avoid using service factory options for plugin meta service Signed-off-by: Patrik Oldsberg --- .changeset/nice-cats-nail.md | 5 +++++ .../backend-app-api/src/wiring/ServiceRegistry.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/nice-cats-nail.md diff --git a/.changeset/nice-cats-nail.md b/.changeset/nice-cats-nail.md new file mode 100644 index 0000000000..0e9b1603b8 --- /dev/null +++ b/.changeset/nice-cats-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Internal refactor that avoids the use of service factory options. diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 6e20dd98e8..56d8d611bb 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -48,13 +48,13 @@ function toInternalServiceFactory( return f; } -const pluginMetadataServiceFactory = createServiceFactory( - (options?: { pluginId: string }) => ({ +function createPluginMetadataServiceFactory(pluginId: string) { + return createServiceFactory({ service: coreServices.pluginMetadata, deps: {}, - factory: async () => ({ getId: () => options?.pluginId! }), - }), -); + factory: async () => ({ getId: () => pluginId }), + }); +} export class ServiceRegistry { static create(factories: Array): ServiceRegistry { @@ -97,7 +97,7 @@ export class ServiceRegistry { // Special case handling of the plugin metadata service, generating a custom factory for it each time if (ref.id === coreServices.pluginMetadata.id) { return Promise.resolve( - toInternalServiceFactory(pluginMetadataServiceFactory({ pluginId })), + toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId)), ); } From a8bd40162c55b9311438e7fd95b55688c213459a Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Tue, 23 Apr 2024 10:01:22 +0100 Subject: [PATCH 086/212] yarn-plugin: validate version of yarn in yarn plugin Signed-off-by: MT Lewis --- packages/yarn-plugin/package.json | 1 + packages/yarn-plugin/src/index.ts | 13 ++++++++++++- yarn.lock | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index ad42bdba23..fd70dc8600 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -34,6 +34,7 @@ "@backstage/release-manifests": "workspace:^", "@yarnpkg/core": "^4.0.3", "@yarnpkg/fslib": "^3.0.2", + "chalk": "^4.0.0", "semver": "^7.6.0" }, "devDependencies": { diff --git a/packages/yarn-plugin/src/index.ts b/packages/yarn-plugin/src/index.ts index 6c1c805fd8..f548463829 100644 --- a/packages/yarn-plugin/src/index.ts +++ b/packages/yarn-plugin/src/index.ts @@ -21,10 +21,21 @@ * @packageDocumentation */ -import { Plugin } from '@yarnpkg/core'; +import { Plugin, semverUtils, YarnVersion } from '@yarnpkg/core'; +import chalk from 'chalk'; import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking'; import { BackstageResolver } from './resolver/BackstageResolver'; +if (!semverUtils.satisfiesWithPrereleases(YarnVersion, '^4.1.1')) { + console.error(); + console.error( + `${chalk.bold.red( + 'Unsupported yarn version.', + )}: The Backstage yarn plugin only works with yarn ^4.1.1. Please upgrade yarn, or remove this plugin with "yarn plugin remove @yarnpkg/plugin-backstage".`, + ); + console.error(); +} + /** * @public */ diff --git a/yarn.lock b/yarn.lock index f50727e805..afe89e4263 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44721,6 +44721,7 @@ __metadata: "@yarnpkg/builder": ^4.0.0 "@yarnpkg/core": ^4.0.3 "@yarnpkg/fslib": ^3.0.2 + chalk: ^4.0.0 nodemon: ^3.0.1 semver: ^7.6.0 languageName: unknown From f9e4e22e5f80dbaadca4cc9ab65e334bd9df836c Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Sun, 14 Apr 2024 21:08:08 +0100 Subject: [PATCH 087/212] sync_release-manifest: full yarn install Since we now need to build the yarn-plugin before pushing it to the versions repo for each release, we need a full yarn install in this repo. Signed-off-by: MT Lewis --- .github/workflows/sync_release-manifest.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index db35945550..34bbabcd0b 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -12,14 +12,6 @@ jobs: with: egress-policy: audit - # Setup node & install deps before checkout, keeping install quick - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: 18.x - - name: Install dependencies - # Just the deps for the assemble manifest script - run: npm install semver@7.3.5 fs-extra@10.0.0 @manypkg/get-packages@1.1.1 - - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: @@ -27,6 +19,18 @@ jobs: # 'v' prefix is added here for the tag, we keep it out of the manifest logic ref: v${{ github.event.client_payload.version }} + - name: use node.js 18.x + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org/ # Needed for auth + + - name: yarn install + uses: backstage/actions/yarn-install@a674369920067381b450d398b27df7039b7ef635 # v0.6.5 + with: + path: backstage + cache-prefix: ${{ runner.os }}-v18.x + # Checkout backstage/versions into /backstage/versions, which is where store the output - name: Checkout versions uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 From 0bdf7ab1ebfdc211a72a394788cfc892a8eb3388 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Sun, 14 Apr 2024 21:16:04 +0100 Subject: [PATCH 088/212] sync_release-manifest: add yarn-plugin to release directory Since yarn plugins aren't installable via npm, we need a place to make the version available for each release. Since versions.backstage.io already provides an API backed by a git repo organized by release (and even includes release line links), it seems like the perfect spot. This commit adds the yarn plugin to the appropriate directory when the sync_release-management workflow runs, reorganizing the existing functionality for pushing the changes to the versions repo with a little bit of refactoring. Signed-off-by: MT Lewis --- .github/workflows/sync_release-manifest.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 34bbabcd0b..e7c06747f7 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -31,6 +31,10 @@ jobs: path: backstage cache-prefix: ${{ runner.os }}-v18.x + - name: Build yarn plugin + working-directory: backstage/packages/yarn-plugin + run: yarn build + # Checkout backstage/versions into /backstage/versions, which is where store the output - name: Checkout versions uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 @@ -51,6 +55,20 @@ jobs: mkdir -p scripts wget -O scripts/assemble-manifest.js https://raw.githubusercontent.com/backstage/backstage/master/scripts/assemble-manifest.js node scripts/assemble-manifest.js ${{ github.event.client_payload.version }} + + # Copies the build output of the yarn-plugin package to the appropriate + # directory, allowing the plugin to be installed with a command like + # `yarn plugin import https://versions.backstage.io/v1/tags/main/yarn-plugin` + - name: Add yarn plugin to release + working-directory: backstage/packages/yarn-plugin + run: >- + cp + bundles/@yarnpkg/plugin-backstage.js + ../../versions/v1/releases/${{ github.event.client_payload.version }}/yarn-plugin + + - name: Commit to versions repo + working-directory: backstage/versions + run: | cd versions git add . git commit -am "${{ github.event.client_payload.version }}" From 513175ae970a34a29b900d4660a2ff45ebc9bedb Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Sun, 14 Apr 2024 21:31:38 +0100 Subject: [PATCH 089/212] sync_release-manifest: remove top-level backstage directory The top-level backstage/ directory included in this repo means we can't use the yarn-install backstage action (or we'd have to add a `path` input option to it). Looking at this workflow, I can't actually spot why the top-level `backstage` dir is needed - nothing is ever written outside it as far as I can spot. With that in mind, proposing we remove it to simplify the workflow and enable the use of the `yarn-install` action without needing to extend it. Signed-off-by: MT Lewis --- .github/workflows/sync_release-manifest.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index e7c06747f7..6d02309f8a 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -15,7 +15,6 @@ jobs: - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: - path: backstage # 'v' prefix is added here for the tag, we keep it out of the manifest logic ref: v${{ github.event.client_payload.version }} @@ -28,11 +27,10 @@ jobs: - name: yarn install uses: backstage/actions/yarn-install@a674369920067381b450d398b27df7039b7ef635 # v0.6.5 with: - path: backstage cache-prefix: ${{ runner.os }}-v18.x - name: Build yarn plugin - working-directory: backstage/packages/yarn-plugin + working-directory: packages/yarn-plugin run: yarn build # Checkout backstage/versions into /backstage/versions, which is where store the output @@ -40,7 +38,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: repository: backstage/versions - path: backstage/versions + path: versions token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - name: Configure Git @@ -51,7 +49,6 @@ jobs: - name: Create release # This grabs the scripts from master in order to support backfills run: | - cd backstage mkdir -p scripts wget -O scripts/assemble-manifest.js https://raw.githubusercontent.com/backstage/backstage/master/scripts/assemble-manifest.js node scripts/assemble-manifest.js ${{ github.event.client_payload.version }} @@ -60,16 +57,15 @@ jobs: # directory, allowing the plugin to be installed with a command like # `yarn plugin import https://versions.backstage.io/v1/tags/main/yarn-plugin` - name: Add yarn plugin to release - working-directory: backstage/packages/yarn-plugin + working-directory: packages/yarn-plugin run: >- cp bundles/@yarnpkg/plugin-backstage.js ../../versions/v1/releases/${{ github.event.client_payload.version }}/yarn-plugin - name: Commit to versions repo - working-directory: backstage/versions + working-directory: versions run: | - cd versions git add . git commit -am "${{ github.event.client_payload.version }}" git push @@ -87,7 +83,7 @@ jobs: workflow_id: 'release.yml', ref: 'master', inputs: { - version: require('./backstage/packages/create-app/package.json').version, - releaseVersion: require('./backstage/package.json').version + version: require('./packages/create-app/package.json').version, + releaseVersion: require('./package.json').version }, }); From 0600286c95a0f9409954facf1ec01826e8248f4f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Jul 2024 13:22:51 +0200 Subject: [PATCH 090/212] Update .changeset/early-deers-beg.md Signed-off-by: Patrik Oldsberg --- .changeset/early-deers-beg.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.changeset/early-deers-beg.md b/.changeset/early-deers-beg.md index 4bac074d8c..735c68aace 100644 --- a/.changeset/early-deers-beg.md +++ b/.changeset/early-deers-beg.md @@ -5,12 +5,3 @@ Fixed bug resulting from missing required owner and repo arguments in `getEnvironmentPublicKey` in action `github:environment:create`. Adding environment secrets now works as expected. - -```diff - const publicKeyResponse = await client.rest.actions.getEnvironmentPublicKey({ - repository_id: repository.data.id, -+ owner, -+ repo, - environment_name: name - }); -``` From 4a77a91da87f67ef91e1f35b2b7269b957e754fd Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 11 Jul 2024 06:58:33 -0500 Subject: [PATCH 091/212] Added comment to resolvers Signed-off-by: Andre Wanlin --- docs/auth/microsoft/azure-easyauth.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/auth/microsoft/azure-easyauth.md b/docs/auth/microsoft/azure-easyauth.md index e8d6f2f105..c2a6eab218 100644 --- a/docs/auth/microsoft/azure-easyauth.md +++ b/docs/auth/microsoft/azure-easyauth.md @@ -66,6 +66,7 @@ auth: azureEasyAuth: signIn: resolvers: + # typically you would pick one of these - resolver: emailMatchingUserEntityProfileEmail - resolver: emailLocalPartMatchingUserEntityName - resolver: idMatchingUserEntityAnnotation From 8fc26223c26e2fe642414f28c8486fd1ef44349d Mon Sep 17 00:00:00 2001 From: Bond Yan Date: Thu, 11 Jul 2024 11:20:17 -0400 Subject: [PATCH 092/212] setting useNavigate in a ref Signed-off-by: Bond Yan --- .changeset/chatty-adults-boil.md | 5 +++++ .../TechDocsReaderPageContent/useNavigateUrl.tsx | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/chatty-adults-boil.md diff --git a/.changeset/chatty-adults-boil.md b/.changeset/chatty-adults-boil.md new file mode 100644 index 0000000000..76f3bddf41 --- /dev/null +++ b/.changeset/chatty-adults-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed an issue that was causing techdocs pages unnecessarily re-render on navigate. diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.tsx index 02d181dccf..f036edef65 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; /** @@ -53,7 +53,8 @@ export function resolveUrlToRelative(url: string, baseUrl: string) { * @public */ export function useNavigateUrl() { - const navigate = useNavigate(); + // useRef prevents useNavigate from causing unnecessary re-renders + const navigate = useRef(useNavigate()); const configApi = useApi(configApiRef); const appBaseUrl = configApi.getOptionalString('app.baseUrl'); const navigateFn = useCallback( @@ -70,9 +71,9 @@ export function useNavigateUrl() { // URL passed in was relative. } } - navigate(url); + navigate.current(url); }, - [navigate, appBaseUrl], + [appBaseUrl], ); return navigateFn; } From a3449e0767d0ce17bee320341ec1309e6830dc88 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 07:40:53 +0000 Subject: [PATCH 093/212] chore(deps): update snyk/actions digest to 640e317 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/sync_snyk-github-issues.yml | 2 +- .github/workflows/sync_snyk-monitor.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index fb3456bfb6..1cdc0cb22a 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -29,7 +29,7 @@ jobs: cache-prefix: ${{ runner.os }}-v18.x - name: Create Snyk report - uses: snyk/actions/node@d406fd286b663eb8c6f8adcced4f7bcd199c0a3f # master + uses: snyk/actions/node@640e31719aac3e44867d239dc86c20c3e34c8e4f # master continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found with: args: > diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index a3c72c1d7b..b1e50397ce 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Monitor and Synchronize Snyk Policies - uses: snyk/actions/node@d406fd286b663eb8c6f8adcced4f7bcd199c0a3f # master + uses: snyk/actions/node@640e31719aac3e44867d239dc86c20c3e34c8e4f # master with: command: monitor args: > @@ -46,7 +46,7 @@ jobs: # Above we run the `monitor` command, this runs the `test` command which is # the one that generates the SARIF report that we can upload to GitHub. - name: Create Snyk report - uses: snyk/actions/node@d406fd286b663eb8c6f8adcced4f7bcd199c0a3f # master + uses: snyk/actions/node@640e31719aac3e44867d239dc86c20c3e34c8e4f # master continue-on-error: true # To make sure that SARIF upload gets called with: args: > From f691c9bf75dc4562f45e51c02ec5e132bfdd8433 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jul 2024 13:24:37 +0200 Subject: [PATCH 094/212] backend-app-api: simplify defaultServiceFactories option Signed-off-by: Patrik Oldsberg --- .changeset/dry-wombats-type.md | 5 +++ packages/backend-app-api/api-report.md | 4 +- .../src/wiring/createSpecializedBackend.ts | 8 +--- packages/backend-app-api/src/wiring/types.ts | 4 +- .../backend-defaults/src/CreateBackend.ts | 40 +++++++++---------- 5 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 .changeset/dry-wombats-type.md diff --git a/.changeset/dry-wombats-type.md b/.changeset/dry-wombats-type.md new file mode 100644 index 0000000000..31fed70293 --- /dev/null +++ b/.changeset/dry-wombats-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING**: Removed the ability to pass callback-form service factories through the `defaultServiceFactories` option of `createSpecializedBackend`. This is an immediate breaking change as usage of this function is expected to be very rare. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 51839b88be..2a447d52ef 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -38,8 +38,8 @@ import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import type { Server } from 'node:http'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; -import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { transport } from 'winston'; import { UrlReaderService } from '@backstage/backend-plugin-api'; @@ -99,7 +99,7 @@ export function createSpecializedBackend( // @public (undocumented) export interface CreateSpecializedBackendOptions { // (undocumented) - defaultServiceFactories: ServiceFactoryOrFunction[]; + defaultServiceFactories: ServiceFactory[]; } // @public @deprecated (undocumented) diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts index f5c4d8b152..fd449be541 100644 --- a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts +++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts @@ -24,13 +24,9 @@ import { Backend, CreateSpecializedBackendOptions } from './types'; export function createSpecializedBackend( options: CreateSpecializedBackendOptions, ): Backend { - const services = options.defaultServiceFactories.map(sf => - typeof sf === 'function' ? sf() : sf, - ); - const exists = new Set(); const duplicates = new Set(); - for (const { service } of services) { + for (const { service } of options.defaultServiceFactories) { if (exists.has(service.id)) { duplicates.add(service.id); } else { @@ -47,5 +43,5 @@ export function createSpecializedBackend( ); } - return new BackstageBackend(services); + return new BackstageBackend(options.defaultServiceFactories); } diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 027b2bf254..776dcdcb35 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -18,7 +18,7 @@ import { BackendFeature, ExtensionPoint, ServiceRef, - ServiceFactoryOrFunction, + ServiceFactory, } from '@backstage/backend-plugin-api'; /** @@ -39,7 +39,7 @@ export interface Backend { * @public */ export interface CreateSpecializedBackendOptions { - defaultServiceFactories: ServiceFactoryOrFunction[]; + defaultServiceFactories: ServiceFactory[]; } /** diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index e3195748a6..47fa09da49 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -40,26 +40,26 @@ import { userInfoServiceFactory } from '@backstage/backend-defaults/userInfo'; import { eventsServiceFactory } from '@backstage/plugin-events-node'; export const defaultServiceFactories = [ - authServiceFactory(), - cacheServiceFactory(), - rootConfigServiceFactory(), - databaseServiceFactory(), - discoveryServiceFactory(), - httpAuthServiceFactory(), - httpRouterServiceFactory(), - identityServiceFactory(), - lifecycleServiceFactory(), - loggerServiceFactory(), - permissionsServiceFactory(), - rootHealthServiceFactory(), - rootHttpRouterServiceFactory(), - rootLifecycleServiceFactory(), - rootLoggerServiceFactory(), - schedulerServiceFactory(), - tokenManagerServiceFactory(), - userInfoServiceFactory(), - urlReaderServiceFactory(), - eventsServiceFactory(), + authServiceFactory, + cacheServiceFactory, + rootConfigServiceFactory, + databaseServiceFactory, + discoveryServiceFactory, + httpAuthServiceFactory, + httpRouterServiceFactory, + identityServiceFactory, + lifecycleServiceFactory, + loggerServiceFactory, + permissionsServiceFactory, + rootHealthServiceFactory, + rootHttpRouterServiceFactory, + rootLifecycleServiceFactory, + rootLoggerServiceFactory, + schedulerServiceFactory, + tokenManagerServiceFactory, + userInfoServiceFactory, + urlReaderServiceFactory, + eventsServiceFactory, ]; /** From 18b96b15edeb2526bf36c60530e1739cb8d8f274 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jul 2024 13:30:14 +0200 Subject: [PATCH 095/212] backend-app-api: deprecate callback form of backend.add(...) Signed-off-by: Patrik Oldsberg --- .changeset/three-lizards-crash.md | 5 +++++ packages/backend-app-api/api-report.md | 9 ++++++++- packages/backend-app-api/src/wiring/types.ts | 11 +++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 .changeset/three-lizards-crash.md diff --git a/.changeset/three-lizards-crash.md b/.changeset/three-lizards-crash.md new file mode 100644 index 0000000000..5dffed0f3b --- /dev/null +++ b/.changeset/three-lizards-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +The ability to install backend features in callback form (`() => BackendFeature`) has been deprecated. This typically means that you need to update the installed features to use the latest version of `@backstage/backend-plugin-api`. If the feature is from a third-party package, please reach out to the package maintainer to update it. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 2a447d52ef..54e2900cc2 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -58,9 +58,16 @@ export interface Backend { add( feature: | BackendFeature + | Promise<{ + default: BackendFeature; + }>, + ): void; + // @deprecated (undocumented) + add( + feature: | (() => BackendFeature) | Promise<{ - default: BackendFeature | (() => BackendFeature); + default: () => BackendFeature; }>, ): void; // (undocumented) diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 776dcdcb35..075eb58b3f 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -25,11 +25,18 @@ import { * @public */ export interface Backend { + add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void; + /** + * @deprecated The ability to add features defined as a callback is being + * removed. Please update the installed feature to no longer be defined as a + * callback, typically this means updating it to use the latest version of + * `@backstage/backend-plugin-api`. If the feature is from a third-party + * package, please reach out to the package maintainer to update it. + */ add( feature: - | BackendFeature | (() => BackendFeature) - | Promise<{ default: BackendFeature | (() => BackendFeature) }>, + | Promise<{ default: () => BackendFeature }>, ): void; start(): Promise; stop(): Promise; From 906c817fd342f1efcd4bbd8dda5c4d9e59345c99 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jul 2024 10:43:19 +0200 Subject: [PATCH 096/212] backend-test-utils: update ServiceFactoryTester to not accept callback form Signed-off-by: Patrik Oldsberg --- .changeset/smooth-pianos-argue.md | 5 +++++ packages/backend-test-utils/api-report.md | 6 ++---- .../src/next/wiring/ServiceFactoryTester.ts | 15 +++++---------- 3 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 .changeset/smooth-pianos-argue.md diff --git a/.changeset/smooth-pianos-argue.md b/.changeset/smooth-pianos-argue.md new file mode 100644 index 0000000000..5ab8be8136 --- /dev/null +++ b/.changeset/smooth-pianos-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Updated `ServiceFactoryTester` to only accept plain service factory objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 566ac5b888..d122e038a6 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -371,9 +371,7 @@ export namespace mockServices { // @public export class ServiceFactoryTester { static from( - subject: - | ServiceFactory - | (() => ServiceFactory), + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, ): ServiceFactoryTester; get( @@ -387,7 +385,7 @@ export class ServiceFactoryTester { // @public export interface ServiceFactoryTesterOptions { - dependencies?: Array ServiceFactory)>; + dependencies?: Array; } // @public (undocumented) diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index 0555ab10a4..9247a9132e 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -35,7 +35,7 @@ export interface ServiceFactoryTesterOptions { * If a service factory is provided for a service that already has a default * implementation, the provided factory will override the default. */ - dependencies?: Array ServiceFactory)>; + dependencies?: Array; } /** @@ -55,20 +55,15 @@ export class ServiceFactoryTester { * @returns A new tester instance for the provided subject. */ static from( - subject: - | ServiceFactory - | (() => ServiceFactory), + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, ) { - const subjectFactory = typeof subject === 'function' ? subject() : subject; const registry = ServiceRegistry.create([ ...defaultServiceFactories, - ...(options?.dependencies?.map(f => - typeof f === 'function' ? f() : f, - ) ?? []), - subjectFactory, + ...(options?.dependencies ?? []), + subject, ]); - return new ServiceFactoryTester(subjectFactory.service, registry); + return new ServiceFactoryTester(subject.service, registry); } private constructor( From 972bcbb57af95ed825056b0608b8f1057892bf48 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jul 2024 10:49:44 +0200 Subject: [PATCH 097/212] backend-test-utils: remove callback form features from startTestBackend Signed-off-by: Patrik Oldsberg --- .changeset/smooth-pianos-argue.md | 2 +- packages/backend-test-utils/api-report.md | 3 +-- packages/backend-test-utils/src/next/wiring/TestBackend.ts | 6 +----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.changeset/smooth-pianos-argue.md b/.changeset/smooth-pianos-argue.md index 5ab8be8136..e7fc66dee4 100644 --- a/.changeset/smooth-pianos-argue.md +++ b/.changeset/smooth-pianos-argue.md @@ -2,4 +2,4 @@ '@backstage/backend-test-utils': patch --- -Updated `ServiceFactoryTester` to only accept plain service factory objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. +Updated `startTestBackend` and `ServiceFactoryTester` to only accept plain service factory or backend feature objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index d122e038a6..be6c22192d 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -430,9 +430,8 @@ export interface TestBackendOptions { // (undocumented) features?: Array< | BackendFeature - | (() => BackendFeature) | Promise<{ - default: BackendFeature | (() => BackendFeature); + default: BackendFeature; }> >; } diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index f23883ba3e..16ee6b78d2 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -48,11 +48,7 @@ export interface TestBackendOptions { ]; }, ]; - features?: Array< - | BackendFeature - | (() => BackendFeature) - | Promise<{ default: BackendFeature | (() => BackendFeature) }> - >; + features?: Array>; } /** @public */ From f4085b87f5bd2fc707a0970395a4b1f129ad7ca4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jul 2024 12:14:17 +0200 Subject: [PATCH 098/212] permission-common: removed the token option from the base evaluator options Signed-off-by: Patrik Oldsberg --- .changeset/eight-dancers-burn.md | 5 +++++ plugins/permission-common/api-report.md | 21 ++++++++++++------ .../permission-common/src/PermissionClient.ts | 14 +++++++++--- plugins/permission-common/src/types/api.ts | 22 +++++++++---------- 4 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 .changeset/eight-dancers-burn.md diff --git a/.changeset/eight-dancers-burn.md b/.changeset/eight-dancers-burn.md new file mode 100644 index 0000000000..228ff036ab --- /dev/null +++ b/.changeset/eight-dancers-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-common': minor +--- + +**BREAKING**: Removed the deprecated and unused `token` option from `EvaluatorRequestOptions`. The `PermissionsClient` now has its own `PermissionClientRequestOptions` type that declares the `token` option instead. diff --git a/plugins/permission-common/api-report.md b/plugins/permission-common/api-report.md index 1c2476d20d..e9935c612d 100644 --- a/plugins/permission-common/api-report.md +++ b/plugins/permission-common/api-report.md @@ -94,9 +94,7 @@ export type EvaluatePermissionResponseBatch = PermissionMessageBatch; // @public -export type EvaluatorRequestOptions = { - token?: string; -}; +export interface EvaluatorRequestOptions {} // @public export type IdentifiedPermissionMessage = T & { @@ -162,14 +160,19 @@ export class PermissionClient implements PermissionEvaluator { constructor(options: { discovery: DiscoveryApi; config: Config }); authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise; authorizeConditional( queries: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise; } +// @public +export type PermissionClientRequestOptions = { + token?: string; +}; + // @public export type PermissionCondition< TResourceType extends string = string, @@ -191,11 +194,15 @@ export type PermissionCriteria = export interface PermissionEvaluator { authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { + _ignored?: never; + }, ): Promise; authorizeConditional( requests: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { + _ignored?: never; + }, ): Promise; } diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts index a5d08205d8..2414774775 100644 --- a/plugins/permission-common/src/PermissionClient.ts +++ b/plugins/permission-common/src/PermissionClient.ts @@ -27,7 +27,6 @@ import { PermissionEvaluator, QueryPermissionRequest, AuthorizePermissionRequest, - EvaluatorRequestOptions, AuthorizePermissionResponse, QueryPermissionResponse, } from './types/api'; @@ -93,6 +92,15 @@ const responseSchema = ( ), }); +/** + * Options for {@link PermissionClient} requests. + * + * @public + */ +export type PermissionClientRequestOptions = { + token?: string; +}; + /** * An isomorphic client for requesting authorization for Backstage permissions. * @public @@ -112,7 +120,7 @@ export class PermissionClient implements PermissionEvaluator { */ async authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise { return this.makeRequest( requests, @@ -126,7 +134,7 @@ export class PermissionClient implements PermissionEvaluator { */ async authorizeConditional( queries: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise { return this.makeRequest(queries, queryPermissionResponseSchema, options); } diff --git a/plugins/permission-common/src/types/api.ts b/plugins/permission-common/src/types/api.ts index e597384a56..072615989b 100644 --- a/plugins/permission-common/src/types/api.ts +++ b/plugins/permission-common/src/types/api.ts @@ -244,7 +244,7 @@ export interface PermissionEvaluator { */ authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { _ignored?: never }, // Since the options are empty we add this placeholder to reject all options ): Promise; /** @@ -257,22 +257,20 @@ export interface PermissionEvaluator { */ authorizeConditional( requests: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { _ignored?: never }, // Since the options are empty we add this placeholder to reject all options ): Promise; } +// Note(Rugvip): I kept the below type around in case we want to add new options +// in the future, for example a signal. It also helps out enabling API +// constraints, as without this we can't have the permissions service implement +// the evaluator interface due to the mismatch in parameter count. + /** * Options for {@link PermissionEvaluator} requests. * + * This is currently empty, as there are no longer any common options for the permission evaluator. + * * @public */ -export type EvaluatorRequestOptions = { - /** - * @deprecated Backend plugins should no longer depend on the - * `PermissionEvaluator`, but instead use the `PermissionService` from - * `@backstage/backend-plugin-api`. Frontend plugins should not need to inject - * this token at all, but instead implicitly rely on underlying fetchApi to do - * it for them. - */ - token?: string; -}; +export interface EvaluatorRequestOptions {} From 36f91e8956a6e6308c066c26f49f34e3542e7b8c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jul 2024 12:17:39 +0200 Subject: [PATCH 099/212] backend-plugin-api,permission-node: remove deprecated token option from permissions service Signed-off-by: Patrik Oldsberg --- .changeset/gold-worms-complain.md | 5 + .changeset/swift-seals-drive.md | 5 + packages/backend-plugin-api/api-report.md | 17 +- .../definitions/PermissionsService.ts | 19 +- .../src/ServerPermissionClient.test.ts | 232 ++++-------------- .../src/ServerPermissionClient.ts | 8 - 6 files changed, 77 insertions(+), 209 deletions(-) create mode 100644 .changeset/gold-worms-complain.md create mode 100644 .changeset/swift-seals-drive.md diff --git a/.changeset/gold-worms-complain.md b/.changeset/gold-worms-complain.md new file mode 100644 index 0000000000..229293cc2f --- /dev/null +++ b/.changeset/gold-worms-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': minor +--- + +**BREAKING**: Updated the `ServerPermissionClient` to match the new `PermissionsService` interface, where the deprecated `token` option has been removed and the options are now required. diff --git a/.changeset/swift-seals-drive.md b/.changeset/swift-seals-drive.md new file mode 100644 index 0000000000..a03f490d3f --- /dev/null +++ b/.changeset/swift-seals-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: The `PermissionsService` no longer supports passing the deprecated `token` option, and the request options are now required. diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 7b1e6ef2d3..1937ba6ca9 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -9,6 +9,7 @@ import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common' import { AuthorizePermissionResponse } from '@backstage/plugin-permission-common'; import { Config } from '@backstage/config'; import { Duration } from 'luxon'; +import { EvaluatorRequestOptions } from '@backstage/plugin-permission-common'; import { Handler } from 'express'; import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -423,22 +424,20 @@ export interface LoggerService { export interface PermissionsService extends PermissionEvaluator { authorize( requests: AuthorizePermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; authorizeConditional( requests: QueryPermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; } // @public -export type PermissionsServiceRequestOptions = - | { - token?: string; - } - | { - credentials: BackstageCredentials; - }; +export interface PermissionsServiceRequestOptions + extends EvaluatorRequestOptions { + // (undocumented) + credentials: BackstageCredentials; +} // @public export interface PluginMetadataService { diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts index c5046a988b..a25081d17a 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts @@ -17,6 +17,7 @@ import { AuthorizePermissionRequest, AuthorizePermissionResponse, + EvaluatorRequestOptions, PermissionEvaluator, QueryPermissionRequest, QueryPermissionResponse, @@ -24,18 +25,14 @@ import { import { BackstageCredentials } from './AuthService'; /** - * Options for {@link @backstage/plugin-permission-common#PermissionEvaluator} requests. + * Options for {@link PermissionsService} requests. * * @public */ -export type PermissionsServiceRequestOptions = - | { - /** @deprecated use the `credentials` option instead. */ - token?: string; - } - | { - credentials: BackstageCredentials; - }; +export interface PermissionsServiceRequestOptions + extends EvaluatorRequestOptions { + credentials: BackstageCredentials; +} /** * Permission system integration for authorization of user/service actions. @@ -59,7 +56,7 @@ export interface PermissionsService extends PermissionEvaluator { */ authorize( requests: AuthorizePermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; /** @@ -79,6 +76,6 @@ export interface PermissionsService extends PermissionEvaluator { */ authorizeConditional( requests: QueryPermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; } diff --git a/plugins/permission-node/src/ServerPermissionClient.test.ts b/plugins/permission-node/src/ServerPermissionClient.test.ts index 6e99ff71ab..028517c5e0 100644 --- a/plugins/permission-node/src/ServerPermissionClient.test.ts +++ b/plugins/permission-node/src/ServerPermissionClient.test.ts @@ -83,18 +83,22 @@ describe('ServerPermissionClient', () => { const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { discovery, tokenManager: mockServices.tokenManager.mock(), + auth: mockServices.auth(), }); - await client.authorize([ - { - permission: testBasicPermission, - }, - ]); + await client.authorize( + [ + { + permission: testBasicPermission, + }, + ], + { credentials: mockCredentials.none() }, + ); expect(mockAuthorizeHandler).not.toHaveBeenCalled(); }); - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { + it('should bypass the permission backend if permissions are enabled and request has valid server credentials', async () => { const client = ServerPermissionClient.fromConfig(config, { discovery, tokenManager: mockServices.tokenManager.mock(), @@ -102,13 +106,13 @@ describe('ServerPermissionClient', () => { }); await client.authorize([{ permission: testBasicPermission }], { - token: mockCredentials.service.token(), + credentials: mockCredentials.service(), }); expect(mockAuthorizeHandler).not.toHaveBeenCalled(); }); - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { + it('should call the permission backend if permissions are enabled and request does not have valid server credentials', async () => { const client = ServerPermissionClient.fromConfig(config, { discovery, tokenManager: mockServices.tokenManager.mock(), @@ -116,10 +120,18 @@ describe('ServerPermissionClient', () => { }); await client.authorize([{ permission: testBasicPermission }], { - token: mockCredentials.user.token(), + credentials: mockCredentials.user(), }); expect(mockAuthorizeHandler).toHaveBeenCalled(); + expect( + mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), + ).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'permission', + }), + ); }); }); @@ -145,201 +157,59 @@ describe('ServerPermissionClient', () => { const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { discovery, tokenManager: mockServices.tokenManager.mock(), - }); - - await client.authorizeConditional([ - { permission: testResourcePermission }, - ]); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), + auth: mockServices.auth(), }); await client.authorizeConditional( [{ permission: testResourcePermission }], { - token: mockCredentials.service.token(), + credentials: mockCredentials.none(), }, ); expect(mockAuthorizeHandler).not.toHaveBeenCalled(); }); - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { - const auth = mockServices.auth(); + it('should bypass the permission backend if permissions are enabled and request has valid server credentials', async () => { const client = ServerPermissionClient.fromConfig(config, { discovery, tokenManager: mockServices.tokenManager.mock(), - auth, + auth: mockServices.auth(), }); await client.authorizeConditional( [{ permission: testResourcePermission }], { - token: mockCredentials.user.token(), + credentials: mockCredentials.service(), + }, + ); + + expect(mockAuthorizeHandler).not.toHaveBeenCalled(); + }); + + it('should call the permission backend if permissions are enabled and request does not have valid server credentials', async () => { + const client = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager: mockServices.tokenManager.mock(), + auth: mockServices.auth(), + }); + + await client.authorizeConditional( + [{ permission: testResourcePermission }], + { + credentials: mockCredentials.user(), }, ); expect(mockAuthorizeHandler).toHaveBeenCalled(); - }); - }); - - describe('with credentials', () => { - describe('authorize', () => { - let mockAuthorizeHandler: jest.Mock; - - beforeEach(() => { - mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { - const responses = req.body.items.map( - (r: IdentifiedPermissionMessage) => ({ - id: r.id, - result: AuthorizeResult.ALLOW, - }), - ); - - return res(json({ items: responses })); - }); - - server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); - }); - - it('should bypass the permission backend if permissions are disabled', async () => { - const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorize( - [ - { - permission: testBasicPermission, - }, - ], - { credentials: mockCredentials.none() }, - ); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorize([{ permission: testBasicPermission }], { - credentials: mockCredentials.service(), - }); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorize([{ permission: testBasicPermission }], { - credentials: mockCredentials.user(), - }); - - expect(mockAuthorizeHandler).toHaveBeenCalled(); - expect( - mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), - ).toBe( - mockCredentials.service.header({ - onBehalfOf: mockCredentials.user(), - targetPluginId: 'permission', - }), - ); - }); - }); - - describe('authorizeConditional', () => { - let mockAuthorizeHandler: jest.Mock; - - beforeEach(() => { - mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { - const responses = req.body.items.map( - (r: IdentifiedPermissionMessage) => ({ - id: r.id, - result: AuthorizeResult.ALLOW, - }), - ); - - return res(json({ items: responses })); - }); - - server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); - }); - - it('should bypass the permission backend if permissions are disabled', async () => { - const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorizeConditional( - [{ permission: testResourcePermission }], - { - credentials: mockCredentials.none(), - }, - ); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorizeConditional( - [{ permission: testResourcePermission }], - { - credentials: mockCredentials.service(), - }, - ); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorizeConditional( - [{ permission: testResourcePermission }], - { - credentials: mockCredentials.user(), - }, - ); - - expect(mockAuthorizeHandler).toHaveBeenCalled(); - expect( - mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), - ).toBe( - mockCredentials.service.header({ - onBehalfOf: mockCredentials.user(), - targetPluginId: 'permission', - }), - ); - }); + expect( + mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), + ).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'permission', + }), + ); }); }); diff --git a/plugins/permission-node/src/ServerPermissionClient.ts b/plugins/permission-node/src/ServerPermissionClient.ts index cf54f3b894..e21067bfa1 100644 --- a/plugins/permission-node/src/ServerPermissionClient.ts +++ b/plugins/permission-node/src/ServerPermissionClient.ts @@ -146,14 +146,6 @@ export class ServerPermissionClient implements PermissionsService { return options.credentials; } - if (options?.token) { - try { - return await this.#auth.authenticate(options.token); - } catch { - // ignore - } - } - return undefined; } From 343f656eb15315164a2722e8b32553494b4b5d69 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jul 2024 12:21:41 +0200 Subject: [PATCH 100/212] search-backend: update to treat requests without credentials as unauthenticated Signed-off-by: Patrik Oldsberg --- .changeset/tiny-pianos-work.md | 5 ++ .../service/AuthorizedSearchEngine.test.ts | 49 ++++++++++++------- .../src/service/AuthorizedSearchEngine.ts | 16 ++++-- plugins/search-backend/src/service/router.ts | 1 + 4 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 .changeset/tiny-pianos-work.md diff --git a/.changeset/tiny-pianos-work.md b/.changeset/tiny-pianos-work.md new file mode 100644 index 0000000000..033e8272e5 --- /dev/null +++ b/.changeset/tiny-pianos-work.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +The `AuthorizedSearchEngine` will now ignore the deprecated `token` option, and treat it as an unauthorized request. This will not have any effect in practice, since credentials are always provided by the router. diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts index 420af5744b..e41f3f888b 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts @@ -32,6 +32,7 @@ import { decodePageCursor, AuthorizedSearchEngine, } from './AuthorizedSearchEngine'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; describe('AuthorizedSearchEngine', () => { const typeUsers = 'users'; @@ -117,10 +118,11 @@ describe('AuthorizedSearchEngine', () => { searchEngine, defaultTypes, permissionEvaluator, + mockServices.auth(), new ConfigReader({}), ); - const options = { token: 'token' }; + const options = { credentials: mockCredentials.user() }; const allowAll: PermissionEvaluator['authorize'] & PermissionEvaluator['authorizeConditional'] = async queries => { @@ -149,7 +151,7 @@ describe('AuthorizedSearchEngine', () => { types: ['one', 'two'], filters, }, - { token: 'token' }, + options, ); }); @@ -160,7 +162,18 @@ describe('AuthorizedSearchEngine', () => { await authorizedSearchEngine.query({ term: '' }, options); expect(mockedQuery).toHaveBeenCalledWith( { term: '', types: ['users', 'templates', 'services', 'groups'] }, - { token: 'token' }, + options, + ); + }); + + it('should fall back to none credentials if a token is provided', async () => { + mockedQuery.mockImplementation(async () => ({ results })); + mockedPermissionQuery.mockImplementation(allowAll); + + await authorizedSearchEngine.query({ term: '' }, { token: 'token' }); + expect(mockedQuery).toHaveBeenCalledWith( + { term: '', types: ['users', 'templates', 'services', 'groups'] }, + { credentials: mockCredentials.none() }, ); }); @@ -184,7 +197,7 @@ describe('AuthorizedSearchEngine', () => { ); expect(mockedQuery).toHaveBeenCalledWith( { term: '', types: ['users', 'templates'] }, - { token: 'token' }, + options, ); expect(mockedPermissionQuery).toHaveBeenCalledTimes(1); expect(mockedPermissionQuery).toHaveBeenLastCalledWith( @@ -192,7 +205,7 @@ describe('AuthorizedSearchEngine', () => { { permission: defaultTypes[typeUsers].visibilityPermission }, { permission: defaultTypes[typeTemplates].visibilityPermission }, ], - { token: 'token' }, + options, ); }); @@ -218,7 +231,7 @@ describe('AuthorizedSearchEngine', () => { expect(mockedQuery).toHaveBeenCalledWith( { term: '', types: ['templates', 'services', 'groups'] }, - { token: 'token' }, + options, ); expect(mockedPermissionQuery).toHaveBeenCalledTimes(1); @@ -270,7 +283,7 @@ describe('AuthorizedSearchEngine', () => { expect(mockedQuery).toHaveBeenCalledWith( { term: '', types: ['users'] }, - { token: 'token' }, + options, ); }); @@ -326,7 +339,7 @@ describe('AuthorizedSearchEngine', () => { }), }, ], - { token: 'token' }, + options, ); expect(mockedAuthorize).toHaveBeenCalledTimes(1); expect(mockedAuthorize).toHaveBeenCalledWith( @@ -338,7 +351,7 @@ describe('AuthorizedSearchEngine', () => { resourceRef: 'template_doc_0', }, ], - { token: 'token' }, + options, ); }); @@ -403,7 +416,7 @@ describe('AuthorizedSearchEngine', () => { expect(mockedQuery).toHaveBeenNthCalledWith( 1, { term: '', types: ['users', 'templates', 'services'] }, - { token: 'token' }, + options, ); expect(mockedQuery).toHaveBeenNthCalledWith( 2, @@ -412,7 +425,7 @@ describe('AuthorizedSearchEngine', () => { types: ['users', 'templates', 'services'], pageCursor: 'MQ==', }, - { token: 'token' }, + options, ); expect(mockedQuery).toHaveBeenNthCalledWith( 3, @@ -421,7 +434,7 @@ describe('AuthorizedSearchEngine', () => { types: ['users', 'templates', 'services'], pageCursor: 'Mg==', }, - { token: 'token' }, + options, ); const expectedResult = allDocuments @@ -494,7 +507,7 @@ describe('AuthorizedSearchEngine', () => { expect(mockedQuery).toHaveBeenNthCalledWith( 1, { term: '', types: ['users', 'templates', 'services', 'groups'] }, - { token: 'token' }, + options, ); expect(mockedQuery).toHaveBeenNthCalledWith( 2, @@ -503,7 +516,7 @@ describe('AuthorizedSearchEngine', () => { types: ['users', 'templates', 'services', 'groups'], pageCursor: 'MQ==', }, - { token: 'token' }, + options, ); expect(mockedQuery).toHaveBeenNthCalledWith( 3, @@ -512,7 +525,7 @@ describe('AuthorizedSearchEngine', () => { types: ['users', 'templates', 'services', 'groups'], pageCursor: 'Mg==', }, - { token: 'token' }, + options, ); const expectedResult = allDocuments @@ -574,7 +587,7 @@ describe('AuthorizedSearchEngine', () => { expect(mockedQuery).toHaveBeenNthCalledWith( 1, { term: '', types: ['users', 'templates', 'services'] }, - { token: 'token' }, + options, ); expect(mockedQuery).toHaveBeenNthCalledWith( 2, @@ -583,7 +596,7 @@ describe('AuthorizedSearchEngine', () => { types: ['users', 'templates', 'services'], pageCursor: 'MQ==', }, - { token: 'token' }, + options, ); expect(mockedQuery).toHaveBeenNthCalledWith( 3, @@ -592,7 +605,7 @@ describe('AuthorizedSearchEngine', () => { types: ['users', 'templates', 'services'], pageCursor: 'Mg==', }, - { token: 'token' }, + options, ); const expectedResults = servicesWithAuth diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index defb535f42..d9d3feeacf 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -39,7 +39,7 @@ import { import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { Writable } from 'stream'; -import { PermissionsService } from '@backstage/backend-plugin-api'; +import { AuthService, PermissionsService } from '@backstage/backend-plugin-api'; export function decodePageCursor(pageCursor?: string): { page: number } { if (!pageCursor) { @@ -71,6 +71,7 @@ export class AuthorizedSearchEngine implements SearchEngine { private readonly searchEngine: SearchEngine, private readonly types: Record, private readonly permissions: PermissionsService, + private readonly auth: AuthService, config: Config, ) { this.queryLatencyBudgetMs = @@ -92,9 +93,14 @@ export class AuthorizedSearchEngine implements SearchEngine { ): Promise { const queryStartTime = Date.now(); + const compatOptions = + 'credentials' in options + ? options + : { credentials: await this.auth.getNoneCredentials() }; + const conditionFetcher = new DataLoader( (requests: readonly QueryPermissionRequest[]) => - this.permissions.authorizeConditional(requests.slice(), options), + this.permissions.authorizeConditional(requests.slice(), compatOptions), { cacheKeyFn: ({ permission: { name } }) => name, }, @@ -102,7 +108,7 @@ export class AuthorizedSearchEngine implements SearchEngine { const authorizer = new DataLoader( (requests: readonly AuthorizePermissionRequest[]) => - this.permissions.authorize(requests.slice(), options), + this.permissions.authorize(requests.slice(), compatOptions), { // Serialize the permission name and resourceRef as // a query string to avoid collisions from overlapping @@ -159,7 +165,7 @@ export class AuthorizedSearchEngine implements SearchEngine { if (!resultByResultFilteringRequired) { return this.searchEngine.query( { ...query, types: authorizedTypes }, - options, + compatOptions, ); } @@ -174,7 +180,7 @@ export class AuthorizedSearchEngine implements SearchEngine { do { const nextPage = await this.searchEngine.query( { ...query, types: authorizedTypes, pageCursor: nextPageCursor }, - options, + compatOptions, ); filteredResults = filteredResults.concat( diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 250be2853c..d226805baf 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -145,6 +145,7 @@ export async function createRouter( inputEngine, types, permissionEvaluator, + auth, config, ) : inputEngine; From ed10fd202c81f4114480fd3f831ad01507f5b021 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jul 2024 13:39:40 +0200 Subject: [PATCH 101/212] permission-node: update policy handler to work with new auth system Signed-off-by: Patrik Oldsberg --- .changeset/brown-rats-judge.md | 5 ++ .changeset/eleven-foxes-sip.md | 11 +++++ .../building-backends/08-migrating.md | 4 +- docs/features/kubernetes/proxy.md | 4 +- ...authorizing-scaffolder-template-details.md | 24 ++++----- docs/permissions/custom-rules.md | 9 ++-- .../02-adding-a-basic-permission-check.md | 11 ++--- .../03-adding-a-resource-permission-check.md | 8 +-- docs/permissions/writing-a-policy.md | 23 ++++----- .../src/policy.ts | 4 +- .../src/service/router.test.ts | 7 +++ .../permission-backend/src/service/router.ts | 14 +++--- plugins/permission-node/api-report.md | 18 +++++-- plugins/permission-node/src/policy/index.ts | 2 +- plugins/permission-node/src/policy/types.ts | 49 +++++++++++++++++-- 15 files changed, 127 insertions(+), 66 deletions(-) create mode 100644 .changeset/brown-rats-judge.md create mode 100644 .changeset/eleven-foxes-sip.md diff --git a/.changeset/brown-rats-judge.md b/.changeset/brown-rats-judge.md new file mode 100644 index 0000000000..0076c65d77 --- /dev/null +++ b/.changeset/brown-rats-judge.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-backend-module-allow-all-policy': patch +--- + +Internal refactor to use new `PolicyQueryUser` type. diff --git a/.changeset/eleven-foxes-sip.md b/.changeset/eleven-foxes-sip.md new file mode 100644 index 0000000000..8b21ddb20f --- /dev/null +++ b/.changeset/eleven-foxes-sip.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-node': patch +--- + +The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + +- `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. +- `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + +Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index d38acb695e..c6db44078e 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -1254,7 +1254,6 @@ In order to add your own permission policy you'll need to do the following: ```ts import { createBackendModule } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { PolicyDecision, AuthorizeResult, @@ -1262,13 +1261,14 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; class CustomPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { // TODO: Add code here that inspects the incoming request and user, and returns AuthorizeResult.ALLOW, AuthorizeResult.DENY, or AuthorizeResult.CONDITIONAL as needed. See the docs at https://backstage.io/docs/permissions/writing-a-policy for more information diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md index 2da797d8ef..85ad86f83b 100644 --- a/docs/features/kubernetes/proxy.md +++ b/docs/features/kubernetes/proxy.md @@ -60,7 +60,6 @@ This feature assumes your backstage instance has enabled the [permissions framew A sample policy like: ```typescript -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, @@ -68,12 +67,13 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; class KubernetesDenyAllProxyEndpointPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (request.permission.name === 'kubernetes.proxy') { return { diff --git a/docs/features/software-templates/authorizing-scaffolder-template-details.md b/docs/features/software-templates/authorizing-scaffolder-template-details.md index 127677757c..942431c993 100644 --- a/docs/features/software-templates/authorizing-scaffolder-template-details.md +++ b/docs/features/software-templates/authorizing-scaffolder-template-details.md @@ -66,14 +66,14 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if ( isPermission(request.permission, templateParameterReadPermission) || isPermission(request.permission, templateStepReadPermission) ) { - if (user?.identity.userEntityRef === 'user:default/spiderman') + if (user?.info.userEntityRef === 'user:default/spiderman') return createScaffolderTemplateConditionalDecision(request.permission, { not: scaffolderTemplateConditions.hasTag({ tag: 'secret' }), }); @@ -109,11 +109,11 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, actionExecutePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return createScaffolderActionConditionalDecision(request.permission, { not: scaffolderActionConditions.hasActionId({ actionId: 'debug:log', @@ -147,11 +147,11 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, actionExecutePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return createScaffolderActionConditionalDecision(request.permission, { not: { allOf: [ @@ -190,25 +190,25 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, taskCreatePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; } } if (isPermission(request.permission, taskCancelPermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; } } if (isPermission(request.permission, taskReadPermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; @@ -239,7 +239,6 @@ Instead of the changes in `permission.ts` noted in the above example you will ma ```ts title="packages/backend/src/index.ts" import { createBackendModule } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { PolicyDecision, AuthorizeResult, @@ -247,13 +246,14 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { // Various scaffolder permission checks ... diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 9063ed7257..57565755fb 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -66,9 +66,8 @@ import { catalogConditions, createCatalogConditionalDecision, createCatalogPermi /* highlight-remove-next-line */ import { createConditionFactory } from '@backstage/plugin-permission-node'; /* highlight-add-next-line */ -import { PermissionPolicy, PolicyQuery, createConditionFactory } from '@backstage/plugin-permission-node'; +import { PermissionPolicy, PolicyQuery, PolicyQueryUser, createConditionFactory } from '@backstage/plugin-permission-node'; /* highlight-add-start */ -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common'; /* highlight-add-end */ ... @@ -102,21 +101,21 @@ const isInSystem = createConditionFactory(isInSystemRule); class TestPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (isResourcePermission(request.permission, 'catalog-entity')) { return createCatalogConditionalDecision( request.permission, /* highlight-remove-start */ catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), /* highlight-remove-end */ /* highlight-add-start */ { anyOf: [ catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), isInSystem({ systemRef: 'interviewing' }), ], diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index aacd60af8c..724edb7ccb 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -169,15 +169,12 @@ Before running this step, please make sure you followed the steps described in [ In order to test the logic above, the integrators of your backstage instance need to change their permission policy to return `DENY` for our newly-created permission: ```ts title="packages/backend/src/plugins/permission.ts" -/* highlight-add-start */ -import { - BackstageIdentityResponse, -} from '@backstage/plugin-auth-node'; -/* highlight-add-end */ import { PermissionPolicy, - /* highlight-add-next-line */ + /* highlight-add-start */ PolicyQuery, + PolicyQueryUser, + /* highlight-add-end */ } from '@backstage/plugin-permission-node'; /* highlight-add-start */ import { isPermission } from '@backstage/plugin-permission-common'; @@ -190,7 +187,7 @@ class TestPermissionPolicy implements PermissionPolicy { /* highlight-add-start */ async handle( request: PolicyQuery, - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index b3aeb13a68..7ad551e926 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -237,12 +237,12 @@ Let's go back to the permission policy's handle function and try to authorize ou ```ts title="packages/backend/src/plugins/permission.ts" import { - BackstageIdentityResponse, IdentityClient } from '@backstage/plugin-auth-node'; import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { isPermission } from '@backstage/plugin-permission-common'; /* highlight-remove-next-line */ @@ -262,9 +262,9 @@ import { async handle( request: PolicyQuery, /* highlight-remove-next-line */ - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, /* highlight-add-next-line */ - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { @@ -276,7 +276,7 @@ async handle( return createTodoListConditionalDecision( request.permission, todoListConditions.isOwner({ - userId: user?.identity.userEntityRef ?? '', + userId: user?.info.userEntityRef ?? '', }), ); } diff --git a/docs/permissions/writing-a-policy.md b/docs/permissions/writing-a-policy.md index 3d4371f038..0f2d4fe091 100644 --- a/docs/permissions/writing-a-policy.md +++ b/docs/permissions/writing-a-policy.md @@ -10,7 +10,10 @@ That policy looked like this: ```typescript title="packages/backend/src/plugins/permission.ts" class TestPermissionPolicy implements PermissionPolicy { - async handle(request: PolicyQuery): Promise { + async handle( + request: PolicyQuery, + _user?: PolicyQueryUser, + ): Promise { if (request.permission.name === 'catalog.entity.delete') { return { result: AuthorizeResult.DENY, @@ -35,14 +38,6 @@ As we confirmed in the previous section, we know that this now prevents us from Let's change the policy to the following: ```ts -/* highlight-remove-next-line */ -import { IdentityClient } from '@backstage/plugin-auth-node'; -/* highlight-add-start */ -import { - BackstageIdentityResponse, - IdentityClient -} from '@backstage/plugin-auth-node'; - /* highlight-add-end */ import { AuthorizeResult, PolicyDecision, @@ -65,7 +60,7 @@ class TestPermissionPolicy implements PermissionPolicy { /* highlight-add-start */ async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-end */ /* highlight-remove-next-line */ @@ -81,7 +76,7 @@ class TestPermissionPolicy implements PermissionPolicy { return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), ); /* highlight-add-end */ @@ -95,7 +90,7 @@ Let's walk through the new code that we just added. Instead of returning an Definitive Policy Decision, we use factory methods to construct a [Conditional Policy Decision](https://backstage.io/docs/reference/plugin-permission-common.conditionalpolicydecision) (See the [Concepts page](./concepts.md) for more details). Since the policy doesn't have enough information to determine if `user` is the entity owner, this criteria is encapsulated within the conditional decision. However, `createCatalogConditionalDecision` will not compile unless `request.permission` is a catalog entity [`ResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.resourcepermission). This type constraint ensures that policies return conditional decisions that are compatible with the requested permission. To address this, we use [`isPermission`](https://backstage.io/docs/reference/plugin-permission-common.ispermission) to ["narrow"](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) the type of `request.permission` to `ResourcePermission<'catalog-entity'>`. This matches the runtime behavior that was in place before, but you'll notice that the type of `request.permission` has changed within the scope of that `if` statement. -The `catalogConditions` object contains all of the rules defined by the catalog plugin. These rules can be combined to form a [`PermissionCriteria`](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) object, but for this case we only need to use the `isEntityOwner` rule. This rule accepts a list of entity refs that represent User identity and Group membership used to determine ownership. The second argument to `PermissionPolicy#handle` provides us with a `BackstageIdentityResponse` object, from which we can grab the user's `ownershipEntityRefs`. We provide an empty array as a fallback since the user may be anonymous. +The `catalogConditions` object contains all of the rules defined by the catalog plugin. These rules can be combined to form a [`PermissionCriteria`](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) object, but for this case we only need to use the `isEntityOwner` rule. This rule accepts a list of entity refs that represent User identity and Group membership used to determine ownership. The second argument to `PermissionPolicy#handle` provides us with a `PolicyQueryUser` object, from which we can grab the user's `ownershipEntityRefs`. We provide an empty array as a fallback since the user may be anonymous. You should now be able to see in your Backstage app that the unregister entity button is enabled for entities that you own, but disabled for all other entities! @@ -125,7 +120,7 @@ import { class TestPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-remove-next-line */ if (isPermission(request.permission, catalogEntityDeletePermission)) { @@ -134,7 +129,7 @@ class TestPermissionPolicy implements PermissionPolicy { return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), ); } diff --git a/plugins/permission-backend-module-policy-allow-all/src/policy.ts b/plugins/permission-backend-module-policy-allow-all/src/policy.ts index da853e47db..90ae21f341 100644 --- a/plugins/permission-backend-module-policy-allow-all/src/policy.ts +++ b/plugins/permission-backend-module-policy-allow-all/src/policy.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, @@ -22,12 +21,13 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; export class AllowAllPermissionPolicy implements PermissionPolicy { async handle( _request: PolicyQuery, - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, ): Promise { return { result: AuthorizeResult.ALLOW, diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts index be90c5f628..f12bacc61d 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -186,6 +186,13 @@ describe('createRouter', () => { mockCredentials.user().principal.userEntityRef, ], }, + info: { + userEntityRef: mockCredentials.user().principal.userEntityRef, + ownershipEntityRefs: [ + mockCredentials.user().principal.userEntityRef, + ], + }, + credentials: mockCredentials.user(), }, ); expect(response.body).toEqual({ diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index cc45d0b41a..fd33f0b629 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -22,10 +22,7 @@ import { errorHandler, } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; -import { - BackstageIdentityResponse, - IdentityApi, -} from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizeResult, EvaluatePermissionRequest, @@ -40,6 +37,7 @@ import { ApplyConditionsRequestEntry, ApplyConditionsResponseEntry, PermissionPolicy, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { memoize } from 'lodash'; @@ -130,9 +128,9 @@ const handleRequest = async ( ); }); - let user: BackstageIdentityResponse | undefined; + let user: PolicyQueryUser | undefined; if (auth.isPrincipal(credentials, 'user')) { - const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials); + const info = await userInfo.getUserInfo(credentials); const { token } = await auth.getPluginRequestToken({ onBehalfOf: credentials, targetPluginId: 'catalog', // TODO: unknown at this point @@ -141,9 +139,11 @@ const handleRequest = async ( identity: { type: 'user', userEntityRef: credentials.principal.userEntityRef, - ownershipEntityRefs, + ownershipEntityRefs: info.ownershipEntityRefs, }, token, + credentials, + info, }; } diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 290c586f2c..81460ef630 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -8,7 +8,9 @@ import { AnyOfCriteria } from '@backstage/plugin-permission-common'; import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common'; import { AuthorizePermissionResponse } from '@backstage/plugin-permission-common'; import { AuthService } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { BackstageUserInfo } from '@backstage/backend-plugin-api'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; import { Config } from '@backstage/config'; import { DefinitivePolicyDecision } from '@backstage/plugin-permission-common'; @@ -246,10 +248,7 @@ export type PermissionIntegrationRouterOptions< // @public export interface PermissionPolicy { // (undocumented) - handle( - request: PolicyQuery, - user?: BackstageIdentityResponse, - ): Promise; + handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; } // @public @@ -272,6 +271,15 @@ export type PolicyQuery = { permission: Permission; }; +// @public +export type PolicyQueryUser = { + token: string; + expiresInSeconds?: number; + identity: BackstageUserIdentity; + credentials: BackstageCredentials; + info: BackstageUserInfo; +}; + // @public export class ServerPermissionClient implements PermissionsService { // (undocumented) diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts index f151cfb4fb..fd47ed7e62 100644 --- a/plugins/permission-node/src/policy/index.ts +++ b/plugins/permission-node/src/policy/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export type { PermissionPolicy, PolicyQuery } from './types'; +export type { PermissionPolicy, PolicyQuery, PolicyQueryUser } from './types'; diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 0f469cfbfe..a7b8b75218 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -18,7 +18,11 @@ import { Permission, PolicyDecision, } from '@backstage/plugin-permission-common'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { + BackstageCredentials, + BackstageUserInfo, +} from '@backstage/backend-plugin-api'; /** * A query to be evaluated by the {@link PermissionPolicy}. @@ -34,6 +38,44 @@ export type PolicyQuery = { permission: Permission; }; +/** + * The context within which a policy query is evaluated. + * + * @public + */ +export type PolicyQueryUser = { + /** + * The token used to authenticate the user within Backstage. + * + * @deprecated User the `credentials` field in combination with `coreServices.auth` to generate a request token instead. + */ + token: string; + + /** + * The number of seconds until the token expires. If not set, it can be assumed that the token does not expire. + * + * @deprecated This field is deprecated and will be removed in a future release. + */ + expiresInSeconds?: number; + + /** + * A plaintext description of the identity that is encapsulated within the token. + * + * @deprecated Use the `info` field instead. + */ + identity: BackstageUserIdentity; + + /** + * The credentials of the user making the request. + */ + credentials: BackstageCredentials; + + /** + * The information for the user making the request. + */ + info: BackstageUserInfo; +}; + /** * A policy to evaluate authorization requests for any permissioned action performed in Backstage. * @@ -51,8 +93,5 @@ export type PolicyQuery = { * @public */ export interface PermissionPolicy { - handle( - request: PolicyQuery, - user?: BackstageIdentityResponse, - ): Promise; + handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; } From 4d7e11f0149b5940fc43b79e2d4d0fa08af4ad8f Mon Sep 17 00:00:00 2001 From: Frank Kong <50030060+Zaperex@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:06:01 -0400 Subject: [PATCH 102/212] chore(scaffolder): allow task viewer to be resizable and disable rendering of empty output (#25495) chore(scaffolder): allow task viewer to be resizable and disable rendering of empty output (#25495) --- .changeset/four-donuts-switch.md | 5 +++ .changeset/witty-keys-share.md | 5 +++ .../DefaultTemplateOutputs.test.tsx | 21 +++++++++-- .../DefaultTemplateOutputs.tsx | 35 ++++++++++++------- plugins/scaffolder/package.json | 2 ++ .../components/OngoingTask/OngoingTask.tsx | 5 +-- yarn.lock | 13 ++++++- 7 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 .changeset/four-donuts-switch.md create mode 100644 .changeset/witty-keys-share.md diff --git a/.changeset/four-donuts-switch.md b/.changeset/four-donuts-switch.md new file mode 100644 index 0000000000..bb876a4341 --- /dev/null +++ b/.changeset/four-donuts-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +enable resizing of the task log stream viewer diff --git a/.changeset/witty-keys-share.md b/.changeset/witty-keys-share.md new file mode 100644 index 0000000000..0ef89e56e2 --- /dev/null +++ b/.changeset/witty-keys-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +disables rendering of output box if no output is returned diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx index 4c631b86b0..7ec3b0867c 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -31,7 +31,7 @@ describe('', () => { ], }; - const { getByRole } = await renderInTestApp( + const { getByRole, queryByTestId } = await renderInTestApp( , { mountedRoutes: { @@ -39,7 +39,8 @@ describe('', () => { }, }, ); - + expect(queryByTestId('output-box')).not.toBeNull(); + expect(queryByTestId('text-output-box')).not.toBeNull(); // first text output default visible expect(getByRole('heading', { level: 2 }).innerHTML).toBe( output.text[0].title, @@ -61,4 +62,20 @@ describe('', () => { expect(getByRole('heading', { level: 2 }).innerHTML).toBe(text.title); } }); + it('should not render anything when output is empty', async () => { + // This is the default case when no output field is present in the template + const output = {}; + const { queryByTestId } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + // Ensure that nothing renders from this component + expect(queryByTestId('output-box')).toBeNull(); + expect(queryByTestId('text-output-box')).toBeNull(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index 7d90623077..cbc299bf1c 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -56,22 +56,31 @@ export const DefaultTemplateOutputs = (props: { return null; } + const emptyOutput = Object.keys(output).length === 0; + return ( <> - - - - - - - - + {!emptyOutput ? ( + + + + + + + + + ) : null} {textOutput ? ( - + + - + ) : null} diff --git a/yarn.lock b/yarn.lock index afe89e4263..b0bff82017 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7314,6 +7314,7 @@ __metadata: "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + "@types/react-resizable": ^3.0.8 "@types/react-window": ^1.8.8 "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 @@ -7326,6 +7327,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 qs: ^6.9.4 + react-resizable: ^3.0.5 react-use: ^17.2.4 react-window: ^1.8.10 swr: ^2.0.0 @@ -18461,6 +18463,15 @@ __metadata: languageName: node linkType: hard +"@types/react-resizable@npm:^3.0.8": + version: 3.0.8 + resolution: "@types/react-resizable@npm:3.0.8" + dependencies: + "@types/react": "*" + checksum: aabdef8056bbab3065559bce7ce93232e645fb4f915fd55f0903a2f4df5d2395762dfd75bb0242d5a73d39461e8c062d81eaab87b91213dddfd973ff908f79e4 + languageName: node + linkType: hard + "@types/react-sparklines@npm:^1.7.0": version: 1.7.5 resolution: "@types/react-sparklines@npm:1.7.5" @@ -38371,7 +38382,7 @@ __metadata: languageName: node linkType: hard -"react-resizable@npm:^3.0.4": +"react-resizable@npm:^3.0.4, react-resizable@npm:^3.0.5": version: 3.0.5 resolution: "react-resizable@npm:3.0.5" dependencies: From 842457d9ad2d2be4f43c6f859f6e4198e1f2f24e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jul 2024 16:14:59 +0200 Subject: [PATCH 103/212] docs: update root logger service docs Signed-off-by: Camila Belo --- docs/backend-system/core-services/logger.md | 44 ------------------- .../core-services/root-logger.md | 44 ++++++++++++++++++- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/docs/backend-system/core-services/logger.md b/docs/backend-system/core-services/logger.md index 015d2055d5..e18bd6a4c0 100644 --- a/docs/backend-system/core-services/logger.md +++ b/docs/backend-system/core-services/logger.md @@ -31,47 +31,3 @@ createBackendPlugin({ }, }); ``` - -## Root Logger - -The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. - -If you want to override the implementation for logging across all of the backend, this is the service that you should override. - -## Configuring the service - -The following example is how you can override the root logger service to add additional metadata to all log lines. - -```ts -import { coreServices } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - createServiceFactory({ - service: coreServices.rootLogger, - deps: { - config: coreServices.rootConfig, - }, - async factory({ config }) { - const logger = WinstonLogger.create({ - meta: { - service: 'backstage', - // here's some additional information that is not part of the - // original implementation - podName: 'myk8spod', - }, - level: process.env.LOG_LEVEL || 'info', - format: - process.env.NODE_ENV === 'production' - ? format.json() - : WinstonLogger.colorFormat(), - transports: [new transports.Console()], - }); - - return logger; - }, - }), -); -``` diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md index 09b7929efe..733b7b8277 100644 --- a/docs/backend-system/core-services/root-logger.md +++ b/docs/backend-system/core-services/root-logger.md @@ -5,4 +5,46 @@ sidebar_label: Root Logger description: Documentation for the Root Logger service --- -TODO +## Root Logger + +The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. + +If you want to override the implementation for logging across all of the backend, this is the service that you should override. + +## Configuring the service + +The following example is how you can override the root logger service to add additional metadata to all log lines. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + createServiceFactory({ + service: coreServices.rootLogger, + deps: { + config: coreServices.rootConfig, + }, + async factory({ config }) { + const logger = WinstonLogger.create({ + meta: { + service: 'backstage', + // here's some additional information that is not part of the + // original implementation + podName: 'myk8spod', + }, + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? format.json() + : WinstonLogger.colorFormat(), + transports: [new transports.Console()], + }); + + return logger; + }, + }), +); +``` From dae85dfd213b1711fd850e48777361065725b1e4 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sat, 13 Jul 2024 14:07:56 +0530 Subject: [PATCH 104/212] Add examples for fetch:cookiecutter scaffolder action & improve related tests Signed-off-by: parmar-abhinav --- .changeset/stupid-dots-relate.md | 5 + .../package.json | 1 + .../fetch/cookiecutter.examples.test.ts | 274 ++++++++++++++++++ .../actions/fetch/cookiecutter.examples.ts | 119 ++++++++ .../src/actions/fetch/cookiecutter.ts | 2 + 5 files changed, 401 insertions(+) create mode 100644 .changeset/stupid-dots-relate.md create mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts create mode 100644 plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.ts diff --git a/.changeset/stupid-dots-relate.md b/.changeset/stupid-dots-relate.md new file mode 100644 index 0000000000..a83777bff9 --- /dev/null +++ b/.changeset/stupid-dots-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +--- + +Add examples for `fetch:cookiecutter` scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 43a7e348a4..261010f681 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -53,6 +53,7 @@ "command-exists": "^1.2.9", "fs-extra": "^11.2.0", "winston": "^3.2.1", + "yaml": "^2.0.0", "yn": "^4.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts new file mode 100644 index 0000000000..ae46ef1bd6 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts @@ -0,0 +1,274 @@ +/* + * Copyright 2021 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. + */ + +import { UrlReader, ContainerRunner } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createFetchCookiecutterAction } from './cookiecutter'; +import { join } from 'path'; +import type { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { examples } from './cookiecutter.examples'; +import yaml from 'yaml'; + +const executeShellCommand = jest.fn(); +const commandExists = jest.fn(); +const fetchContents = jest.fn(); + +jest.mock('@backstage/plugin-scaffolder-node', () => ({ + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + fetchContents: (...args: any[]) => fetchContents(...args), + executeShellCommand: (...args: any[]) => executeShellCommand(...args), +})); + +jest.mock( + 'command-exists', + () => + (...args: any[]) => + commandExists(...args), +); + +describe('fetch:cookiecutter', () => { + const mockDir = createMockDirectory({ mockOsTmpDir: true }); + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + }), + ); + + const mockTmpDir = mockDir.path; + + let mockContext: ActionContext<{ + url: string; + targetPath?: string; + values: JsonObject; + copyWithoutRender?: string[]; + extensions?: string[]; + imageName?: string; + }>; + + const containerRunner: jest.Mocked = { + runContainer: jest.fn(), + }; + + const mockReader: UrlReader = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const action = createFetchCookiecutterAction({ + integrations, + containerRunner, + reader: mockReader, + }); + + beforeEach(() => { + jest.resetAllMocks(); + + mockContext = createMockActionContext({ + input: { + url: 'https://google.com/cookie/cutter', + targetPath: 'something', + values: { + help: 'me', + }, + }, + templateInfo: { + entityRef: 'template:default/cookiecutter', + baseUrl: 'somebase', + }, + workspacePath: mockTmpDir, + }); + mockDir.setContent({ template: {} }); + + commandExists.mockResolvedValue(null); + + // Mock when run container is called it creates some new files in the mock filesystem + containerRunner.runContainer.mockImplementation(async () => { + mockDir.setContent({ + 'intermediate/testfile.json': '{}', + }); + }); + + // Mock when executeShellCommand is called it creates some new files in the mock filesystem + executeShellCommand.mockImplementation(async () => { + mockDir.setContent({ + 'intermediate/testfile.json': '{}', + }); + }); + }); + + it(`should ${examples[0].description}`, async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[1].description}`, async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: input, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[2].description}`, async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[3].description}`, async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[4].description}`, async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); +}); diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.ts new file mode 100644 index 0000000000..6ebada50f0 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.ts @@ -0,0 +1,119 @@ +/* + * Copyright 2021 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. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Fetch and template using cookiecutter with minimal options.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with custom target path.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + targetPath: 'custom-path', + values: { + help: 'me', + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with copyWithoutRender.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + copyWithoutRender: ['.github/*', '*.md'], + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with custom extensions.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + extensions: ['jinja2_time.TimeExtension'], + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with a custom Docker image.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + imageName: 'custom/cookiecutter-image', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index d170acb5d0..efbe0a07cf 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -28,6 +28,7 @@ import { fetchContents, executeShellCommand, } from '@backstage/plugin-scaffolder-node'; +import { examples } from './cookiecutter.examples'; export class CookiecutterRunner { private readonly containerRunner?: ContainerRunner; @@ -156,6 +157,7 @@ export function createFetchCookiecutterAction(options: { id: 'fetch:cookiecutter', description: 'Downloads a template from the given URL into the workspace, and runs cookiecutter on it.', + examples, schema: { input: { type: 'object', From 6fc03c7d3ab0b1ff75ea037c113314bf47fecb99 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sat, 13 Jul 2024 16:06:42 +0530 Subject: [PATCH 105/212] Add examples for notification:send scaffolder action & improve related tests Signed-off-by: parmar-abhinav --- .changeset/cool-insects-remember.md | 5 + .../package.json | 3 +- .../actions/sendNotification.examples.test.ts | 272 ++++++++++++++++++ .../src/actions/sendNotification.examples.ts | 255 ++++++++++++++++ .../src/actions/sendNotification.ts | 2 + 5 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 .changeset/cool-insects-remember.md create mode 100644 plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.test.ts create mode 100644 plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.ts diff --git a/.changeset/cool-insects-remember.md b/.changeset/cool-insects-remember.md new file mode 100644 index 0000000000..ab33015377 --- /dev/null +++ b/.changeset/cool-insects-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-notifications': patch +--- + +Add examples for notification:send scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index 7c983c36ce..5e45676f99 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -38,7 +38,8 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-notifications-node": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "octokit": "^3.0.0" + "octokit": "^3.0.0", + "yaml": "^2.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.test.ts b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.test.ts new file mode 100644 index 0000000000..84473335e0 --- /dev/null +++ b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.test.ts @@ -0,0 +1,272 @@ +/* + * Copyright 2024 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. + */ +import { createSendNotificationAction } from './sendNotification'; +import { NotificationService } from '@backstage/plugin-notifications-node'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { examples } from './sendNotification.examples'; +import yaml from 'yaml'; + +describe('notification:send', () => { + const notificationService: jest.Mocked = { + send: jest.fn(), + }; + + let action: TemplateAction; + + beforeEach(() => { + jest.resetAllMocks(); + action = createSendNotificationAction({ + notifications: notificationService, + }); + }); + + const mockContext = createMockActionContext({ + input: { + recipients: 'broadcast', + title: 'Test notification', + }, + }); + + it(`should ${examples[0].description}`, async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + title: 'Test notification', + }, + }); + }); + + it(`should ${examples[1].description}`, async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + entityRef: ['entity:component:1'], + type: 'entity', + }, + payload: { + description: 'A security update has been applied. Please review.', + title: 'Security Update', + link: 'https://example.com/security/update', + severity: 'high', + scope: 'internal', + }, + }); + }); + + it(`should ${examples[2].description}`, async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'entity', + entityRef: ['entity:component:1'], + }, + payload: { + description: 'Here is your weekly update.', + link: undefined, + severity: 'low', + scope: undefined, + title: 'Weekly Update', + }, + }); + }); + + it(`should ${examples[3].description}`, async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: 'Version 2.0.0 is now available. Upgrade now!', + link: undefined, + severity: 'normal', + scope: 'public', + title: 'New Release Available', + }, + }); + }); + + it(`should ${examples[4].description}`, async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'entity', + entityRef: ['entity:component:1'], + }, + payload: { + description: + 'A critical bug has been identified. Immediate action required.', + link: undefined, + scope: 'internal', + severity: 'critical', + title: 'Critical Bug Found', + }, + }); + }); + + it(`should ${examples[5].description}`, async () => { + const input = yaml.parse(examples[5].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'broadcast', + }, + payload: { + description: 'Server maintenance will occur tonight at 11 PM.', + link: undefined, + scope: 'internal', + severity: 'normal', + title: 'Server Maintenance Scheduled', + }, + }); + }); + + it(`should ${examples[6].description}`, async () => { + const input = yaml.parse(examples[6].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: 'New features have been deployed. Explore them now!', + link: undefined, + scope: undefined, + severity: 'normal', + title: 'New Feature Deployment', + }, + }); + }); + + it(`should ${examples[7].description}`, async () => { + const input = yaml.parse(examples[7].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: undefined, + link: undefined, + scope: 'internal', + severity: 'low', + title: 'Holiday Office Closure', + }, + }); + }); + + it(`should ${examples[8].description}`, async () => { + const input = yaml.parse(examples[8].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: + "Don't forget, the weekly meeting is scheduled for tomorrow.", + link: undefined, + scope: 'internal', + severity: undefined, + title: 'Reminder: Weekly Meeting Tomorrow', + }, + }); + }); + + it(`should ${examples[9].description}`, async () => { + const input = yaml.parse(examples[9].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + title: 'Important Announcement', + description: + 'Please read the latest announcement regarding the upcoming changes.', + link: undefined, + scope: undefined, + severity: 'high', + }, + }); + }); + + it(`should ${examples[10].description}`, async () => { + const input = yaml.parse(examples[10].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + title: 'Broadcast Notification', + description: 'This is a broadcast notification', + link: undefined, + scope: undefined, + severity: 'low', + }, + }); + }); + + it(`should ${examples[11].description}`, async () => { + const input = yaml.parse(examples[11].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'entity', + entityRef: ['entity:service1'], + }, + payload: { + title: 'Entity Notification', + description: 'This is a notification for entity service1', + link: undefined, + scope: undefined, + severity: 'normal', + }, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.ts b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.ts new file mode 100644 index 0000000000..05bbd368f8 --- /dev/null +++ b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.ts @@ -0,0 +1,255 @@ +/* + * 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. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Sends a notification with minimal options', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Test notification', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with entity recipients and link', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'entity', + entityRefs: ['entity:component:1'], + title: 'Security Update', + info: 'A security update has been applied. Please review.', + link: 'https://example.com/security/update', + severity: 'high', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with entity recipients and optional flag', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'entity', + entityRefs: ['entity:component:1'], + title: 'Weekly Update', + info: 'Here is your weekly update.', + severity: 'low', + optional: true, + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and custom scope', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'New Release Available', + info: 'Version 2.0.0 is now available. Upgrade now!', + severity: 'normal', + scope: 'public', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with entity recipients and custom severity', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'entity', + entityRefs: ['entity:component:1'], + title: 'Critical Bug Found', + info: 'A critical bug has been identified. Immediate action required.', + severity: 'critical', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with broadcast recipients and no link', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Server Maintenance Scheduled', + info: 'Server maintenance will occur tonight at 11 PM.', + severity: 'normal', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and optional flag', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'New Feature Deployment', + info: 'New features have been deployed. Explore them now!', + severity: 'normal', + optional: true, + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and no description', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Holiday Office Closure', + severity: 'low', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and no severity', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Reminder: Weekly Meeting Tomorrow', + info: "Don't forget, the weekly meeting is scheduled for tomorrow.", + scope: 'internal', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with broadcast recipients and no scope', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Important Announcement', + info: 'Please read the latest announcement regarding the upcoming changes.', + severity: 'high', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with optional parameters', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Broadcast Notification', + info: 'This is a broadcast notification', + severity: 'low', + optional: true, + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with entity recipients and optional set to false', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification to Entity', + input: { + recipients: 'entity', + entityRefs: ['entity:service1'], + title: 'Entity Notification', + info: 'This is a notification for entity service1', + severity: 'normal', + optional: false, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts index f50e178174..9646de0001 100644 --- a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts +++ b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts @@ -22,6 +22,7 @@ import { NotificationSeverity, } from '@backstage/plugin-notifications-common'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { examples } from './sendNotification.examples'; /** * @public @@ -42,6 +43,7 @@ export function createSendNotificationAction(options: { }>({ id: 'notification:send', description: 'Sends a notification using NotificationService', + examples, schema: { input: { type: 'object', From 449def7c2c3445f234106becf9e8b74291758ddf Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sat, 13 Jul 2024 17:30:14 +0530 Subject: [PATCH 106/212] Add examples for fetch:rails scaffolder action & improve related tests Signed-off-by: parmar-abhinav --- .changeset/friendly-feet-refuse.md | 5 + .../package.json | 3 +- .../fetch/rails/index.examples.test.ts | 292 ++++++++++++++ .../src/actions/fetch/rails/index.examples.ts | 370 ++++++++++++++++++ .../src/actions/fetch/rails/index.ts | 2 + 5 files changed, 671 insertions(+), 1 deletion(-) create mode 100644 .changeset/friendly-feet-refuse.md create mode 100644 plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts create mode 100644 plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.ts diff --git a/.changeset/friendly-feet-refuse.md b/.changeset/friendly-feet-refuse.md new file mode 100644 index 0000000000..677d21d48a --- /dev/null +++ b/.changeset/friendly-feet-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-rails': patch +--- + +Add examples for fetch:rails scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index c15815d2fa..fb917f4e25 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -51,7 +51,8 @@ "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "command-exists": "^1.2.9", - "fs-extra": "^11.0.0" + "fs-extra": "^11.0.0", + "yaml": "^2.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts new file mode 100644 index 0000000000..d35cb3f319 --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts @@ -0,0 +1,292 @@ +/* + * Copyright 2021 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. + */ + +const mockRailsTemplater = { run: jest.fn() }; +jest.mock('@backstage/plugin-scaffolder-node', () => ({ + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + fetchContents: jest.fn(), +})); +jest.mock('./railsNewRunner', () => { + return { + RailsNewRunner: jest.fn().mockImplementation(() => { + return mockRailsTemplater; + }), + }; +}); + +import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { resolve as resolvePath } from 'path'; +import { createFetchRailsAction } from './index'; +import { fetchContents } from '@backstage/plugin-scaffolder-node'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { Writable } from 'stream'; +import { examples } from './index.examples'; +import yaml from 'yaml'; + +describe('fetch:rails', () => { + const mockDir = createMockDirectory(); + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + }), + ); + + const mockContext = createMockActionContext({ + input: { + url: 'https://rubyonrails.org/generator', + targetPath: 'something', + values: { + help: 'me', + }, + }, + templateInfo: { + baseUrl: 'somebase', + entityRef: 'template:default/myTemplate', + }, + workspacePath: mockDir.path, + }); + + const mockReader: UrlReader = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + const containerRunner: ContainerRunner = { + runContainer: jest.fn(), + }; + + const action = createFetchRailsAction({ + integrations, + reader: mockReader, + containerRunner, + allowedImageNames: ['foo/rails-custom-image'], + }); + + beforeEach(() => { + mockDir.setContent({ + result: '{}', + }); + jest.clearAllMocks(); + }); + + it(`should ${examples[0].description}`, async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[1].description}`, async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[2].description}`, async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[3].description}`, async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[4].description}`, async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[5].description}`, async () => { + const input = yaml.parse(examples[5].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[6].description}`, async () => { + const input = yaml.parse(examples[6].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[7].description}`, async () => { + const input = yaml.parse(examples[7].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[8].description}`, async () => { + const input = yaml.parse(examples[8].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[9].description}`, async () => { + const input = yaml.parse(examples[9].example).steps[0].input; + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.ts new file mode 100644 index 0000000000..4c77eb3c1b --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.ts @@ -0,0 +1,370 @@ +/* + * Copyright 2021 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. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Fetch and template a minimal Rails app', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: true, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: true, + api: true, + webpacker: 'react', + database: 'postgresql', + railsVersion: 'dev', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Fetch and template a Rails app with custom options', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + targetPath: 'custom-path', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: false, + api: false, + webpacker: 'vue', + database: 'mysql', + railsVersion: 'edge', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Fetch and template a Rails app with Docker image', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: true, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: true, + api: true, + webpacker: 'react', + database: 'postgresql', + railsVersion: 'dev', + }, + imageName: 'custom/rails-image', + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with different database and action mailer settings', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: true, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: true, + api: true, + webpacker: 'angular', + database: 'sqlite3', + railsVersion: 'master', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with full options including action mailer and active storage', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: false, + api: false, + webpacker: 'stimulus', + database: 'postgresql', + railsVersion: 'fromImage', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with API-only stack and custom template path', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: true, + api: true, + webpacker: 'elm', + database: 'mysql', + railsVersion: 'dev', + template: 'path/to/custom/template', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Fetch and template a Rails app with custom Webpack setup', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: false, + api: false, + webpacker: 'angular', + database: 'postgresql', + railsVersion: 'dev', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with specific Rails version and Action Mailer enabled', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: true, + api: true, + webpacker: 'react', + database: 'sqlite3', + railsVersion: 'master', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with specific database and Active Storage enabled', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: true, + api: true, + webpacker: 'vue', + database: 'postgresql', + railsVersion: 'fromImage', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with full options including database and skip options', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: false, + api: false, + webpacker: 'stimulus', + database: 'sqlite3', + railsVersion: 'dev', + }, + }, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 394d0fb02c..94a2a3539b 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -27,6 +27,7 @@ import { import { resolve as resolvePath } from 'path'; import { RailsNewRunner } from './railsNewRunner'; import { PassThrough } from 'stream'; +import { examples } from './index.examples'; /** * Creates the `fetch:rails` Scaffolder action. @@ -56,6 +57,7 @@ export function createFetchRailsAction(options: { id: 'fetch:rails', description: 'Downloads a template from the given `url` into the workspace, and runs a rails new generator on it.', + examples, schema: { input: { type: 'object', From 1145181834f58328fc846c6776fdb8ff26c1d79f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 13 Jul 2024 10:45:17 -0500 Subject: [PATCH 107/212] Moved from root note to title Signed-off-by: Andre Wanlin --- docs/auth/guest/provider.md | 3 +-- docs/features/kubernetes/authenticationstrategy.md | 3 +-- docs/features/kubernetes/installation.md | 9 +++------ docs/features/search/getting-started.md | 6 ++---- docs/features/software-catalog/configuration.md | 6 ++---- docs/features/software-templates/builtin-actions.md | 3 +-- docs/features/techdocs/getting-started.md | 9 +++------ docs/getting-started/config/database.md | 3 +-- docs/getting-started/configure-app-with-plugins.md | 3 +-- docs/getting-started/homepage.md | 3 +-- docs/integrations/aws-s3/discovery--old.md | 3 +-- docs/integrations/aws-s3/discovery.md | 3 +-- docs/integrations/azure/discovery--old.md | 3 +-- docs/integrations/azure/discovery.md | 3 +-- docs/integrations/azure/org--old.md | 3 +-- docs/integrations/azure/org.md | 3 +-- docs/integrations/bitbucketCloud/discovery.md | 3 +-- docs/integrations/bitbucketServer/discovery.md | 3 +-- docs/integrations/gerrit/discovery.md | 3 +-- docs/integrations/github/discovery--old.md | 6 ++---- docs/integrations/github/discovery.md | 3 +-- docs/integrations/github/org--old.md | 6 ++---- docs/integrations/github/org.md | 3 +-- docs/integrations/gitlab/discovery.md | 3 +-- docs/integrations/gitlab/org.md | 3 +-- docs/integrations/ldap/org--old.md | 3 +-- docs/integrations/ldap/org.md | 3 +-- docs/permissions/getting-started.md | 3 +-- docs/permissions/plugin-authors/01-setup.md | 3 +-- docs/plugins/backend-plugin.md | 3 +-- docs/releases/v1.6.0-changelog.md | 6 ++---- docs/releases/v1.6.0-next.0-changelog.md | 6 ++---- docs/tutorials/configuring-plugin-databases.md | 13 ++++++++----- docs/tutorials/switching-sqlite-postgres.md | 3 +-- 34 files changed, 51 insertions(+), 91 deletions(-) diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md index b28274d2ba..b541178571 100644 --- a/docs/auth/guest/provider.md +++ b/docs/auth/guest/provider.md @@ -25,8 +25,7 @@ This will only work with the new backend system. There is no support for this in Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation. -```sh -# From your Backstage root directory +```sh title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider ``` diff --git a/docs/features/kubernetes/authenticationstrategy.md b/docs/features/kubernetes/authenticationstrategy.md index a7d093248a..ffea69d610 100644 --- a/docs/features/kubernetes/authenticationstrategy.md +++ b/docs/features/kubernetes/authenticationstrategy.md @@ -173,8 +173,7 @@ To create the Backend module, run `yarn new`, select `backend-module`. Then fill This will create a new package at `plugins/kubernetes-backend-module-pinniped`. We are going to need also the `@backstage/plugin-kubernetes-node` and `@backstage/plugin-kubernetes-common` dependencies, the `@backstage/plugin-kubernetes-node` houses the [kubernetesAuthStrategyExtensionPoint](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-node/src/extensions.ts#L77) and a [Pinniped Helper](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-node/src/auth/PinnipedHelper.ts#L53) class. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd plugins/kubernetes-backend-module-pinniped add @backstage/plugin-kubernetes-node yarn --cwd plugins/kubernetes-backend-module-pinniped add @backstage/plugin-kubernetes-common ``` diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index b3d03cc0b4..279c9cbf3f 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -15,8 +15,7 @@ If you haven't setup Backstage already, read the The first step is to add the Kubernetes frontend plugin to your Backstage application. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-kubernetes ``` @@ -53,8 +52,7 @@ work. Navigate to `packages/backend` of your Backstage app, and install the `@backstage/plugin-kubernetes-backend` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend ``` @@ -109,8 +107,7 @@ To get the Kubernetes plugin install using the New Backend System you will need Run this command to add the package: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend ``` diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 7989b60ae2..b2a074974f 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -16,8 +16,7 @@ If you haven't setup Backstage already, start ## Adding Search to the Frontend -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-search @backstage/plugin-search-react ``` @@ -133,8 +132,7 @@ For more information about using `Root.tsx`, please see Add the following plugins into your backend app: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-search-backend @backstage/plugin-search-backend-node ``` diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index f5df9eafca..17d5f49205 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -184,8 +184,7 @@ Catalog errors are published to the [events plugin](https://github.com/backstage The first step is to add the events backend plugin to your Backstage application. Navigate to your Backstage application directory and add the plugin package. -```ts -# From your Backstage root directory +```ts title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-events-backend ``` @@ -201,8 +200,7 @@ If you want to log catalog errors you can install the `@backstage/plugin-catalog Install the catalog logs module. -```ts -# From your Backstage root directory +```ts title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-logs ``` diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index 5b1e4347da..301fb4c457 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -24,8 +24,7 @@ There are also several modules available for various SCM tools: Here's how to add an action module, first you need to run this command: -```sh -# From your Backstage root directory +```sh title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-github ``` diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 7909f8166f..a157d29c0d 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -21,8 +21,7 @@ The first step is to add the TechDocs plugin to your Backstage application. Navigate to your new Backstage application directory. And then to your `packages/app` directory, and install the `@backstage/plugin-techdocs` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-techdocs ``` @@ -106,8 +105,7 @@ That's it! Now, we need the TechDocs Backend plugin for the frontend to work. Navigate to `packages/backend` of your Backstage app, and install the `@backstage/plugin-techdocs-backend` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-techdocs-backend ``` @@ -199,8 +197,7 @@ To install TechDocs when using the New Backend system you will need to do the fo Navigate to `packages/backend` of your Backstage app, and install the `@backstage/plugin-techdocs-backend` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-techdocs-backend ``` diff --git a/docs/getting-started/config/database.md b/docs/getting-started/config/database.md index a560fe507b..0fbd66d5a3 100644 --- a/docs/getting-started/config/database.md +++ b/docs/getting-started/config/database.md @@ -73,8 +73,7 @@ to install and configure the client. Go to the root directory of your freshly installed Backstage App. Run the following to install the PostgreSQL client into your backend: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add pg ``` diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index 2fdb016e7b..95fd456dc0 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -21,8 +21,7 @@ to an entity in the software catalog. 1. Add the plugin's npm package to the repo: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn --cwd packages/app add @circleci/backstage-plugin ``` diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 7e1a2ed909..d7c9b46faa 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -28,8 +28,7 @@ Now, let's get started by installing the home plugin and creating a simple homep #### 1. Install the plugin -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-home ``` diff --git a/docs/integrations/aws-s3/discovery--old.md b/docs/integrations/aws-s3/discovery--old.md index bff63af9e5..09b6eb6cc0 100644 --- a/docs/integrations/aws-s3/discovery--old.md +++ b/docs/integrations/aws-s3/discovery--old.md @@ -66,8 +66,7 @@ catalog: As this provider is not one of the default providers, you will first need to install the AWS catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-aws ``` diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index cce410ea8b..db9119680e 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -66,8 +66,7 @@ catalog: As this provider is not one of the default providers, you will first need to install the AWS catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-aws ``` diff --git a/docs/integrations/azure/discovery--old.md b/docs/integrations/azure/discovery--old.md index be490d70fe..56a2fe559f 100644 --- a/docs/integrations/azure/discovery--old.md +++ b/docs/integrations/azure/discovery--old.md @@ -102,8 +102,7 @@ It may take some time before the branch is indexed and searchable. As this provider is not one of the default providers, you will first need to install the Azure catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure ``` diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 7e7b465118..c8a8a3af7c 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -102,8 +102,7 @@ It may take some time before the branch is indexed and searchable. As this provider is not one of the default providers, you will first need to install the Azure catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure ``` diff --git a/docs/integrations/azure/org--old.md b/docs/integrations/azure/org--old.md index 13d53b24b3..899120e88a 100644 --- a/docs/integrations/azure/org--old.md +++ b/docs/integrations/azure/org--old.md @@ -18,8 +18,7 @@ Microsoft Graph API. The package is not installed by default, therefore you have to add `@backstage/plugin-catalog-backend-module-msgraph` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph ``` diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 2dfdee7bce..0a4eda2db5 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -18,8 +18,7 @@ Microsoft Graph API. The package is not installed by default, therefore you have to add `@backstage/plugin-catalog-backend-module-msgraph` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph ``` diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 160827cb0a..04dc111d30 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -19,8 +19,7 @@ backend. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-bitbucket-cloud` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-cloud ``` diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index d9fc946460..99e04affa8 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -19,8 +19,7 @@ backend. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server ``` diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index e2922c1f76..96dfbd47fb 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -16,8 +16,7 @@ stored in the root of the matching projects. As this provider is not one of the default providers, you will first need to install the Gerrit provider plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gerrit ``` diff --git a/docs/integrations/github/discovery--old.md b/docs/integrations/github/discovery--old.md index 138e54a538..752aa51bde 100644 --- a/docs/integrations/github/discovery--old.md +++ b/docs/integrations/github/discovery--old.md @@ -25,8 +25,7 @@ backend. They are not installed by default, therefore you have to add a dependency on `@backstage/plugin-catalog-backend-module-github` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` @@ -273,8 +272,7 @@ backend. They are not installed by default, therefore you have to add a dependency on `@backstage/plugin-catalog-backend-module-github` to your backend package, plus `@backstage/integration` for the basic credentials management: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/integration @backstage/plugin-catalog-backend-module-github ``` diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index d8374932b8..43e63a618c 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -24,8 +24,7 @@ You will have to add the GitHub Entity provider to your backend as it is not ins dependency on `@backstage/plugin-catalog-backend-module-github` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` diff --git a/docs/integrations/github/org--old.md b/docs/integrations/github/org--old.md index 3d1639a924..bf12cad036 100644 --- a/docs/integrations/github/org--old.md +++ b/docs/integrations/github/org--old.md @@ -29,8 +29,7 @@ the Processor method (not recommended), it is described separately below. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-github` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` @@ -334,8 +333,7 @@ frequency with which they are refreshed, separately from other processors. The `GithubOrgReaderProcessor` is not registered by default, so you have to install and register it in the catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index f94ffe6c7a..18865cd630 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -38,8 +38,7 @@ You will have to add the GitHub Org provider to your backend as it is not instal dependency on `@backstage/plugin-catalog-backend-module-github-org` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github-org ``` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 98fbb0fbce..13ffd79801 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -20,8 +20,7 @@ This provider can also be configured to ingest GitLab data based on [GitLab Webh As this provider is not one of the default providers, you will first need to install the gitlab catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab ``` diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md index 46d720c164..65f66aa763 100644 --- a/docs/integrations/gitlab/org.md +++ b/docs/integrations/gitlab/org.md @@ -25,8 +25,7 @@ This provider can also be configured to ingest GitLab data based on [GitLab Syst As this provider is not one of the default providers, you will first need to install the Gitlab provider plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab @backstage/plugin-catalog-backend-module-gitlab-org ``` diff --git a/docs/integrations/ldap/org--old.md b/docs/integrations/ldap/org--old.md index 63f052371f..448e9bbed1 100644 --- a/docs/integrations/ldap/org--old.md +++ b/docs/integrations/ldap/org--old.md @@ -24,8 +24,7 @@ the Processor method (not recommended), it is described separately below. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap ``` diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 36cffb9b1f..b1aa460bd6 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -21,8 +21,7 @@ Backstage in general supports OpenLDAP compatible vendors, as well as Active Dir The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap ``` diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index 4dabf3fcbd..e8132e571b 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -50,8 +50,7 @@ The permissions framework uses a new `permission-backend` plugin to accept autho 1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-permission-backend ``` diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index a674d82e25..f11bdff930 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -39,8 +39,7 @@ The source code is available here: 2. Add these packages as dependencies for your Backstage app: - ```sh - # From your Backstage root directory + ```sh title="From your Backstage root directory" yarn --cwd packages/backend add @internal/plugin-todo-list-backend @internal/plugin-todo-list-common yarn --cwd packages/app add @internal/plugin-todo-list ``` diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 7f155f3b05..1366a59099 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -71,8 +71,7 @@ Backstage application / backend exposes it. To actually attach and run the plugin router, you will make some modifications to your backend. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @internal/plugin-carmen-backend@^0.1.0 # Change this to match the plugin's package.json ``` diff --git a/docs/releases/v1.6.0-changelog.md b/docs/releases/v1.6.0-changelog.md index 0532243cbf..e9692f4f1b 100644 --- a/docs/releases/v1.6.0-changelog.md +++ b/docs/releases/v1.6.0-changelog.md @@ -248,8 +248,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` @@ -313,8 +312,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` diff --git a/docs/releases/v1.6.0-next.0-changelog.md b/docs/releases/v1.6.0-next.0-changelog.md index 5ff2311b79..fbe9caefde 100644 --- a/docs/releases/v1.6.0-next.0-changelog.md +++ b/docs/releases/v1.6.0-next.0-changelog.md @@ -10,8 +10,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` @@ -69,8 +68,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index 528d2c6325..db3f126de7 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -36,12 +36,15 @@ Please ensure the appropriate database drivers are installed in your `backend` package. If you intend to use both PostgreSQL and SQLite, you can install both of them. -```bash -# From your Backstage root directory -# install pg if you need PostgreSQL -yarn --cwd packages/backend add pg +Install pg if you need PostgreSQL: -# install SQLite 3 if you intend to set it as the client +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add pg +``` + +Install SQLite 3 if you intend to set it as the client: + +```bash title="From your Backstage root directory" yarn --cwd packages/backend add better-sqlite3 ``` diff --git a/docs/tutorials/switching-sqlite-postgres.md b/docs/tutorials/switching-sqlite-postgres.md index 2ef481dbf3..aec9ca2ee9 100644 --- a/docs/tutorials/switching-sqlite-postgres.md +++ b/docs/tutorials/switching-sqlite-postgres.md @@ -19,8 +19,7 @@ switch between database backends. First, add PostgreSQL to your `backend` package: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add pg ``` From b9e73a0e5a7545bb659f626b02fba9a6d82e7c3e Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 14 Jul 2024 10:45:49 +0530 Subject: [PATCH 108/212] chore: run yarn install to update dependencies Signed-off-by: parmar-abhinav --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index b0bff82017..574792b052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7055,6 +7055,7 @@ __metadata: command-exists: ^1.2.9 fs-extra: ^11.0.0 jest-when: ^3.1.0 + yaml: ^2.0.0 languageName: unknown linkType: soft From b687beb2dcc16eddc89b85675b425fec7cc02c51 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 14 Jul 2024 10:52:16 +0530 Subject: [PATCH 109/212] chore: run yarn install to update dependencies Signed-off-by: parmar-abhinav --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index b0bff82017..18f51570ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7032,6 +7032,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" octokit: ^3.0.0 + yaml: ^2.0.0 languageName: unknown linkType: soft From b084597b3299049b92c9cbc3e76c704ec8f41a7e Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 14 Jul 2024 10:55:19 +0530 Subject: [PATCH 110/212] chore: run yarn install to update dependencies Signed-off-by: parmar-abhinav --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index b0bff82017..4dcfba4743 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6917,6 +6917,7 @@ __metadata: command-exists: ^1.2.9 fs-extra: ^11.2.0 winston: ^3.2.1 + yaml: ^2.0.0 yn: ^4.0.0 languageName: unknown linkType: soft From 065353f144c00589e5f4cbb427738ccc6f74deb4 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 14 Jul 2024 10:58:53 +0530 Subject: [PATCH 111/212] chore: removed unused imports Signed-off-by: parmar-abhinav --- .../src/actions/fetch/rails/index.examples.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts index d35cb3f319..a80985321e 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts @@ -35,7 +35,6 @@ import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; -import { Writable } from 'stream'; import { examples } from './index.examples'; import yaml from 'yaml'; From 0ab868dfd1510b6ef85d1967db2ac3688de496bd Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Sun, 14 Jul 2024 04:00:51 -0400 Subject: [PATCH 112/212] Wip Signed-off-by: Stephen Glass --- .../components/ReviewState/ReviewState.tsx | 42 +--------- .../src/next/components/ReviewState/util.ts | 84 +++++++++++++------ 2 files changed, 59 insertions(+), 67 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index 95f5d76212..d320ad80a6 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -18,7 +18,7 @@ import { StructuredMetadataTable } from '@backstage/core-components'; import { JsonObject } from '@backstage/types'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; -import { flattenObject, isJsonObject } from './util'; +import { processEntry } from './util'; /** * The props for the {@link ReviewState} component. @@ -38,49 +38,11 @@ export const ReviewState = (props: ReviewStateProps) => { Object.entries(props.formState) .flatMap(([key, value]) => { for (const step of props.schemas) { - const parsedSchema = new JSONSchema(step.mergedSchema); - const definitionInSchema = parsedSchema.getSchema({ - pointer: `#/${key}`, - data: props.formState, - }); - - if (definitionInSchema) { - const backstageReviewOptions = - definitionInSchema['ui:backstage']?.review; - - if (backstageReviewOptions) { - if (backstageReviewOptions.mask) { - return [[key, backstageReviewOptions.mask]]; - } - if (backstageReviewOptions.show === false) { - return []; - } - if (backstageReviewOptions.explode && isJsonObject(value)) { - return flattenObject(value, key, parsedSchema, props.formState); - } - } - - if (definitionInSchema['ui:widget'] === 'password') { - return [[key, '******']]; - } - - if (definitionInSchema.enum && definitionInSchema.enumNames) { - return [ - [ - key, - definitionInSchema.enumNames[ - definitionInSchema.enum.indexOf(value) - ] || value, - ], - ]; - } - } + return processEntry(key, value, step, props.formState); } - return [[key, value]]; }) .filter(prop => prop.length > 0), ); - return ; }; diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts index 0bb6fc08d2..e040b851d3 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* * Copyright 2022 The Backstage Authors * @@ -16,34 +17,63 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { Draft07 as JSONSchema } from 'json-schema-library'; - -export function flattenObject( - obj: JsonObject, - prefix: string, - schema: JSONSchema, - formState: JsonObject, -): [string, JsonValue | undefined][] { - return Object.entries(obj).flatMap(([key, value]) => { - const prefixedKey = prefix ? `${prefix}/${key}` : key; - - const definitionInSchema = schema.getSchema({ - pointer: `#/${prefixedKey}`, - data: formState, - }); - - if (definitionInSchema) { - const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; - - // Recurse into nested objects - if (backstageReviewOptions?.explode && isJsonObject(value)) { - return flattenObject(value, prefixedKey, schema, formState); - } - } - - return [[key, value]]; - }); -} +import { ParsedTemplateSchema } from '../../hooks'; export function isJsonObject(value?: JsonValue): value is JsonObject { return typeof value === 'object' && value !== null && !Array.isArray(value); } + +export function processEntry( + key: string, + value: JsonValue | undefined, + schema: ParsedTemplateSchema, + formState: JsonObject, +): [string, JsonValue | undefined][] { + const parsedSchema = new JSONSchema(schema.mergedSchema); + const definitionInSchema = parsedSchema.getSchema({ + pointer: `#/${key}`, + data: formState, + }); + + if (definitionInSchema) { + const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; + if (backstageReviewOptions) { + if (backstageReviewOptions.mask) { + return [[getLastKey(key), backstageReviewOptions.mask]]; + } + if (backstageReviewOptions.show === false) { + return []; + } + } + + if (isJsonObject(value)) { + // Process nested objects + return Object.entries(value).flatMap(([nestedKey, nestedValue]) => + processEntry(`${key}/${nestedKey}`, nestedValue, schema, formState), + ); + } + + if (definitionInSchema['ui:widget'] === 'password') { + return [[getLastKey(key), '******']]; + } + + if (definitionInSchema.enum && definitionInSchema.enumNames) { + return [ + [ + getLastKey(key), + definitionInSchema.enumNames[ + definitionInSchema.enum.indexOf(value) + ] || value, + ], + ]; + } + } + + return [[getLastKey(key), value]]; +} + +// Helper function to get the last part of the key +function getLastKey(key: string): string { + const parts = key.split('/'); + return parts[parts.length - 1]; +} From 3ac2a6a6b85f5ac054a421c661d24fb67d45a770 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 14 Jul 2024 13:05:51 -0500 Subject: [PATCH 113/212] Added the Postgres Search Engine to the `create-app` template Signed-off-by: Andre Wanlin --- .changeset/twenty-buses-carry.md | 5 +++++ .../templates/default-app/packages/backend/package.json.hbs | 1 + .../templates/default-app/packages/backend/src/index.ts | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 .changeset/twenty-buses-carry.md diff --git a/.changeset/twenty-buses-carry.md b/.changeset/twenty-buses-carry.md new file mode 100644 index 0000000000..00347e60d8 --- /dev/null +++ b/.changeset/twenty-buses-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Added the Postgres Search Engine to the `create-app` template diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index e26c788496..1ec475b0ed 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -36,6 +36,7 @@ "@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}", "@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}", "@backstage/plugin-search-backend-module-catalog": "^{{version '@backstage/plugin-search-backend-module-catalog'}}", + "@backstage/plugin-search-backend-module-pg": "^{{version '@backstage/plugin-search-backend-module-pg'}}", "@backstage/plugin-search-backend-module-techdocs": "^{{version '@backstage/plugin-search-backend-module-techdocs'}}", "@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 239269288e..6bb9b1322d 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -38,6 +38,12 @@ backend.add( // search plugin backend.add(import('@backstage/plugin-search-backend/alpha')); + +// search engine +// See https://backstage.io/docs/features/search/search-engines +backend.add(import('@backstage/plugin-search-backend-module-pg/alpha')); + +// search collators backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); From c0a705d44298c5274f1d94b230d642a86cada2c9 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 14 Jun 2024 10:49:04 -0500 Subject: [PATCH 114/212] Added the Kubernetes plugin to `create-app` Signed-off-by: Andre Wanlin --- .changeset/blue-pumas-cheer.md | 5 +++++ packages/create-app/src/lib/versions.ts | 4 ++++ .../templates/default-app/app-config.yaml.hbs | 3 +++ .../default-app/packages/app/package.json.hbs | 1 + .../app/src/components/catalog/EntityPage.tsx | 21 +++++++++++++++++++ .../packages/backend/package.json.hbs | 1 + .../default-app/packages/backend/src/index.ts | 3 +++ 7 files changed, 38 insertions(+) create mode 100644 .changeset/blue-pumas-cheer.md diff --git a/.changeset/blue-pumas-cheer.md b/.changeset/blue-pumas-cheer.md new file mode 100644 index 0000000000..f6cbf180ff --- /dev/null +++ b/.changeset/blue-pumas-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Added the Kubernetes plugin to `create-app` diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 25fd7b7c3c..194ffa21d9 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -62,6 +62,8 @@ import { version as pluginCatalogBackendModuleLogs } from '../../../../plugins/c import { version as pluginCatalogBackendModuleScaffolderEntityModel } from '../../../../plugins/catalog-backend-module-scaffolder-entity-model/package.json'; import { version as pluginCatalogGraph } from '../../../../plugins/catalog-graph/package.json'; import { version as pluginCatalogImport } from '../../../../plugins/catalog-import/package.json'; +import { version as pluginKubernetes } from '../../../../plugins/kubernetes/package.json'; +import { version as pluginKubernetesBackend } from '../../../../plugins/kubernetes-backend/package.json'; import { version as pluginOrg } from '../../../../plugins/org/package.json'; import { version as pluginPermissionBackend } from '../../../../plugins/permission-backend/package.json'; import { version as pluginPermissionBackendModulePolicyAllowAll } from '../../../../plugins/permission-backend-module-policy-allow-all/package.json'; @@ -118,6 +120,8 @@ export const packageVersions = { pluginCatalogBackendModuleScaffolderEntityModel, '@backstage/plugin-catalog-graph': pluginCatalogGraph, '@backstage/plugin-catalog-import': pluginCatalogImport, + '@backstage/plugin-kubernetes': pluginKubernetes, + '@backstage/plugin-kubernetes-backend': pluginKubernetesBackend, '@backstage/plugin-org': pluginOrg, '@backstage/plugin-permission-backend': pluginPermissionBackend, '@backstage/plugin-permission-backend-module-allow-all-policy': diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 248cff137a..a4797d4c41 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -103,3 +103,6 @@ catalog: # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml # rules: # - allow: [User, Group] + +kubernetes: + # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options \ No newline at end of file diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index d4db9e5885..08a1d58362 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -27,6 +27,7 @@ "@backstage/plugin-catalog-graph": "^{{version '@backstage/plugin-catalog-graph'}}", "@backstage/plugin-catalog-import": "^{{version '@backstage/plugin-catalog-import'}}", "@backstage/plugin-catalog-react": "^{{version '@backstage/plugin-catalog-react'}}", + "@backstage/plugin-kubernetes": "^{{version '@backstage/plugin-kubernetes'}}", "@backstage/plugin-org": "^{{version '@backstage/plugin-org'}}", "@backstage/plugin-permission-react": "^{{version '@backstage/plugin-permission-react'}}", "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 53b6685c3f..6164c76004 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -54,6 +54,11 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { + EntityKubernetesContent, + isKubernetesAvailable, +} from '@backstage/plugin-kubernetes'; + const techdocsContent = ( @@ -150,6 +155,14 @@ const serviceEntityPage = ( {cicdContent} + + + + @@ -188,6 +201,14 @@ const websiteEntityPage = ( {cicdContent} + + + + diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index e26c788496..8aad8a4b5d 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -28,6 +28,7 @@ "@backstage/plugin-catalog-backend": "^{{version '@backstage/plugin-catalog-backend'}}", "@backstage/plugin-catalog-backend-module-logs": "^{{version '@backstage/plugin-catalog-backend-module-logs'}}", "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^{{version '@backstage/plugin-catalog-backend-module-scaffolder-entity-model'}}", + "@backstage/plugin-kubernetes-backend": "^{{version '@backstage/plugin-kubernetes-backend'}}", "@backstage/plugin-permission-backend": "^{{version '@backstage/plugin-permission-backend'}}", "@backstage/plugin-permission-backend-module-allow-all-policy": "^{{version '@backstage/plugin-permission-backend-module-allow-all-policy'}}", "@backstage/plugin-permission-common": "^{{version '@backstage/plugin-permission-common'}}", diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 239269288e..983202caa6 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -41,4 +41,7 @@ backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); +// kubernetes +backend.add(import('@backstage/plugin-kubernetes-backend/alpha')); + backend.start(); From dba4a14ee9b4ec8e2b99f94e9271cc9c26c951b8 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 14 Jun 2024 11:17:30 -0500 Subject: [PATCH 115/212] Added new line Signed-off-by: Andre Wanlin --- packages/create-app/templates/default-app/app-config.yaml.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index a4797d4c41..f784d5ba88 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -105,4 +105,4 @@ catalog: # - allow: [User, Group] kubernetes: - # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options \ No newline at end of file + # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options From f26905d7a88559eb5ca5472a3842afe745e15059 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 14 Jun 2024 11:48:09 -0500 Subject: [PATCH 116/212] Fixed test Signed-off-by: Andre Wanlin --- packages/create-app/src/lib/tasks.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 1f507b95ed..7b17307721 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -88,6 +88,8 @@ jest.mock('./versions', () => ({ '@backstage/plugin-catalog-graph': '1.0.0', '@backstage/plugin-catalog-import': '1.0.0', '@backstage/plugin-catalog-react': '1.0.0', + '@backstage/plugin-kubernetes': '1.0.0', + '@backstage/plugin-kubernetes-backend': '1.0.0', '@backstage/plugin-org': '1.0.0', '@backstage/plugin-scaffolder': '1.0.0', '@backstage/plugin-permission-react': '1.0.0', From fe6ab86d9207cd1c2586aa016efdfbc007915ade Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Sun, 14 Jul 2024 15:24:38 -0400 Subject: [PATCH 117/212] Refactor review state schema processing code to support nested objects Signed-off-by: Stephen Glass --- .../ReviewState/ReviewState.test.tsx | 179 +++++++++++------- .../components/ReviewState/ReviewState.tsx | 55 +++++- .../next/components/ReviewState/util.test.ts | 170 +++-------------- .../src/next/components/ReviewState/util.ts | 55 +----- 4 files changed, 189 insertions(+), 270 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx index dfa1c9c097..194c5f3e77 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx @@ -69,6 +69,9 @@ describe('ReviewState', () => { const formState = { name: 'John Doe', test: 'bob', + nest: { + foo: 'bar', + }, }; const schemas: ParsedTemplateSchema[] = [ @@ -85,6 +88,20 @@ describe('ReviewState', () => { }, }, }, + nest: { + type: 'object', + properties: { + foo: { + type: 'string', + 'ui:widget': 'password', + 'ui:backstage': { + review: { + show: false, + }, + }, + }, + }, + }, }, }, schema: {}, @@ -99,12 +116,16 @@ describe('ReviewState', () => { ); expect(getAllByRole('row').length).toEqual(1); expect(queryByRole('row', { name: 'Name ******' })).not.toBeInTheDocument(); + expect(queryByRole('row', { name: 'Foo ******' })).not.toBeInTheDocument(); }); it('should allow for masking an option with a set text', () => { const formState = { name: 'John Doe', test: 'bob', + nest: { + foo: 'bar', + }, }; const schemas: ParsedTemplateSchema[] = [ @@ -121,6 +142,20 @@ describe('ReviewState', () => { }, }, }, + nest: { + type: 'object', + properties: { + foo: { + type: 'string', + 'ui:widget': 'password', + 'ui:backstage': { + review: { + mask: 'lols', + }, + }, + }, + }, + }, }, }, schema: {}, @@ -135,11 +170,15 @@ describe('ReviewState', () => { ); expect(getByRole('row', { name: 'Name lols' })).toBeInTheDocument(); + expect(getByRole('row', { name: 'Foo lols' })).toBeInTheDocument(); }); it('should display enum label from enumNames', async () => { const formState = { name: 'type2', + nest: { + foo: 'type2', + }, }; const schemas: ParsedTemplateSchema[] = [ @@ -153,6 +192,17 @@ describe('ReviewState', () => { enum: ['type1', 'type2', 'type3'], enumNames: ['Label-type1', 'Label-type2', 'Label-type3'], }, + nest: { + type: 'object', + properties: { + foo: { + type: 'string', + default: 'type1', + enum: ['type1', 'type2', 'type3'], + enumNames: ['Label-type1', 'Label-type2', 'Label-type3'], + }, + }, + }, }, }, schema: {}, @@ -169,6 +219,7 @@ describe('ReviewState', () => { expect( queryByRole('row', { name: 'Name Label-type2' }), ).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Foo Label-type2' })).toBeInTheDocument(); }); it('should display enum value if no corresponding enumNames', async () => { @@ -203,7 +254,7 @@ describe('ReviewState', () => { expect(queryByRole('row', { name: 'Name type4' })).toBeInTheDocument(); }); - it('should display exploded object in separate rows', async () => { + it('should display object in separate rows', async () => { const formState = { name: { foo: 'type3', @@ -218,11 +269,6 @@ describe('ReviewState', () => { properties: { name: { type: 'object', - 'ui:backstage': { - review: { - explode: true, - }, - }, properties: { foo: { type: 'string', @@ -239,7 +285,6 @@ describe('ReviewState', () => { schema: {}, title: 'test', uiSchema: {}, - description: 'asd', }, ]; @@ -251,7 +296,7 @@ describe('ReviewState', () => { expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); }); - it('should display exploded nested objects', async () => { + it('should display nested objects in separate rows', async () => { const formState = { name: { foo: 'type3', @@ -269,11 +314,60 @@ describe('ReviewState', () => { properties: { name: { type: 'object', - 'ui:backstage': { - review: { - explode: true, + properties: { + foo: { + type: 'string', + default: 'type1', + }, + bar: { + type: 'string', + default: 'type2', + }, + example: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, }, }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + }, + ]; + + const { queryByRole } = render( + , + ); + + expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Test type6' })).toBeInTheDocument(); + }); + + it('should display partially nested objects', async () => { + const formState = { + name: { + foo: 'type3', + bar: 'type4', + example: { + test: 'type6', + }, + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', properties: { foo: { type: 'string', @@ -287,7 +381,7 @@ describe('ReviewState', () => { type: 'object', 'ui:backstage': { review: { - explode: true, + explode: false, }, }, properties: { @@ -303,67 +397,6 @@ describe('ReviewState', () => { schema: {}, title: 'test', uiSchema: {}, - description: 'asd', - }, - ]; - - const { queryByRole } = render( - , - ); - - expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); - expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); - expect(queryByRole('row', { name: 'Test type6' })).toBeInTheDocument(); - }); - - it('should display partially exploded nested objects', async () => { - const formState = { - name: { - foo: 'type3', - bar: 'type4', - example: { - test: 'type6', - }, - }, - }; - - const schemas: ParsedTemplateSchema[] = [ - { - mergedSchema: { - type: 'object', - properties: { - name: { - type: 'object', - 'ui:backstage': { - review: { - explode: true, - }, - }, - properties: { - foo: { - type: 'string', - default: 'type1', - }, - bar: { - type: 'string', - default: 'type2', - }, - example: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - schema: {}, - title: 'test', - uiSchema: {}, - description: 'asd', }, ]; diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index d320ad80a6..d87584afff 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -15,10 +15,10 @@ */ import React from 'react'; import { StructuredMetadataTable } from '@backstage/core-components'; -import { JsonObject } from '@backstage/types'; +import { JsonObject, JsonValue } from '@backstage/types'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; -import { processEntry } from './util'; +import { isJsonObject, getLastKey } from './util'; /** * The props for the {@link ReviewState} component. @@ -29,6 +29,55 @@ export type ReviewStateProps = { formState: JsonObject; }; +function processSchema( + key: string, + value: JsonValue | undefined, + schema: ParsedTemplateSchema, + formState: JsonObject, +): [string, JsonValue | undefined][] { + const parsedSchema = new JSONSchema(schema.mergedSchema); + const definitionInSchema = parsedSchema.getSchema({ + pointer: `#/${key}`, + data: formState, + }); + + if (definitionInSchema) { + const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; + if (backstageReviewOptions) { + if (backstageReviewOptions.mask) { + return [[getLastKey(key), backstageReviewOptions.mask]]; + } + if (backstageReviewOptions.show === false) { + return []; + } + } + + if (definitionInSchema['ui:widget'] === 'password') { + return [[getLastKey(key), '******']]; + } + + if (definitionInSchema.enum && definitionInSchema.enumNames) { + return [ + [ + getLastKey(key), + definitionInSchema.enumNames[ + definitionInSchema.enum.indexOf(value) + ] || value, + ], + ]; + } + + if (backstageReviewOptions?.explode !== false && isJsonObject(value)) { + // Recurse nested objects + return Object.entries(value).flatMap(([nestedKey, nestedValue]) => + processSchema(`${key}/${nestedKey}`, nestedValue, schema, formState), + ); + } + } + + return [[getLastKey(key), value]]; +} + /** * The component used by the {@link Stepper} to render the review step. * @alpha @@ -38,7 +87,7 @@ export const ReviewState = (props: ReviewStateProps) => { Object.entries(props.formState) .flatMap(([key, value]) => { for (const step of props.schemas) { - return processEntry(key, value, step, props.formState); + return processSchema(key, value, step, props.formState); } return [[key, value]]; }) diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts index e2a10587dc..9b88ff157e 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { flattenObject, isJsonObject } from './util'; -import { Draft07 as JSONSchema } from 'json-schema-library'; -import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; +import { isJsonObject, getLastKey } from './util'; describe('isJsonObject', () => { it('should return true for non-null objects', () => { @@ -24,10 +22,6 @@ describe('isJsonObject', () => { expect(isJsonObject({ key: 'value' })).toBe(true); }); - it('should return false for null', () => { - expect(isJsonObject(null)).toBe(false); - }); - it('should return false for arrays', () => { expect(isJsonObject([])).toBe(false); expect(isJsonObject([1, 2, 3])).toBe(false); @@ -41,146 +35,40 @@ describe('isJsonObject', () => { }); }); -describe('flattenObject', () => { - it('should handle an empty object', () => { - const schemas: ParsedTemplateSchema[] = [ - { - mergedSchema: { - type: 'object', - properties: { - name: { - type: 'object', - 'ui:backstage': { - review: { - explode: true, - }, - }, - properties: {}, - }, - }, - }, - schema: {}, - title: 'test', - uiSchema: {}, - }, - ]; - - const parsedSchema = new JSONSchema(schemas[0].mergedSchema); - - const result = flattenObject({}, '', parsedSchema, {}); - - expect(result).toEqual([]); +describe('getLastKey', () => { + it('should return the last part of a simple key', () => { + expect(getLastKey('simple')).toBe('simple'); }); - it('should flatten a simple object', () => { - const formState = { - name: { - foo: 'value1', - bar: 'value2', - }, - }; - - const schemas: ParsedTemplateSchema[] = [ - { - mergedSchema: { - type: 'object', - properties: { - name: { - type: 'object', - 'ui:backstage': { - review: { - explode: true, - }, - }, - properties: { - foo: { - type: 'string', - }, - bar: { - type: 'string', - }, - }, - }, - }, - }, - schema: {}, - title: 'test', - uiSchema: {}, - }, - ]; - - const [key, value] = Object.entries(formState)[0]; - const parsedSchema = new JSONSchema(schemas[0].mergedSchema); - - const result = flattenObject(value, key, parsedSchema, formState); - - expect(result).toEqual([ - ['foo', 'value1'], - ['bar', 'value2'], - ]); + it('should return the last part of a nested key', () => { + expect(getLastKey('parent/child')).toBe('child'); }); - it('should recurse into a nested object', () => { - const formState = { - name: { - foo: 'value1', - bar: 'value2', - example: { - test: 'value3', - }, - }, - }; - const schemas: ParsedTemplateSchema[] = [ - { - mergedSchema: { - type: 'object', - properties: { - name: { - type: 'object', - 'ui:backstage': { - review: { - explode: true, - }, - }, - properties: { - foo: { - type: 'string', - }, - bar: { - type: 'string', - }, - example: { - type: 'object', - 'ui:backstage': { - review: { - explode: true, - }, - }, - properties: { - test: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - schema: {}, - title: 'test', - uiSchema: {}, - }, - ]; + it('should return the last part of a deeply nested key', () => { + expect(getLastKey('grandparent/parent/child')).toBe('child'); + }); - const [key, value] = Object.entries(formState)[0]; - const parsedSchema = new JSONSchema(schemas[0].mergedSchema); + it('should handle keys with trailing slash', () => { + expect(getLastKey('parent/child/')).toBe(''); + }); - const result = flattenObject(value, key, parsedSchema, formState); + it('should handle empty string', () => { + expect(getLastKey('')).toBe(''); + }); - expect(result).toEqual([ - ['foo', 'value1'], - ['bar', 'value2'], - ['test', 'value3'], - ]); + it('should handle keys with multiple consecutive slashes', () => { + expect(getLastKey('parent//child')).toBe('child'); + }); + + it('should handle keys with only slashes', () => { + expect(getLastKey('////')).toBe(''); + }); + + it('should handle keys with spaces', () => { + expect(getLastKey('parent/child with spaces')).toBe('child with spaces'); + }); + + it('should handle keys with special characters', () => { + expect(getLastKey('parent/child@!#$%^&*()')).toBe('child@!#$%^&*()'); }); }); diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts index e040b851d3..52503bedfd 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts @@ -16,64 +16,13 @@ */ import { JsonObject, JsonValue } from '@backstage/types'; -import { Draft07 as JSONSchema } from 'json-schema-library'; -import { ParsedTemplateSchema } from '../../hooks'; export function isJsonObject(value?: JsonValue): value is JsonObject { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -export function processEntry( - key: string, - value: JsonValue | undefined, - schema: ParsedTemplateSchema, - formState: JsonObject, -): [string, JsonValue | undefined][] { - const parsedSchema = new JSONSchema(schema.mergedSchema); - const definitionInSchema = parsedSchema.getSchema({ - pointer: `#/${key}`, - data: formState, - }); - - if (definitionInSchema) { - const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; - if (backstageReviewOptions) { - if (backstageReviewOptions.mask) { - return [[getLastKey(key), backstageReviewOptions.mask]]; - } - if (backstageReviewOptions.show === false) { - return []; - } - } - - if (isJsonObject(value)) { - // Process nested objects - return Object.entries(value).flatMap(([nestedKey, nestedValue]) => - processEntry(`${key}/${nestedKey}`, nestedValue, schema, formState), - ); - } - - if (definitionInSchema['ui:widget'] === 'password') { - return [[getLastKey(key), '******']]; - } - - if (definitionInSchema.enum && definitionInSchema.enumNames) { - return [ - [ - getLastKey(key), - definitionInSchema.enumNames[ - definitionInSchema.enum.indexOf(value) - ] || value, - ], - ]; - } - } - - return [[getLastKey(key), value]]; + return typeof value === 'object' && !Array.isArray(value); } // Helper function to get the last part of the key -function getLastKey(key: string): string { +export function getLastKey(key: string): string { const parts = key.split('/'); return parts[parts.length - 1]; } From ba67e2884712d3036c10e3d9b19c126305a5d16d Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Sun, 14 Jul 2024 15:37:16 -0400 Subject: [PATCH 118/212] Remove comment Signed-off-by: Stephen Glass --- plugins/scaffolder-react/src/next/components/ReviewState/util.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts index 52503bedfd..49c3b30e7a 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ /* * Copyright 2022 The Backstage Authors * From 486c7d7b2638263c252ead495ad04113bb32dcdd Mon Sep 17 00:00:00 2001 From: Evan Shortiss Date: Sun, 14 Jul 2024 21:36:53 -0700 Subject: [PATCH 119/212] docs: fix typo and link in contributing.md Signed-off-by: Evan Shortiss --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce0e97c781..f10116844b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,7 @@ You'll see how you get both logs for the frontend `webpack-dev-server` which ser Visit and you should see the bleeding edge of Backstage ready for contributions! -If you want to get a better understanding of the layout of the repo not that you have a local copy running feel free to review the [Backstage Project Structure](https://backstage.io/docs/getting-started/project-structure) documentation. +If you want to get a better understanding of the layout of the repo now that you have a local copy running feel free to review the [Backstage Project Structure](https://backstage.io/docs/getting-started/#general-folder-structure) documentation. ## Coding Guidelines From 8c4d6bca9af7ae412d700d5d049dfa9e4e6665af Mon Sep 17 00:00:00 2001 From: David Weber Date: Mon, 15 Jul 2024 08:11:36 +0200 Subject: [PATCH 120/212] ci: add timeout to cron job Signed-off-by: David Weber Signed-off-by: David Weber --- .github/workflows/cron.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 923293667a..bdbe22aae8 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -7,6 +7,7 @@ on: jobs: cron: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Harden Runner uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 From a5bcb9c4d59af4c50bff0160bec566e49753a3a0 Mon Sep 17 00:00:00 2001 From: Niall Thomson Date: Mon, 15 Jul 2024 05:58:02 -0600 Subject: [PATCH 121/212] Update plugins/catalog-backend-module-aws/src/awsOrganization/config.ts Co-authored-by: Vincenzo Scamporlino Signed-off-by: Niall Thomson --- plugins/catalog-backend-module-aws/src/awsOrganization/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts b/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts index ff00af48e4..ea16394e0a 100644 --- a/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts +++ b/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts @@ -22,6 +22,7 @@ import { Config } from '@backstage/config'; export type AwsOrganizationProviderConfig = { /** * The role to assume for the processor. + * @deprecated Use `accountId` instead. */ roleArn?: string; From 133464cfe04566513fc26633fadd2874a9d61bdc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Jul 2024 13:14:03 +0200 Subject: [PATCH 122/212] cli: add initial experimental support for dynamic plugins Signed-off-by: Patrik Oldsberg --- .changeset/clever-carrots-refuse.md | 5 + .changeset/fair-meals-juggle.md | 5 + packages/cli-node/src/roles/PackageRoles.ts | 5 + packages/cli/package.json | 1 + .../cli/src/commands/build/buildFrontend.ts | 8 + packages/cli/src/commands/build/command.ts | 19 + packages/cli/src/lib/bundler/bundle.ts | 48 +- packages/cli/src/lib/bundler/config.ts | 92 ++- packages/cli/src/lib/bundler/optimization.ts | 7 + packages/cli/src/lib/bundler/types.ts | 9 + yarn.lock | 577 ++++++++++++++++-- 11 files changed, 696 insertions(+), 80 deletions(-) create mode 100644 .changeset/clever-carrots-refuse.md create mode 100644 .changeset/fair-meals-juggle.md diff --git a/.changeset/clever-carrots-refuse.md b/.changeset/clever-carrots-refuse.md new file mode 100644 index 0000000000..24217b1045 --- /dev/null +++ b/.changeset/clever-carrots-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added internal metadata for the new experimental `frontend-dynamic-container` role. diff --git a/.changeset/fair-meals-juggle.md b/.changeset/fair-meals-juggle.md new file mode 100644 index 0000000000..94db114170 --- /dev/null +++ b/.changeset/fair-meals-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added experimental support for dynamic frontend plugin builds, enabled via setting `EXPERIMENTAL_MODULE_FEDERATION` for the app build, and using the `frontend-dynamic-container` package role to create a container. Both of these are experimental and will change in the future. diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index 401568ad6d..70b62e2479 100644 --- a/packages/cli-node/src/roles/PackageRoles.ts +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -58,6 +58,11 @@ const packageRoleInfos: PackageRoleInfo[] = [ platform: 'web', output: ['types', 'esm'], }, + { + role: 'frontend-dynamic-container' as PackageRole, // experimental + platform: 'web', + output: ['bundle'], + }, { role: 'backend-plugin', platform: 'node', diff --git a/packages/cli/package.json b/packages/cli/package.json index 393f1217e1..97c15b16f2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -53,6 +53,7 @@ "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", + "@module-federation/enhanced": "^0.1.19", "@octokit/graphql": "^5.0.0", "@octokit/graphql-schema": "^13.7.0", "@octokit/oauth-app": "^4.2.0", diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 6d7ca2e6a9..931a3b376a 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -24,6 +24,7 @@ interface BuildAppOptions { targetDir: string; writeStats: boolean; configPaths: string[]; + moduleFederationMode?: 'host' | 'remote'; } export async function buildFrontend(options: BuildAppOptions) { @@ -34,6 +35,13 @@ export async function buildFrontend(options: BuildAppOptions) { entry: 'src/index', parallelism: getEnvironmentParallelism(), statsJsonEnabled: writeStats, + moduleFederation: options.moduleFederationMode && { + // The default output mode requires the name to be a usable as a code + // symbol, there might be better options here but for now we need to + // sanitize the name. + name: name.replaceAll('@', '').replaceAll(/[/\\_-]/g, '_'), + mode: options.moduleFederationMode, + }, ...(await loadCliConfig({ args: configPaths, fromPackage: name, diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index f15ae27ee0..285cba0e81 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -22,6 +22,7 @@ import { paths } from '../../lib/paths'; import { buildFrontend } from './buildFrontend'; import { buildBackend } from './buildBackend'; import { isValidUrl } from '../../lib/urls'; +import chalk from 'chalk'; export async function command(opts: OptionValues): Promise { const role = await findRoleFromCommand(opts); @@ -39,6 +40,9 @@ export async function command(opts: OptionValues): Promise { targetDir: paths.targetDir, configPaths, writeStats: Boolean(opts.stats), + moduleFederationMode: process.env.EXPERIMENTAL_MODULE_FEDERATION + ? 'host' + : undefined, }); } return buildBackend({ @@ -49,6 +53,21 @@ export async function command(opts: OptionValues): Promise { }); } + // experimental + if ((role as string) === 'frontend-dynamic-container') { + console.log( + chalk.yellow( + `⚠️ WARNING: The 'frontend-dynamic-container' package role is experimental and will receive immediate breaking changes in the future.`, + ), + ); + return buildFrontend({ + targetDir: paths.targetDir, + configPaths: [], + writeStats: Boolean(opts.stats), + moduleFederationMode: 'remote', + }); + } + const roleInfo = PackageRoles.getRoleInfo(role); const outputs = new Set(); diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 232657a3f2..d24d4252f4 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -46,37 +46,45 @@ export async function buildBundle(options: BuildOptions) { dist: 'dist/public', }); - const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ - config: options.fullConfig, - targetPath: paths.targetPath, - }); - const commonConfigOptions = { ...options, checksEnabled: false, isDev: false, getFrontendAppConfigs: () => options.frontendAppConfigs, }; - const configs = [ - await createConfig(paths, { - ...commonConfigOptions, - additionalEntryPoints: detectedModulesEntryPoint, - appMode: publicPaths ? 'protected' : 'public', - }), - ]; - if (publicPaths) { - console.log( - chalk.yellow( - `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`, - ), - ); + const configs = []; + + if (options.moduleFederation?.mode === 'remote') { + // Package detection is disabled for remote bundles + configs.push(await createConfig(paths, commonConfigOptions)); + } else { + const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ + config: options.fullConfig, + targetPath: paths.targetPath, + }); + configs.push( - await createConfig(publicPaths, { + await createConfig(paths, { ...commonConfigOptions, - appMode: 'public', + additionalEntryPoints: detectedModulesEntryPoint, + appMode: publicPaths ? 'protected' : 'public', }), ); + + if (publicPaths) { + console.log( + chalk.yellow( + `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`, + ), + ); + configs.push( + await createConfig(publicPaths, { + ...commonConfigOptions, + appMode: 'public', + }), + ); + } } const isCi = yn(process.env.CI, { default: false }); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 53db7f3597..c2621b20a4 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -24,6 +24,7 @@ import { Config } from '@backstage/config'; import ESLintPlugin from 'eslint-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; +import { ModuleFederationPlugin } from '@module-federation/enhanced'; import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin'; @@ -128,18 +129,81 @@ export async function createConfig( }), ); - plugins.push( - new HtmlWebpackPlugin({ - meta: { - 'backstage-app-mode': options?.appMode ?? 'public', - }, - template: paths.targetHtml, - templateParameters: { - publicPath, - config: frontendConfig, - }, - }), - ); + if (options.moduleFederation?.mode !== 'remote') { + plugins.push( + new HtmlWebpackPlugin({ + meta: { + 'backstage-app-mode': options?.appMode ?? 'public', + }, + template: paths.targetHtml, + templateParameters: { + publicPath, + config: frontendConfig, + }, + }), + ); + } + + if (options.moduleFederation) { + plugins.push( + new ModuleFederationPlugin({ + ...(options.moduleFederation?.mode === 'remote' && { + filename: 'remoteEntry.js', + exposes: { + '.': paths.targetEntry, + }, + }), + name: options.moduleFederation.name, + runtime: false, + shared: { + // React + react: { + singleton: true, + requiredVersion: '*', + eager: true, + }, + 'react-dom': { + singleton: true, + requiredVersion: '*', + eager: true, + }, + // React Router + 'react-router': { + singleton: true, + requiredVersion: '*', + eager: true, + }, + 'react-router-dom': { + singleton: true, + requiredVersion: '*', + eager: true, + }, + // MUI v4 + '@material-ui/core/styles': { + singleton: true, + requiredVersion: '*', + eager: true, + }, + '@material-ui/styles': { + singleton: true, + requiredVersion: '*', + eager: true, + }, + // MUI v5 + '@mui/material/styles/': { + singleton: true, + requiredVersion: '*', + eager: true, + }, + '@emotion/react': { + singleton: true, + requiredVersion: '*', + eager: true, + }, + }, + }), + ); + } const buildInfo = await readBuildInfo(); plugins.push( @@ -211,8 +275,10 @@ export async function createConfig( rules: loaders, }, output: { + uniqueName: options.moduleFederation?.name, path: paths.targetDist, - publicPath: `${publicPath}/`, + publicPath: + options.moduleFederation?.mode === 'remote' ? 'auto' : `${publicPath}/`, filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js', chunkFilename: isDev ? '[name].chunk.js' diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index 88a0343099..891e0793fe 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -30,6 +30,13 @@ export const optimization = ( new EsbuildPlugin({ target: 'es2019', format: 'iife', + exclude: 'remoteEntry.js', + }), + // Avoid iife wrapping of module federation remote entry as it breaks the variable assignment + new EsbuildPlugin({ + target: 'es2019', + format: undefined, + include: 'remoteEntry.js', }), ], runtimeChunk: 'single', diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 85a641465f..7291ac0818 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -18,6 +18,13 @@ import { AppConfig, Config } from '@backstage/config'; import { BundlingPathsOptions } from './paths'; import { ConfigSchema } from '@backstage/config-loader'; +export type ModuleFederationOptions = { + // Unique name for this module federation bundle + name: string; + // Whether this is a host or remote bundle + mode: 'host' | 'remote'; +}; + export type BundlingOptions = { checksEnabled: boolean; isDev: boolean; @@ -29,6 +36,7 @@ export type BundlingOptions = { publicSubPath?: string; // Mode that the app is running in, 'protected' or 'public', default is 'public' appMode?: string; + moduleFederation?: ModuleFederationOptions; }; export type ServeOptions = BundlingPathsOptions & { @@ -46,6 +54,7 @@ export type BuildOptions = BundlingPathsOptions & { frontendConfig: Config; frontendAppConfigs: AppConfig[]; fullConfig: Config; + moduleFederation?: ModuleFederationOptions; }; export type BackendBundlingOptions = { diff --git a/yarn.lock b/yarn.lock index afe89e4263..14f731a803 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3917,6 +3917,7 @@ __metadata: "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": ^1.1.3 + "@module-federation/enhanced": ^0.1.19 "@octokit/graphql": ^5.0.0 "@octokit/graphql-schema": ^13.7.0 "@octokit/oauth-app": ^4.2.0 @@ -11038,6 +11039,145 @@ __metadata: languageName: node linkType: hard +"@module-federation/dts-plugin@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/dts-plugin@npm:0.1.21" + dependencies: + "@module-federation/managers": 0.1.21 + "@module-federation/sdk": 0.1.21 + "@module-federation/third-party-dts-extractor": 0.1.21 + adm-zip: ^0.5.10 + ansi-colors: ^4.1.3 + axios: ^1.6.7 + chalk: 3.0.0 + fs-extra: 9.1.0 + isomorphic-ws: 5.0.0 + koa: 2.11.0 + lodash.clonedeepwith: 4.5.0 + log4js: 6.9.1 + node-schedule: 2.1.1 + rambda: ^9.1.0 + ws: 8.17.0 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ^1.0.24 + peerDependenciesMeta: + vue-tsc: + optional: true + checksum: ec4cd030a25617698754cbac2da5463f8942cdd0a64bdc95f6ff5fd29fff6b88cf3db90e53e6b260cd3593893fe6ee2d6e149d0a46698eb7f9cc19a9df26193d + languageName: node + linkType: hard + +"@module-federation/enhanced@npm:^0.1.19": + version: 0.1.21 + resolution: "@module-federation/enhanced@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/manifest": 0.1.21 + "@module-federation/rspack": 0.1.21 + "@module-federation/runtime-tools": 0.1.21 + "@module-federation/sdk": 0.1.21 + upath: 2.0.1 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ^1.0.24 + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + checksum: bc0ff541db1066b290b3ad9ab868437dc3d0754b9d06ab263f8fb7f54e08238eae9232a482a681dd6152e4a175578d65df9f27fe181fc8fb602d7cc1ae34807d + languageName: node + linkType: hard + +"@module-federation/managers@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/managers@npm:0.1.21" + dependencies: + "@module-federation/sdk": 0.1.21 + find-pkg: 2.0.0 + fs-extra: 9.1.0 + checksum: 5f230d5795d86dfd68c404ee2b7a1264950c283a4b1c6f4ee9cc9579fabb413718dfbc1ff726b9c213f9d3223d944dd38dd9d04b700962e6398c3c3728d6323e + languageName: node + linkType: hard + +"@module-federation/manifest@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/manifest@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/sdk": 0.1.21 + chalk: 3.0.0 + find-pkg: 2.0.0 + checksum: cef2011875f14e853a355626ae1dbc8ae3b0714d31140e329b5dd71525782b08c2e1d6ca45276a563bb3c3b7f7c4e64a31f0698ef12606f05aa6da46e759f345 + languageName: node + linkType: hard + +"@module-federation/rspack@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/rspack@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/manifest": 0.1.21 + "@module-federation/runtime-tools": 0.1.21 + "@module-federation/sdk": 0.1.21 + checksum: 55516285e23f4ca7127afafb14af667defbe46dc3224f85d7e07edbc8937d7fac909dfebc2f9dd73120b99bbe5135372cf0fbbe282990d80e6953a60dfa4c93e + languageName: node + linkType: hard + +"@module-federation/runtime-tools@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/runtime-tools@npm:0.1.21" + dependencies: + "@module-federation/runtime": 0.1.21 + "@module-federation/webpack-bundler-runtime": 0.1.21 + checksum: 628c0c4834093520f9c71481d587c9e18163f82e481b05b1900f04e2d5da4abb69af6d814ac5cd1951057b28d73f3adeb1cee7cd83628305b10cc7988405fbc5 + languageName: node + linkType: hard + +"@module-federation/runtime@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/runtime@npm:0.1.21" + dependencies: + "@module-federation/sdk": 0.1.21 + checksum: ce4de8515b54f1cd07a3c7c4cbd35fea163294b9fb24be10827872f3ebb62cd5c289f3602efe4149d963282739f79b51947afa039ee6f36be7f66dea83d590fc + languageName: node + linkType: hard + +"@module-federation/sdk@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/sdk@npm:0.1.21" + checksum: 6856dcfe2ef5ae939890b82010aaad911fa6c4330a05f290ae054c316c9b532d3691456a1f9e176fe05f1df2d6f2d8c7e0c842ca5648a0fd7abf270e44ed9ecb + languageName: node + linkType: hard + +"@module-federation/third-party-dts-extractor@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/third-party-dts-extractor@npm:0.1.21" + dependencies: + find-pkg: 2.0.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + checksum: e394fd7c2e6dbdf8df6937628680e7356ac897ee6f1309d7fbc38c00bcf4be9c0363f8bc1a75c29f7987a5a2f11f7855481813889b18e8b444ee9006aeb4a299 + languageName: node + linkType: hard + +"@module-federation/webpack-bundler-runtime@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/webpack-bundler-runtime@npm:0.1.21" + dependencies: + "@module-federation/runtime": 0.1.21 + "@module-federation/sdk": 0.1.21 + checksum: 7d96002066e63bdb503964fd5fb2798be25f4135a599d87721f4d26ebe1de1affbf447c56b082f7ee850ae7798d0ac637f6a486f58591269065e114051b466e5 + languageName: node + linkType: hard + "@motionone/animation@npm:^10.12.0": version: 10.16.3 resolution: "@motionone/animation@npm:10.16.3" @@ -20325,7 +20465,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": +"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -20402,6 +20542,13 @@ __metadata: languageName: node linkType: hard +"adm-zip@npm:^0.5.10": + version: 0.5.14 + resolution: "adm-zip@npm:0.5.14" + checksum: 83a4bc0bdff70f3276992810db96b10ffbf3c07667a9c3395e3dc60ca40ecbb920812dde212e1e21633b1508f1ea6fc427ac451aa68ee1a597b7251b2c1fc844 + languageName: node + linkType: hard + "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -20695,7 +20842,7 @@ __metadata: languageName: node linkType: hard -"any-promise@npm:^1.0.0": +"any-promise@npm:^1.0.0, any-promise@npm:^1.1.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" checksum: 0ee8a9bdbe882c90464d75d1f55cf027f5458650c4bd1f0467e65aec38ccccda07ca5844969ee77ed46d04e7dded3eaceb027e8d32f385688523fe305fa7e1de @@ -21342,7 +21489,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.6.8, axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0": +"axios@npm:1.6.8": version: 1.6.8 resolution: "axios@npm:1.6.8" dependencies: @@ -21363,6 +21510,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0, axios@npm:^1.6.7": + version: 1.7.2 + resolution: "axios@npm:1.7.2" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: e457e2b0ab748504621f6fa6609074ac08c824bf0881592209dfa15098ece7e88495300e02cd22ba50b3468fd712fe687e629dcb03d6a3f6a51989727405aedf + languageName: node + linkType: hard + "axobject-query@npm:^3.2.1": version: 3.2.1 resolution: "axobject-query@npm:3.2.1" @@ -22292,6 +22450,16 @@ __metadata: languageName: node linkType: hard +"cache-content-type@npm:^1.0.0": + version: 1.0.1 + resolution: "cache-content-type@npm:1.0.1" + dependencies: + mime-types: ^2.1.18 + ylru: ^1.2.0 + checksum: 18db4d59452669ccbfd7146a1510a37eb28e9eccf18ca7a4eb603dff2edc5cccdca7498fc3042a2978f76f11151fba486eb9eb69d9afa3fb124957870aef4fd3 + languageName: node + linkType: hard + "cacheable-lookup@npm:^5.0.3": version: 5.0.3 resolution: "cacheable-lookup@npm:5.0.3" @@ -22457,6 +22625,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:3.0.0, chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505 + languageName: node + linkType: hard + "chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -22467,16 +22645,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^3.0.0": - version: 3.0.0 - resolution: "chalk@npm:3.0.0" - dependencies: - ansi-styles: ^4.1.0 - supports-color: ^7.1.0 - checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505 - languageName: node - linkType: hard - "chalk@npm:~5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -23478,7 +23646,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.3": +"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.2": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -23487,7 +23655,7 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": +"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 @@ -23574,6 +23742,16 @@ __metadata: languageName: node linkType: hard +"cookies@npm:~0.8.0": + version: 0.8.0 + resolution: "cookies@npm:0.8.0" + dependencies: + depd: ~2.0.0 + keygrip: ~1.1.0 + checksum: 806055a44f128705265b1bc6a853058da18bf80dea3654ad99be20985b1fa1b14f86c1eef73644aab8071241f8a78acd57202b54c4c5c70769fc694fbb9c4edc + languageName: node + linkType: hard + "copy-to-clipboard@npm:^3.2.0, copy-to-clipboard@npm:^3.3.1": version: 3.3.1 resolution: "copy-to-clipboard@npm:3.3.1" @@ -23791,6 +23969,15 @@ __metadata: languageName: node linkType: hard +"cron-parser@npm:^4.2.0": + version: 4.9.0 + resolution: "cron-parser@npm:4.9.0" + dependencies: + luxon: ^3.2.1 + checksum: 3cf248fc5cae6c19ec7124962b1cd84b76f02b9bc4f58976b3bd07624db3ef10aaf1548efcc2d2dcdab0dad4f12029d640a55ecce05ea5e1596af9db585502cf + languageName: node + linkType: hard + "cron@npm:^3.0.0": version: 3.1.7 resolution: "cron@npm:3.1.7" @@ -24400,6 +24587,13 @@ __metadata: languageName: node linkType: hard +"date-format@npm:^4.0.14": + version: 4.0.14 + resolution: "date-format@npm:4.0.14" + checksum: dfe5139df6af5759b9dd3c007b899b3f60d45a9240ffeee6314ab74e6ab52e9b519a44ccf285888bdd6b626c66ee9b4c8a523075fa1140617b5beb1cbb9b18d1 + languageName: node + linkType: hard + "dateformat@npm:^3.0.3": version: 3.0.3 resolution: "dateformat@npm:3.0.3" @@ -24460,6 +24654,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:~3.1.0": + version: 3.1.0 + resolution: "debug@npm:3.1.0" + dependencies: + ms: 2.0.0 + checksum: 0b52718ab957254a5b3ca07fc34543bc778f358620c206a08452251eb7fc193c3ea3505072acbf4350219c14e2d71ceb7bdaa0d3370aa630b50da790458d08b3 + languageName: node + linkType: hard + "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -24529,6 +24732,13 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:~1.0.1": + version: 1.0.1 + resolution: "deep-equal@npm:1.0.1" + checksum: 5af8cbfcebf190491878a498caccc7dc9592f8ebd1685b976eacc3825619d222b5e929923163b92c4f414494e2b884f7ebf00c022e8198e8292deb70dd9785f4 + languageName: node + linkType: hard + "deep-extend@npm:0.6.0, deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -24735,7 +24945,7 @@ __metadata: languageName: node linkType: hard -"destroy@npm:1.2.0": +"destroy@npm:1.2.0, destroy@npm:^1.0.4": version: 1.2.0 resolution: "destroy@npm:1.2.0" checksum: 0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 @@ -25401,7 +25611,7 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:~1.0.2": +"encodeurl@npm:^1.0.2, encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" checksum: e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c @@ -25490,6 +25700,13 @@ __metadata: languageName: node linkType: hard +"error-inject@npm:^1.0.0": + version: 1.0.0 + resolution: "error-inject@npm:1.0.0" + checksum: 258cb26c7c7e04d9b730d074926ff5e18755b6945781540fdd124cafc5015610d97e4b971eb3226469f407fd34fa899a60fbcf9ade8923ab42fa2a3c61e246cf + languageName: node + linkType: hard + "error-stack-parser@npm:^2.0.6": version: 2.0.6 resolution: "error-stack-parser@npm:2.0.6" @@ -26861,6 +27078,15 @@ __metadata: languageName: node linkType: hard +"expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": + version: 2.0.2 + resolution: "expand-tilde@npm:2.0.2" + dependencies: + homedir-polyfill: ^1.0.1 + checksum: 2efe6ed407d229981b1b6ceb552438fbc9e5c7d6a6751ad6ced3e0aa5cf12f0b299da695e90d6c2ac79191b5c53c613e508f7149e4573abfbb540698ddb7301a + languageName: node + linkType: hard + "expect@npm:>28.1.3, expect@npm:^29.0.0, expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" @@ -27400,6 +27626,15 @@ __metadata: languageName: node linkType: hard +"find-file-up@npm:^2.0.1": + version: 2.0.1 + resolution: "find-file-up@npm:2.0.1" + dependencies: + resolve-dir: ^1.0.1 + checksum: dfe820bfb80e75bed5dd5080057858c0ad2393e1438c48a3bb682663e9ecdcfbe3224ed4768bfedd00776800b4ae76bc8953d250d15ae3feabf381d2c6d04268 + languageName: node + linkType: hard + "find-my-way@npm:^4.3.3": version: 4.5.1 resolution: "find-my-way@npm:4.5.1" @@ -27412,6 +27647,15 @@ __metadata: languageName: node linkType: hard +"find-pkg@npm:2.0.0": + version: 2.0.0 + resolution: "find-pkg@npm:2.0.0" + dependencies: + find-file-up: ^2.0.1 + checksum: 44785204c8bbbdfeaece6b834ba81a35163421c30e20f531281d26e6b5890663d7ac884b82a9aebf6ce23e479336cd6f70ea5597da35495c16abdeba2fd4f845 + languageName: node + linkType: hard + "find-process@npm:^1.4.5": version: 1.4.7 resolution: "find-process@npm:1.4.7" @@ -27497,7 +27741,7 @@ __metadata: languageName: node linkType: hard -"flatted@npm:3.3.1, flatted@npm:^3.1.0": +"flatted@npm:3.3.1, flatted@npm:^3.1.0, flatted@npm:^3.2.7": version: 3.3.1 resolution: "flatted@npm:3.3.1" checksum: 85ae7181650bb728c221e7644cbc9f4bf28bc556f2fc89bb21266962bdf0ce1029cc7acc44bb646cd469d9baac7c317f64e841c4c4c00516afa97320cdac7f94 @@ -27727,7 +27971,7 @@ __metadata: languageName: node linkType: hard -"fresh@npm:0.5.2, fresh@npm:^0.5.2": +"fresh@npm:0.5.2, fresh@npm:^0.5.2, fresh@npm:~0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 @@ -27769,6 +28013,18 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:9.1.0, fs-extra@npm:^9.0.0": + version: 9.1.0 + resolution: "fs-extra@npm:9.1.0" + dependencies: + at-least-node: ^1.0.0 + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: ba71ba32e0faa74ab931b7a0031d1523c66a73e225de7426e275e238e312d07313d2da2d33e34a52aa406c8763ade5712eb3ec9ba4d9edce652bcacdc29e6b20 + languageName: node + linkType: hard + "fs-extra@npm:^11.0.0, fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" @@ -27802,18 +28058,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^9.0.0": - version: 9.1.0 - resolution: "fs-extra@npm:9.1.0" - dependencies: - at-least-node: ^1.0.0 - graceful-fs: ^4.2.0 - jsonfile: ^6.0.1 - universalify: ^2.0.0 - checksum: ba71ba32e0faa74ab931b7a0031d1523c66a73e225de7426e275e238e312d07313d2da2d33e34a52aa406c8763ade5712eb3ec9ba4d9edce652bcacdc29e6b20 - languageName: node - linkType: hard - "fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -28265,6 +28509,17 @@ __metadata: languageName: node linkType: hard +"global-modules@npm:^1.0.0": + version: 1.0.0 + resolution: "global-modules@npm:1.0.0" + dependencies: + global-prefix: ^1.0.1 + is-windows: ^1.0.1 + resolve-dir: ^1.0.0 + checksum: 10be68796c1e1abc1e2ba87ec4ea507f5629873b119ab0cd29c07284ef2b930f1402d10df01beccb7391dedd9cd479611dd6a24311c71be58937beaf18edf85e + languageName: node + linkType: hard + "global-modules@npm:^2.0.0": version: 2.0.0 resolution: "global-modules@npm:2.0.0" @@ -28274,6 +28529,19 @@ __metadata: languageName: node linkType: hard +"global-prefix@npm:^1.0.1": + version: 1.0.2 + resolution: "global-prefix@npm:1.0.2" + dependencies: + expand-tilde: ^2.0.2 + homedir-polyfill: ^1.0.1 + ini: ^1.3.4 + is-windows: ^1.0.1 + which: ^1.2.14 + checksum: 061b43470fe498271bcd514e7746e8a8535032b17ab9570517014ae27d700ff0dca749f76bbde13ba384d185be4310d8ba5712cb0e74f7d54d59390db63dd9a0 + languageName: node + linkType: hard + "global-prefix@npm:^3.0.0": version: 3.0.0 resolution: "global-prefix@npm:3.0.0" @@ -28895,6 +29163,15 @@ __metadata: languageName: node linkType: hard +"homedir-polyfill@npm:^1.0.1": + version: 1.0.3 + resolution: "homedir-polyfill@npm:1.0.3" + dependencies: + parse-passwd: ^1.0.0 + checksum: 18dd4db87052c6a2179d1813adea0c4bfcfa4f9996f0e226fefb29eb3d548e564350fa28ec46b0bf1fbc0a1d2d6922ceceb80093115ea45ff8842a4990139250 + languageName: node + linkType: hard + "hoopy@npm:^0.1.4": version: 0.1.4 resolution: "hoopy@npm:0.1.4" @@ -29019,6 +29296,16 @@ __metadata: languageName: node linkType: hard +"http-assert@npm:^1.3.0": + version: 1.5.0 + resolution: "http-assert@npm:1.5.0" + dependencies: + deep-equal: ~1.0.1 + http-errors: ~1.8.0 + checksum: 69c9b3c14cf8b2822916360a365089ce936c883c49068f91c365eccba5c141a9964d19fdda589150a480013bf503bf37d8936c732e9635819339e730ab0e7527 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -29057,6 +29344,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:^1.6.3, http-errors@npm:~1.8.0": + version: 1.8.1 + resolution: "http-errors@npm:1.8.1" + dependencies: + depd: ~1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: ">= 1.5.0 < 2" + toidentifier: 1.0.1 + checksum: d3c7e7e776fd51c0a812baff570bdf06fe49a5dc448b700ab6171b1250e4cf7db8b8f4c0b133e4bfe2451022a5790c1ca6c2cae4094dedd6ac8304a1267f91d2 + languageName: node + linkType: hard + "http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3" @@ -29520,7 +29820,7 @@ __metadata: languageName: node linkType: hard -"ini@npm:^1.3.5, ini@npm:~1.3.0": +"ini@npm:^1.3.4, ini@npm:^1.3.5, ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 @@ -30417,7 +30717,7 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.0, is-windows@npm:^1.0.2": +"is-windows@npm:^1.0.0, is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -31989,6 +32289,15 @@ __metadata: languageName: node linkType: hard +"keygrip@npm:~1.1.0": + version: 1.1.0 + resolution: "keygrip@npm:1.1.0" + dependencies: + tsscmp: 1.0.6 + checksum: 078cd16a463d187121f0a27c1c9c95c52ad392b620f823431689f345a0501132cee60f6e96914b07d570105af470b96960402accd6c48a0b1f3cd8fac4fa2cae + languageName: node + linkType: hard + "keyv@npm:*, keyv@npm:^4.0.0, keyv@npm:^4.5.2": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -32099,6 +32408,64 @@ __metadata: languageName: node linkType: hard +"koa-compose@npm:^3.0.0": + version: 3.2.1 + resolution: "koa-compose@npm:3.2.1" + dependencies: + any-promise: ^1.1.0 + checksum: ff8e5fc0348455acf751179c6c613eb030a5fac6406d3b49ae9e00460b7ee8770db3ef62633fd3db0306cd4a6d2a0b5152399ebd5bb5e684418f9eeeb251c2de + languageName: node + linkType: hard + +"koa-compose@npm:^4.1.0": + version: 4.1.0 + resolution: "koa-compose@npm:4.1.0" + checksum: 46cb16792d96425e977c2ae4e5cb04930280740e907242ec9c25e3fb8b4a1d7b54451d7432bc24f40ec62255edea71894d2ceeb8238501842b4e48014f2e83db + languageName: node + linkType: hard + +"koa-convert@npm:^1.2.0": + version: 1.2.0 + resolution: "koa-convert@npm:1.2.0" + dependencies: + co: ^4.6.0 + koa-compose: ^3.0.0 + checksum: a33944dbda4ed87565985f5b37ba1122a012db872724b216b6fd8f9176d4bba42c4a9bf3c129330e45f6474d28f50ca0ed28d41b9bccd2ab5d36d6436cf0d676 + languageName: node + linkType: hard + +"koa@npm:2.11.0": + version: 2.11.0 + resolution: "koa@npm:2.11.0" + dependencies: + accepts: ^1.3.5 + cache-content-type: ^1.0.0 + content-disposition: ~0.5.2 + content-type: ^1.0.4 + cookies: ~0.8.0 + debug: ~3.1.0 + delegates: ^1.0.0 + depd: ^1.1.2 + destroy: ^1.0.4 + encodeurl: ^1.0.2 + error-inject: ^1.0.0 + escape-html: ^1.0.3 + fresh: ~0.5.2 + http-assert: ^1.3.0 + http-errors: ^1.6.3 + is-generator-function: ^1.0.7 + koa-compose: ^4.1.0 + koa-convert: ^1.2.0 + on-finished: ^2.3.0 + only: ~0.0.2 + parseurl: ^1.3.2 + statuses: ^1.5.0 + type-is: ^1.6.16 + vary: ^1.1.2 + checksum: b08e1aea03e70fe4ff6e35dee9f9e979e8608461ee1002f6e8dd72f45fc49404873888ea9a3aab2904e24bf43522df7c601033522f4151189e4055e87f94a979 + languageName: node + linkType: hard + "kubernetes-models@npm:^4.1.0, kubernetes-models@npm:^4.3.1": version: 4.3.1 resolution: "kubernetes-models@npm:4.3.1" @@ -32445,6 +32812,13 @@ __metadata: languageName: node linkType: hard +"lodash.clonedeepwith@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeepwith@npm:4.5.0" + checksum: 9fbf4ebfa04b381df226a2298eba680327bea3d0d5d19c5118de7ae218fd219186e30e9fd0d33b13729f34ffbc83c1cf09cb27aff265ba94cb602b8a2b1e71c9 + languageName: node + linkType: hard + "lodash.curry@npm:^4.0.1, lodash.curry@npm:^4.1.1": version: 4.1.1 resolution: "lodash.curry@npm:4.1.1" @@ -32689,6 +33063,19 @@ __metadata: languageName: node linkType: hard +"log4js@npm:6.9.1": + version: 6.9.1 + resolution: "log4js@npm:6.9.1" + dependencies: + date-format: ^4.0.14 + debug: ^4.3.4 + flatted: ^3.2.7 + rfdc: ^1.3.0 + streamroller: ^3.1.5 + checksum: 59d98c37d4163138dab5d9b06ae26965d1353106fece143973d57b1003b3a482791aa21374fd2cca81a953b8837b2f9756ac225404e60cbfa4dd3ab59f082e2e + languageName: node + linkType: hard + "log@npm:^6.3.1": version: 6.3.1 resolution: "log@npm:6.3.1" @@ -32725,6 +33112,13 @@ __metadata: languageName: node linkType: hard +"long-timeout@npm:0.1.1": + version: 0.1.1 + resolution: "long-timeout@npm:0.1.1" + checksum: 48668e5362cb74c4b77a6b833d59f149b9bb9e99c5a5097609807e2597cd0920613b2a42b89bd0870848298be3691064d95599a04ae010023d07dba39932afa7 + languageName: node + linkType: hard + "long@npm:^5.0.0, long@npm:^5.2.1": version: 5.2.3 resolution: "long@npm:5.2.3" @@ -32862,7 +33256,7 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.0.0, luxon@npm:^3.4.3, luxon@npm:~3.4.0": +"luxon@npm:^3.0.0, luxon@npm:^3.2.1, luxon@npm:^3.4.3, luxon@npm:~3.4.0": version: 3.4.4 resolution: "luxon@npm:3.4.4" checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c @@ -33808,7 +34202,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -34856,6 +35250,17 @@ __metadata: languageName: node linkType: hard +"node-schedule@npm:2.1.1": + version: 2.1.1 + resolution: "node-schedule@npm:2.1.1" + dependencies: + cron-parser: ^4.2.0 + long-timeout: 0.1.1 + sorted-array-functions: ^1.3.0 + checksum: 6a8822b16fb024277c42efe710bdb35b6f1f6ab3a2f826283640511247d693f34ebd5ddf2863cd91609e7f323574e36c81cd2084dc204fa521f931380f0f963f + languageName: node + linkType: hard + "node-stdlib-browser@npm:^1.2.0": version: 1.2.0 resolution: "node-stdlib-browser@npm:1.2.0" @@ -35450,6 +35855,13 @@ __metadata: languageName: node linkType: hard +"only@npm:~0.0.2": + version: 0.0.2 + resolution: "only@npm:0.0.2" + checksum: d399710db867a1ef436dd3ce74499c87ece794aa81ab0370b5d153968766ee4aed2f98d3f92fc87c963e45b7a74d400d6f463ef651a5e7cfb861b15e88e9efe6 + languageName: node + linkType: hard + "ono@npm:^7.1.3": version: 7.1.3 resolution: "ono@npm:7.1.3" @@ -35971,6 +36383,13 @@ __metadata: languageName: node linkType: hard +"parse-passwd@npm:^1.0.0": + version: 1.0.0 + resolution: "parse-passwd@npm:1.0.0" + checksum: 4e55e0231d58f828a41d0f1da2bf2ff7bcef8f4cb6146e69d16ce499190de58b06199e6bd9b17fbf0d4d8aef9052099cdf8c4f13a6294b1a522e8e958073066e + languageName: node + linkType: hard + "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -36005,7 +36424,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": +"parseurl@npm:^1.3.2, parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 @@ -37802,6 +38221,13 @@ __metadata: languageName: node linkType: hard +"rambda@npm:^9.1.0": + version: 9.2.1 + resolution: "rambda@npm:9.2.1" + checksum: d877b61f2c1e8a1878ca1e40a181702b360f6c3978bf0e55e62315a1aa215863b7ecb8f00077e2bc0f1ca0d455387fafd7a84b5c8dd004eb3cf071b15827cff7 + languageName: node + linkType: hard + "ramda-adjunct@npm:^5.0.0": version: 5.0.1 resolution: "ramda-adjunct@npm:5.0.1" @@ -39199,6 +39625,16 @@ __metadata: languageName: node linkType: hard +"resolve-dir@npm:^1.0.0, resolve-dir@npm:^1.0.1": + version: 1.0.1 + resolution: "resolve-dir@npm:1.0.1" + dependencies: + expand-tilde: ^2.0.0 + global-modules: ^1.0.0 + checksum: ef736b8ed60d6645c3b573da17d329bfb50ec4e1d6c5ffd6df49e3497acef9226f9810ea6823b8ece1560e01dcb13f77a9f6180d4f242d00cc9a8f4de909c65c + languageName: node + linkType: hard + "resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -39227,7 +39663,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1": +"resolve@npm:1.22.8, resolve@npm:^1.1.6, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -39263,7 +39699,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin, resolve@patch:resolve@~1.22.1#~builtin": +"resolve@patch:resolve@1.22.8#~builtin, resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin, resolve@patch:resolve@~1.22.1#~builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -39398,10 +39834,10 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.3.1": - version: 1.3.1 - resolution: "rfdc@npm:1.3.1" - checksum: d5d1e930aeac7e0e0a485f97db1356e388bdbeff34906d206fe524dd5ada76e95f186944d2e68307183fdc39a54928d4426bbb6734851692cfe9195efba58b79 +"rfdc@npm:^1.3.0, rfdc@npm:^1.3.1": + version: 1.4.1 + resolution: "rfdc@npm:1.4.1" + checksum: 3b05bd55062c1d78aaabfcea43840cdf7e12099968f368e9a4c3936beb744adb41cbdb315eac6d4d8c6623005d6f87fdf16d8a10e1ff3722e84afea7281c8d13 languageName: node linkType: hard @@ -40618,6 +41054,13 @@ __metadata: languageName: node linkType: hard +"sorted-array-functions@npm:^1.3.0": + version: 1.3.0 + resolution: "sorted-array-functions@npm:1.3.0" + checksum: 673fd39ca3b6c92644d4483eac1700bb7d7555713a536822a7522a35af559bef3e72f10d89356b75042dc394cd7c2e2ab6f40024385218ec3c85bb7335032857 + languageName: node + linkType: hard + "source-list-map@npm:^2.0.0": version: 2.0.1 resolution: "source-list-map@npm:2.0.1" @@ -40974,7 +41417,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:>= 1.4.0 < 2, statuses@npm:~1.5.0": +"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0, statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" checksum: c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c @@ -41072,6 +41515,17 @@ __metadata: languageName: node linkType: hard +"streamroller@npm:^3.1.5": + version: 3.1.5 + resolution: "streamroller@npm:3.1.5" + dependencies: + date-format: ^4.0.14 + debug: ^4.3.4 + fs-extra: ^8.1.0 + checksum: c1df5612b785ffa4b6bbf16460590b62994c57265bc55a5166eebeeb0daf648e84bc52dc6d57e0cd4e5c7609bda93076753c63ff54589febd1e0b95590f0e443 + languageName: node + linkType: hard + "streamsearch@npm:^1.1.0": version: 1.1.0 resolution: "streamsearch@npm:1.1.0" @@ -42542,6 +42996,13 @@ __metadata: languageName: node linkType: hard +"tsscmp@npm:1.0.6": + version: 1.0.6 + resolution: "tsscmp@npm:1.0.6" + checksum: 1512384def36bccc9125cabbd4c3b0e68608d7ee08127ceaa0b84a71797263f1a01c7f82fa69be8a3bd3c1396e2965d2f7b52d581d3a5eeaf3967fbc52e3b3bf + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -42678,7 +43139,7 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^1.6.18, type-is@npm:^1.6.4, type-is@npm:~1.6.18": +"type-is@npm:^1.6.16, type-is@npm:^1.6.18, type-is@npm:^1.6.4, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" dependencies: @@ -43228,7 +43689,7 @@ __metadata: languageName: node linkType: hard -"upath@npm:^2.0.1": +"upath@npm:2.0.1, upath@npm:^2.0.1": version: 2.0.1 resolution: "upath@npm:2.0.1" checksum: 2db04f24a03ef72204c7b969d6991abec9e2cb06fb4c13a1fd1c59bc33b46526b16c3325e55930a11ff86a77a8cbbcda8f6399bf914087028c5beae21ecdb33c @@ -43647,7 +44108,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1, vary@npm:~1.1.2": +"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b @@ -44234,7 +44695,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.9, which@npm:^1.3.1": +"which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -44443,6 +44904,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.17.0": + version: 8.17.0 + resolution: "ws@npm:8.17.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 147ef9eab0251364e1d2c55338ad0efb15e6913923ccbfdf20f7a8a6cb8f88432bcd7f4d8f66977135bfad35575644f9983201c1a361019594a4e53977bf6d4e + languageName: node + linkType: hard + "ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.5": version: 7.5.10 resolution: "ws@npm:7.5.10" @@ -44784,6 +45260,13 @@ __metadata: languageName: node linkType: hard +"ylru@npm:^1.2.0": + version: 1.4.0 + resolution: "ylru@npm:1.4.0" + checksum: e0bf797476487e3d57a6e8790cbb749cff2089e2afc87e46bc84ce7605c329d578ff422c8e8c2ddf167681ddd218af0f58e099733ae1044cba9e9472ebedc01d + languageName: node + linkType: hard + "yml-loader@npm:^2.1.0": version: 2.1.0 resolution: "yml-loader@npm:2.1.0" From b56723adf0afed281fb6d85c0efaf8fb731a466a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Jul 2024 14:00:23 +0200 Subject: [PATCH 123/212] cli: only eagely load shared dependencies in the host Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/bundler/config.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index c2621b20a4..ee201ef2db 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -145,9 +145,11 @@ export async function createConfig( } if (options.moduleFederation) { + const isRemote = options.moduleFederation?.mode === 'remote'; + plugins.push( new ModuleFederationPlugin({ - ...(options.moduleFederation?.mode === 'remote' && { + ...(isRemote && { filename: 'remoteEntry.js', exposes: { '.': paths.targetEntry, @@ -160,45 +162,45 @@ export async function createConfig( react: { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, 'react-dom': { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, // React Router 'react-router': { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, 'react-router-dom': { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, // MUI v4 '@material-ui/core/styles': { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, '@material-ui/styles': { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, // MUI v5 '@mui/material/styles/': { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, '@emotion/react': { singleton: true, requiredVersion: '*', - eager: true, + eager: !isRemote, }, }, }), From 5e7f0491fa9c41a3c69c33c795e8d6f0ad16597c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Jul 2024 14:08:31 +0200 Subject: [PATCH 124/212] cli: update ModuleFederationPlugin import to avoid type error Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/bundler/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index ee201ef2db..60c8a7be66 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -24,7 +24,7 @@ import { Config } from '@backstage/config'; import ESLintPlugin from 'eslint-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; -import { ModuleFederationPlugin } from '@module-federation/enhanced'; +import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack'; import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin'; From 8e2aba9bd01343d72915a69148ccaa3d40658cbd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Jul 2024 17:25:11 +0200 Subject: [PATCH 125/212] cli: add a warning when app is build with module federation enabled Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/command.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 285cba0e81..f868ca462c 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -36,13 +36,19 @@ export async function command(opts: OptionValues): Promise { }); if (role === 'frontend') { + const enableModuleFederation = process.env.EXPERIMENTAL_MODULE_FEDERATION; + if (enableModuleFederation) { + console.log( + chalk.yellow( + `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, + ), + ); + } return buildFrontend({ targetDir: paths.targetDir, configPaths, writeStats: Boolean(opts.stats), - moduleFederationMode: process.env.EXPERIMENTAL_MODULE_FEDERATION - ? 'host' - : undefined, + moduleFederationMode: enableModuleFederation ? 'host' : undefined, }); } return buildBackend({ From 59375fc411deb3e418893720875c6c697ae9a8c5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Jul 2024 17:36:58 +0200 Subject: [PATCH 126/212] cli: refactor module federation check to support all paths to calling buildFrontend Signed-off-by: Patrik Oldsberg --- .../cli/src/commands/build/buildFrontend.ts | 39 +++++++++++++++---- packages/cli/src/commands/build/command.ts | 11 +----- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 931a3b376a..62da74e6d3 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -19,29 +19,52 @@ import { resolve as resolvePath } from 'path'; import { buildBundle } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; +import chalk from 'chalk'; +import { BuildOptions } from '../../lib/bundler/types'; interface BuildAppOptions { targetDir: string; writeStats: boolean; configPaths: string[]; - moduleFederationMode?: 'host' | 'remote'; + isModuleFederationRemote?: true; +} + +function getModuleFederationOptions( + name: string, + isRemote?: boolean, +): BuildOptions['moduleFederation'] { + if (!isRemote && !process.env.EXPERIMENTAL_MODULE_FEDERATION) { + return undefined; + } + + console.log( + chalk.yellow( + `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, + ), + ); + + return { + mode: isRemote ? 'remote' : 'host', + // The default output mode requires the name to be a usable as a code + // symbol, there might be better options here but for now we need to + // sanitize the name. + name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), + }; } export async function buildFrontend(options: BuildAppOptions) { const { targetDir, writeStats, configPaths } = options; const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); + await buildBundle({ targetDir, entry: 'src/index', parallelism: getEnvironmentParallelism(), statsJsonEnabled: writeStats, - moduleFederation: options.moduleFederationMode && { - // The default output mode requires the name to be a usable as a code - // symbol, there might be better options here but for now we need to - // sanitize the name. - name: name.replaceAll('@', '').replaceAll(/[/\\_-]/g, '_'), - mode: options.moduleFederationMode, - }, + moduleFederation: getModuleFederationOptions( + name, + options.isModuleFederationRemote, + ), ...(await loadCliConfig({ args: configPaths, fromPackage: name, diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index f868ca462c..c0ad3c912b 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -36,19 +36,10 @@ export async function command(opts: OptionValues): Promise { }); if (role === 'frontend') { - const enableModuleFederation = process.env.EXPERIMENTAL_MODULE_FEDERATION; - if (enableModuleFederation) { - console.log( - chalk.yellow( - `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, - ), - ); - } return buildFrontend({ targetDir: paths.targetDir, configPaths, writeStats: Boolean(opts.stats), - moduleFederationMode: enableModuleFederation ? 'host' : undefined, }); } return buildBackend({ @@ -70,7 +61,7 @@ export async function command(opts: OptionValues): Promise { targetDir: paths.targetDir, configPaths: [], writeStats: Boolean(opts.stats), - moduleFederationMode: 'remote', + isModuleFederationRemote: true, }); } From 24a98c940b384efa245d044e07fae5ac37bc2ee7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 08:08:47 +0000 Subject: [PATCH 127/212] Bump ws from 8.8.1 to 8.18.0 in /storybook Bumps [ws](https://github.com/websockets/ws) from 8.8.1 to 8.18.0. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.8.1...8.18.0) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] --- storybook/yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 1702d6f4cc..28bbe4b144 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -11912,17 +11912,17 @@ __metadata: linkType: hard "ws@npm:^8.2.3": - version: 8.8.1 - resolution: "ws@npm:8.8.1" + version: 8.18.0 + resolution: "ws@npm:8.18.0" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 2152cf862cae0693f3775bc688a6afb2e989d19d626d215e70f5fcd8eb55b1c3b0d3a6a4052905ec320e2d7734e20aeedbf9744496d62f15a26ad79cf4cf7dae + checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 languageName: node linkType: hard From 419f3878bd519d40db3bbad21cb8760c0da64e97 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Jul 2024 14:29:18 +0200 Subject: [PATCH 128/212] backend-defaults: refactor rootHttpRouterServiceFactory to define options on its own Signed-off-by: Patrik Oldsberg --- .changeset/lucky-wolves-pull.md | 5 +++++ packages/backend-app-api/api-report.md | 9 ++++----- .../backend-defaults/api-report-rootHttpRouter.md | 11 +++++------ .../rootHttpRouter/rootHttpRouterServiceFactory.ts | 14 ++++++++++---- packages/backend-test-utils/api-report.md | 9 ++++----- 5 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 .changeset/lucky-wolves-pull.md diff --git a/.changeset/lucky-wolves-pull.md b/.changeset/lucky-wolves-pull.md new file mode 100644 index 0000000000..17ed4572b6 --- /dev/null +++ b/.changeset/lucky-wolves-pull.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Refactor of `rootHttpRouterServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 54e2900cc2..7f9b54dd8f 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -292,11 +292,10 @@ export type RootHttpRouterConfigureContext = RootHttpRouterConfigureContext_2; export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; // @public @deprecated (undocumented) -export const rootHttpRouterServiceFactory: ServiceFactoryCompat< - RootHttpRouterService, - 'root', - RootHttpRouterFactoryOptions_2 ->; +export const rootHttpRouterServiceFactory: (( + options?: RootHttpRouterFactoryOptions_2 | undefined, +) => ServiceFactory) & + ServiceFactory; // @public @deprecated export const rootLifecycleServiceFactory: ServiceFactoryCompat< diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index 9ea4cd7ebb..329398a7b8 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -19,7 +19,7 @@ import { RequestListener } from 'http'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; import type { Server } from 'node:http'; -import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public export function createHttpServer( @@ -141,11 +141,10 @@ export type RootHttpRouterFactoryOptions = { }; // @public (undocumented) -export const rootHttpRouterServiceFactory: ServiceFactoryCompat< - RootHttpRouterService, - 'root', - RootHttpRouterFactoryOptions ->; +export const rootHttpRouterServiceFactory: (( + options?: RootHttpRouterFactoryOptions, +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts index 22f5c2f310..c3a97737e4 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -69,9 +69,10 @@ function defaultConfigure({ applyDefaults }: RootHttpRouterConfigureContext) { applyDefaults(); } -/** @public */ -export const rootHttpRouterServiceFactory = createServiceFactory( - (options?: RootHttpRouterFactoryOptions) => ({ +const rootHttpRouterServiceFactoryWithOptions = ( + options?: RootHttpRouterFactoryOptions, +) => + createServiceFactory({ service: coreServices.rootHttpRouter, deps: { config: coreServices.rootConfig, @@ -122,5 +123,10 @@ export const rootHttpRouterServiceFactory = createServiceFactory( return router; }, - }), + })(); + +/** @public */ +export const rootHttpRouterServiceFactory = Object.assign( + rootHttpRouterServiceFactoryWithOptions, + rootHttpRouterServiceFactoryWithOptions(), ); diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index af7edba061..3439d37231 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -290,11 +290,10 @@ export namespace mockServices { // (undocumented) export namespace rootHttpRouter { const // (undocumented) - factory: ServiceFactoryCompat< - RootHttpRouterService, - 'root', - RootHttpRouterFactoryOptions - >; + factory: (( + options?: RootHttpRouterFactoryOptions | undefined, + ) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, From e28af58b4ba24495d7d1db03feb53e5737fe33e7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Jul 2024 11:12:21 +0200 Subject: [PATCH 129/212] backend-defaults: deprecate config service options Signed-off-by: Patrik Oldsberg --- .changeset/wet-rivers-travel.md | 5 ++++ .../core-services/root-config.md | 30 ++++++++++++------- .../backend-defaults/api-report-rootConfig.md | 2 +- .../rootConfig/rootConfigServiceFactory.ts | 1 + 4 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .changeset/wet-rivers-travel.md diff --git a/.changeset/wet-rivers-travel.md b/.changeset/wet-rivers-travel.md new file mode 100644 index 0000000000..44dedf3ccb --- /dev/null +++ b/.changeset/wet-rivers-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +The `RootConfigFactoryOptions` have been deprecated alongside the deprecation of service factory options. For more information on how to customize the config service, see the [Root Config Service docs](https://backstage.io/docs/backend-system/core-services/root-config#configuring-the-service). diff --git a/docs/backend-system/core-services/root-config.md b/docs/backend-system/core-services/root-config.md index 7cdc10f38c..9561a8f0cd 100644 --- a/docs/backend-system/core-services/root-config.md +++ b/docs/backend-system/core-services/root-config.md @@ -36,12 +36,12 @@ createBackendPlugin({ ## Configuring the service -There's additional configuration that you can optionally pass to setup the `config` core service. +There are several APIs from the `@backstage/config-loader` package that allow you to customize the implementation of the config service. The default implementation uses the `ConfigSources.default` method, which has several options, for example: - `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` - `remote` - Configure remote configuration loading -You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: +You can use these to create your own config service implementation: ```ts import { rootConfigServiceFactory } from '@backstage/backend-app-api'; @@ -49,14 +49,24 @@ import { rootConfigServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend(); backend.add( - rootConfigServiceFactory({ - argv: [ - '--config', - '/backstage/app-config.development.yaml', - '--config', - '/backstage/app-config.yaml', - ], - remote: { reloadIntervalSeconds: 60 }, + createServiceFactory({ + service: coreServices.rootConfig, + deps: {}, + async factory() { + const source = ConfigSources.default({ + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, + }); + console.log(`Loading config from ${source}`); + return await ConfigSources.toConfig(source); + }, }), ); ``` + +You can also use other config source such as `StaticConfigSource` and combine them with other sources using `ConfigSources.merge(...)`. You can also create your own config source by implementing the `ConfigSource` interface. diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 24e827f520..8ad2e01a90 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -17,7 +17,7 @@ export function createConfigSecretEnumerator(options: { schema?: ConfigSchema; }): Promise<(config: Config) => Iterable>; -// @public +// @public @deprecated export interface RootConfigFactoryOptions { argv?: string[]; remote?: Pick; diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts index 5752a9c634..7bcb9ef56b 100644 --- a/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts @@ -31,6 +31,7 @@ import { * for more information. * * @public + * @deprecated These service options will be removed, please use the `ConfigSources` API from `@backstage/config-loader` to implement your own version of the service factory instead. */ export interface RootConfigFactoryOptions { /** From e375bb93df617dc101df8bb8a7a2da7b743c752b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jul 2024 10:20:11 +0200 Subject: [PATCH 130/212] backend-defaults: update root config implementation to use manual factory pattern + update docs Signed-off-by: Patrik Oldsberg --- .changeset/wet-rivers-travel.md | 2 +- .../core-services/root-config.md | 25 +++++++++++++++++-- .../backend-defaults/api-report-rootConfig.md | 13 +++++----- .../rootConfig/rootConfigServiceFactory.ts | 19 ++++++++------ 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/.changeset/wet-rivers-travel.md b/.changeset/wet-rivers-travel.md index 44dedf3ccb..1778a3db23 100644 --- a/.changeset/wet-rivers-travel.md +++ b/.changeset/wet-rivers-travel.md @@ -2,4 +2,4 @@ '@backstage/backend-defaults': patch --- -The `RootConfigFactoryOptions` have been deprecated alongside the deprecation of service factory options. For more information on how to customize the config service, see the [Root Config Service docs](https://backstage.io/docs/backend-system/core-services/root-config#configuring-the-service). +Refactor of `rootConfigServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. diff --git a/docs/backend-system/core-services/root-config.md b/docs/backend-system/core-services/root-config.md index 9561a8f0cd..8505b9e7ec 100644 --- a/docs/backend-system/core-services/root-config.md +++ b/docs/backend-system/core-services/root-config.md @@ -36,18 +36,39 @@ createBackendPlugin({ ## Configuring the service -There are several APIs from the `@backstage/config-loader` package that allow you to customize the implementation of the config service. The default implementation uses the `ConfigSources.default` method, which has several options, for example: +There's additional configuration that you can optionally pass to setup the `config` core service. - `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` - `remote` - Configure remote configuration loading -You can use these to create your own config service implementation: +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts import { rootConfigServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend(); +backend.add( + rootConfigServiceFactory({ + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, + }), +); +``` + +For more advanced customization, there are several APIs from the `@backstage/config-loader` package that allow you to customize the implementation of the config service. The default implementation uses the `ConfigSources.default` method, which has the same options as the `rootConfigServiceFactory` function. You can use these to create your own config service implementation: + +```ts +import { ConfigSources } from '@backstage/config-loader'; +import { createServiceFactory } from '@backstage/backend-plugin-api'; + +const backend = createBackend(); + backend.add( createServiceFactory({ service: coreServices.rootConfig, diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 8ad2e01a90..61f5c9dc9d 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -8,7 +8,7 @@ import { ConfigSchema } from '@backstage/config-loader'; import { LoggerService } from '@backstage/backend-plugin-api'; import { RemoteConfigSourceOptions } from '@backstage/config-loader'; import { RootConfigService } from '@backstage/backend-plugin-api'; -import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public (undocumented) export function createConfigSecretEnumerator(options: { @@ -17,7 +17,7 @@ export function createConfigSecretEnumerator(options: { schema?: ConfigSchema; }): Promise<(config: Config) => Iterable>; -// @public @deprecated +// @public export interface RootConfigFactoryOptions { argv?: string[]; remote?: Pick; @@ -26,11 +26,10 @@ export interface RootConfigFactoryOptions { } // @public (undocumented) -export const rootConfigServiceFactory: ServiceFactoryCompat< - RootConfigService, - 'root', - RootConfigFactoryOptions ->; +export const rootConfigServiceFactory: (( + options?: RootConfigFactoryOptions, +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts index 7bcb9ef56b..63a7353241 100644 --- a/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts @@ -31,7 +31,6 @@ import { * for more information. * * @public - * @deprecated These service options will be removed, please use the `ConfigSources` API from `@backstage/config-loader` to implement your own version of the service factory instead. */ export interface RootConfigFactoryOptions { /** @@ -46,11 +45,10 @@ export interface RootConfigFactoryOptions { watch?: boolean; } -/** - * @public - */ -export const rootConfigServiceFactory = createServiceFactory( - (options?: RootConfigFactoryOptions) => ({ +export const rootConfigServiceFactoryWithOptions = ( + options?: RootConfigFactoryOptions, +) => + createServiceFactory({ service: coreServices.rootConfig, deps: {}, async factory() { @@ -62,5 +60,12 @@ export const rootConfigServiceFactory = createServiceFactory( console.log(`Loading config from ${source}`); return await ConfigSources.toConfig(source); }, - }), + })(); + +/** + * @public + */ +export const rootConfigServiceFactory = Object.assign( + rootConfigServiceFactoryWithOptions, + rootConfigServiceFactoryWithOptions(), ); From 98ccf0020a5ed6d8a3da2f7e1ff0ac7678f95297 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Jul 2024 14:21:48 +0200 Subject: [PATCH 131/212] backend-test-utils: refactor httpAuth.factory to define options on its own Signed-off-by: Patrik Oldsberg --- .changeset/wicked-impalas-explode.md | 5 ++++ packages/backend-test-utils/api-report.md | 11 +++----- .../src/next/services/mockServices.ts | 25 +++++++++++-------- 3 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 .changeset/wicked-impalas-explode.md diff --git a/.changeset/wicked-impalas-explode.md b/.changeset/wicked-impalas-explode.md new file mode 100644 index 0000000000..f8a01bc36e --- /dev/null +++ b/.changeset/wicked-impalas-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Internal refactor of `mockServices.httpAuth.factory` to allow it to still be constructed with options, but without declaring options via `createServiceFactory`. diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index af7edba061..d77aea09a8 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -206,13 +206,10 @@ export namespace mockServices { }): HttpAuthService; // (undocumented) export namespace httpAuth { - const factory: ServiceFactoryCompat< - HttpAuthService, - 'plugin', - { - defaultCredentials?: BackstageCredentials | undefined; - } - >; + const factory: ((options?: { + defaultCredentials?: BackstageCredentials; + }) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index d56179d958..38ff819228 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -270,15 +270,10 @@ export namespace mockServices { ); } export namespace httpAuth { - /** - * Creates a mock service factory for the `HttpAuthService`. - * - * By default all requests without credentials are treated as requests from - * the default mock user principal. This behavior can be configured with the - * `defaultCredentials` option. - */ - export const factory = createServiceFactory( - (options?: { defaultCredentials?: BackstageCredentials }) => ({ + const factoryWithOptions = (options?: { + defaultCredentials?: BackstageCredentials; + }) => + createServiceFactory({ service: coreServices.httpAuth, deps: { plugin: coreServices.pluginMetadata }, factory: ({ plugin }) => @@ -286,7 +281,17 @@ export namespace mockServices { plugin.getId(), options?.defaultCredentials ?? mockCredentials.user(), ), - }), + })(); + /** + * Creates a mock service factory for the `HttpAuthService`. + * + * By default all requests without credentials are treated as requests from + * the default mock user principal. This behavior can be configured with the + * `defaultCredentials` option. + */ + export const factory = Object.assign( + factoryWithOptions, + factoryWithOptions(), ); export const mock = simpleMock(coreServices.httpAuth, () => ({ credentials: jest.fn(), From fccc99b7d902ce1f4d062fa14d5d1fb2a935fac6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jul 2024 11:07:51 +0200 Subject: [PATCH 132/212] backend-test-utils: refactor other services to declare options on their own too Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 18 ++++--- .../src/next/services/mockServices.ts | 50 +++++++++++++------ 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index d77aea09a8..b888168827 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -229,7 +229,7 @@ export namespace mockServices { // (undocumented) export namespace identity { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -271,9 +271,10 @@ export namespace mockServices { data?: JsonObject; }; const // (undocumented) - factory: ( - options?: Options | undefined, - ) => ServiceFactory; + factory: ServiceFactory & + (( + options?: Options | undefined, + ) => ServiceFactory); } // (undocumented) export namespace rootHealth { @@ -315,9 +316,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; const // (undocumented) - factory: ( - options?: Options | undefined, - ) => ServiceFactory; + factory: ServiceFactory & + (( + options?: Options | undefined, + ) => ServiceFactory); const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -337,7 +339,7 @@ export namespace mockServices { // (undocumented) export namespace tokenManager { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 38ff819228..8d36fec50e 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -70,21 +70,28 @@ function createLoggerMock() { } /** @internal */ -function simpleFactory< +function simpleFactoryWithOptions< TService, TScope extends 'root' | 'plugin', TOptions extends [options?: object] = [], >( ref: ServiceRef, factory: (...options: TOptions) => TService, -): (...options: TOptions) => ServiceFactory { - return createServiceFactory((options: unknown) => ({ - service: ref as ServiceRef, - deps: {}, - async factory() { - return (factory as any)(options); - }, - })) as (...options: TOptions) => ServiceFactory; +): ServiceFactory & + ((...options: TOptions) => ServiceFactory) { + const factoryWithOptions = (...options: TOptions) => + createServiceFactory({ + service: ref as ServiceRef, + deps: {}, + async factory() { + return factory(...options); + }, + })(); + return Object.assign( + factoryWithOptions, + factoryWithOptions(...([undefined] as unknown as TOptions)), + ) as ServiceFactory & + ((...options: TOptions) => ServiceFactory); } /** @public */ @@ -134,7 +141,10 @@ export namespace mockServices { export namespace rootConfig { export type Options = { data?: JsonObject }; - export const factory = simpleFactory(coreServices.rootConfig, rootConfig); + export const factory = simpleFactoryWithOptions( + coreServices.rootConfig, + rootConfig, + ); } export function rootLogger(options?: rootLogger.Options): LoggerService { @@ -145,7 +155,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; - export const factory = simpleFactory(coreServices.rootLogger, rootLogger); + export const factory = simpleFactoryWithOptions( + coreServices.rootLogger, + rootLogger, + ); export const mock = simpleMock(coreServices.rootLogger, () => ({ child: jest.fn(), debug: jest.fn(), @@ -168,10 +181,11 @@ export namespace mockServices { }; } export namespace tokenManager { - export const factory = simpleFactory( - coreServices.tokenManager, - tokenManager, - ); + export const factory = createServiceFactory({ + service: coreServices.tokenManager, + deps: {}, + factory: () => tokenManager(), + }); export const mock = simpleMock(coreServices.tokenManager, () => ({ authenticate: jest.fn(), getToken: jest.fn(), @@ -182,7 +196,11 @@ export namespace mockServices { return new MockIdentityService(); } export namespace identity { - export const factory = simpleFactory(coreServices.identity, identity); + export const factory = createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => identity(), + }); export const mock = simpleMock(coreServices.identity, () => ({ getIdentity: jest.fn(), })); From 994211d1110410b6febb2c96fdc158dfaa16fac7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jul 2024 10:49:24 +0200 Subject: [PATCH 133/212] changesets: exit prerelease Signed-off-by: Patrik Oldsberg --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index acd37c2559..cebbc6e919 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.98", From 8e6ba753bc2da001c53edc79e387d443311d5fca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jul 2024 10:58:31 +0200 Subject: [PATCH 134/212] docs: prepare 1.29 release notes Signed-off-by: Patrik Oldsberg --- docs/releases/v1.29.0.md | 40 ++++++++++++++++++++++++++++++++++ microsite/docusaurus.config.ts | 2 +- microsite/sidebars.json | 1 + 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 docs/releases/v1.29.0.md diff --git a/docs/releases/v1.29.0.md b/docs/releases/v1.29.0.md new file mode 100644 index 0000000000..7b17abd544 --- /dev/null +++ b/docs/releases/v1.29.0.md @@ -0,0 +1,40 @@ +--- +id: v1.29.0 +title: v1.29.0 +description: Backstage Release v1.29.0 +--- + +These are the release notes for the v1.29.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### + +<short description>. Contributed by @<user> in [#<pr>](https://github.com/backstage/backstage/pull/<pr>) + +## Security Fixes + +This release does not contain any security fixes. + +<or> + +@backstage/<package-name>, please upgrade to the latest version if you are using this module. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.29.0-changelog.md) +- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) + +Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage. diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index b1a6533ab9..67f6de4962 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -241,7 +241,7 @@ const config: Config = { position: 'left', }, { - to: 'docs/releases/v1.28.0', + to: 'docs/releases/v1.29.0', label: 'Releases', position: 'left', }, diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 075025afbb..1a3a0eb4d3 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -1,6 +1,7 @@ { "releases": { "Release Notes": [ + "releases/v1.29.0", "releases/v1.28.0", "releases/v1.27.0", "releases/v1.26.0", From 2764d6453f91b4416e76e5fe923c6ed2552ea14a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 11:32:50 +0200 Subject: [PATCH 135/212] docs,changesets: update to mention the manual service factory options pattern Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/spotty-apricots-tan.md | 20 ++++++++++ .../architecture/03-services.md | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/.changeset/spotty-apricots-tan.md b/.changeset/spotty-apricots-tan.md index 8eea2b1dce..a2be38d775 100644 --- a/.changeset/spotty-apricots-tan.md +++ b/.changeset/spotty-apricots-tan.md @@ -52,6 +52,26 @@ export const customFooServiceFactory = createServiceFactory<FooService>({ This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. +In cases where the old options patterns significantly improves the usability of the service factory, the old pattern can still be implemented like this: + +```ts +const fooServiceFactoryWithOptions = (options?: { bar: string }) => + createServiceFactory<FooService>({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }); + +export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), +); +``` + This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index e1d86164c3..95e3d5305a 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -223,3 +223,43 @@ export const customFooServiceFactory = createServiceFactory({ ``` This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. + +## Service Factory Options Pattern + +:::note Note + +This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration or re-implementation instead. + +::: + +In some cases it might be beneficial to allow users of your service factory to pass options to the factory itself, rather than to the service implementation. This can be enabled by also defining the service factory as a function that returns a reconfigured factory. For example: + +```ts +const fooServiceFactoryWithOptions = (options?: { + transform: (foo: string) => string; +}) => + createServiceFactory<FooService>({ + service: fooServiceRef, + deps: {}, + factory() { + return DefaultFooService.create({ + transform: options?.transform, + }); + }, + }); + +export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), +); +``` + +This makes it possible to use the `fooServiceFactory` directly, as well passing additional options to create a customized factory: + +```ts +backend.add(fooServiceFactory); +// OR +backend.add(fooServiceFactory({ transform: foo => foo.toLowerCase() })); +``` + +This pattern is discouraged due to the inability to access other services through dependency injection. It is however used in a few places in the Backstage framework where the ability to directly pass options without re-implementing the service is very convenient, such as the `mockServices` from `@backstage/backend-test-utils`. From 8dd6843f03c64f4a2b9abf40eb625eb2fc8f14b5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 12:39:55 +0200 Subject: [PATCH 136/212] docs: add root-health to sidebar Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- microsite/sidebars.json | 1 + 1 file changed, 1 insertion(+) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 075025afbb..6d34798f03 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -429,6 +429,7 @@ "backend-system/core-services/permissions", "backend-system/core-services/plugin-metadata", "backend-system/core-services/root-config", + "backend-system/core-services/root-health", "backend-system/core-services/root-http-router", "backend-system/core-services/root-lifecycle", "backend-system/core-services/root-logger", From 8f794cf022834c888b3440aef9dc6bf372c40225 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 11:31:57 +0200 Subject: [PATCH 137/212] docs: 1.29 release notes Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- docs/releases/v1.29.0.md | 89 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/docs/releases/v1.29.0.md b/docs/releases/v1.29.0.md index 7b17abd544..5f7ed1db19 100644 --- a/docs/releases/v1.29.0.md +++ b/docs/releases/v1.29.0.md @@ -10,18 +10,95 @@ A huge thanks to the whole team of maintainers and contributors as well as the a ## Highlights -### <title> +### BREAKING: Backend System deprecations and removals -<short description>. Contributed by @<user> in [#<pr>](https://github.com/backstage/backstage/pull/<pr>) +As part of the [work towards a stable 1.0 release of the new backend system](https://github.com/backstage/backstage/issues/24493), there are several new deprecations and breaking changes in the backend system packages: + +**Breaking**: + +- The deprecated `token` option has been removed from `PermissionsService`, and the request options are now required and must contain a `credentials` object. +- The deprecated `getPath` option has been removed from `httpRouterServiceFactory`, the plugin paths are now always `/api/<pluginId>`. +- It is no longer possible to pass service factory callbacks to the `defaultServiceFactories` option of `createSpecializedBackend`. + +**Deprecations**: + +- The ability to define options for service factories through `createServiceFactory` has been deprecated. See the [service architecture documentation](https://backstage.io/docs/backend-system/architecture/services) for more information on how to define customizable services. +- The ability to install backend features in callback form, i.e. `() => BackendFeature`, has been deprecated. This also includes other usages such as `startTestBackend`, and dynamically imported backend features. No manual changes should be needed for this change, as all backend feature creators have been updated to return `BackendFeature` instances directly. +- The `ServiceFactoryTest.get` method has been renamed to `ServiceFactoryTest.getSubject`, and is now deprecated. +- The following types have been renamed to use an `*Options` suffix instead: `ServiceRefConfig`, `RootServiceFactoryConfig`, `PluginServiceFactoryConfig`. +- Deprecated all exports related to the legacy status checker in `@backstage/backend-common`. +- The `isDockerDisabledForTests` function exported by `@backstage/backend-test-utils` has been deprecated. + +### Backend Health Service + +A new health service as been added to the new backend system. This service provides health check endpoints for the backend, and replaces `createStatusCheckRouter` from `@backstage/backend-common`. + +The service helps implement the new `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints, which provide health checks for the entire backend instance. + +You can read more about this new service and how to customize it in the [Root Health Service documentation](https://backstage.io/docs/backend-system/core-services/root-health). + +### New Catalog Logs module + +This new `@backstage/plugin-catalog-backend-module-logs` module is a minimal module that will log any error events that are published by the catalog. This module is useful for making sure that catalog errors are visible in the logs, but you may want to replace it with a more customized solution if the resulting logs are too verbose. + +### Updates to the `@backstage/create-app` template + +New backstage projects created with `@backstage/create-app` will now include the Catalog Logs module for logging catalog error events, as well as support for the Postgres Search Engine. + +### Permission Policy deprecations + +The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + +- `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. +- `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + +Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +### Renaming the `setupRequestMockHandlers` test utility + +The `setupRequestMockHandlers` utility function exported by `@backstage/test-utils` and `@backstage/backend-test-utils` has been renamed to `registerMswTestHooks`. This is done to better reflect the context and the purpose of the function. The old name is deprecated and will be removed in a future release. + +### Catalog GitHub module support for `repository` events + +The GitHub provider module and `GithubEntityProvider` for the Catalog now supports event driven ingestion of repositories by subscribing to `repository` events from GitHub. This includes the actions `archived`, `deleted`, `edited`, `renamed`, `transferred`, and `unarchived`. This is in addition to the existing support for `push` events, which you can read more about in the integration documentation for [GitHub Discovery](https://backstage.io/docs/integrations/github/discovery#events-support). + +Contributed by [@pjungermann](https://github.com/pjungermann) in [#25360](https://github.com/backstage/backstage/pull/25360) + +### Catalog i18n support + +The Catalog plugin as well as the Catalog React library now support internationalization (i18n). This means that you can customize the messaging in the catalog, as well as add translations. You can read more about this in the [i18n documentation](https://backstage.io/docs/plugins/internationalization/). + +Contributed by [@mario-mui](https://github.com/mario-mui) in [#23392](https://github.com/backstage/backstage/pull/23392) + +### Route Binding configuration improvements + +It is now possible to explicitly remove default route bindings, for cases where you don't want a plugin route to be bound to any target at all: + +```yaml +app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false +``` + +### Scaffolder Fields performance improvements + +The `EntityPicker` and `MultiEntityPicker` fields have been updated to improve performance with large catalogs. Contributed by [@kmikko](https://github.com/kmikko) in [#25315](https://github.com/backstage/backstage/pull/25315), [#25380](https://github.com/backstage/backstage/pull/25380) + +### BREAKING: Catalog LDAP Module improvements + +The `@backstage/plugin-catalog-backend-module-ldap` module has been improved to support multiple or no declarations of both user and group configs. + +This change is breaking for `readLdapOrg` and `LdapProviderConfig`, which now both always accept arrays of `users` and `groups` configurations. + +Contributed by [@Jenson3210](https://github.com/Jenson3210) in [#25261](https://github.com/backstage/backstage/pull/25261) ## Security Fixes This release does not contain any security fixes. -<or> - -@backstage/<package-name>, please upgrade to the latest version if you are using this module. - ## Upgrade path We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). From e9c39485fffb0cc9cf9185208ac2906961055753 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 13:23:40 +0200 Subject: [PATCH 138/212] create-app: add lock seed for @mui/material Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- packages/create-app/seed-yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index 2f41ba9ca3..b09a0ce1a7 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -21,3 +21,8 @@ ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0, ajv@^8.12.0, ajv@^8.14.0, ajv@^8.6.0, ajv@ version "8.14.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.14.0.tgz#f514ddfd4756abb200e1704414963620a625ebbb" integrity sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA== + +"@mui/material@^5.12.2": + version "5.16.1" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.16.1.tgz#6fcef9b5709df5864cf0b0bc0ea7b453a9d9e420" + integrity sha512-BGTgJRb0d/hX9tus5CEb6N/Fo8pE4tYA+s9r4/S0PCrtZ3urCLXlTH4qrAvggQbiF1cYRAbHCkVHoQ+4Pdxl+w== From 8d2fe707cbd584f4492013b31bc52f968aab08b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:06:55 +0000 Subject: [PATCH 139/212] Version Packages --- .changeset/bright-fireants-sit.md | 5 - .changeset/bright-panthers-leave.md | 6 - .changeset/brown-rats-judge.md | 5 - .changeset/calm-jeans-ring.md | 5 - .changeset/chatty-adults-boil.md | 5 - .changeset/chilly-coats-report.md | 7 - .changeset/chilly-rabbits-sneeze.md | 5 - .changeset/chilly-roses-trade.md | 5 - .changeset/clever-carrots-refuse.md | 5 - .changeset/clever-geese-jump.md | 5 - .changeset/clever-waves-judge.md | 5 - .changeset/cold-jars-punch.md | 6 - .changeset/create-app-1719320674.md | 5 - .changeset/curvy-teachers-smell.md | 8 - .changeset/dirty-wasps-do.md | 5 - .changeset/dry-wombats-type.md | 5 - .changeset/early-deers-beg.md | 7 - .changeset/eight-dancers-burn.md | 5 - .changeset/eighty-games-wink.md | 5 - .changeset/eleven-foxes-sip.md | 11 - .changeset/eleven-radios-begin.md | 5 - .changeset/fair-fans-flow.md | 6 - .changeset/fair-meals-juggle.md | 5 - .changeset/fair-pillows-know.md | 7 - .changeset/fair-rockets-leave.md | 6 - .changeset/fair-spoons-work.md | 5 - .changeset/famous-clocks-visit.md | 5 - .changeset/famous-dodos-crash.md | 15 - .changeset/fast-pens-smell.md | 5 - .changeset/fifty-pumpkins-smell.md | 5 - .changeset/forty-buttons-cover.md | 5 - .changeset/four-donuts-switch.md | 5 - .changeset/friendly-bulldogs-pay.md | 7 - .changeset/friendly-experts-fail.md | 5 - .changeset/friendly-masks-type.md | 5 - .changeset/friendly-oranges-greet.md | 6 - .changeset/friendly-stingrays-occur.md | 7 - .changeset/funny-laws-tease.md | 5 - .changeset/fuzzy-socks-do.md | 5 - .changeset/gentle-avocados-obey.md | 5 - .changeset/gold-worms-complain.md | 5 - .changeset/grumpy-wolves-hang.md | 5 - .changeset/heavy-moose-pull.md | 5 - .changeset/honest-pears-run.md | 7 - .changeset/hot-items-kick.md | 9 - .changeset/khaki-rivers-obey.md | 6 - .changeset/kind-dancers-boil.md | 5 - .changeset/light-avocados-worry.md | 6 - .changeset/little-games-fail.md | 7 - .changeset/lucky-wolves-pull.md | 5 - .changeset/metal-jokes-add.md | 5 - .changeset/metal-parents-brush.md | 5 - .changeset/metal-suits-drum.md | 5 - .changeset/mighty-keys-bow.md | 5 - .changeset/moody-llamas-breathe.md | 5 - .changeset/neat-dingos-tickle.md | 5 - .changeset/new-sheep-arrive.md | 5 - .changeset/nice-cats-nail.md | 5 - .changeset/poor-cheetahs-raise.md | 30 - .changeset/pre.json | 258 -- .changeset/rare-peas-dream.md | 5 - .changeset/renovate-7b61228.md | 6 - .changeset/renovate-ced359b.md | 11 - .changeset/rich-bears-march.md | 5 - .changeset/selfish-turtles-jog.md | 6 - .changeset/serious-kings-trade.md | 5 - .changeset/sharp-rice-tickle.md | 5 - .changeset/short-flowers-cry.md | 5 - .changeset/short-impalas-suffer.md | 5 - .changeset/shy-hounds-battle.md | 5 - .changeset/silent-experts-move.md | 5 - .changeset/silent-lobsters-tease.md | 5 - .changeset/silent-moose-eat.md | 5 - .changeset/smooth-pianos-argue.md | 5 - .changeset/soft-clocks-bake.md | 5 - .changeset/sour-jokes-sneeze.md | 5 - .changeset/spotty-apricots-tan.md | 77 - .changeset/spotty-crabs-shop.md | 5 - .changeset/stale-beds-sneeze.md | 5 - .changeset/swift-dots-cough.md | 5 - .changeset/swift-seals-drive.md | 5 - .changeset/tall-walls-learn.md | 9 - .changeset/ten-dancers-drum.md | 5 - .changeset/ten-nails-hear.md | 7 - .changeset/ten-pots-walk.md | 8 - .changeset/tender-turkeys-begin.md | 5 - .changeset/thick-lizards-divide.md | 6 - .changeset/thick-moose-decide.md | 5 - .changeset/three-carpets-smoke.md | 5 - .changeset/three-lizards-crash.md | 5 - .changeset/tiny-pianos-work.md | 5 - .changeset/tiny-schools-draw.md | 7 - .changeset/tough-lies-mate.md | 7 - .changeset/twenty-buses-carry.md | 5 - .changeset/weak-boats-float.md | 8 - .changeset/weak-emus-doubt.md | 7 - .changeset/wet-rivers-travel.md | 5 - .changeset/wicked-impalas-explode.md | 5 - .changeset/witty-keys-share.md | 5 - .changeset/yellow-cows-kick.md | 5 - .changeset/young-donuts-swim.md | 6 - .changeset/young-fishes-lie.md | 5 - .changeset/young-houses-unite.md | 5 - docs/releases/v1.29.0-changelog.md | 2495 +++++++++++++++++ package.json | 2 +- packages/app-defaults/CHANGELOG.md | 11 + packages/app-defaults/package.json | 2 +- packages/app-next-example-plugin/CHANGELOG.md | 8 + packages/app-next-example-plugin/package.json | 2 +- packages/app-next/CHANGELOG.md | 43 + packages/app-next/package.json | 2 +- packages/app/CHANGELOG.md | 41 + packages/app/package.json | 2 +- packages/backend-app-api/CHANGELOG.md | 28 + packages/backend-app-api/package.json | 2 +- packages/backend-common/CHANGELOG.md | 21 + packages/backend-common/package.json | 2 +- packages/backend-defaults/CHANGELOG.md | 32 + packages/backend-defaults/package.json | 2 +- .../CHANGELOG.md | 27 + .../package.json | 2 +- packages/backend-legacy/CHANGELOG.md | 42 + packages/backend-legacy/package.json | 2 +- packages/backend-openapi-utils/CHANGELOG.md | 8 + packages/backend-openapi-utils/package.json | 2 +- packages/backend-plugin-api/CHANGELOG.md | 98 + packages/backend-plugin-api/package.json | 2 +- packages/backend-tasks/CHANGELOG.md | 12 + packages/backend-tasks/package.json | 2 +- packages/backend-test-utils/CHANGELOG.md | 22 + packages/backend-test-utils/package.json | 2 +- packages/backend/CHANGELOG.md | 37 + packages/backend/package.json | 2 +- packages/cli-node/CHANGELOG.md | 10 + packages/cli-node/package.json | 2 +- packages/cli/CHANGELOG.md | 28 + packages/cli/package.json | 2 +- packages/core-app-api/CHANGELOG.md | 24 + packages/core-app-api/package.json | 2 +- packages/core-compat-api/CHANGELOG.md | 9 + packages/core-compat-api/package.json | 2 +- packages/core-components/CHANGELOG.md | 13 + packages/core-components/package.json | 2 +- packages/create-app/CHANGELOG.md | 11 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 14 + packages/dev-utils/package.json | 2 +- packages/e2e-test/CHANGELOG.md | 9 + packages/e2e-test/package.json | 2 +- packages/frontend-app-api/CHANGELOG.md | 26 + packages/frontend-app-api/package.json | 2 +- packages/frontend-plugin-api/CHANGELOG.md | 10 + packages/frontend-plugin-api/package.json | 2 +- packages/frontend-test-utils/CHANGELOG.md | 11 + packages/frontend-test-utils/package.json | 2 +- packages/integration-react/CHANGELOG.md | 9 + packages/integration-react/package.json | 2 +- packages/integration/CHANGELOG.md | 12 + packages/integration/package.json | 2 +- packages/repo-tools/CHANGELOG.md | 12 + packages/repo-tools/package.json | 2 +- .../techdocs-cli-embedded-app/CHANGELOG.md | 19 + .../techdocs-cli-embedded-app/package.json | 2 +- packages/techdocs-cli/CHANGELOG.md | 12 + packages/techdocs-cli/package.json | 2 +- packages/test-utils/CHANGELOG.md | 14 + packages/test-utils/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 15 + plugins/api-docs/package.json | 2 +- plugins/app-backend/CHANGELOG.md | 14 + plugins/app-backend/package.json | 2 +- plugins/app-node/CHANGELOG.md | 8 + plugins/app-node/package.json | 2 +- plugins/app-visualizer/CHANGELOG.md | 9 + plugins/app-visualizer/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- plugins/auth-backend/CHANGELOG.md | 30 + plugins/auth-backend/package.json | 2 +- plugins/auth-node/CHANGELOG.md | 14 + plugins/auth-node/package.json | 2 +- plugins/auth-react/CHANGELOG.md | 9 + plugins/auth-react/package.json | 2 +- plugins/bitbucket-cloud-common/CHANGELOG.md | 8 + plugins/bitbucket-cloud-common/package.json | 2 +- .../catalog-backend-module-aws/CHANGELOG.md | 18 + .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/CHANGELOG.md | 13 + .../catalog-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 17 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../catalog-backend-module-gcp/CHANGELOG.md | 13 + .../catalog-backend-module-gcp/package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 44 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../catalog-backend-module-ldap/CHANGELOG.md | 21 + .../catalog-backend-module-ldap/package.json | 2 +- .../catalog-backend-module-logs/CHANGELOG.md | 14 + .../catalog-backend-module-logs/package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 26 + plugins/catalog-backend/package.json | 2 +- plugins/catalog-common/CHANGELOG.md | 9 + plugins/catalog-common/package.json | 2 +- plugins/catalog-graph/CHANGELOG.md | 14 + plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/CHANGELOG.md | 18 + plugins/catalog-import/package.json | 2 +- plugins/catalog-node/CHANGELOG.md | 14 + plugins/catalog-node/package.json | 2 +- plugins/catalog-react/CHANGELOG.md | 20 + plugins/catalog-react/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../catalog-unprocessed-entities/CHANGELOG.md | 10 + .../catalog-unprocessed-entities/package.json | 2 +- plugins/catalog/CHANGELOG.md | 23 + plugins/catalog/package.json | 2 +- plugins/config-schema/CHANGELOG.md | 10 + plugins/config-schema/package.json | 2 +- plugins/devtools-backend/CHANGELOG.md | 16 + plugins/devtools-backend/package.json | 2 +- plugins/devtools-common/CHANGELOG.md | 8 + plugins/devtools-common/package.json | 2 +- plugins/devtools/CHANGELOG.md | 13 + plugins/devtools/package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../events-backend-module-azure/CHANGELOG.md | 8 + .../events-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../events-backend-module-gerrit/CHANGELOG.md | 8 + .../events-backend-module-gerrit/package.json | 2 +- .../events-backend-module-github/CHANGELOG.md | 9 + .../events-backend-module-github/package.json | 2 +- .../events-backend-module-gitlab/CHANGELOG.md | 9 + .../events-backend-module-gitlab/package.json | 2 +- .../events-backend-test-utils/CHANGELOG.md | 7 + .../events-backend-test-utils/package.json | 2 +- plugins/events-backend/CHANGELOG.md | 10 + plugins/events-backend/package.json | 2 +- plugins/events-node/CHANGELOG.md | 8 + plugins/events-node/package.json | 2 +- .../example-todo-list-backend/CHANGELOG.md | 10 + .../example-todo-list-backend/package.json | 2 +- plugins/example-todo-list-common/CHANGELOG.md | 7 + plugins/example-todo-list-common/package.json | 2 +- plugins/example-todo-list/CHANGELOG.md | 8 + plugins/example-todo-list/package.json | 2 +- plugins/home-react/CHANGELOG.md | 12 + plugins/home-react/package.json | 2 +- plugins/home/CHANGELOG.md | 21 + plugins/home/package.json | 2 +- plugins/kubernetes-backend/CHANGELOG.md | 20 + plugins/kubernetes-backend/package.json | 2 +- plugins/kubernetes-cluster/CHANGELOG.md | 12 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes-common/CHANGELOG.md | 9 + plugins/kubernetes-common/package.json | 2 +- plugins/kubernetes-node/CHANGELOG.md | 10 + plugins/kubernetes-node/package.json | 2 +- plugins/kubernetes-react/CHANGELOG.md | 14 + plugins/kubernetes-react/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 12 + plugins/kubernetes/package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 18 + plugins/notifications-backend/package.json | 2 +- plugins/notifications-common/CHANGELOG.md | 8 + plugins/notifications-common/package.json | 2 +- plugins/notifications-node/CHANGELOG.md | 13 + plugins/notifications-node/package.json | 2 +- plugins/notifications/CHANGELOG.md | 14 + plugins/notifications/package.json | 2 +- plugins/org-react/CHANGELOG.md | 11 + plugins/org-react/package.json | 2 +- plugins/org/CHANGELOG.md | 18 + plugins/org/package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- plugins/permission-backend/CHANGELOG.md | 20 + plugins/permission-backend/package.json | 2 +- plugins/permission-common/CHANGELOG.md | 13 + plugins/permission-common/package.json | 2 +- plugins/permission-node/CHANGELOG.md | 24 + plugins/permission-node/package.json | 2 +- plugins/permission-react/CHANGELOG.md | 10 + plugins/permission-react/package.json | 2 +- plugins/proxy-backend/CHANGELOG.md | 10 + plugins/proxy-backend/package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 26 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 40 + plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-common/CHANGELOG.md | 9 + plugins/scaffolder-common/package.json | 2 +- .../scaffolder-node-test-utils/CHANGELOG.md | 11 + .../scaffolder-node-test-utils/package.json | 2 +- plugins/scaffolder-node/CHANGELOG.md | 16 + plugins/scaffolder-node/package.json | 2 +- plugins/scaffolder-react/CHANGELOG.md | 26 + plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 34 + plugins/scaffolder/package.json | 2 +- .../CHANGELOG.md | 18 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- plugins/search-backend-module-pg/CHANGELOG.md | 12 + plugins/search-backend-module-pg/package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 18 + .../package.json | 2 +- plugins/search-backend-node/CHANGELOG.md | 13 + plugins/search-backend-node/package.json | 2 +- plugins/search-backend/CHANGELOG.md | 18 + plugins/search-backend/package.json | 2 +- plugins/search-common/CHANGELOG.md | 8 + plugins/search-common/package.json | 2 +- plugins/search-react/CHANGELOG.md | 13 + plugins/search-react/package.json | 2 +- plugins/search/CHANGELOG.md | 17 + plugins/search/package.json | 2 +- plugins/signals-backend/CHANGELOG.md | 13 + plugins/signals-backend/package.json | 2 +- plugins/signals-node/CHANGELOG.md | 12 + plugins/signals-node/package.json | 2 +- plugins/signals/CHANGELOG.md | 11 + plugins/signals/package.json | 2 +- .../techdocs-addons-test-utils/CHANGELOG.md | 15 + .../techdocs-addons-test-utils/package.json | 2 +- plugins/techdocs-backend/CHANGELOG.md | 18 + plugins/techdocs-backend/package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- plugins/techdocs-node/CHANGELOG.md | 16 + plugins/techdocs-node/package.json | 2 +- plugins/techdocs-react/CHANGELOG.md | 12 + plugins/techdocs-react/package.json | 2 +- plugins/techdocs/CHANGELOG.md | 25 + plugins/techdocs/package.json | 2 +- plugins/user-settings-backend/CHANGELOG.md | 14 + plugins/user-settings-backend/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 17 + plugins/user-settings/package.json | 2 +- yarn.lock | 264 +- 436 files changed, 5209 insertions(+), 1305 deletions(-) delete mode 100644 .changeset/bright-fireants-sit.md delete mode 100644 .changeset/bright-panthers-leave.md delete mode 100644 .changeset/brown-rats-judge.md delete mode 100644 .changeset/calm-jeans-ring.md delete mode 100644 .changeset/chatty-adults-boil.md delete mode 100644 .changeset/chilly-coats-report.md delete mode 100644 .changeset/chilly-rabbits-sneeze.md delete mode 100644 .changeset/chilly-roses-trade.md delete mode 100644 .changeset/clever-carrots-refuse.md delete mode 100644 .changeset/clever-geese-jump.md delete mode 100644 .changeset/clever-waves-judge.md delete mode 100644 .changeset/cold-jars-punch.md delete mode 100644 .changeset/create-app-1719320674.md delete mode 100644 .changeset/curvy-teachers-smell.md delete mode 100644 .changeset/dirty-wasps-do.md delete mode 100644 .changeset/dry-wombats-type.md delete mode 100644 .changeset/early-deers-beg.md delete mode 100644 .changeset/eight-dancers-burn.md delete mode 100644 .changeset/eighty-games-wink.md delete mode 100644 .changeset/eleven-foxes-sip.md delete mode 100644 .changeset/eleven-radios-begin.md delete mode 100644 .changeset/fair-fans-flow.md delete mode 100644 .changeset/fair-meals-juggle.md delete mode 100644 .changeset/fair-pillows-know.md delete mode 100644 .changeset/fair-rockets-leave.md delete mode 100644 .changeset/fair-spoons-work.md delete mode 100644 .changeset/famous-clocks-visit.md delete mode 100644 .changeset/famous-dodos-crash.md delete mode 100644 .changeset/fast-pens-smell.md delete mode 100644 .changeset/fifty-pumpkins-smell.md delete mode 100644 .changeset/forty-buttons-cover.md delete mode 100644 .changeset/four-donuts-switch.md delete mode 100644 .changeset/friendly-bulldogs-pay.md delete mode 100644 .changeset/friendly-experts-fail.md delete mode 100644 .changeset/friendly-masks-type.md delete mode 100644 .changeset/friendly-oranges-greet.md delete mode 100644 .changeset/friendly-stingrays-occur.md delete mode 100644 .changeset/funny-laws-tease.md delete mode 100644 .changeset/fuzzy-socks-do.md delete mode 100644 .changeset/gentle-avocados-obey.md delete mode 100644 .changeset/gold-worms-complain.md delete mode 100644 .changeset/grumpy-wolves-hang.md delete mode 100644 .changeset/heavy-moose-pull.md delete mode 100644 .changeset/honest-pears-run.md delete mode 100644 .changeset/hot-items-kick.md delete mode 100644 .changeset/khaki-rivers-obey.md delete mode 100644 .changeset/kind-dancers-boil.md delete mode 100644 .changeset/light-avocados-worry.md delete mode 100644 .changeset/little-games-fail.md delete mode 100644 .changeset/lucky-wolves-pull.md delete mode 100644 .changeset/metal-jokes-add.md delete mode 100644 .changeset/metal-parents-brush.md delete mode 100644 .changeset/metal-suits-drum.md delete mode 100644 .changeset/mighty-keys-bow.md delete mode 100644 .changeset/moody-llamas-breathe.md delete mode 100644 .changeset/neat-dingos-tickle.md delete mode 100644 .changeset/new-sheep-arrive.md delete mode 100644 .changeset/nice-cats-nail.md delete mode 100644 .changeset/poor-cheetahs-raise.md delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/rare-peas-dream.md delete mode 100644 .changeset/renovate-7b61228.md delete mode 100644 .changeset/renovate-ced359b.md delete mode 100644 .changeset/rich-bears-march.md delete mode 100644 .changeset/selfish-turtles-jog.md delete mode 100644 .changeset/serious-kings-trade.md delete mode 100644 .changeset/sharp-rice-tickle.md delete mode 100644 .changeset/short-flowers-cry.md delete mode 100644 .changeset/short-impalas-suffer.md delete mode 100644 .changeset/shy-hounds-battle.md delete mode 100644 .changeset/silent-experts-move.md delete mode 100644 .changeset/silent-lobsters-tease.md delete mode 100644 .changeset/silent-moose-eat.md delete mode 100644 .changeset/smooth-pianos-argue.md delete mode 100644 .changeset/soft-clocks-bake.md delete mode 100644 .changeset/sour-jokes-sneeze.md delete mode 100644 .changeset/spotty-apricots-tan.md delete mode 100644 .changeset/spotty-crabs-shop.md delete mode 100644 .changeset/stale-beds-sneeze.md delete mode 100644 .changeset/swift-dots-cough.md delete mode 100644 .changeset/swift-seals-drive.md delete mode 100644 .changeset/tall-walls-learn.md delete mode 100644 .changeset/ten-dancers-drum.md delete mode 100644 .changeset/ten-nails-hear.md delete mode 100644 .changeset/ten-pots-walk.md delete mode 100644 .changeset/tender-turkeys-begin.md delete mode 100644 .changeset/thick-lizards-divide.md delete mode 100644 .changeset/thick-moose-decide.md delete mode 100644 .changeset/three-carpets-smoke.md delete mode 100644 .changeset/three-lizards-crash.md delete mode 100644 .changeset/tiny-pianos-work.md delete mode 100644 .changeset/tiny-schools-draw.md delete mode 100644 .changeset/tough-lies-mate.md delete mode 100644 .changeset/twenty-buses-carry.md delete mode 100644 .changeset/weak-boats-float.md delete mode 100644 .changeset/weak-emus-doubt.md delete mode 100644 .changeset/wet-rivers-travel.md delete mode 100644 .changeset/wicked-impalas-explode.md delete mode 100644 .changeset/witty-keys-share.md delete mode 100644 .changeset/yellow-cows-kick.md delete mode 100644 .changeset/young-donuts-swim.md delete mode 100644 .changeset/young-fishes-lie.md delete mode 100644 .changeset/young-houses-unite.md create mode 100644 docs/releases/v1.29.0-changelog.md diff --git a/.changeset/bright-fireants-sit.md b/.changeset/bright-fireants-sit.md deleted file mode 100644 index 320bc69ff9..0000000000 --- a/.changeset/bright-fireants-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Added `MyGroupsSidebarItem` to the sidebar in the `create-app` template diff --git a/.changeset/bright-panthers-leave.md b/.changeset/bright-panthers-leave.md deleted file mode 100644 index 35d9c0f869..0000000000 --- a/.changeset/bright-panthers-leave.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-plugin-api': patch -'@backstage/backend-defaults': patch ---- - -Added a new Root Health Service which adds new endpoints for health checks. diff --git a/.changeset/brown-rats-judge.md b/.changeset/brown-rats-judge.md deleted file mode 100644 index 0076c65d77..0000000000 --- a/.changeset/brown-rats-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-backend-module-allow-all-policy': patch ---- - -Internal refactor to use new `PolicyQueryUser` type. diff --git a/.changeset/calm-jeans-ring.md b/.changeset/calm-jeans-ring.md deleted file mode 100644 index 3d1efcf7bd..0000000000 --- a/.changeset/calm-jeans-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor ---- - -Use virtualization with `EntityPicker` as done earlier with `MultiEntityPicker` to fix performance issues with large data sets. `VirtualizedListbox` extracted into reusable component. diff --git a/.changeset/chatty-adults-boil.md b/.changeset/chatty-adults-boil.md deleted file mode 100644 index 76f3bddf41..0000000000 --- a/.changeset/chatty-adults-boil.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fixed an issue that was causing techdocs pages unnecessarily re-render on navigate. diff --git a/.changeset/chilly-coats-report.md b/.changeset/chilly-coats-report.md deleted file mode 100644 index 2fd044b57c..0000000000 --- a/.changeset/chilly-coats-report.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/backend-test-utils': patch -'@backstage/backend-defaults': patch -'@backstage/backend-app-api': patch ---- - -The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. diff --git a/.changeset/chilly-rabbits-sneeze.md b/.changeset/chilly-rabbits-sneeze.md deleted file mode 100644 index cbce80ebbc..0000000000 --- a/.changeset/chilly-rabbits-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend-module-atlassian-provider': patch ---- - -Fix several issues with the Atlassian auth provider (type definition, profile url, profile transformation, scopes) diff --git a/.changeset/chilly-roses-trade.md b/.changeset/chilly-roses-trade.md deleted file mode 100644 index 73fc3ff766..0000000000 --- a/.changeset/chilly-roses-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Deprecate legacy status check factory, handler and types. diff --git a/.changeset/clever-carrots-refuse.md b/.changeset/clever-carrots-refuse.md deleted file mode 100644 index 24217b1045..0000000000 --- a/.changeset/clever-carrots-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-node': patch ---- - -Added internal metadata for the new experimental `frontend-dynamic-container` role. diff --git a/.changeset/clever-geese-jump.md b/.changeset/clever-geese-jump.md deleted file mode 100644 index 1c4694fa33..0000000000 --- a/.changeset/clever-geese-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -Adds a dynamic provider for the plugin-catalog-backend-module-msgraph. Configuration is now runtime configurable through the ProviderConfigTransformer. diff --git a/.changeset/clever-waves-judge.md b/.changeset/clever-waves-judge.md deleted file mode 100644 index 100ab66f90..0000000000 --- a/.changeset/clever-waves-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-react': patch ---- - -Resolved the issue where changes in TechDoc add-ons, including the TextSize add-on, were not reapplying during navigation diff --git a/.changeset/cold-jars-punch.md b/.changeset/cold-jars-punch.md deleted file mode 100644 index 8708e1f670..0000000000 --- a/.changeset/cold-jars-punch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-defaults': minor -'@backstage/backend-app-api': minor ---- - -**BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. diff --git a/.changeset/create-app-1719320674.md b/.changeset/create-app-1719320674.md deleted file mode 100644 index b50d431d4b..0000000000 --- a/.changeset/create-app-1719320674.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Bumped create-app version. diff --git a/.changeset/curvy-teachers-smell.md b/.changeset/curvy-teachers-smell.md deleted file mode 100644 index 3f032f2177..0000000000 --- a/.changeset/curvy-teachers-smell.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/backend-plugin-api': patch -'@backstage/backend-defaults': patch -'@backstage/backend-tasks': patch ---- - -Fix bug where ISO durations could no longer be used for schedules diff --git a/.changeset/dirty-wasps-do.md b/.changeset/dirty-wasps-do.md deleted file mode 100644 index f8b4e614eb..0000000000 --- a/.changeset/dirty-wasps-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Updated `catalog:write` scaffolder action to show correct file path location in log message diff --git a/.changeset/dry-wombats-type.md b/.changeset/dry-wombats-type.md deleted file mode 100644 index 31fed70293..0000000000 --- a/.changeset/dry-wombats-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': minor ---- - -**BREAKING**: Removed the ability to pass callback-form service factories through the `defaultServiceFactories` option of `createSpecializedBackend`. This is an immediate breaking change as usage of this function is expected to be very rare. diff --git a/.changeset/early-deers-beg.md b/.changeset/early-deers-beg.md deleted file mode 100644 index 735c68aace..0000000000 --- a/.changeset/early-deers-beg.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': patch ---- - -Fixed bug resulting from missing required owner and repo arguments in `getEnvironmentPublicKey` in action `github:environment:create`. - -Adding environment secrets now works as expected. diff --git a/.changeset/eight-dancers-burn.md b/.changeset/eight-dancers-burn.md deleted file mode 100644 index 228ff036ab..0000000000 --- a/.changeset/eight-dancers-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-common': minor ---- - -**BREAKING**: Removed the deprecated and unused `token` option from `EvaluatorRequestOptions`. The `PermissionsClient` now has its own `PermissionClientRequestOptions` type that declares the `token` option instead. diff --git a/.changeset/eighty-games-wink.md b/.changeset/eighty-games-wink.md deleted file mode 100644 index e87dc56b5f..0000000000 --- a/.changeset/eighty-games-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Fixed bug where `<Select>` component with empty string as placeholder gave an error diff --git a/.changeset/eleven-foxes-sip.md b/.changeset/eleven-foxes-sip.md deleted file mode 100644 index 8b21ddb20f..0000000000 --- a/.changeset/eleven-foxes-sip.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/plugin-permission-backend': patch -'@backstage/plugin-permission-node': patch ---- - -The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: - -- `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. -- `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. - -Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. diff --git a/.changeset/eleven-radios-begin.md b/.changeset/eleven-radios-begin.md deleted file mode 100644 index 3fc2dd7895..0000000000 --- a/.changeset/eleven-radios-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Improve default sorting of docs table diff --git a/.changeset/fair-fans-flow.md b/.changeset/fair-fans-flow.md deleted file mode 100644 index abdc22440b..0000000000 --- a/.changeset/fair-fans-flow.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': patch -'@backstage/plugin-techdocs-node': patch ---- - -Adds extension point for publishers to the techdocs backend diff --git a/.changeset/fair-meals-juggle.md b/.changeset/fair-meals-juggle.md deleted file mode 100644 index 94db114170..0000000000 --- a/.changeset/fair-meals-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Added experimental support for dynamic frontend plugin builds, enabled via setting `EXPERIMENTAL_MODULE_FEDERATION` for the app build, and using the `frontend-dynamic-container` package role to create a container. Both of these are experimental and will change in the future. diff --git a/.changeset/fair-pillows-know.md b/.changeset/fair-pillows-know.md deleted file mode 100644 index f0e5034393..0000000000 --- a/.changeset/fair-pillows-know.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-scaffolder-backend-module-gitlab': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch ---- - -Updated configuration instructions diff --git a/.changeset/fair-rockets-leave.md b/.changeset/fair-rockets-leave.md deleted file mode 100644 index 20f2e97552..0000000000 --- a/.changeset/fair-rockets-leave.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch ---- - -Instead of using hardcoded `targetBranch` now fetch the default branch from Bitbucket repository. -This prevents from errors when no `targetBranch` is provided and the default repository branch is different from `master`, for example: `main`. diff --git a/.changeset/fair-spoons-work.md b/.changeset/fair-spoons-work.md deleted file mode 100644 index e4b94625db..0000000000 --- a/.changeset/fair-spoons-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Added the Catalog logs module to the `create-app` template diff --git a/.changeset/famous-clocks-visit.md b/.changeset/famous-clocks-visit.md deleted file mode 100644 index 66c2048872..0000000000 --- a/.changeset/famous-clocks-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-node': patch ---- - -Fix invalid cross-reference in API Reference docs diff --git a/.changeset/famous-dodos-crash.md b/.changeset/famous-dodos-crash.md deleted file mode 100644 index 5442c1b378..0000000000 --- a/.changeset/famous-dodos-crash.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@backstage/core-app-api': minor -'@backstage/frontend-app-api': patch ---- - -Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. - -```yaml -app: - routes: - bindings: - # This has the effect of removing the button for registering new - # catalog entities in the scaffolder template list view - scaffolder.registerComponent: false -``` diff --git a/.changeset/fast-pens-smell.md b/.changeset/fast-pens-smell.md deleted file mode 100644 index 6d909a397c..0000000000 --- a/.changeset/fast-pens-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -The `useGetEntities` hook could result in requests to `/api/catalog/entities` where the headers exceed the default maximum Node.js header size of 16KB. The hook logic has been adjusted to batch the requests. diff --git a/.changeset/fifty-pumpkins-smell.md b/.changeset/fifty-pumpkins-smell.md deleted file mode 100644 index 95f4fd3fc1..0000000000 --- a/.changeset/fifty-pumpkins-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gitlab': patch ---- - -The Gitlab configuration supports an additional optional boolean key `catalog.providers.gitlab.<your-org>.restrictUsersToGroup`. Setting this to `true` will make Backstage only import users from the group defined in the `group` key, instead of all users in the organisation (self-hosted) or of the root group (SaaS). It will default to false, keeping the original implementation intact, when not explicitly set. diff --git a/.changeset/forty-buttons-cover.md b/.changeset/forty-buttons-cover.md deleted file mode 100644 index 16e66df061..0000000000 --- a/.changeset/forty-buttons-cover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -The function `isDockerDisabledForTests` is deprecated and will no longer be exported in the near future as it should only be used internally. diff --git a/.changeset/four-donuts-switch.md b/.changeset/four-donuts-switch.md deleted file mode 100644 index bb876a4341..0000000000 --- a/.changeset/four-donuts-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -enable resizing of the task log stream viewer diff --git a/.changeset/friendly-bulldogs-pay.md b/.changeset/friendly-bulldogs-pay.md deleted file mode 100644 index d77c739bb0..0000000000 --- a/.changeset/friendly-bulldogs-pay.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-ldap': minor ---- - -**BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items. - -Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one. diff --git a/.changeset/friendly-experts-fail.md b/.changeset/friendly-experts-fail.md deleted file mode 100644 index df2d2e2b24..0000000000 --- a/.changeset/friendly-experts-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -Updated config schema to support app.routes.bindings diff --git a/.changeset/friendly-masks-type.md b/.changeset/friendly-masks-type.md deleted file mode 100644 index 6f38ddefa3..0000000000 --- a/.changeset/friendly-masks-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': minor ---- - -Add support for `token` for `bitbucketCloud` integration diff --git a/.changeset/friendly-oranges-greet.md b/.changeset/friendly-oranges-greet.md deleted file mode 100644 index d327c40640..0000000000 --- a/.changeset/friendly-oranges-greet.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-catalog': patch ---- - -Support i18n for catalog and catalog-react plugins diff --git a/.changeset/friendly-stingrays-occur.md b/.changeset/friendly-stingrays-occur.md deleted file mode 100644 index 3383203b40..0000000000 --- a/.changeset/friendly-stingrays-occur.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/cli': patch ---- - -- remove unused dependencies `winston` and `yn` from the template of backend plugins; -- update `msw` to version `2.3.1` in the template of backend plugins; - starting with v1 and switching later to v2 is tedious and not straight forward; it's easier to start with v2; diff --git a/.changeset/funny-laws-tease.md b/.changeset/funny-laws-tease.md deleted file mode 100644 index 8ecbc87b65..0000000000 --- a/.changeset/funny-laws-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Updated the scaffolding output message for `plugin-common` in `backstage-cli`. Now, when executing `backstage-cli new` to create a new `plugin-common` package, the output message accurately reflects the action by displaying `Creating common plugin package...` instead of the previous, less accurate `Creating backend plugin...`. diff --git a/.changeset/fuzzy-socks-do.md b/.changeset/fuzzy-socks-do.md deleted file mode 100644 index aabc10f0cd..0000000000 --- a/.changeset/fuzzy-socks-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Allow for searching TechDocs by entity title diff --git a/.changeset/gentle-avocados-obey.md b/.changeset/gentle-avocados-obey.md deleted file mode 100644 index 80bf5f89c7..0000000000 --- a/.changeset/gentle-avocados-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Only bootstrap global-agent if it's actually being used diff --git a/.changeset/gold-worms-complain.md b/.changeset/gold-worms-complain.md deleted file mode 100644 index 229293cc2f..0000000000 --- a/.changeset/gold-worms-complain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-node': minor ---- - -**BREAKING**: Updated the `ServerPermissionClient` to match the new `PermissionsService` interface, where the deprecated `token` option has been removed and the options are now required. diff --git a/.changeset/grumpy-wolves-hang.md b/.changeset/grumpy-wolves-hang.md deleted file mode 100644 index 8ab3740e74..0000000000 --- a/.changeset/grumpy-wolves-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fix issue with CLI that was preventing upgrading from 1.28 diff --git a/.changeset/heavy-moose-pull.md b/.changeset/heavy-moose-pull.md deleted file mode 100644 index 86b2753a28..0000000000 --- a/.changeset/heavy-moose-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor ---- - -Use virtualization with `MultiEntityPicker`. Fixes performance issues with large data sets. diff --git a/.changeset/honest-pears-run.md b/.changeset/honest-pears-run.md deleted file mode 100644 index 65ea8d1721..0000000000 --- a/.changeset/honest-pears-run.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-azure': patch -'@backstage/plugin-scaffolder-node': patch -'@backstage/plugin-scaffolder': patch ---- - -Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` diff --git a/.changeset/hot-items-kick.md b/.changeset/hot-items-kick.md deleted file mode 100644 index a3a9f129c1..0000000000 --- a/.changeset/hot-items-kick.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/backend-dynamic-feature-service': patch -'@backstage/backend-test-utils': patch -'@backstage/backend-defaults': patch -'@backstage/backend-app-api': patch -'@backstage/plugin-events-node': patch ---- - -Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. diff --git a/.changeset/khaki-rivers-obey.md b/.changeset/khaki-rivers-obey.md deleted file mode 100644 index 856c8caf09..0000000000 --- a/.changeset/khaki-rivers-obey.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/cli': patch ---- - -Update default backend plugin created by the cli to use non-deprecated error handling middleware diff --git a/.changeset/kind-dancers-boil.md b/.changeset/kind-dancers-boil.md deleted file mode 100644 index 4451cab4f4..0000000000 --- a/.changeset/kind-dancers-boil.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Added a documentation how to use checkpoints diff --git a/.changeset/light-avocados-worry.md b/.changeset/light-avocados-worry.md deleted file mode 100644 index a3605a1451..0000000000 --- a/.changeset/light-avocados-worry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch -'@backstage/plugin-bitbucket-cloud-common': patch ---- - -Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` diff --git a/.changeset/little-games-fail.md b/.changeset/little-games-fail.md deleted file mode 100644 index 199f983be8..0000000000 --- a/.changeset/little-games-fail.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`. - -Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property. diff --git a/.changeset/lucky-wolves-pull.md b/.changeset/lucky-wolves-pull.md deleted file mode 100644 index 17ed4572b6..0000000000 --- a/.changeset/lucky-wolves-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Refactor of `rootHttpRouterServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. diff --git a/.changeset/metal-jokes-add.md b/.changeset/metal-jokes-add.md deleted file mode 100644 index faef714d87..0000000000 --- a/.changeset/metal-jokes-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Make EntityOwnerPicker display metadata.title or spec.profile.displayName for mode=only-owners instead of metadata.name diff --git a/.changeset/metal-parents-brush.md b/.changeset/metal-parents-brush.md deleted file mode 100644 index f73b0040fd..0000000000 --- a/.changeset/metal-parents-brush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fix double scrollbar bug in reader diff --git a/.changeset/metal-suits-drum.md b/.changeset/metal-suits-drum.md deleted file mode 100644 index 4cc7e18ced..0000000000 --- a/.changeset/metal-suits-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-notifications': patch ---- - -The toolbar on the Notifications page is hidden when there are no listed notifications. diff --git a/.changeset/mighty-keys-bow.md b/.changeset/mighty-keys-bow.md deleted file mode 100644 index 5f1bb408c9..0000000000 --- a/.changeset/mighty-keys-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-aws': patch ---- - -Export `defaultEksClusterEntityTransformer` to allow library consumers to layer additional changes on top of the default transformer. diff --git a/.changeset/moody-llamas-breathe.md b/.changeset/moody-llamas-breathe.md deleted file mode 100644 index 418cf7480e..0000000000 --- a/.changeset/moody-llamas-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Add dependencies that are needed by cross-imports from backend-defaults diff --git a/.changeset/neat-dingos-tickle.md b/.changeset/neat-dingos-tickle.md deleted file mode 100644 index 43929e532f..0000000000 --- a/.changeset/neat-dingos-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-node': patch ---- - -Bumps default version of techdocs docker image to latest diff --git a/.changeset/new-sheep-arrive.md b/.changeset/new-sheep-arrive.md deleted file mode 100644 index 00e759a187..0000000000 --- a/.changeset/new-sheep-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Modified the `Select` component to take in a `data-testid` parameter ensuring backwards compatibility with default value corresponding to previously hardcoded `data-testid` of "select". diff --git a/.changeset/nice-cats-nail.md b/.changeset/nice-cats-nail.md deleted file mode 100644 index 0e9b1603b8..0000000000 --- a/.changeset/nice-cats-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': patch ---- - -Internal refactor that avoids the use of service factory options. diff --git a/.changeset/poor-cheetahs-raise.md b/.changeset/poor-cheetahs-raise.md deleted file mode 100644 index 0ee2162a22..0000000000 --- a/.changeset/poor-cheetahs-raise.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-github': patch ---- - -Adds support for `repository` events. - -The provider adds a subscription to the topic `github.repository`. - -Hereby, it supports events of type `repository` with actions - -- `archived` -- `deleted` -- `edited` -- `renamed` -- `transferred` -- `unarchived` - -Actions skipped as they don't require entity changes: - -- `created` -- `privatized` -- `publicized` - -If the config option `validateLocationsExist` is enabled, an API request -is necessary and will be executed. -This affects the actions `renamed`, `transferred`, and `unarchive` -of event type `repository`. - -Catalog entities related to the `GithubEntityProvider` instance will be adjusted -according to action and its meaning for them. diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index cebbc6e919..0000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,258 +0,0 @@ -{ - "mode": "exit", - "tag": "next", - "initialVersions": { - "example-app": "0.2.98", - "@backstage/app-defaults": "1.5.6", - "example-app-next": "0.0.12", - "app-next-example-plugin": "0.0.12", - "example-backend": "0.0.27", - "@backstage/backend-app-api": "0.7.6", - "@backstage/backend-common": "0.23.0", - "@backstage/backend-defaults": "0.3.0", - "@backstage/backend-dev-utils": "0.1.4", - "@backstage/backend-dynamic-feature-service": "0.2.11", - "example-backend-legacy": "0.2.99", - "@backstage/backend-openapi-utils": "0.1.12", - "@backstage/backend-plugin-api": "0.6.19", - "@backstage/backend-tasks": "0.5.24", - "@backstage/backend-test-utils": "0.4.0", - "@backstage/catalog-client": "1.6.5", - "@backstage/catalog-model": "1.5.0", - "@backstage/cli": "0.26.7", - "@backstage/cli-common": "0.1.14", - "@backstage/cli-node": "0.2.6", - "@backstage/codemods": "0.1.49", - "@backstage/config": "1.2.0", - "@backstage/config-loader": "1.8.1", - "@backstage/core-app-api": "1.12.6", - "@backstage/core-compat-api": "0.2.6", - "@backstage/core-components": "0.14.8", - "@backstage/core-plugin-api": "1.9.3", - "@backstage/create-app": "0.5.16", - "@backstage/dev-utils": "1.0.33", - "e2e-test": "0.2.17", - "@backstage/e2e-test-utils": "0.1.1", - "@backstage/errors": "1.2.4", - "@backstage/eslint-plugin": "0.1.8", - "@backstage/frontend-app-api": "0.7.1", - "@backstage/frontend-plugin-api": "0.6.6", - "@backstage/frontend-test-utils": "0.1.8", - "@backstage/integration": "1.12.0", - "@backstage/integration-aws-node": "0.1.12", - "@backstage/integration-react": "1.1.28", - "@backstage/release-manifests": "0.0.11", - "@backstage/repo-tools": "0.9.1", - "@techdocs/cli": "1.8.12", - "techdocs-cli-embedded-app": "0.2.97", - "@backstage/test-utils": "1.5.6", - "@backstage/theme": "0.5.6", - "@backstage/types": "1.1.1", - "@backstage/version-bridge": "1.0.8", - "yarn-plugin-backstage": "0.0.1", - "@backstage/plugin-api-docs": "0.11.6", - "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.7", - "@backstage/plugin-app-backend": "0.3.68", - "@backstage/plugin-app-node": "0.1.19", - "@backstage/plugin-app-visualizer": "0.1.7", - "@backstage/plugin-auth-backend": "0.22.6", - "@backstage/plugin-auth-backend-module-atlassian-provider": "0.2.0", - "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.1.11", - "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.1.2", - "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.1.2", - "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.1.2", - "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.2.14", - "@backstage/plugin-auth-backend-module-github-provider": "0.1.16", - "@backstage/plugin-auth-backend-module-gitlab-provider": "0.1.16", - "@backstage/plugin-auth-backend-module-google-provider": "0.1.16", - "@backstage/plugin-auth-backend-module-guest-provider": "0.1.5", - "@backstage/plugin-auth-backend-module-microsoft-provider": "0.1.14", - "@backstage/plugin-auth-backend-module-oauth2-provider": "0.2.0", - "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.1.12", - "@backstage/plugin-auth-backend-module-oidc-provider": "0.2.0", - "@backstage/plugin-auth-backend-module-okta-provider": "0.0.12", - "@backstage/plugin-auth-backend-module-onelogin-provider": "0.1.0", - "@backstage/plugin-auth-backend-module-pinniped-provider": "0.1.13", - "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.2.0", - "@backstage/plugin-auth-node": "0.4.14", - "@backstage/plugin-auth-react": "0.1.3", - "@backstage/plugin-bitbucket-cloud-common": "0.2.20", - "@backstage/plugin-catalog": "1.21.0", - "@backstage/plugin-catalog-backend": "1.23.0", - "@backstage/plugin-catalog-backend-module-aws": "0.3.14", - "@backstage/plugin-catalog-backend-module-azure": "0.1.39", - "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.2.2", - "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.2.6", - "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.33", - "@backstage/plugin-catalog-backend-module-gcp": "0.1.20", - "@backstage/plugin-catalog-backend-module-gerrit": "0.1.36", - "@backstage/plugin-catalog-backend-module-github": "0.6.2", - "@backstage/plugin-catalog-backend-module-github-org": "0.1.14", - "@backstage/plugin-catalog-backend-module-gitlab": "0.3.18", - "@backstage/plugin-catalog-backend-module-gitlab-org": "0.0.2", - "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.24", - "@backstage/plugin-catalog-backend-module-ldap": "0.6.0", - "@backstage/plugin-catalog-backend-module-msgraph": "0.5.27", - "@backstage/plugin-catalog-backend-module-openapi": "0.1.37", - "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.25", - "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.1.17", - "@backstage/plugin-catalog-backend-module-unprocessed": "0.4.6", - "@backstage/plugin-catalog-common": "1.0.24", - "@backstage/plugin-catalog-graph": "0.4.6", - "@backstage/plugin-catalog-import": "0.12.0", - "@backstage/plugin-catalog-node": "1.12.1", - "@backstage/plugin-catalog-react": "1.12.1", - "@backstage/plugin-catalog-unprocessed-entities": "0.2.5", - "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.2", - "@backstage/plugin-config-schema": "0.1.56", - "@backstage/plugin-devtools": "0.1.15", - "@backstage/plugin-devtools-backend": "0.3.5", - "@backstage/plugin-devtools-common": "0.1.10", - "@backstage/plugin-events-backend": "0.3.6", - "@backstage/plugin-events-backend-module-aws-sqs": "0.3.5", - "@backstage/plugin-events-backend-module-azure": "0.2.5", - "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.5", - "@backstage/plugin-events-backend-module-gerrit": "0.2.5", - "@backstage/plugin-events-backend-module-github": "0.2.5", - "@backstage/plugin-events-backend-module-gitlab": "0.2.5", - "@backstage/plugin-events-backend-test-utils": "0.1.29", - "@backstage/plugin-events-node": "0.3.5", - "@internal/plugin-todo-list": "1.0.28", - "@internal/plugin-todo-list-backend": "1.0.28", - "@internal/plugin-todo-list-common": "1.0.19", - "@backstage/plugin-home": "0.7.5", - "@backstage/plugin-home-react": "0.1.14", - "@backstage/plugin-kubernetes": "0.11.11", - "@backstage/plugin-kubernetes-backend": "0.18.0", - "@backstage/plugin-kubernetes-cluster": "0.0.12", - "@backstage/plugin-kubernetes-common": "0.8.0", - "@backstage/plugin-kubernetes-node": "0.1.13", - "@backstage/plugin-kubernetes-react": "0.4.0", - "@backstage/plugin-notifications": "0.2.2", - "@backstage/plugin-notifications-backend": "0.3.0", - "@backstage/plugin-notifications-backend-module-email": "0.1.0", - "@backstage/plugin-notifications-common": "0.0.4", - "@backstage/plugin-notifications-node": "0.2.0", - "@backstage/plugin-org": "0.6.26", - "@backstage/plugin-org-react": "0.1.25", - "@backstage/plugin-permission-backend": "0.5.43", - "@backstage/plugin-permission-backend-module-allow-all-policy": "0.1.16", - "@backstage/plugin-permission-common": "0.7.14", - "@backstage/plugin-permission-node": "0.7.30", - "@backstage/plugin-permission-react": "0.4.23", - "@backstage/plugin-proxy-backend": "0.5.0", - "@backstage/plugin-scaffolder": "1.21.0", - "@backstage/plugin-scaffolder-backend": "1.22.9", - "@backstage/plugin-scaffolder-backend-module-azure": "0.1.11", - "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.2.9", - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.1.9", - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.1.9", - "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.20", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.43", - "@backstage/plugin-scaffolder-backend-module-gerrit": "0.1.11", - "@backstage/plugin-scaffolder-backend-module-gitea": "0.1.9", - "@backstage/plugin-scaffolder-backend-module-github": "0.3.0", - "@backstage/plugin-scaffolder-backend-module-gitlab": "0.4.1", - "@backstage/plugin-scaffolder-backend-module-notifications": "0.0.2", - "@backstage/plugin-scaffolder-backend-module-rails": "0.4.36", - "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.27", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.3.2", - "@backstage/plugin-scaffolder-common": "1.5.3", - "@backstage/plugin-scaffolder-node": "0.4.5", - "@backstage/plugin-scaffolder-node-test-utils": "0.1.5", - "@backstage/plugin-scaffolder-react": "1.9.0", - "@backstage/plugin-search": "1.4.12", - "@backstage/plugin-search-backend": "1.5.10", - "@backstage/plugin-search-backend-module-catalog": "0.1.25", - "@backstage/plugin-search-backend-module-elasticsearch": "1.5.0", - "@backstage/plugin-search-backend-module-explore": "0.1.25", - "@backstage/plugin-search-backend-module-pg": "0.5.28", - "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.1.12", - "@backstage/plugin-search-backend-module-techdocs": "0.1.24", - "@backstage/plugin-search-backend-node": "1.2.24", - "@backstage/plugin-search-common": "1.2.12", - "@backstage/plugin-search-react": "1.7.12", - "@backstage/plugin-signals": "0.0.7", - "@backstage/plugin-signals-backend": "0.1.5", - "@backstage/plugin-signals-node": "0.1.5", - "@backstage/plugin-signals-react": "0.0.4", - "@backstage/plugin-techdocs": "1.10.6", - "@backstage/plugin-techdocs-addons-test-utils": "1.0.33", - "@backstage/plugin-techdocs-backend": "1.10.6", - "@backstage/plugin-techdocs-module-addons-contrib": "1.1.11", - "@backstage/plugin-techdocs-node": "1.12.5", - "@backstage/plugin-techdocs-react": "1.2.5", - "@backstage/plugin-user-settings": "0.8.7", - "@backstage/plugin-user-settings-backend": "0.2.18", - "@backstage/plugin-user-settings-common": "0.0.1", - "@backstage/plugin-catalog-backend-module-logs": "0.0.0", - "@backstage/plugin-scaffolder-backend-module-gcp": "0.0.1-next.1" - }, - "changesets": [ - "bright-fireants-sit", - "bright-panthers-leave", - "calm-jeans-ring", - "chilly-roses-trade", - "clever-waves-judge", - "create-app-1719320674", - "curvy-teachers-smell", - "eighty-games-wink", - "eleven-radios-begin", - "fair-fans-flow", - "fair-pillows-know", - "fair-rockets-leave", - "fair-spoons-work", - "famous-dodos-crash", - "fast-pens-smell", - "fifty-pumpkins-smell", - "friendly-bulldogs-pay", - "friendly-experts-fail", - "friendly-masks-type", - "friendly-oranges-greet", - "friendly-stingrays-occur", - "funny-laws-tease", - "gentle-avocados-obey", - "grumpy-wolves-hang", - "heavy-moose-pull", - "honest-pears-run", - "khaki-rivers-obey", - "kind-dancers-boil", - "light-avocados-worry", - "little-games-fail", - "metal-jokes-add", - "metal-parents-brush", - "metal-suits-drum", - "moody-llamas-breathe", - "neat-dingos-tickle", - "new-sheep-arrive", - "poor-cheetahs-raise", - "rare-peas-dream", - "renovate-7b61228", - "renovate-ced359b", - "rich-bears-march", - "selfish-turtles-jog", - "serious-kings-trade", - "sharp-rice-tickle", - "short-flowers-cry", - "shy-hounds-battle", - "silent-experts-move", - "silent-lobsters-tease", - "silent-moose-eat", - "soft-clocks-bake", - "sour-jokes-sneeze", - "spotty-crabs-shop", - "swift-dots-cough", - "ten-dancers-drum", - "ten-nails-hear", - "ten-pots-walk", - "thick-lizards-divide", - "thick-moose-decide", - "three-carpets-smoke", - "tough-lies-mate", - "weak-boats-float", - "yellow-cows-kick", - "young-donuts-swim", - "young-fishes-lie", - "young-houses-unite" - ] -} diff --git a/.changeset/rare-peas-dream.md b/.changeset/rare-peas-dream.md deleted file mode 100644 index 443f8c566d..0000000000 --- a/.changeset/rare-peas-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Repack the package to fix issues with typescript with named exports diff --git a/.changeset/renovate-7b61228.md b/.changeset/renovate-7b61228.md deleted file mode 100644 index 74794c2155..0000000000 --- a/.changeset/renovate-7b61228.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/cli': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Updated dependency `esbuild` to `^0.21.0`. diff --git a/.changeset/renovate-ced359b.md b/.changeset/renovate-ced359b.md deleted file mode 100644 index 2c100b6e23..0000000000 --- a/.changeset/renovate-ced359b.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/plugin-home-react': patch -'@backstage/plugin-home': patch -'@backstage/plugin-scaffolder-react': patch -'@backstage/plugin-scaffolder': patch ---- - -Updated dependency `@rjsf/utils` to `5.18.5`. -Updated dependency `@rjsf/core` to `5.18.5`. -Updated dependency `@rjsf/material-ui` to `5.18.5`. -Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. diff --git a/.changeset/rich-bears-march.md b/.changeset/rich-bears-march.md deleted file mode 100644 index ec4a4a9404..0000000000 --- a/.changeset/rich-bears-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Import discovery from backend-defaults instead of backend-common diff --git a/.changeset/selfish-turtles-jog.md b/.changeset/selfish-turtles-jog.md deleted file mode 100644 index 02506ec068..0000000000 --- a/.changeset/selfish-turtles-jog.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor -'@backstage/plugin-scaffolder-node': patch ---- - -Add support for `autocomplete` extension point to provide additional `autocomplete` handlers diff --git a/.changeset/serious-kings-trade.md b/.changeset/serious-kings-trade.md deleted file mode 100644 index 826392bf98..0000000000 --- a/.changeset/serious-kings-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -Added mock for the Root Health Service in `mockServices`. diff --git a/.changeset/sharp-rice-tickle.md b/.changeset/sharp-rice-tickle.md deleted file mode 100644 index 6104e11e75..0000000000 --- a/.changeset/sharp-rice-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-react': patch ---- - -Add a namespace label to RolloutDrawer diff --git a/.changeset/short-flowers-cry.md b/.changeset/short-flowers-cry.md deleted file mode 100644 index bf2c354465..0000000000 --- a/.changeset/short-flowers-cry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': minor ---- - -Adds support for custom tag policies when creating GitHub environments. diff --git a/.changeset/short-impalas-suffer.md b/.changeset/short-impalas-suffer.md deleted file mode 100644 index 34bc31e85d..0000000000 --- a/.changeset/short-impalas-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-react': patch ---- - -Fix the `HTML` markup of the `FixDialog` component, `ul` and `li` are not allowed inside a `p` tag. diff --git a/.changeset/shy-hounds-battle.md b/.changeset/shy-hounds-battle.md deleted file mode 100644 index bfcf46c076..0000000000 --- a/.changeset/shy-hounds-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fix a few minor issues with the backend template that were causing failing linting checks in the main repo. diff --git a/.changeset/silent-experts-move.md b/.changeset/silent-experts-move.md deleted file mode 100644 index 06441a62c6..0000000000 --- a/.changeset/silent-experts-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Subpath export `package.json` should be of a unique name to avoid typescript resolution issues diff --git a/.changeset/silent-lobsters-tease.md b/.changeset/silent-lobsters-tease.md deleted file mode 100644 index 2dea26d8ff..0000000000 --- a/.changeset/silent-lobsters-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -The `backendPlugin` and `backendModule` factory now includes a step for automatically adding the new backend plugin/module to the `index.ts` file of the backend. diff --git a/.changeset/silent-moose-eat.md b/.changeset/silent-moose-eat.md deleted file mode 100644 index bc66f55181..0000000000 --- a/.changeset/silent-moose-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Support `catalogFilter` array on `OwnedEntityPicker` diff --git a/.changeset/smooth-pianos-argue.md b/.changeset/smooth-pianos-argue.md deleted file mode 100644 index e7fc66dee4..0000000000 --- a/.changeset/smooth-pianos-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -Updated `startTestBackend` and `ServiceFactoryTester` to only accept plain service factory or backend feature objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. diff --git a/.changeset/soft-clocks-bake.md b/.changeset/soft-clocks-bake.md deleted file mode 100644 index 29d7658869..0000000000 --- a/.changeset/soft-clocks-bake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Added small notes to AboutCard to discourage customizability PRs diff --git a/.changeset/sour-jokes-sneeze.md b/.changeset/sour-jokes-sneeze.md deleted file mode 100644 index 5cafaea595..0000000000 --- a/.changeset/sour-jokes-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-notifications-backend': patch ---- - -Added an option to filter notifications by topic diff --git a/.changeset/spotty-apricots-tan.md b/.changeset/spotty-apricots-tan.md deleted file mode 100644 index a2be38d775..0000000000 --- a/.changeset/spotty-apricots-tan.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -'@backstage/backend-plugin-api': patch ---- - -Deprecated the ability to define options for service factories through `createServiceFactory`. In the future all service factories will return a plain `ServiceFactory` object, rather than allowing users to pass options to the factory. To allow for customization of a service implementation one can instead export one or a few building blocks that allows for simple re-implementation of the service instead. - -For example, instead of: - -```ts -export const fooServiceFactory = createServiceFactory<FooService>( - (options?: { bar: string }) => ({ - service: fooServiceRef, - deps: { logger: coreServices.logger }, - factory({ logger }) { - return { - // Implementation of the foo service using the `bar` option. - }; - }, - }), -); -``` - -We instead encourage service implementations to provide an easy to use API for re-implementing the service for advanced use-cases: - -```ts -/** @public */ -export class DefaultFooService implements FooService { - static create(options: { bar: string; logger: LoggerService }) { - return new DefaultFooService(options.logger, options.bar ?? 'default'); - } - - private constructor( - private readonly logger: string, - private readonly bar: string, - ) {} - - // The rest of the implementation -} -``` - -A user that wishes to customize the service can then easily do so by defining their own factory: - -```ts -export const customFooServiceFactory = createServiceFactory<FooService>({ - service: fooServiceRef, - deps: { logger: coreServices.logger }, - factory({ logger }) { - return DefaultFooService.create({ logger, bar: 'baz' }); - }, -}); -``` - -This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. - -In cases where the old options patterns significantly improves the usability of the service factory, the old pattern can still be implemented like this: - -```ts -const fooServiceFactoryWithOptions = (options?: { bar: string }) => - createServiceFactory<FooService>({ - service: fooServiceRef, - deps: { logger: coreServices.logger }, - factory({ logger }) { - return { - // Implementation of the foo service using the `bar` option. - }; - }, - }); - -export const fooServiceFactory = Object.assign( - fooServiceFactoryWithOptions, - fooServiceFactoryWithOptions(), -); -``` - -This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. - -A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. diff --git a/.changeset/spotty-crabs-shop.md b/.changeset/spotty-crabs-shop.md deleted file mode 100644 index 6bc1bcfe0c..0000000000 --- a/.changeset/spotty-crabs-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': patch ---- - -Added action to enable GitHub Pages on a repo diff --git a/.changeset/stale-beds-sneeze.md b/.changeset/stale-beds-sneeze.md deleted file mode 100644 index 695e09a624..0000000000 --- a/.changeset/stale-beds-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-node': patch ---- - -Fix issues with Atlassian OAuth provider: retrieve the email and photo that were not in arrays but rather in single props. diff --git a/.changeset/swift-dots-cough.md b/.changeset/swift-dots-cough.md deleted file mode 100644 index 255ee192a0..0000000000 --- a/.changeset/swift-dots-cough.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': minor ---- - -Improve validation error display text in scaffolder diff --git a/.changeset/swift-seals-drive.md b/.changeset/swift-seals-drive.md deleted file mode 100644 index a03f490d3f..0000000000 --- a/.changeset/swift-seals-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-plugin-api': minor ---- - -**BREAKING**: The `PermissionsService` no longer supports passing the deprecated `token` option, and the request options are now required. diff --git a/.changeset/tall-walls-learn.md b/.changeset/tall-walls-learn.md deleted file mode 100644 index d79c439221..0000000000 --- a/.changeset/tall-walls-learn.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/backend-plugin-api': patch ---- - -All service config types were renamed to option types in order to standardize frontend and backend `create*` function signatures: - -- The `ServiceRefConfig` type was renamed to`ServiceRefOptions`; -- The `RootServiceFactoryConfig` type was renamed to `RootServiceFactoryOptions`; -- The `PluginServiceFactoryConfig` type was renamed to `PluginServiceFactoryOptions` diff --git a/.changeset/ten-dancers-drum.md b/.changeset/ten-dancers-drum.md deleted file mode 100644 index 3f54a7a6b3..0000000000 --- a/.changeset/ten-dancers-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': patch ---- - -Fixed issue with octokit call missing owner and repo when creating environment variables and secrets using github:environment:create action diff --git a/.changeset/ten-nails-hear.md b/.changeset/ten-nails-hear.md deleted file mode 100644 index 8d3c4d59c3..0000000000 --- a/.changeset/ten-nails-hear.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': patch ---- - -Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:publish` action - -Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:repo:push` action diff --git a/.changeset/ten-pots-walk.md b/.changeset/ten-pots-walk.md deleted file mode 100644 index cf540fd0e2..0000000000 --- a/.changeset/ten-pots-walk.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-logs': patch ---- - -Creates a new module to make logging catalog errors simple. This module subscribes to catalog events and logs them. - -See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install -and configure the plugin. diff --git a/.changeset/tender-turkeys-begin.md b/.changeset/tender-turkeys-begin.md deleted file mode 100644 index 26e376cecf..0000000000 --- a/.changeset/tender-turkeys-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-react': patch ---- - -Improve API Reference documentation diff --git a/.changeset/thick-lizards-divide.md b/.changeset/thick-lizards-divide.md deleted file mode 100644 index 84c1ab311a..0000000000 --- a/.changeset/thick-lizards-divide.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-app-api': patch -'@backstage/backend-common': patch ---- - -Fixing exporting of classes properly from new packages diff --git a/.changeset/thick-moose-decide.md b/.changeset/thick-moose-decide.md deleted file mode 100644 index 194932979d..0000000000 --- a/.changeset/thick-moose-decide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -Update deps in search api extension to include fetch api diff --git a/.changeset/three-carpets-smoke.md b/.changeset/three-carpets-smoke.md deleted file mode 100644 index ab6d857f7d..0000000000 --- a/.changeset/three-carpets-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-gitlab': patch ---- - -Added support for passing `variables` to `gitlab:pipeline:trigger` diff --git a/.changeset/three-lizards-crash.md b/.changeset/three-lizards-crash.md deleted file mode 100644 index 5dffed0f3b..0000000000 --- a/.changeset/three-lizards-crash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': patch ---- - -The ability to install backend features in callback form (`() => BackendFeature`) has been deprecated. This typically means that you need to update the installed features to use the latest version of `@backstage/backend-plugin-api`. If the feature is from a third-party package, please reach out to the package maintainer to update it. diff --git a/.changeset/tiny-pianos-work.md b/.changeset/tiny-pianos-work.md deleted file mode 100644 index 033e8272e5..0000000000 --- a/.changeset/tiny-pianos-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend': patch ---- - -The `AuthorizedSearchEngine` will now ignore the deprecated `token` option, and treat it as an unauthorized request. This will not have any effect in practice, since credentials are always provided by the router. diff --git a/.changeset/tiny-schools-draw.md b/.changeset/tiny-schools-draw.md deleted file mode 100644 index 2042f13fac..0000000000 --- a/.changeset/tiny-schools-draw.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/backend-test-utils': patch -'@backstage/frontend-test-utils': patch -'@backstage/test-utils': patch ---- - -Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. diff --git a/.changeset/tough-lies-mate.md b/.changeset/tough-lies-mate.md deleted file mode 100644 index 99edcf3616..0000000000 --- a/.changeset/tough-lies-mate.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-gcp': minor -'@backstage/plugin-scaffolder-backend': minor -'@backstage/plugin-scaffolder-node': patch ---- - -Serialization of the scaffolder workspace into GCP bucket diff --git a/.changeset/twenty-buses-carry.md b/.changeset/twenty-buses-carry.md deleted file mode 100644 index 00347e60d8..0000000000 --- a/.changeset/twenty-buses-carry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Added the Postgres Search Engine to the `create-app` template diff --git a/.changeset/weak-boats-float.md b/.changeset/weak-boats-float.md deleted file mode 100644 index 4fe6fe73a0..0000000000 --- a/.changeset/weak-boats-float.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor -'@backstage/backend-test-utils': patch -'@backstage/backend-defaults': patch -'@backstage/backend-common': patch ---- - -bumped better-sqlite3 from ^9.0.0 to ^11.0.0 diff --git a/.changeset/weak-emus-doubt.md b/.changeset/weak-emus-doubt.md deleted file mode 100644 index cd3b4a836b..0000000000 --- a/.changeset/weak-emus-doubt.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-notifications-backend-module-email': patch -'@backstage/plugin-notifications-common': patch -'@backstage/plugin-notifications-node': patch ---- - -Move notification processor filter parsing to common package diff --git a/.changeset/wet-rivers-travel.md b/.changeset/wet-rivers-travel.md deleted file mode 100644 index 1778a3db23..0000000000 --- a/.changeset/wet-rivers-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Refactor of `rootConfigServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. diff --git a/.changeset/wicked-impalas-explode.md b/.changeset/wicked-impalas-explode.md deleted file mode 100644 index f8a01bc36e..0000000000 --- a/.changeset/wicked-impalas-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -Internal refactor of `mockServices.httpAuth.factory` to allow it to still be constructed with options, but without declaring options via `createServiceFactory`. diff --git a/.changeset/witty-keys-share.md b/.changeset/witty-keys-share.md deleted file mode 100644 index 0ef89e56e2..0000000000 --- a/.changeset/witty-keys-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': patch ---- - -disables rendering of output box if no output is returned diff --git a/.changeset/yellow-cows-kick.md b/.changeset/yellow-cows-kick.md deleted file mode 100644 index 93c7e1cff1..0000000000 --- a/.changeset/yellow-cows-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-node-test-utils': patch ---- - -Filename and imports correction for mockActionContext.ts diff --git a/.changeset/young-donuts-swim.md b/.changeset/young-donuts-swim.md deleted file mode 100644 index edd854f6dc..0000000000 --- a/.changeset/young-donuts-swim.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': minor -'@backstage/plugin-scaffolder': minor ---- - -Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` diff --git a/.changeset/young-fishes-lie.md b/.changeset/young-fishes-lie.md deleted file mode 100644 index 769d046375..0000000000 --- a/.changeset/young-fishes-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix user entity not being fetched for scaffolder dry runner diff --git a/.changeset/young-houses-unite.md b/.changeset/young-houses-unite.md deleted file mode 100644 index 47ffe43c21..0000000000 --- a/.changeset/young-houses-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': patch ---- - -Fixing issue with `MiddlewareFactory` deprecation wrapping diff --git a/docs/releases/v1.29.0-changelog.md b/docs/releases/v1.29.0-changelog.md new file mode 100644 index 0000000000..86cb84d1f1 --- /dev/null +++ b/docs/releases/v1.29.0-changelog.md @@ -0,0 +1,2495 @@ +# Release v1.29.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.29.0](https://backstage.github.io/upgrade-helper/?to=1.29.0) + +## @backstage/backend-app-api@0.8.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. +- f691c9b: **BREAKING**: Removed the ability to pass callback-form service factories through the `defaultServiceFactories` option of `createSpecializedBackend`. This is an immediate breaking change as usage of this function is expected to be very rare. + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 617a7d2: Internal refactor that avoids the use of service factory options. +- b60db08: Fixing exporting of classes properly from new packages +- 18b96b1: The ability to install backend features in callback form (`() => BackendFeature`) has been deprecated. This typically means that you need to update the installed features to use the latest version of `@backstage/backend-plugin-api`. If the feature is from a third-party package, please reach out to the package maintainer to update it. +- a63c4b6: Fixing issue with `MiddlewareFactory` deprecation wrapping +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.4.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 419f387: Refactor of `rootHttpRouterServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- cb14a05: Repack the package to fix issues with typescript with named exports +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- e28af58: Refactor of `rootConfigServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/backend-plugin-api@0.7.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: The `PermissionsService` no longer supports passing the deprecated `token` option, and the request options are now required. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules + +- 062c01c: Deprecated the ability to define options for service factories through `createServiceFactory`. In the future all service factories will return a plain `ServiceFactory` object, rather than allowing users to pass options to the factory. To allow for customization of a service implementation one can instead export one or a few building blocks that allows for simple re-implementation of the service instead. + + For example, instead of: + + ```ts + export const fooServiceFactory = createServiceFactory<FooService>( + (options?: { bar: string }) => ({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }), + ); + ``` + + We instead encourage service implementations to provide an easy to use API for re-implementing the service for advanced use-cases: + + ```ts + /** @public */ + export class DefaultFooService implements FooService { + static create(options: { bar: string; logger: LoggerService }) { + return new DefaultFooService(options.logger, options.bar ?? 'default'); + } + + private constructor( + private readonly logger: string, + private readonly bar: string, + ) {} + + // The rest of the implementation + } + ``` + + A user that wishes to customize the service can then easily do so by defining their own factory: + + ```ts + export const customFooServiceFactory = createServiceFactory<FooService>({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return DefaultFooService.create({ logger, bar: 'baz' }); + }, + }); + ``` + + This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. + + In cases where the old options patterns significantly improves the usability of the service factory, the old pattern can still be implemented like this: + + ```ts + const fooServiceFactoryWithOptions = (options?: { bar: string }) => + createServiceFactory<FooService>({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }); + + export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), + ); + ``` + + This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. + + A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. + +- fe47a3e: All service config types were renamed to option types in order to standardize frontend and backend `create*` function signatures: + + - The `ServiceRefConfig` type was renamed to`ServiceRefOptions`; + - The `RootServiceFactoryConfig` type was renamed to `RootServiceFactoryOptions`; + - The `PluginServiceFactoryConfig` type was renamed to `PluginServiceFactoryOptions` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-app-api@1.14.0 + +### Minor Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +### Patch Changes + +- db2e2d5: Updated config schema to support app.routes.bindings +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/integration@1.13.0 + +### Minor Changes + +- b5deed0: Add support for `token` for `bitbucketCloud` integration + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend@1.24.0 + +### Minor Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.0 + +### Minor Changes + +- cb32ca7: **BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items. + + Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one. + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-common@0.8.0 + +### Minor Changes + +- f4085b8: **BREAKING**: Removed the deprecated and unused `token` option from `EvaluatorRequestOptions`. The `PermissionsClient` now has its own `PermissionClientRequestOptions` type that declares the `token` option instead. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-node@0.8.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: Updated the `ServerPermissionClient` to match the new `PermissionsService` interface, where the deprecated `token` option has been removed and the options are now required. + +### Patch Changes + +- ed10fd2: The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + + - `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. + - `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + + Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +- 28b2cfb: Fix invalid cross-reference in API Reference docs + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder@1.23.0 + +### Minor Changes + +- 52b6db0: Use virtualization with `EntityPicker` as done earlier with `MultiEntityPicker` to fix performance issues with large data sets. `VirtualizedListbox` extracted into reusable component. +- 3583ce5: Use virtualization with `MultiEntityPicker`. Fixes performance issues with large data sets. +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- 4d7e11f: enable resizing of the task log stream viewer +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- 89c44b3: Support `catalogFilter` array on `OwnedEntityPicker` +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.23.0 + +### Minor Changes + +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- b9451dd: Updated `catalog:write` scaffolder action to show correct file path location in log message +- ff1bb4c: Added a documentation how to use checkpoints +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- 62d1fe3: Fix user entity not being fetched for scaffolder dry runner +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.14 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.12 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.0 + +### Minor Changes + +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.0 + +### Minor Changes + +- 70c4b36: Adds support for custom tag policies when creating GitHub environments. + +### Patch Changes + +- ccfc9d1: Fixed bug resulting from missing required owner and repo arguments in `getEnvironmentPublicKey` in action `github:environment:create`. + + Adding environment secrets now works as expected. + +- 141f366: Added action to enable GitHub Pages on a repo + +- 4410fed: Fixed issue with octokit call missing owner and repo when creating environment variables and secrets using github:environment:create action + +- dfaa28d: Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:publish` action + + Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:repo:push` action + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-react@1.10.0 + +### Minor Changes + +- 354e68c: Improve validation error display text in scaffolder +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- 4d7e11f: disables rendering of output box if no output is returned +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/app-defaults@1.5.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/backend-common@0.23.3 + +### Patch Changes + +- 8c09c97: Deprecate legacy status check factory, handler and types. +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- c964a3d: Add dependencies that are needed by cross-imports from backend-defaults +- b60db08: Fixing exporting of classes properly from new packages +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/backend-dynamic-feature-service@0.2.15 + +### Patch Changes + +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-node@0.1.22 + - @backstage/plugin-events-backend@0.3.9 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/config-loader@1.8.1 + - @backstage/plugin-search-common@1.2.13 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-openapi-utils@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/errors@1.2.4 + +## @backstage/backend-tasks@0.5.27 + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.4 + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- edf5cc3: The function `isDockerDisabledForTests` is deprecated and will no longer be exported in the near future as it should only be used internally. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- fce7887: Added mock for the Root Health Service in `mockServices`. +- 906c817: Updated `startTestBackend` and `ServiceFactoryTester` to only accept plain service factory or backend feature objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- 98ccf00: Internal refactor of `mockServices.httpAuth.factory` to allow it to still be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/cli@0.26.11 + +### Patch Changes + +- 133464c: Added experimental support for dynamic frontend plugin builds, enabled via setting `EXPERIMENTAL_MODULE_FEDERATION` for the app build, and using the `frontend-dynamic-container` package role to create a container. Both of these are experimental and will change in the future. +- e2e320c: - remove unused dependencies `winston` and `yn` from the template of backend plugins; + - update `msw` to version `2.3.1` in the template of backend plugins; + starting with v1 and switching later to v2 is tedious and not straight forward; it's easier to start with v2; +- 0540c5a: Updated the scaffolding output message for `plugin-common` in `backstage-cli`. Now, when executing `backstage-cli new` to create a new `plugin-common` package, the output message accurately reflects the action by displaying `Creating common plugin package...` instead of the previous, less accurate `Creating backend plugin...`. +- 7652db4: Only bootstrap global-agent if it's actually being used +- f0c0039: Fix issue with CLI that was preventing upgrading from 1.28 +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- a60d73b: Fix a few minor issues with the backend template that were causing failing linting checks in the main repo. +- 0510d98: Subpath export `package.json` should be of a unique name to avoid typescript resolution issues +- 4baac0c: The `backendPlugin` and `backendModule` factory now includes a step for automatically adding the new backend plugin/module to the `index.ts` file of the backend. +- Updated dependencies + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.13.0 + - @backstage/config-loader@1.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/cli-node@0.2.7 + +### Patch Changes + +- 133464c: Added internal metadata for the new experimental `frontend-dynamic-container` role. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-components@0.14.9 + +### Patch Changes + +- d4ffdbb: Fixed bug where `<Select>` component with empty string as placeholder gave an error +- 99d672d: Modified the `Select` component to take in a `data-testid` parameter ensuring backwards compatibility with default value corresponding to previously hardcoded `data-testid` of "select". +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.17 + +### Patch Changes + +- 780d994: Added `MyGroupsSidebarItem` to the sidebar in the `create-app` template +- f03d12a: Bumped create-app version. +- e90a2cd: Added the Catalog logs module to the `create-app` template +- 3ac2a6a: Added the Postgres Search Engine to the `create-app` template +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.35 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.3 + +### Patch Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-plugin-api@0.6.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.10 + +### Patch Changes + +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- Updated dependencies + - @backstage/frontend-app-api@0.7.3 + - @backstage/test-utils@1.5.8 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/types@1.1.1 + +## @backstage/integration-react@1.1.29 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.13.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/repo-tools@0.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/cli-node@0.2.7 + - @backstage/config-loader@1.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.16 + +### Patch Changes + +- c964a3d: Import discovery from backend-defaults instead of backend-common +- Updated dependencies + - @backstage/backend-defaults@0.4.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.8 + +### Patch Changes + +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## @backstage/plugin-api-docs@0.11.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-app-backend@0.3.71 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-app-node@0.1.22 + - @backstage/config-loader@1.8.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/config-loader@1.8.1 + +## @backstage/plugin-app-visualizer@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.3 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.14 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.5 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.5 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.5 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.17 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.19 + - @backstage/plugin-auth-backend-module-google-provider@0.1.19 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.17 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.3 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.15 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.3 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.15 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.3 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.3 + +### Patch Changes + +- b9832ae: Fix several issues with the Atlassian auth provider (type definition, profile url, profile transformation, scopes) +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend@0.22.9 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-auth-node@0.4.17 + +### Patch Changes + +- 55c1a72: Fix issues with Atlassian OAuth provider: retrieve the email and photo that were not in arrays but rather in single props. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-react@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-bitbucket-cloud-common@0.2.21 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/integration@1.13.0 + +## @backstage/plugin-catalog@1.21.1 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- d133eaa: Added small notes to AboutCard to discourage customizability PRs +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-search-react@1.7.13 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-aws@0.3.17 + +### Patch Changes + +- 4afa050: Export `defaultEksClusterEntityTransformer` to allow library consumers to layer additional changes on top of the default transformer. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + +## @backstage/plugin-catalog-backend-module-azure@0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-tasks@0.5.27 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.36 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.5 + +### Patch Changes + +- 9112efc: Adds support for `repository` events. + + The provider adds a subscription to the topic `github.repository`. + + Hereby, it supports events of type `repository` with actions + + - `archived` + - `deleted` + - `edited` + - `renamed` + - `transferred` + - `unarchived` + + Actions skipped as they don't require entity changes: + + - `created` + - `privatized` + - `publicized` + + If the config option `validateLocationsExist` is enabled, an API request + is necessary and will be executed. + This affects the actions `renamed`, `transferred`, and `unarchive` + of event type `repository`. + + Catalog entities related to the `GithubEntityProvider` instance will be adjusted + according to action and its meaning for them. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend-module-github@0.6.5 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.21 + +### Patch Changes + +- 8db30ad: The Gitlab configuration supports an additional optional boolean key `catalog.providers.gitlab.<your-org>.restrictUsersToGroup`. Setting this to `true` will make Backstage only import users from the group defined in the `group` key, instead of all users in the organisation (self-hosted) or of the root group (SaaS). It will default to false, keeping the original implementation intact, when not explicitly set. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.21 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-node@1.12.4 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-logs@0.0.1 + +### Patch Changes + +- 97caf55: Creates a new module to make logging catalog errors simple. This module subscribes to catalog events and logs them. + + See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install + and configure the plugin. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.30 + +### Patch Changes + +- f7bdcea: Adds a dynamic provider for the plugin-catalog-backend-module-msgraph. Configuration is now runtime configurable through the ProviderConfigTransformer. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.40 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.3 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-common@1.0.25 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-graph@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-node@1.12.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-react@1.12.2 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- 2030962: Make EntityOwnerPicker display metadata.title or spec.profile.displayName for mode=only-owners instead of metadata.name +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/core-components@0.14.9 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + +## @backstage/plugin-config-schema@0.1.57 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools@0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-devtools-backend@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/config-loader@1.8.1 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools-common@0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + +## @backstage/plugin-events-backend-module-gerrit@0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + +## @backstage/plugin-events-backend-module-github@0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-test-utils@0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.8 + +## @backstage/plugin-events-node@0.3.8 + +### Patch Changes + +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + +## @backstage/plugin-home@0.7.7 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-home-react@0.1.15 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/plugin-home-react@0.1.15 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-kubernetes@0.11.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-kubernetes-react@0.4.1 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-kubernetes-backend@0.18.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-kubernetes-node@0.1.16 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-cluster@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-kubernetes-react@0.4.1 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-kubernetes-common@0.8.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-node@0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-react@0.4.1 + +### Patch Changes + +- e3cb6ab: Add a namespace label to RolloutDrawer +- 2414d86: Fix the `HTML` markup of the `FixDialog` component, `ul` and `li` are not allowed inside a `p` tag. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications@0.2.3 + +### Patch Changes + +- 3bf0697: The toolbar on the Notifications page is hidden when there are no listed notifications. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-notifications-backend@0.3.3 + +### Patch Changes + +- d7b8ca5: Added an option to filter notifications by topic +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-notifications-backend-module-email@0.1.3 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications-common@0.0.5 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/config@1.2.0 + +## @backstage/plugin-notifications-node@0.2.3 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-org@0.6.27 + +### Patch Changes + +- 5132d28: The `useGetEntities` hook could result in requests to `/api/catalog/entities` where the headers exceed the default maximum Node.js header size of 16KB. The hook logic has been adjusted to batch the requests. + +- c307ef4: Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`. + + Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property. + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-org-react@0.1.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-permission-backend@0.5.46 + +### Patch Changes + +- ed10fd2: The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + + - `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. + - `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + + Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.19 + +### Patch Changes + +- ed10fd2: Internal refactor to use new `PolicyQueryUser` type. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + +## @backstage/plugin-permission-react@0.4.24 + +### Patch Changes + +- 28b2cfb: Improve API Reference documentation +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-proxy-backend@0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.14 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + +### Patch Changes + +- 6a4ad4e: Instead of using hardcoded `targetBranch` now fetch the default branch from Bitbucket repository. + This prevents from errors when no `targetBranch` is provided and the default repository branch is different from `master`, for example: `main`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.46 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- 2fb0eb8: Added support for passing `variables` to `gitlab:pipeline:trigger` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.39 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.30 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-node-test-utils@0.1.9 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-common@1.5.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.8 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.9 + +### Patch Changes + +- 54429b5: Filename and imports correction for mockActionContext.ts +- Updated dependencies + - @backstage/backend-test-utils@0.4.4 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/types@1.1.1 + +## @backstage/plugin-search@1.4.14 + +### Patch Changes + +- 1117aba: Update deps in search api extension to include fetch api +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/plugin-search-react@1.7.13 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search-backend@1.5.14 + +### Patch Changes + +- 343f656: The `AuthorizedSearchEngine` will now ignore the deprecated `token` option, and treat it as an unauthorized request. This will not have any effect in practice, since credentials are always provided by the router. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + +## @backstage/plugin-search-backend-module-explore@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-pg@0.5.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-techdocs@0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-node@1.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-common@1.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-react@1.7.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-signals@0.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-signals-backend@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-techdocs@1.10.7 + +### Patch Changes + +- 8fc2622: Fixed an issue that was causing techdocs pages unnecessarily re-render on navigate. +- 6fa652c: Improve default sorting of docs table +- 605b691: Allow for searching TechDocs by entity title +- 60caa92: Fix double scrollbar bug in reader +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-search-react@1.7.13 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.35 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/test-utils@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-search-react@1.7.13 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-techdocs-backend@1.10.9 + +### Patch Changes + +- 9ecf5fd: Adds extension point for publishers to the techdocs backend +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/integration@1.13.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/integration-react@1.1.29 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-techdocs-node@1.12.8 + +### Patch Changes + +- 9ecf5fd: Adds extension point for publishers to the techdocs backend +- 4c4d077: Bumps default version of techdocs docker image to latest +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + +## @backstage/plugin-techdocs-react@1.2.6 + +### Patch Changes + +- 8ac9ce5: Resolved the issue where changes in TechDoc add-ons, including the TextSize add-on, were not reapplying during navigation +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-user-settings@0.8.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.99 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.23.0 + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-app-api@0.7.3 + - @backstage/plugin-org@0.6.27 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-notifications@0.2.3 + - @backstage/plugin-home@0.7.7 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.14 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-api-docs@0.11.7 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-catalog-graph@0.4.7 + - @backstage/plugin-catalog-import@0.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6 + - @backstage/plugin-devtools@0.1.16 + - @backstage/plugin-kubernetes@0.11.12 + - @backstage/plugin-kubernetes-cluster@0.0.13 + - @backstage/plugin-search-react@1.7.13 + - @backstage/plugin-signals@0.0.8 + - @backstage/plugin-user-settings@0.8.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## example-app-next@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.23.0 + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-app-api@0.7.3 + - @backstage/plugin-org@0.6.27 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-notifications@0.2.3 + - @backstage/plugin-home@0.7.7 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.14 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/app-defaults@1.5.8 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-api-docs@0.11.7 + - @backstage/plugin-app-visualizer@0.1.8 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-catalog-graph@0.4.7 + - @backstage/plugin-catalog-import@0.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6 + - @backstage/plugin-kubernetes@0.11.12 + - @backstage/plugin-kubernetes-cluster@0.0.13 + - @backstage/plugin-search-react@1.7.13 + - @backstage/plugin-signals@0.0.8 + - @backstage/plugin-user-settings@0.8.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## app-next-example-plugin@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/frontend-plugin-api@0.6.7 + +## example-backend@0.0.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.19 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend@1.23.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-backend@0.5.46 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-techdocs-backend@1.10.9 + - @backstage/plugin-notifications-backend@0.3.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-search-backend@1.5.14 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-backend@0.3.71 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.8 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.5 + - @backstage/plugin-catalog-backend-module-openapi@0.1.40 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9 + - @backstage/plugin-devtools-backend@0.3.8 + - @backstage/plugin-kubernetes-backend@0.18.3 + - @backstage/plugin-proxy-backend@0.5.3 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-search-backend-module-explore@0.1.28 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-signals-backend@0.1.8 + - @backstage/catalog-model@1.5.0 + +## example-backend-legacy@0.2.100 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend@1.23.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-backend@0.5.46 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-techdocs-backend@1.10.9 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.39 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-search-backend@1.5.14 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-backend@0.3.71 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-devtools-backend@0.3.8 + - @backstage/plugin-events-backend@0.3.9 + - @backstage/plugin-kubernetes-backend@0.18.3 + - @backstage/plugin-proxy-backend@0.5.3 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.23 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.3 + - @backstage/plugin-search-backend-module-explore@0.1.28 + - @backstage/plugin-search-backend-module-pg@0.5.32 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-signals-backend@0.1.8 + - @backstage/plugin-signals-node@0.1.8 + - example-app@0.2.99 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## e2e-test@0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.98 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog@1.21.1 + - @backstage/test-utils@1.5.8 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @internal/plugin-todo-list@1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + +## @internal/plugin-todo-list-backend@1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + +## @internal/plugin-todo-list-common@1.0.20 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 diff --git a/package.json b/package.json index d3d042c2f5..07c9b2917b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.29.0-next.2", + "version": "1.29.0", "private": true, "repository": { "type": "git", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 21fe959edb..a03034112c 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.5.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 1.5.8-next.2 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 4393164fcd..8997ba860f 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.5.8-next.2", + "version": "1.5.8", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 5456da237c..fb61388f22 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # app-next-example-plugin +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/frontend-plugin-api@0.6.7 + ## 0.0.13-next.1 ### Patch Changes diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index c3faec66d6..c48d5a92be 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.13-next.1", + "version": "0.0.13", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 22d53e4c78..3bbf698ebd 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,48 @@ # example-app-next +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.23.0 + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-app-api@0.7.3 + - @backstage/plugin-org@0.6.27 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-notifications@0.2.3 + - @backstage/plugin-home@0.7.7 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.14 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/app-defaults@1.5.8 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-api-docs@0.11.7 + - @backstage/plugin-app-visualizer@0.1.8 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-catalog-graph@0.4.7 + - @backstage/plugin-catalog-import@0.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6 + - @backstage/plugin-kubernetes@0.11.12 + - @backstage/plugin-kubernetes-cluster@0.0.13 + - @backstage/plugin-search-react@1.7.13 + - @backstage/plugin-signals@0.0.8 + - @backstage/plugin-user-settings@0.8.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 0.0.13-next.2 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index c38eacfcd5..c0c19bc40c 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.13-next.2", + "version": "0.0.13", "private": true, "repository": { "type": "git", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 4977cc1d6d..a75cf24a07 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,46 @@ # example-app +## 0.2.99 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.23.0 + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-app-api@0.7.3 + - @backstage/plugin-org@0.6.27 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-notifications@0.2.3 + - @backstage/plugin-home@0.7.7 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.14 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-api-docs@0.11.7 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-catalog-graph@0.4.7 + - @backstage/plugin-catalog-import@0.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6 + - @backstage/plugin-devtools@0.1.16 + - @backstage/plugin-kubernetes@0.11.12 + - @backstage/plugin-kubernetes-cluster@0.0.13 + - @backstage/plugin-search-react@1.7.13 + - @backstage/plugin-signals@0.0.8 + - @backstage/plugin-user-settings@0.8.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 0.2.99-next.2 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index f15dbac9db..29cfbf0357 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.99-next.2", + "version": "0.2.99", "backstage": { "role": "frontend" }, diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index 4456d8438b..70a163ee34 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/backend-app-api +## 0.8.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. +- f691c9b: **BREAKING**: Removed the ability to pass callback-form service factories through the `defaultServiceFactories` option of `createSpecializedBackend`. This is an immediate breaking change as usage of this function is expected to be very rare. + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 617a7d2: Internal refactor that avoids the use of service factory options. +- b60db08: Fixing exporting of classes properly from new packages +- 18b96b1: The ability to install backend features in callback form (`() => BackendFeature`) has been deprecated. This typically means that you need to update the installed features to use the latest version of `@backstage/backend-plugin-api`. If the feature is from a third-party package, please reach out to the package maintainer to update it. +- a63c4b6: Fixing issue with `MiddlewareFactory` deprecation wrapping +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.10-next.1 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 4d3ffd874f..8bd156c3a4 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "0.7.10-next.1", + "version": "0.8.0", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index d144d461b8..6d3218e0ed 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/backend-common +## 0.23.3 + +### Patch Changes + +- 8c09c97: Deprecate legacy status check factory, handler and types. +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- c964a3d: Add dependencies that are needed by cross-imports from backend-defaults +- b60db08: Fixing exporting of classes properly from new packages +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + ## 0.23.3-next.1 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index f23f0d2ce9..553c800ea5 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-common", - "version": "0.23.3-next.1", + "version": "0.23.3", "description": "Common functionality library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index a9b20bfbea..7d2ad7d6b7 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/backend-defaults +## 0.4.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 419f387: Refactor of `rootHttpRouterServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- cb14a05: Repack the package to fix issues with typescript with named exports +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- e28af58: Refactor of `rootConfigServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + ## 0.3.4-next.1 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 0606d942ff..87da6e0017 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.3.4-next.1", + "version": "0.4.0", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index 02f47ef7b4..3003a8e177 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/backend-dynamic-feature-service +## 0.2.15 + +### Patch Changes + +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-node@0.1.22 + - @backstage/plugin-events-backend@0.3.9 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/config-loader@1.8.1 + - @backstage/plugin-search-common@1.2.13 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.15-next.1 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 3e3e566879..0d05b97bd9 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-dynamic-feature-service", "description": "Backstage dynamic feature service", - "version": "0.2.15-next.1", + "version": "0.2.15", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-legacy/CHANGELOG.md b/packages/backend-legacy/CHANGELOG.md index 35ed7a6c93..04538ca02e 100644 --- a/packages/backend-legacy/CHANGELOG.md +++ b/packages/backend-legacy/CHANGELOG.md @@ -1,5 +1,47 @@ # example-backend-legacy +## 0.2.100 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend@1.23.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-backend@0.5.46 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-techdocs-backend@1.10.9 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.39 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-search-backend@1.5.14 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-backend@0.3.71 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-devtools-backend@0.3.8 + - @backstage/plugin-events-backend@0.3.9 + - @backstage/plugin-kubernetes-backend@0.18.3 + - @backstage/plugin-proxy-backend@0.5.3 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.23 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.3 + - @backstage/plugin-search-backend-module-explore@0.1.28 + - @backstage/plugin-search-backend-module-pg@0.5.32 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-signals-backend@0.1.8 + - @backstage/plugin-signals-node@0.1.8 + - example-app@0.2.99 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.2.100-next.2 ### Patch Changes diff --git a/packages/backend-legacy/package.json b/packages/backend-legacy/package.json index a537361746..5861719455 100644 --- a/packages/backend-legacy/package.json +++ b/packages/backend-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-legacy", - "version": "0.2.100-next.2", + "version": "0.2.100", "backstage": { "role": "backend" }, diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 43a57f7d8c..540a239ad6 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-openapi-utils +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index f35863c6e3..3059af5ffe 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-openapi-utils", "description": "OpenAPI typescript support.", - "version": "0.1.15-next.1", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index cb341b218e..3f47354561 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,103 @@ # @backstage/backend-plugin-api +## 0.7.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: The `PermissionsService` no longer supports passing the deprecated `token` option, and the request options are now required. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- 062c01c: Deprecated the ability to define options for service factories through `createServiceFactory`. In the future all service factories will return a plain `ServiceFactory` object, rather than allowing users to pass options to the factory. To allow for customization of a service implementation one can instead export one or a few building blocks that allows for simple re-implementation of the service instead. + + For example, instead of: + + ```ts + export const fooServiceFactory = createServiceFactory<FooService>( + (options?: { bar: string }) => ({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }), + ); + ``` + + We instead encourage service implementations to provide an easy to use API for re-implementing the service for advanced use-cases: + + ```ts + /** @public */ + export class DefaultFooService implements FooService { + static create(options: { bar: string; logger: LoggerService }) { + return new DefaultFooService(options.logger, options.bar ?? 'default'); + } + + private constructor( + private readonly logger: string, + private readonly bar: string, + ) {} + + // The rest of the implementation + } + ``` + + A user that wishes to customize the service can then easily do so by defining their own factory: + + ```ts + export const customFooServiceFactory = createServiceFactory<FooService>({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return DefaultFooService.create({ logger, bar: 'baz' }); + }, + }); + ``` + + This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. + + In cases where the old options patterns significantly improves the usability of the service factory, the old pattern can still be implemented like this: + + ```ts + const fooServiceFactoryWithOptions = (options?: { bar: string }) => + createServiceFactory<FooService>({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }); + + export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), + ); + ``` + + This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. + + A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. + +- fe47a3e: All service config types were renamed to option types in order to standardize frontend and backend `create*` function signatures: + + - The `ServiceRefConfig` type was renamed to`ServiceRefOptions`; + - The `RootServiceFactoryConfig` type was renamed to `RootServiceFactoryOptions`; + - The `PluginServiceFactoryConfig` type was renamed to `PluginServiceFactoryOptions` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.6.22-next.1 ### Patch Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 631141a57f..52fbfb5806 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "0.6.22-next.1", + "version": "0.7.0", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 590d824635..fef4fd56a9 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/backend-tasks +## 0.5.27 + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.5.27-next.1 ### Patch Changes diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index df10190443..998e1d0650 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.5.27-next.1", + "version": "0.5.27", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 9f3d84a906..9bee6fae55 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/backend-test-utils +## 0.4.4 + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- edf5cc3: The function `isDockerDisabledForTests` is deprecated and will no longer be exported in the near future as it should only be used internally. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- fce7887: Added mock for the Root Health Service in `mockServices`. +- 906c817: Updated `startTestBackend` and `ServiceFactoryTester` to only accept plain service factory or backend feature objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- 98ccf00: Internal refactor of `mockServices.httpAuth.factory` to allow it to still be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.4-next.1 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index e975dc01d9..875949c0a0 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "0.4.4-next.1", + "version": "0.4.4", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 4ceb786f2a..854993119c 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,42 @@ # example-backend +## 0.0.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.19 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend@1.23.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-backend@0.5.46 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-techdocs-backend@1.10.9 + - @backstage/plugin-notifications-backend@0.3.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-search-backend@1.5.14 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-backend@0.3.71 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.8 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.5 + - @backstage/plugin-catalog-backend-module-openapi@0.1.40 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9 + - @backstage/plugin-devtools-backend@0.3.8 + - @backstage/plugin-kubernetes-backend@0.18.3 + - @backstage/plugin-proxy-backend@0.5.3 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-search-backend-module-explore@0.1.28 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-signals-backend@0.1.8 + - @backstage/catalog-model@1.5.0 + ## 0.0.28-next.2 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 459db3c317..ae47716326 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.28-next.2", + "version": "0.0.28", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index dcf9073277..21995e4812 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/cli-node +## 0.2.7 + +### Patch Changes + +- 133464c: Added internal metadata for the new experimental `frontend-dynamic-container` role. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.6 ### Patch Changes diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 56cf50a096..c727918510 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-node", - "version": "0.2.6", + "version": "0.2.7", "description": "Node.js library for Backstage CLIs", "backstage": { "role": "node-library" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a2fae6e3c0..c062324abb 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/cli +## 0.26.11 + +### Patch Changes + +- 133464c: Added experimental support for dynamic frontend plugin builds, enabled via setting `EXPERIMENTAL_MODULE_FEDERATION` for the app build, and using the `frontend-dynamic-container` package role to create a container. Both of these are experimental and will change in the future. +- e2e320c: - remove unused dependencies `winston` and `yn` from the template of backend plugins; + - update `msw` to version `2.3.1` in the template of backend plugins; + starting with v1 and switching later to v2 is tedious and not straight forward; it's easier to start with v2; +- 0540c5a: Updated the scaffolding output message for `plugin-common` in `backstage-cli`. Now, when executing `backstage-cli new` to create a new `plugin-common` package, the output message accurately reflects the action by displaying `Creating common plugin package...` instead of the previous, less accurate `Creating backend plugin...`. +- 7652db4: Only bootstrap global-agent if it's actually being used +- f0c0039: Fix issue with CLI that was preventing upgrading from 1.28 +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- a60d73b: Fix a few minor issues with the backend template that were causing failing linting checks in the main repo. +- 0510d98: Subpath export `package.json` should be of a unique name to avoid typescript resolution issues +- 4baac0c: The `backendPlugin` and `backendModule` factory now includes a step for automatically adding the new backend plugin/module to the `index.ts` file of the backend. +- Updated dependencies + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.13.0 + - @backstage/config-loader@1.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + ## 0.26.11-next.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 97c15b16f2..ccc4818331 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.26.11-next.1", + "version": "0.26.11", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index d80ed2fdcb..ff09befd63 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/core-app-api +## 1.14.0 + +### Minor Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +### Patch Changes + +- db2e2d5: Updated config schema to support app.routes.bindings +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.13.1-next.1 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 35e32f9726..5f297b16d5 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "1.13.1-next.1", + "version": "1.14.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 3ad2bd654e..829c6f41ff 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/core-compat-api +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + ## 0.2.7-next.1 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 0f164a6187..bbf131c761 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.2.7-next.1", + "version": "0.2.7", "backstage": { "role": "web-library" }, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 163406bbca..3d46390597 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/core-components +## 0.14.9 + +### Patch Changes + +- d4ffdbb: Fixed bug where `<Select>` component with empty string as placeholder gave an error +- 99d672d: Modified the `Select` component to take in a `data-testid` parameter ensuring backwards compatibility with default value corresponding to previously hardcoded `data-testid` of "select". +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/version-bridge@1.0.8 + ## 0.14.9-next.1 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 21c4a19dbf..55faac9dba 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.14.9-next.1", + "version": "0.14.9", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 708a9b33f8..d7ad8037e6 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/create-app +## 0.5.17 + +### Patch Changes + +- 780d994: Added `MyGroupsSidebarItem` to the sidebar in the `create-app` template +- f03d12a: Bumped create-app version. +- e90a2cd: Added the Catalog logs module to the `create-app` template +- 3ac2a6a: Added the Postgres Search Engine to the `create-app` template +- Updated dependencies + - @backstage/cli-common@0.1.14 + ## 0.5.17-next.2 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index c1ca92f87b..7d33d03a4b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.5.17-next.2", + "version": "0.5.17", "publishConfig": { "access": "public" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index ee9f56dec7..f46a65827a 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/dev-utils +## 1.0.35 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 1.0.35-next.2 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 50220c36e1..0902d7c322 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.0.35-next.2", + "version": "1.0.35", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 5144d9fd86..ad76bf8b75 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,14 @@ # e2e-test +## 0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + ## 0.2.18-next.2 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 1d65314e83..7fa0349a46 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.18-next.2", + "version": "0.2.18", "private": true, "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index dde282a831..7203efb8d8 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/frontend-app-api +## 0.7.3 + +### Patch Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.7.3-next.2 ### Patch Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 68c2fd8fab..a55284ff68 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.7.3-next.2", + "version": "0.7.3", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index dda46968a4..4c0e6fe9f8 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/frontend-plugin-api +## 0.6.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.6.7-next.1 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index b62fe36059..f02f3fdedf 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.6.7-next.1", + "version": "0.6.7", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 34d9b28143..de21194f4e 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/frontend-test-utils +## 0.1.10 + +### Patch Changes + +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- Updated dependencies + - @backstage/frontend-app-api@0.7.3 + - @backstage/test-utils@1.5.8 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/types@1.1.1 + ## 0.1.10-next.2 ### Patch Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 2a366fa152..0816a25443 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.1.10-next.2", + "version": "0.1.10", "backstage": { "role": "web-library" }, diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index e5e5759f1e..ed54a92f5c 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/integration-react +## 1.1.29 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.13.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + ## 1.1.29-next.0 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 375f99fba6..b04b603327 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.1.29-next.0", + "version": "1.1.29", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index a76d031e35..3fb630ccfc 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/integration +## 1.13.0 + +### Minor Changes + +- b5deed0: Add support for `token` for `bitbucketCloud` integration + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 1.13.0-next.0 ### Minor Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 6f4743fd13..810150cf2d 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.13.0-next.0", + "version": "1.13.0", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index daa0684e41..18c696a51c 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/repo-tools +## 0.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/cli-node@0.2.7 + - @backstage/config-loader@1.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + ## 0.9.4-next.1 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index cd1658b417..9f3a177b4f 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.9.4-next.1", + "version": "0.9.4", "publishConfig": { "access": "public" }, diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 123e82edf7..6c8cc3f4ac 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,24 @@ # techdocs-cli-embedded-app +## 0.2.98 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog@1.21.1 + - @backstage/test-utils@1.5.8 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 0.2.98-next.2 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 979d18620f..6c395636b1 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.98-next.2", + "version": "0.2.98", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 0097d35ef6..34f612932c 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,17 @@ # @techdocs/cli +## 1.8.16 + +### Patch Changes + +- c964a3d: Import discovery from backend-defaults instead of backend-common +- Updated dependencies + - @backstage/backend-defaults@0.4.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + ## 1.8.16-next.1 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index d947af0ee8..a44213abb7 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.8.16-next.1", + "version": "1.8.16", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 6a038fd0d8..3f40af6299 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/test-utils +## 1.5.8 + +### Patch Changes + +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + ## 1.5.8-next.1 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 9cf0c7a6a7..9d36217b46 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.5.8-next.1", + "version": "1.5.8", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 58d69f516d..b6d585c0f0 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-api-docs +## 0.11.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + ## 0.11.7-next.2 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 5a6d97b7ce..7cda749423 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.11.7-next.2", + "version": "0.11.7", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index b472d51f0f..63265e33d9 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-app-backend +## 0.3.71 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-app-node@0.1.22 + - @backstage/config-loader@1.8.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.71-next.1 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 0d44e3dc61..e6661f9d73 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.71-next.1", + "version": "0.3.71", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index e5ff0eb99f..4dce17870a 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-node +## 0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/config-loader@1.8.1 + ## 0.1.22-next.1 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 11ddf46cf8..a846190f31 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.22-next.1", + "version": "0.1.22", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index cdb8bab69e..ec24e1cc8b 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-app-visualizer +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.8-next.1 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index 95df128040..6b10ffc220 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.8-next.1", + "version": "0.1.8", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index 0449bc6933..55287ebd46 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.2.3 + +### Patch Changes + +- b9832ae: Fix several issues with the Atlassian auth provider (type definition, profile url, profile transformation, scopes) +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index d938144603..309dff7f50 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", - "version": "0.2.3-next.1", + "version": "0.2.3", "description": "The atlassian-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index f1ae8a4981..cc2db5e210 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/errors@1.2.4 + ## 0.1.14-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 6ee5c274c1..9d15f0ea09 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", - "version": "0.1.14-next.1", + "version": "0.1.14", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index 5e78ec6abb..26eb0d17b4 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.5-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 287059eb8f..52293f2480 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-azure-easyauth-provider", - "version": "0.1.5-next.1", + "version": "0.1.5", "description": "The azure-easyauth-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md index 33b434f86e..86ec768727 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.1.5-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 95189d2cea..8ddb0f8acd 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", - "version": "0.1.5-next.1", + "version": "0.1.5", "description": "The bitbucket-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md index 9754e9a73a..2910ffccce 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.5-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index b0cd52d022..30c60ed6a4 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider", - "version": "0.1.5-next.1", + "version": "0.1.5", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 8fe2c331a4..19549dc23a 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.17-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 8e00b9e935..d0059e7a69 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", - "version": "0.2.17-next.1", + "version": "0.2.17", "description": "A GCP IAP auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index f73e6adf3c..5d9704a86b 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 138aaa4e02..42ef2ccd4e 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", - "version": "0.1.19-next.1", + "version": "0.1.19", "description": "The github-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index a8d5844847..57a6aa189b 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index 97e680237c..b0e0d2e647 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", - "version": "0.1.19-next.1", + "version": "0.1.19", "description": "The gitlab-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index e7eeb2779c..17efe0d1a5 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 55b2616502..1153cbb37a 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", - "version": "0.1.19-next.1", + "version": "0.1.19", "description": "A Google auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md index fd046e8dca..7c9e48041d 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.8-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index 78fbd098f6..1648ebea6a 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "version": "0.1.8-next.1", + "version": "0.1.8", "description": "The guest-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index 668047c816..c4df7007f3 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 76d3236f8b..67a898cf80 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", - "version": "0.1.17-next.1", + "version": "0.1.17", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 9872a64385..edfcfa0bcb 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index 71934913fe..c98a36d104 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", - "version": "0.2.3-next.1", + "version": "0.2.3", "description": "The oauth2-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index 895ada0e6e..554468bcfe 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index 4123403eb2..16b4126781 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", - "version": "0.1.15-next.1", + "version": "0.1.15", "description": "The oauth2-proxy-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index b9f8ffc98d..5e0d6aeb5c 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend@0.22.9 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 34e6c6f296..d825bd2986 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", - "version": "0.2.3-next.1", + "version": "0.2.3", "description": "The oidc-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index dfb5f638bf..029892a34a 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.0.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.0.15-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index cf099d40ee..4070ffd31f 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", - "version": "0.0.15-next.1", + "version": "0.0.15", "description": "The okta-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md index 8e21a1868e..896f6b7e0c 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index ac6a38a4f5..045dce0392 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-onelogin-provider", - "version": "0.1.3-next.1", + "version": "0.1.3", "description": "The onelogin-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index 4435efc501..c3ea4665e8 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index 2698a56a2e..e6f50e4008 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", - "version": "0.1.16-next.1", + "version": "0.1.16", "description": "The pinniped-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index edfc30c099..f652d6135c 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index d6b7eaf263..77dafe38e1 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", - "version": "0.2.3-next.1", + "version": "0.2.3", "description": "The vmware-cloud-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 2bb083ed66..393820ec27 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-auth-backend +## 0.22.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.3 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.14 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.5 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.5 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.5 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.17 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.19 + - @backstage/plugin-auth-backend-module-google-provider@0.1.19 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.17 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.3 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.15 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.3 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.15 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.3 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.22.9-next.1 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index cd31a18064..c9d437ceba 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.22.9-next.1", + "version": "0.22.9", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index e931a1c26a..298f0e4472 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-auth-node +## 0.4.17 + +### Patch Changes + +- 55c1a72: Fix issues with Atlassian OAuth provider: retrieve the email and photo that were not in arrays but rather in single props. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.17-next.1 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index ab84eb7fe6..0f4b0f5a77 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.4.17-next.1", + "version": "0.4.17", "backstage": { "role": "node-library", "pluginId": "auth", diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index 84bfadc98c..4651cf7eae 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-react +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 8af97a0b04..44a4ce9fd0 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.4-next.1", + "version": "0.1.4", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index 73060ebd4a..7b2b8a936b 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.2.21 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/integration@1.13.0 + ## 0.2.21-next.0 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index 0e98fcc505..d52122f995 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", - "version": "0.2.21-next.0", + "version": "0.2.21", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 15f944de19..f2dd32b7f2 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.3.17 + +### Patch Changes + +- 4afa050: Export `defaultEksClusterEntityTransformer` to allow library consumers to layer additional changes on top of the default transformer. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + ## 0.3.17-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 7081b09d2b..91ac7c4eae 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.3.17-next.1", + "version": "0.3.17", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 1f672302ac..e96ca2d650 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/config@1.2.0 + ## 0.1.42-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 1159aff6db..05b920bcb0 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.1.42-next.1", + "version": "0.1.42", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 125778b4b6..620ab36c40 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-tasks@0.5.27 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.5-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index fad1dc47c9..de6c2a8410 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.2.5-next.1", + "version": "0.2.5", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 4ad56f2aa3..920d2bc111 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index e691c2a3c3..9faa941fd6 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.2.9-next.1", + "version": "0.2.9", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index a5901702ff..857d9a33d9 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.36 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.36-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 4e47fcbdcc..793a4213f2 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.1.36-next.1", + "version": "0.1.36", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index b76bc9d7a9..779039f858 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.1.23-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index 0888a9eb03..7ebe7cbefc 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.1.23-next.1", + "version": "0.1.23", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index 6492e65ffa..159c9bb550 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.39-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index b99347e6f5..0732db4465 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.1.39-next.1", + "version": "0.1.39", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index 0236b1d30c..424465d813 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend-module-github@0.6.5 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/config@1.2.0 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index abd92889a0..3fdcc2fedf 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.1.17-next.1", + "version": "0.1.17", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index 02975d2b68..661712456b 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-catalog-backend-module-github +## 0.6.5 + +### Patch Changes + +- 9112efc: Adds support for `repository` events. + + The provider adds a subscription to the topic `github.repository`. + + Hereby, it supports events of type `repository` with actions + + - `archived` + - `deleted` + - `edited` + - `renamed` + - `transferred` + - `unarchived` + + Actions skipped as they don't require entity changes: + + - `created` + - `privatized` + - `publicized` + + If the config option `validateLocationsExist` is enabled, an API request + is necessary and will be executed. + This affects the actions `renamed`, `transferred`, and `unarchive` + of event type `repository`. + + Catalog entities related to the `GithubEntityProvider` instance will be adjusted + according to action and its meaning for them. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.6.5-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index c8b78c5e92..50b838be1d 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.6.5-next.1", + "version": "0.6.5", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index d86cb76c24..b36bcb4d71 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.21 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-node@1.12.4 + ## 0.0.5-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index a08a42d1f3..9c4c1f725f 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.0.5-next.1", + "version": "0.0.5", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 8d6eb4bfef..97ed683942 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.3.21 + +### Patch Changes + +- 8db30ad: The Gitlab configuration supports an additional optional boolean key `catalog.providers.gitlab.<your-org>.restrictUsersToGroup`. Setting this to `true` will make Backstage only import users from the group defined in the `group` key, instead of all users in the organisation (self-hosted) or of the root group (SaaS). It will default to false, keeping the original implementation intact, when not explicitly set. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.3.21-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 0780ccdd6a..fa21798a62 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.3.21-next.1", + "version": "0.3.21", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 9cd19e259e..0d04145063 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.27-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index c9cf1f5a82..0180f36d03 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.4.27-next.1", + "version": "0.4.27", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 6842b99282..9168ce0991 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.7.0 + +### Minor Changes + +- cb32ca7: **BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items. + + Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one. + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.0-next.1 ### Minor Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 411f168fd8..62b3328de7 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.7.0-next.1", + "version": "0.7.0", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-logs/CHANGELOG.md b/plugins/catalog-backend-module-logs/CHANGELOG.md index c609fc369d..2712f4a134 100644 --- a/plugins/catalog-backend-module-logs/CHANGELOG.md +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-logs +## 0.0.1 + +### Patch Changes + +- 97caf55: Creates a new module to make logging catalog errors simple. This module subscribes to catalog events and logs them. + + See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install + and configure the plugin. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + ## 0.0.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json index fe3f09c29c..560b93737e 100644 --- a/plugins/catalog-backend-module-logs/package.json +++ b/plugins/catalog-backend-module-logs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-logs", - "version": "0.0.1-next.1", + "version": "0.0.1", "description": "A module that subscribes to catalog releated events and logs them.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index eb3591241f..a33bea94b8 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.30 + +### Patch Changes + +- f7bdcea: Adds a dynamic provider for the plugin-catalog-backend-module-msgraph. Configuration is now runtime configurable through the ProviderConfigTransformer. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.5.30-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index b04cd8788c..3f5fa722a8 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.5.30-next.1", + "version": "0.5.30", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index bf35045dcc..6cc947ee17 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.40 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.40-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index b37cb09b15..2dbe6612ac 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.1.40-next.1", + "version": "0.1.40", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index ead44a56aa..cb4cd9664b 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.1.28-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 114daa45e8..cf2a739380 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.1.28-next.1", + "version": "0.1.28", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 758d59c7c0..87299358b7 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-model@1.5.0 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 58bcdc6fb5..3aaa768f76 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.1.20-next.1", + "version": "0.1.20", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index afac0ebeed..a7d74143a1 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.3 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.4.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index c49a3d700d..6f1f4bb6bc 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.4.9-next.1", + "version": "0.4.9", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 65ed28cb6d..8f99ab1f4c 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog-backend +## 1.24.0 + +### Minor Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.24.0-next.1 ### Minor Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index e554f0a334..19d40bd34c 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "1.24.0-next.1", + "version": "1.24.0", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index fd3c714f97..e2859fcc83 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-common +## 1.0.25 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-model@1.5.0 + ## 1.0.24 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 0ea4ce5e3e..865dc02f4c 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-common", - "version": "1.0.24", + "version": "1.0.25", "description": "Common functionalities for the catalog plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index f397955e1c..332ea17dd0 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-graph +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + ## 0.4.7-next.2 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index eb27725d45..1e7c376436 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.7-next.2", + "version": "0.4.7", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 455f094a86..29ec3631b4 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-import +## 0.12.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.12.1-next.2 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index f41d239792..c3babc09a3 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.12.1-next.2", + "version": "0.12.1", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 54f39e9e51..74982e481f 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-node +## 1.12.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.12.4-next.1 ### Patch Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 575a1c65f5..ccd83cd129 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.12.4-next.1", + "version": "1.12.4", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index b29a49e47d..38402e8824 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-catalog-react +## 1.12.2 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- 2030962: Make EntityOwnerPicker display metadata.title or spec.profile.displayName for mode=only-owners instead of metadata.name +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/core-components@0.14.9 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.12.2-next.2 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index f522a4f111..c6a725ecb6 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.12.2-next.2", + "version": "1.12.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md index f2f489b6a0..ca38c923bd 100644 --- a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-unprocessed-entities-common +## 0.0.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + ## 0.0.2 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities-common/package.json b/plugins/catalog-unprocessed-entities-common/package.json index f491d27157..990f75bbea 100644 --- a/plugins/catalog-unprocessed-entities-common/package.json +++ b/plugins/catalog-unprocessed-entities-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities-common", - "version": "0.0.2", + "version": "0.0.3", "description": "Common functionalities for the catalog-unprocessed-entities plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index fefeffe9d2..e3fd1b6b76 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.2.6-next.1 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 142b850bd3..872ce7b11b 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.2.6-next.1", + "version": "0.2.6", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-unprocessed-entities", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 08a70d4956..2667fdf638 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-catalog +## 1.21.1 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- d133eaa: Added small notes to AboutCard to discourage customizability PRs +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-search-react@1.7.13 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.21.1-next.2 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0d101af3df..fc49a8c36b 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.21.1-next.2", + "version": "1.21.1", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 8458a50aa3..2344473925 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-config-schema +## 0.1.57 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.1.57-next.1 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 9a1971d80b..6f91094e5f 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-config-schema", - "version": "0.1.57-next.1", + "version": "0.1.57", "description": "A Backstage plugin that lets you browse the configuration schema of your app", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index cc0dc806c8..f027384637 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-devtools-backend +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/config-loader@1.8.1 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.8-next.1 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index bd3b61f22d..ce5e143f24 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.3.8-next.1", + "version": "0.3.8", "backstage": { "role": "backend-plugin", "pluginId": "devtools", diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md index 3e060571ad..37923e3fc7 100644 --- a/plugins/devtools-common/CHANGELOG.md +++ b/plugins/devtools-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-devtools-common +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/types@1.1.1 + ## 0.1.10 ### Patch Changes diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json index 24b17d6742..f476580ac0 100644 --- a/plugins/devtools-common/package.json +++ b/plugins/devtools-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-common", - "version": "0.1.10", + "version": "0.1.11", "description": "Common functionalities for the devtools plugin", "backstage": { "role": "common-library", diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index 5bc6220aa3..3403b7f2fa 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-devtools +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index 90d724692c..83d2a6b352 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.16-next.1", + "version": "0.1.16", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 399ed2689d..e27fcefa6e 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.3.8-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index b2c09d3b8a..983e15567b 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.3.8-next.1", + "version": "0.3.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 5e5ccffc2a..225ee30beb 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 8bb6ac4f6b..49c9b9439f 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.8-next.1", + "version": "0.2.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 1a339c3807..4676feabbd 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 7efffdd361..6fd443a3b1 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.8-next.1", + "version": "0.2.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 78963fbbaf..eddce5d6c5 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index f1bea2868c..7d870063df 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.8-next.1", + "version": "0.2.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index a095cf4ab3..4597583ea8 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-github +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 3b37f29022..955793ee6d 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.2.8-next.1", + "version": "0.2.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index dc8470bd7a..f22f90405e 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index 21c95448b1..c97a9e2f02 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.2.8-next.1", + "version": "0.2.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 967ffc80da..4fcbcfc172 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.8 + ## 0.1.32-next.1 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index 120c7f5fa6..09a36a7a1f 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.32-next.1", + "version": "0.1.32", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index a4b4b550bb..4840d9a623 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-events-backend +## 0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + ## 0.3.9-next.1 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index 82066d5a91..f9f2a4b781 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.3.9-next.1", + "version": "0.3.9", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index 022d50fdd1..d5b1c98372 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-node +## 0.3.8 + +### Patch Changes + +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + ## 0.3.8-next.1 ### Patch Changes diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index f89617dd14..965f317357 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.3.8-next.1", + "version": "0.3.8", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index d1553258ed..dd1a2087dc 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @internal/plugin-todo-list-backend +## 1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + ## 1.0.29-next.1 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 3d46a9f5f7..0ca987d30b 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.29-next.1", + "version": "1.0.29", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index 3761ead6a3..e30cb653df 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-common +## 1.0.20 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + ## 1.0.19 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 317cfa991d..efccce611f 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-common", - "version": "1.0.19", + "version": "1.0.20", "backstage": { "role": "common-library", "pluginId": "todo-list", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index f61f8b8d0a..e55f882b0a 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/plugin-todo-list +## 1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + ## 1.0.29-next.1 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index d008b73538..c8c0a90312 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.29-next.1", + "version": "1.0.29", "backstage": { "role": "frontend-plugin", "pluginId": "todo-list", diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index 1f1c9a0e2e..c4db30307f 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-home-react +## 0.1.15 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.15-next.2 ### Patch Changes diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index ec05852818..433a91b475 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-react", - "version": "0.1.15-next.2", + "version": "0.1.15", "description": "A Backstage plugin that contains react components helps you build a home page", "backstage": { "role": "web-library", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index e65b3dec1c..eb49e3217b 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-home +## 0.7.7 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-home-react@0.1.15 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 0.7.7-next.2 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 01dd27a295..189aeec2ac 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.7.7-next.2", + "version": "0.7.7", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 7b8d3fb534..6a41d014f5 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-kubernetes-backend +## 0.18.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-kubernetes-node@0.1.16 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + ## 0.18.3-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index c9d8733319..0e439ecd91 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.18.3-next.1", + "version": "0.18.3", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 8932454b3c..a4b12a0556 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-kubernetes-react@0.4.1 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.0.13-next.2 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 3c6e6c8f1b..af63bf4102 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.13-next.2", + "version": "0.0.13", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index e4de39bfd9..a9cabafdd0 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-common +## 0.8.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.8.0 ### Minor Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 9005c5ffe9..db94d165e4 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-common", - "version": "0.8.0", + "version": "0.8.1", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index 4a6514a2d0..608fe97cd2 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes-node +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 461bf1bbbe..798978b5d2 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.1.16-next.1", + "version": "0.1.16", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 3755d259a8..0fd0cfd07e 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-kubernetes-react +## 0.4.1 + +### Patch Changes + +- e3cb6ab: Add a namespace label to RolloutDrawer +- 2414d86: Fix the `HTML` markup of the `FixDialog` component, `ul` and `li` are not allowed inside a `p` tag. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.1-next.1 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index fc6f382d3d..42ef4f7913 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.4.1-next.1", + "version": "0.4.1", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index a6d07dc706..fcb80b7b26 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes +## 0.11.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-kubernetes-react@0.4.1 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.11.12-next.2 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index d0ee97a882..1ab92f5b3c 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.11.12-next.2", + "version": "0.11.12", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index c536369352..4bfbfbd05d 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-notifications-backend-module-email +## 0.1.3 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index c47a65d4f4..b7107a4c6b 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.1.3-next.1", + "version": "0.1.3", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index a4be935628..b4fe307f15 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-notifications-backend +## 0.3.3 + +### Patch Changes + +- d7b8ca5: Added an option to filter notifications by topic +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.3.3-next.1 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index bfa1f2f0bd..abd0a1bbfe 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.3.3-next.1", + "version": "0.3.3", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-common/CHANGELOG.md b/plugins/notifications-common/CHANGELOG.md index 17231f0147..6e10272c36 100644 --- a/plugins/notifications-common/CHANGELOG.md +++ b/plugins/notifications-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-notifications-common +## 0.0.5 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/config@1.2.0 + ## 0.0.4 ### Patch Changes diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json index 3802d6535c..139681e6c6 100644 --- a/plugins/notifications-common/package.json +++ b/plugins/notifications-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-common", - "version": "0.0.4", + "version": "0.0.5", "description": "Common functionalities for the notifications plugin", "backstage": { "role": "common-library", diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index 69e594b513..764ee3affb 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-notifications-node +## 0.2.3 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index f18ce420f7..04564da21e 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.3-next.1", + "version": "0.2.3", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 97e6c50b44..263bd9ba70 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-notifications +## 0.2.3 + +### Patch Changes + +- 3bf0697: The toolbar on the Notifications page is hidden when there are no listed notifications. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + ## 0.2.3-next.2 ### Patch Changes diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 4ef28f9508..1575c48c5e 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.2.3-next.2", + "version": "0.2.3", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index d13578fd32..d6e117d579 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-org-react +## 0.1.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.26-next.2 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index e6494eaa28..9405b38b59 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.26-next.2", + "version": "0.1.26", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index aa4bd9a93f..6d1fb352f8 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-org +## 0.6.27 + +### Patch Changes + +- 5132d28: The `useGetEntities` hook could result in requests to `/api/catalog/entities` where the headers exceed the default maximum Node.js header size of 16KB. The hook logic has been adjusted to batch the requests. +- c307ef4: Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`. + + Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property. + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + ## 0.6.27-next.2 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index c2d43bc5ed..2530ca2ea1 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.27-next.2", + "version": "0.6.27", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 616c6acda2..e06db25a91 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.1.19 + +### Patch Changes + +- ed10fd2: Internal refactor to use new `PolicyQueryUser` type. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json index 60e11825f4..27303483f7 100644 --- a/plugins/permission-backend-module-policy-allow-all/package.json +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend-module-allow-all-policy", - "version": "0.1.19-next.1", + "version": "0.1.19", "description": "Allow all policy backend module for the permission plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 39ae10e557..e5a826f67a 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-permission-backend +## 0.5.46 + +### Patch Changes + +- ed10fd2: The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + + - `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. + - `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + + Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.5.46-next.1 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 43a57407e1..95647ba82b 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.46-next.1", + "version": "0.5.46", "backstage": { "role": "backend-plugin", "pluginId": "permission", diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index e6b637a092..92a33afea2 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-permission-common +## 0.8.0 + +### Minor Changes + +- f4085b8: **BREAKING**: Removed the deprecated and unused `token` option from `EvaluatorRequestOptions`. The `PermissionsClient` now has its own `PermissionClientRequestOptions` type that declares the `token` option instead. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.14 ### Patch Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index ee54459589..8d6f54fac0 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-common", - "version": "0.7.14", + "version": "0.8.0", "description": "Isomorphic types and client for Backstage permissions and authorization", "backstage": { "role": "common-library", diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index a63abb1d95..326dfb99d3 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-permission-node +## 0.8.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: Updated the `ServerPermissionClient` to match the new `PermissionsService` interface, where the deprecated `token` option has been removed and the options are now required. + +### Patch Changes + +- ed10fd2: The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + + - `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. + - `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + + Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +- 28b2cfb: Fix invalid cross-reference in API Reference docs +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.7.33-next.1 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index db3c067088..4bbf4d5faf 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-node", - "version": "0.7.33-next.1", + "version": "0.8.0", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 1eb951e625..2b19b7e849 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-permission-react +## 0.4.24 + +### Patch Changes + +- 28b2cfb: Improve API Reference documentation +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.4.23 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 9ebb13e633..c119be8eb9 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.23", + "version": "0.4.24", "backstage": { "role": "web-library", "pluginId": "permission", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index a6c1ef02d2..30cacb1d02 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-proxy-backend +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.5.3-next.1 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 23acc935f3..2e7bd08550 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.5.3-next.1", + "version": "0.5.3", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index c634d8c8ca..e386b5d9b8 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.1.14 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.14-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index c0e00e8ffe..205beae578 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", - "version": "0.1.14-next.1", + "version": "0.1.14", "description": "The azure module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 4e1e2b9531..c8c79a302b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.1.12 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.12-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 5ce826ff1a..67f275462b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", - "version": "0.1.12-next.1", + "version": "0.1.12", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 3443242e91..5d2a0754cd 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.1.12 + +### Patch Changes + +- 6a4ad4e: Instead of using hardcoded `targetBranch` now fetch the default branch from Bitbucket repository. + This prevents from errors when no `targetBranch` is provided and the default repository branch is different from `master`, for example: `main`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.12-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index b396faa5e5..a33bf90040 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.1.12-next.1", + "version": "0.1.12", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index 35f5382652..414f8b7bd8 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.12-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index bca6e94878..95d92c39b8 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", - "version": "0.2.12-next.1", + "version": "0.2.12", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index b3c7bfbe36..1613f1881c 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.23-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index f607696136..9fe6781eff 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", - "version": "0.2.23-next.1", + "version": "0.2.23", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 5854e9ff47..f2dc2a822d 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.46 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.46-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 43a7e348a4..0b0036ee5a 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.2.46-next.1", + "version": "0.2.46", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md index e32c2ef08f..51dd92b2cf 100644 --- a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-scaffolder-backend-module-gcp +## 0.1.0 + +### Minor Changes + +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.0-next.3 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gcp/package.json b/plugins/scaffolder-backend-module-gcp/package.json index c441acb21a..ae64d1de61 100644 --- a/plugins/scaffolder-backend-module-gcp/package.json +++ b/plugins/scaffolder-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gcp", - "version": "0.1.0-next.3", + "version": "0.1.0", "description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index dd26e1d5a8..7b95a80811 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.14-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index 31a30c3f7e..96c069ea97 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", - "version": "0.1.14-next.1", + "version": "0.1.14", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index f3ceb7ce8c..4c03220562 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.12-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 3cacbc66db..69a84f43e7 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", - "version": "0.1.12-next.1", + "version": "0.1.12", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index 2d3c6ff0f2..680cb0469d 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.4.0 + +### Minor Changes + +- 70c4b36: Adds support for custom tag policies when creating GitHub environments. + +### Patch Changes + +- ccfc9d1: Fixed bug resulting from missing required owner and repo arguments in `getEnvironmentPublicKey` in action `github:environment:create`. + + Adding environment secrets now works as expected. + +- 141f366: Added action to enable GitHub Pages on a repo +- 4410fed: Fixed issue with octokit call missing owner and repo when creating environment variables and secrets using github:environment:create action +- dfaa28d: Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:publish` action + + Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:repo:push` action + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.0-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 500ec86f9f..7732c7c11a 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.4.0-next.1", + "version": "0.4.0", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index 0c8f0529a6..60e7f1bccf 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.4.4 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- 2fb0eb8: Added support for passing `variables` to `gitlab:pipeline:trigger` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.4-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 499cfd6872..8228a3ec0f 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.4.4-next.1", + "version": "0.4.4", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md index 6371124957..db52ded0c8 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + ## 0.0.5-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index 7c983c36ce..68e177abfa 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-notifications", - "version": "0.0.5-next.1", + "version": "0.0.5", "description": "The notifications backend module for the scaffolder plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 0db7584509..cd34152910 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.39 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.39-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index c15815d2fa..496174d510 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.4.39-next.1", + "version": "0.4.39", "description": "A module for the scaffolder backend that lets you template projects using Rails", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 29e8c17b04..113a63222f 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.30 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.30-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index c0866f979d..264736692a 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.30-next.1", + "version": "0.1.30", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 4e99b51284..53d2369d85 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-node-test-utils@0.1.9 + - @backstage/types@1.1.1 + ## 0.3.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 76158fdc01..5c65315e22 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.3.6-next.1", + "version": "0.3.6", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index a4df9e72e9..27e4b4465e 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-backend +## 1.23.0 + +### Minor Changes + +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- b9451dd: Updated `catalog:write` scaffolder action to show correct file path location in log message +- ff1bb4c: Added a documentation how to use checkpoints +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- 62d1fe3: Fix user entity not being fetched for scaffolder dry runner +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.14 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.12 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.23.0-next.2 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 10d827762e..c86a048d46 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "1.23.0-next.2", + "version": "1.23.0", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 12f3f9e235..914faf70f3 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-common +## 1.5.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 1.5.3 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 3e56510daf..d86f0d67c0 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.5.3", + "version": "1.5.4", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md index 9104fbcb26..7d578a2b14 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.1.9 + +### Patch Changes + +- 54429b5: Filename and imports correction for mockActionContext.ts +- Updated dependencies + - @backstage/backend-test-utils@0.4.4 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/types@1.1.1 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index d361d0ae1d..827b654b4e 100644 --- a/plugins/scaffolder-node-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node-test-utils", - "version": "0.1.9-next.1", + "version": "0.1.9", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index f67c8c8cda..59658dcb11 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-scaffolder-node +## 0.4.8 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.8-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 079eebfb00..07e468620b 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.4.8-next.1", + "version": "0.4.8", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index a1ed542bd5..68f94ac3fd 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-scaffolder-react +## 1.10.0 + +### Minor Changes + +- 354e68c: Improve validation error display text in scaffolder +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- 4d7e11f: disables rendering of output box if no output is returned +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.10.0-next.2 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 36ee0870fc..f2d7d44a01 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.10.0-next.2", + "version": "1.10.0", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 3c4cfe54ab..2da2802808 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-scaffolder +## 1.23.0 + +### Minor Changes + +- 52b6db0: Use virtualization with `EntityPicker` as done earlier with `MultiEntityPicker` to fix performance issues with large data sets. `VirtualizedListbox` extracted into reusable component. +- 3583ce5: Use virtualization with `MultiEntityPicker`. Fixes performance issues with large data sets. +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- 4d7e11f: enable resizing of the task log stream viewer +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- 89c44b3: Support `catalogFilter` array on `OwnedEntityPicker` +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.22.1-next.2 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index aaa681d174..5cc587fe4d 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.22.1-next.2", + "version": "1.23.0", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index d064814ecb..29304c6cfd 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.28-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 0f45f3ce43..6ec054f10d 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.1.28-next.1", + "version": "0.1.28", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index f0628d0254..d561b1127a 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + ## 1.5.3-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 9cb61ee97b..d96b4c924d 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", - "version": "1.5.3-next.1", + "version": "1.5.3", "description": "A module for the search backend that implements search using ElasticSearch", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index 888be119be..f5b60fa87e 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + ## 0.1.28-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index f37c4d55d3..9fd252df66 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-explore", - "version": "0.1.28-next.1", + "version": "0.1.28", "description": "A module for the search backend that exports explore modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 3fe18213bb..c19c797bde 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + ## 0.5.32-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index ec6782535f..8bdcf14306 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.5.32-next.1", + "version": "0.5.32", "description": "A module for the search backend that implements search using PostgreSQL", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md index 96214102b7..6c86337062 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index dd09d75b11..4d06f928a0 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", - "version": "0.1.15-next.1", + "version": "0.1.15", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 706d178601..2c41ecda1f 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.1.27-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index b26065cc60..a04f64756b 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.1.27-next.1", + "version": "0.1.27", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 26cd8e91eb..f43f3c4009 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend-node +## 1.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 1.2.27-next.1 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index e8cc89d117..a236eff6c0 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "1.2.27-next.1", + "version": "1.2.27", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 2de90e3960..eb391c6bba 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend +## 1.5.14 + +### Patch Changes + +- 343f656: The `AuthorizedSearchEngine` will now ignore the deprecated `token` option, and treat it as an unauthorized request. This will not have any effect in practice, since credentials are always provided by the router. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-search-common@1.2.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.5.14-next.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 8d266c63a8..a0e7b48f5b 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "1.5.14-next.1", + "version": "1.5.14", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin", diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index e034c62315..5b163eab0d 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-common +## 1.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/types@1.1.1 + ## 1.2.12 ### Patch Changes diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index 61edfd13f3..8d0f4e9585 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-common", - "version": "1.2.12", + "version": "1.2.13", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", "backstage": { "role": "common-library", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 494ce07772..04d8bf312a 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-react +## 1.7.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.7.13-next.1 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index a0701e1f3b..b5c17c369a 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.7.13-next.1", + "version": "1.7.13", "backstage": { "role": "web-library", "pluginId": "search", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index a6cab671b5..bce322e95a 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-search +## 1.4.14 + +### Patch Changes + +- 1117aba: Update deps in search api extension to include fetch api +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/plugin-search-react@1.7.13 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.4.14-next.2 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index ad8210df4c..56271eca58 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.14-next.2", + "version": "1.4.14", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index aaf4a0c15f..e0525a0e78 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-signals-backend +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.8-next.1 ### Patch Changes diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 0bded11c2e..1e0ac41fcb 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.1.8-next.1", + "version": "0.1.8", "backstage": { "role": "backend-plugin", "pluginId": "signals", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index cbcdea7a98..0353548fe5 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-signals-node +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.8-next.1 ### Patch Changes diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index 033dcb8246..1cea62c21b 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-node", - "version": "0.1.8-next.1", + "version": "0.1.8", "description": "Node.js library for the signals plugin", "backstage": { "role": "node-library", diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index 9ccdff8b97..a4a7daec35 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-signals +## 0.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + ## 0.0.8-next.1 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 58b82fab7a..88ca252c1f 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.8-next.1", + "version": "0.0.8", "backstage": { "role": "frontend-plugin", "pluginId": "signals", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 5b247addab..3135cc5ffd 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.35 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/test-utils@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-search-react@1.7.13 + - @backstage/core-plugin-api@1.9.3 + ## 1.0.35-next.2 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 966308ee94..57cd4d06a6 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.35-next.2", + "version": "1.0.35", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index d50062c744..571b0d68fc 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-techdocs-backend +## 1.10.9 + +### Patch Changes + +- 9ecf5fd: Adds extension point for publishers to the techdocs backend +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/integration@1.13.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 1.10.9-next.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 1533cdc3fe..ce0df2ca78 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "1.10.9-next.1", + "version": "1.10.9", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 2cc99a8158..4072d96d9f 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/integration-react@1.1.29 + - @backstage/core-plugin-api@1.9.3 + ## 1.1.12-next.1 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index f68df2a883..3c226f6a50 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", - "version": "1.1.12-next.1", + "version": "1.1.12", "description": "Plugin module for contributed TechDocs Addons", "backstage": { "role": "frontend-plugin-module", diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 99240bd9d4..d392e3e662 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-techdocs-node +## 1.12.8 + +### Patch Changes + +- 9ecf5fd: Adds extension point for publishers to the techdocs backend +- 4c4d077: Bumps default version of techdocs docker image to latest +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + ## 1.12.8-next.1 ### Patch Changes diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 70d10aeed4..572b76e052 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.12.8-next.1", + "version": "1.12.8", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", "backstage": { "role": "node-library", diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index 97677352df..5c9bd0004f 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs-react +## 1.2.6 + +### Patch Changes + +- 8ac9ce5: Resolved the issue where changes in TechDoc add-ons, including the TextSize add-on, were not reapplying during navigation +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + ## 1.2.6-next.1 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 1686b176ca..e65079b212 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.2.6-next.1", + "version": "1.2.6", "description": "Shared frontend utilities for TechDocs and Addons", "backstage": { "role": "web-library", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 34dc9a310f..08a841f25c 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-techdocs +## 1.10.7 + +### Patch Changes + +- 8fc2622: Fixed an issue that was causing techdocs pages unnecessarily re-render on navigate. +- 6fa652c: Improve default sorting of docs table +- 605b691: Allow for searching TechDocs by entity title +- 60caa92: Fix double scrollbar bug in reader +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-search-react@1.7.13 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + ## 1.10.7-next.2 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index cbf1f828ce..3a90078c72 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.10.7-next.2", + "version": "1.10.7", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index c830df43e5..6a945081e8 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-user-settings-backend +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.2.21-next.1 ### Patch Changes diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index e975b38c08..e33bf0bd4b 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.2.21-next.1", + "version": "0.2.21", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 8d886aee80..f10a4c0ea3 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-user-settings +## 0.8.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.8.9-next.2 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 34c719d9df..5ae95e0e45 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.9-next.2", + "version": "0.8.9", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", diff --git a/yarn.lock b/yarn.lock index c7d871b1d9..c6a1d2139a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3839,7 +3839,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-client@^1.6.5, @backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": +"@backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": version: 0.0.0-use.local resolution: "@backstage/catalog-client@workspace:packages/catalog-client" dependencies: @@ -3852,7 +3852,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@^1.4.5, @backstage/catalog-model@^1.5.0, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": +"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@^1.4.5, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": version: 0.0.0-use.local resolution: "@backstage/catalog-model@workspace:packages/catalog-model" dependencies: @@ -4105,7 +4105,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/config@^1.1.1, @backstage/config@^1.2.0, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": +"@backstage/config@^1.1.1, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": version: 0.0.0-use.local resolution: "@backstage/config@workspace:packages/config" dependencies: @@ -4177,107 +4177,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-components@npm:^0.13.10": - version: 0.13.10 - resolution: "@backstage/core-components@npm:0.13.10" - dependencies: - "@backstage/config": ^1.1.1 - "@backstage/core-plugin-api": ^1.8.2 - "@backstage/errors": ^1.2.3 - "@backstage/theme": ^0.5.0 - "@backstage/version-bridge": ^1.0.7 - "@date-io/core": ^1.3.13 - "@material-table/core": ^3.1.0 - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@material-ui/lab": 4.0.0-alpha.61 - "@react-hookz/web": ^23.0.0 - "@types/react": ^16.13.1 || ^17.0.0 - "@types/react-sparklines": ^1.7.0 - "@types/react-text-truncate": ^0.14.0 - ansi-regex: ^6.0.1 - classnames: ^2.2.6 - d3-selection: ^3.0.0 - d3-shape: ^3.0.0 - d3-zoom: ^3.0.0 - dagre: ^0.8.5 - linkify-react: 4.1.3 - linkifyjs: 4.1.3 - lodash: ^4.17.21 - pluralize: ^8.0.0 - qs: ^6.9.4 - rc-progress: 3.5.1 - react-helmet: 6.1.0 - react-hook-form: ^7.12.2 - react-idle-timer: 5.6.2 - react-markdown: ^8.0.0 - react-sparklines: ^1.7.0 - react-syntax-highlighter: ^15.4.5 - react-text-truncate: ^0.19.0 - react-use: ^17.3.2 - react-virtualized-auto-sizer: ^1.0.11 - react-window: ^1.8.6 - remark-gfm: ^3.0.1 - zen-observable: ^0.10.0 - zod: ^3.22.4 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: ec2a0d0a27bc4d6b9d4da97f0b4df600148529ee51bf2c8e7d3adc45c3950adac662535d3beabc451db346f2c7e3c412ceaa756fc774012b43dff5e2695f2271 - languageName: node - linkType: hard - -"@backstage/core-components@npm:^0.14.4, @backstage/core-components@npm:^0.14.8": - version: 0.14.8 - resolution: "@backstage/core-components@npm:0.14.8" - dependencies: - "@backstage/config": ^1.2.0 - "@backstage/core-plugin-api": ^1.9.3 - "@backstage/errors": ^1.2.4 - "@backstage/theme": ^0.5.6 - "@backstage/version-bridge": ^1.0.8 - "@date-io/core": ^1.3.13 - "@material-table/core": ^3.1.0 - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@material-ui/lab": 4.0.0-alpha.61 - "@react-hookz/web": ^24.0.0 - "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - "@types/react-sparklines": ^1.7.0 - ansi-regex: ^6.0.1 - classnames: ^2.2.6 - d3-selection: ^3.0.0 - d3-shape: ^3.0.0 - d3-zoom: ^3.0.0 - dagre: ^0.8.5 - linkify-react: 4.1.3 - linkifyjs: 4.1.3 - lodash: ^4.17.21 - pluralize: ^8.0.0 - qs: ^6.9.4 - rc-progress: 3.5.1 - react-helmet: 6.1.0 - react-hook-form: ^7.12.2 - react-idle-timer: 5.7.2 - react-markdown: ^8.0.0 - react-sparklines: ^1.7.0 - react-syntax-highlighter: ^15.4.5 - react-use: ^17.3.2 - react-virtualized-auto-sizer: ^1.0.11 - react-window: ^1.8.6 - remark-gfm: ^3.0.1 - zen-observable: ^0.10.0 - zod: ^3.22.4 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: b833e833c83f72e0cc695fe2726cbf01b58819ad062ed9ab1e604514bb398ade20e1abb208d2e39ab1f746fdbd4547486e3935bacb44b457a47070f9c77895dc - languageName: node - linkType: hard - -"@backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": +"@backstage/core-components@^0.14.4, @backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": version: 0.0.0-use.local resolution: "@backstage/core-components@workspace:packages/core-components" dependencies: @@ -4348,7 +4248,58 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-plugin-api@^1.8.2, @backstage/core-plugin-api@^1.9.2, @backstage/core-plugin-api@^1.9.3, @backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": +"@backstage/core-components@npm:^0.13.10": + version: 0.13.10 + resolution: "@backstage/core-components@npm:0.13.10" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/core-plugin-api": ^1.8.2 + "@backstage/errors": ^1.2.3 + "@backstage/theme": ^0.5.0 + "@backstage/version-bridge": ^1.0.7 + "@date-io/core": ^1.3.13 + "@material-table/core": ^3.1.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^23.0.0 + "@types/react": ^16.13.1 || ^17.0.0 + "@types/react-sparklines": ^1.7.0 + "@types/react-text-truncate": ^0.14.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + linkify-react: 4.1.3 + linkifyjs: 4.1.3 + lodash: ^4.17.21 + pluralize: ^8.0.0 + qs: ^6.9.4 + rc-progress: 3.5.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-idle-timer: 5.6.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-text-truncate: ^0.19.0 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.11 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: ec2a0d0a27bc4d6b9d4da97f0b4df600148529ee51bf2c8e7d3adc45c3950adac662535d3beabc451db346f2c7e3c412ceaa756fc774012b43dff5e2695f2271 + languageName: node + linkType: hard + +"@backstage/core-plugin-api@^1.8.2, @backstage/core-plugin-api@^1.9.2, @backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": version: 0.0.0-use.local resolution: "@backstage/core-plugin-api@workspace:packages/core-plugin-api" dependencies: @@ -4495,26 +4446,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/frontend-plugin-api@npm:^0.6.6": - version: 0.6.6 - resolution: "@backstage/frontend-plugin-api@npm:0.6.6" - dependencies: - "@backstage/core-components": ^0.14.8 - "@backstage/core-plugin-api": ^1.9.3 - "@backstage/types": ^1.1.1 - "@backstage/version-bridge": ^1.0.8 - "@material-ui/core": ^4.12.4 - "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - lodash: ^4.17.21 - zod: ^3.22.4 - zod-to-json-schema: ^3.21.4 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: c2af07bb73751f050937ba0f34941cc21639defdf13c6e219069ff4143324404229113874caef28f41b9a92a00121991da2a88782595e5221b48910e2f8be7c7 - languageName: node - linkType: hard - "@backstage/frontend-plugin-api@workspace:^, @backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api": version: 0.0.0-use.local resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" @@ -4578,24 +4509,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/integration-react@npm:^1.1.28": - version: 1.1.28 - resolution: "@backstage/integration-react@npm:1.1.28" - dependencies: - "@backstage/config": ^1.2.0 - "@backstage/core-plugin-api": ^1.9.3 - "@backstage/integration": ^1.12.0 - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@types/react": ^16.13.1 || ^17.0.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 15443a7ebc457114715651e652a40472085ca18ec10da078bad1e71e3aa9cac33d10704b164d09108f6195a9cb4d2eae11751d853598ef0dd0e4c160858f782d - languageName: node - linkType: hard - "@backstage/integration-react@workspace:^, @backstage/integration-react@workspace:packages/integration-react": version: 0.0.0-use.local resolution: "@backstage/integration-react@workspace:packages/integration-react" @@ -4620,23 +4533,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/integration@npm:^1.12.0": - version: 1.12.0 - resolution: "@backstage/integration@npm:1.12.0" - dependencies: - "@azure/identity": ^4.0.0 - "@backstage/config": ^1.2.0 - "@backstage/errors": ^1.2.4 - "@octokit/auth-app": ^4.0.0 - "@octokit/rest": ^19.0.3 - cross-fetch: ^4.0.0 - git-url-parse: ^14.0.0 - lodash: ^4.17.21 - luxon: ^3.0.0 - checksum: 603c08058dadfe54b5d1788db87bab951b53ea4d1b19f609873717e0ab514859cc8608d0d60755c91eaea000ad71f817c5dd50892a3215443d7eed5a1d24a849 - languageName: node - linkType: hard - "@backstage/integration@workspace:^, @backstage/integration@workspace:packages/integration": version: 0.0.0-use.local resolution: "@backstage/integration@workspace:packages/integration" @@ -5722,7 +5618,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-common@^1.0.20, @backstage/plugin-catalog-common@^1.0.24, @backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": +"@backstage/plugin-catalog-common@^1.0.20, @backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-common@workspace:plugins/catalog-common" dependencies: @@ -5831,43 +5727,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-react@npm:^1.11.3, @backstage/plugin-catalog-react@npm:^1.9.3": - version: 1.12.1 - resolution: "@backstage/plugin-catalog-react@npm:1.12.1" - dependencies: - "@backstage/catalog-client": ^1.6.5 - "@backstage/catalog-model": ^1.5.0 - "@backstage/core-components": ^0.14.8 - "@backstage/core-plugin-api": ^1.9.3 - "@backstage/errors": ^1.2.4 - "@backstage/frontend-plugin-api": ^0.6.6 - "@backstage/integration-react": ^1.1.28 - "@backstage/plugin-catalog-common": ^1.0.24 - "@backstage/plugin-permission-common": ^0.7.14 - "@backstage/plugin-permission-react": ^0.4.23 - "@backstage/types": ^1.1.1 - "@backstage/version-bridge": ^1.0.8 - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@material-ui/lab": 4.0.0-alpha.61 - "@react-hookz/web": ^24.0.0 - "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - classnames: ^2.2.6 - lodash: ^4.17.21 - material-ui-popup-state: ^1.9.3 - qs: ^6.9.4 - react-use: ^17.2.4 - yaml: ^2.0.0 - zen-observable: ^0.10.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: adbf967e978fa6bb4798d0620853e41f64cc08c39dd0056fc01b2146ca427d1bfab401b8972d7b551d4188b90f80f69db9254799bca095608b7079cec73da9c5 - languageName: node - linkType: hard - -"@backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": +"@backstage/plugin-catalog-react@^1.11.3, @backstage/plugin-catalog-react@^1.9.3, @backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-react@workspace:plugins/catalog-react" dependencies: @@ -6711,7 +6571,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-permission-common@^0.7.14, @backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": +"@backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-common@workspace:plugins/permission-common" dependencies: @@ -6749,7 +6609,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-permission-react@^0.4.23, @backstage/plugin-permission-react@workspace:^, @backstage/plugin-permission-react@workspace:plugins/permission-react": +"@backstage/plugin-permission-react@workspace:^, @backstage/plugin-permission-react@workspace:plugins/permission-react": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-react@workspace:plugins/permission-react" dependencies: @@ -8072,7 +7932,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/theme@^0.5.0, @backstage/theme@^0.5.6, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": +"@backstage/theme@^0.5.0, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": version: 0.0.0-use.local resolution: "@backstage/theme@workspace:packages/theme" dependencies: @@ -8103,7 +7963,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/version-bridge@^1.0.7, @backstage/version-bridge@^1.0.8, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": +"@backstage/version-bridge@^1.0.7, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": version: 0.0.0-use.local resolution: "@backstage/version-bridge@workspace:packages/version-bridge" dependencies: From 392b26a0ed848730e5c332136d391a6726763d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= <piatkiewicz.piotr@gmail.com> Date: Tue, 16 Jul 2024 15:17:07 +0200 Subject: [PATCH 140/212] Update writing-custom-actions.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz <piatkiewicz.piotr@gmail.com> --- docs/features/software-templates/writing-custom-actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 6c1ea573db..98457d2e8d 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -195,7 +195,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({ const backend = createBackend(); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); /* highlight-add-next-line */ -backend.add(scaffolderModuleCustomExtensions()); +backend.add(scaffolderModuleCustomExtensions); ``` If your custom action requires core services such as `config` or `cache` they can be imported in the dependencies and passed to the custom action function. From 5e6eccc84ca26e4dffe516f1eacc7d49da6d868f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 16:26:15 +0200 Subject: [PATCH 141/212] docs: sidebar label fix for root health Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- docs/backend-system/core-services/root-health.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backend-system/core-services/root-health.md b/docs/backend-system/core-services/root-health.md index fc73ba9546..663dd699ae 100644 --- a/docs/backend-system/core-services/root-health.md +++ b/docs/backend-system/core-services/root-health.md @@ -1,7 +1,7 @@ --- id: root-health title: Root Health Service -sidebar_label: Health +sidebar_label: Root Health description: Documentation for the Health service --- From d5e567efff47e2756c6e17a85df9ea52812c4cf0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 17:27:24 +0200 Subject: [PATCH 142/212] docs: update threat model and auth docs to mention public entry point Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- docs/auth/index.md | 7 +------ docs/overview/threat-model.md | 2 ++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/auth/index.md b/docs/auth/index.md index 4a0e56a36e..5188d7e2a4 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -12,12 +12,7 @@ access to external resources. :::note Note -Identity management and the Sign-In page in Backstage is NOT a method for blocking -access for unauthorized users. The identity system only serves to provide a personalized -experience and access to a Backstage Identity Token, which can be passed to backend plugins. -This also means that your Backstage backend APIs are by default unauthenticated. -Thus, if your Backstage instance is exposed to the Internet, anyone can access -information in the Backstage. You can learn more [here](../overview/threat-model.md#integrator-responsibilities). +Identity management and the Sign-In page in Backstage will only block external access when using the new backend system, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). ::: diff --git a/docs/overview/threat-model.md b/docs/overview/threat-model.md index da97f3fc83..c0e2d61e25 100644 --- a/docs/overview/threat-model.md +++ b/docs/overview/threat-model.md @@ -37,6 +37,8 @@ The operator is ultimately responsible for auditing usage of internal and extern The operator is also responsible for maintaining the resolved NPM dependencies of their Backstage project. This involves ensuring that `yarn.lock` receives updated versions of packages that have vulnerabilities, when those fixed versions are in range of what the Backstage packages request in their respective `package.json` files. This is commonly done by employing automated tooling such as [Dependabot](https://dependabot.com/), [Snyk](https://snyk.io/), and/or [Renovate](https://docs.renovatebot.com/) on your own repository. When fixed versions exist that are _not_ in range of what Backstage packages request, or when larger operations such as switching out an entire dependency for another one is required, maintainers collaborate with contributors to try to address those dependency declarations in the main project as soon as possible. +The built-in protection against unauthorized access does not by default include protection of the frontend bundle. The frontend bundle includes all the code of your frontend plugins and code in minified form, as well as any other frontend resources like images, fonts, etc. If this is a concern, you can use the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/) to create two separate frontend builds, where authenticated users only have access to the full one. + ## Common Backend Configuration There are many common facilities that are configured centrally and available to all Backstage backend plugins. For example there is a `DatabaseManager` that provides access to a SQL database, `TaskScheduler` for scheduling long-running tasks, `Logger` as a general logging facility, and `UrlReader` for reading content from external sources. These are all configured either directly in code, or within the `backend` block of the static configuration. The appropriate care needs to be taken to ensure that any secrets remain confidential and no malicious configuration is injected. From 7e13b7af04b24105619e34b17ea2b2547b1a6e12 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 15:30:50 +0200 Subject: [PATCH 143/212] backend-common: deprecate remaining methods Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/nine-cherries-decide.md | 14 ++++++++++++++ packages/backend-common/api-report.md | 12 ++++++------ .../src/compat/auth/createLegacyAuthAdapters.ts | 1 + .../src/compat/cache/cacheToPluginCacheManager.ts | 1 + .../backend-common/src/compat/legacy/legacy.ts | 3 +++ .../src/compat/logging/loggerToWinstonLogger.ts | 5 ++++- 6 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 .changeset/nine-cherries-decide.md diff --git a/.changeset/nine-cherries-decide.md b/.changeset/nine-cherries-decide.md new file mode 100644 index 0000000000..b20c24aa85 --- /dev/null +++ b/.changeset/nine-cherries-decide.md @@ -0,0 +1,14 @@ +--- +'@backstage/backend-common': patch +--- + +The remaining exports in the package have now been deprecated: + +- `cacheToPluginCacheManager` +- `createLegacyAuthAdapters` +- `LegacyCreateRouter` +- `legacyPlugin` +- `loggerToWinstonLogger` +- `makeLegacyPlugin` + +Users of these export should fully [migrate to the new backend system](https://backstage.io/docs/backend-system/building-backends/migrating). diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 52b661bcaf..23ed13b62c 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -129,7 +129,7 @@ export class CacheManager extends CacheManager_2 {} // @public @deprecated (undocumented) export type CacheManagerOptions = CacheManagerOptions_2; -// @public +// @public @deprecated export function cacheToPluginCacheManager(cache: CacheService): { getClient(options?: CacheServiceOptions): CacheService; }; @@ -142,7 +142,7 @@ export interface ContainerRunner { runContainer(opts: RunContainerOptions): Promise<void>; } -// @public +// @public @deprecated export function createLegacyAuthAdapters< TOptions extends { auth?: AuthService; @@ -401,10 +401,10 @@ export type KubernetesContainerRunnerOptions = { timeoutMs?: number; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type LegacyCreateRouter<TEnv> = (deps: TEnv) => Promise<RequestHandler>; -// @public +// @public @deprecated export const legacyPlugin: ( name: string, createRouterImport: Promise<{ @@ -447,13 +447,13 @@ export function loadBackendConfig(options: { watch?: boolean; }): Promise<Config>; -// @public (undocumented) +// @public @deprecated (undocumented) export function loggerToWinstonLogger( logger: LoggerService, opts?: TransportStreamOptions, ): Logger; -// @public +// @public @deprecated export function makeLegacyPlugin< TEnv extends Record<string, unknown>, TEnvTransforms extends { diff --git a/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts index 0f591eac6c..15cd881649 100644 --- a/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts @@ -292,6 +292,7 @@ export class UserInfoCompat implements UserInfoService { /** * An adapter that ensures presence of the auth and/or httpAuth services. * @public + * @deprecated Migrate to use the new backend system and auth services instead. */ export function createLegacyAuthAdapters< TOptions extends { diff --git a/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts b/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts index 2654934804..2ae55b016f 100644 --- a/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts +++ b/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts @@ -24,6 +24,7 @@ import { * old-backend plugin cache manager. * * @public + * @deprecated Migrate to use the new CacheService instead. */ export function cacheToPluginCacheManager(cache: CacheService): { getClient(options?: CacheServiceOptions): CacheService; diff --git a/packages/backend-common/src/compat/legacy/legacy.ts b/packages/backend-common/src/compat/legacy/legacy.ts index 57220a8430..c779daacd2 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -27,6 +27,7 @@ import { TokenManager } from '../../deprecated'; /** * @public + * @deprecated Fully use the new backend system instead. */ export type LegacyCreateRouter<TEnv> = (deps: TEnv) => Promise<RequestHandler>; @@ -69,6 +70,7 @@ function wrapTokenManager(tokenManager: TokenManager, auth: AuthService) { * Creates a new custom plugin compatibility wrapper. * * @public + * @deprecated Fully use the new backend system instead. * @remarks * * Usually you can use {@link legacyPlugin} directly instead, but you might @@ -128,6 +130,7 @@ export function makeLegacyPlugin< * register it with the http router based on the plugin id. * * @public + * @deprecated Fully use the new backend system instead. * @remarks * * This is intended to be used by plugin authors to ease the transition to the diff --git a/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts b/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts index 4ba88358d2..83871f48e3 100644 --- a/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts +++ b/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts @@ -53,7 +53,10 @@ class BackstageLoggerTransport extends Transport { } } -/** @public */ +/** + * @public + * @deprecated Migrate to use the new LoggerService instead. + */ export function loggerToWinstonLogger( logger: LoggerService, opts?: TransportStreamOptions, From b3769f14b3f3fa767546d0d806e0a1967fc0a940 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 18:37:01 +0200 Subject: [PATCH 144/212] changesets: enter prerelease Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/pre.json | 192 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..be1b694c9e --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,192 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.99", + "@backstage/app-defaults": "1.5.8", + "example-app-next": "0.0.13", + "app-next-example-plugin": "0.0.13", + "example-backend": "0.0.28", + "@backstage/backend-app-api": "0.8.0", + "@backstage/backend-common": "0.23.3", + "@backstage/backend-defaults": "0.4.0", + "@backstage/backend-dev-utils": "0.1.4", + "@backstage/backend-dynamic-feature-service": "0.2.15", + "example-backend-legacy": "0.2.100", + "@backstage/backend-openapi-utils": "0.1.15", + "@backstage/backend-plugin-api": "0.7.0", + "@backstage/backend-tasks": "0.5.27", + "@backstage/backend-test-utils": "0.4.4", + "@backstage/catalog-client": "1.6.5", + "@backstage/catalog-model": "1.5.0", + "@backstage/cli": "0.26.11", + "@backstage/cli-common": "0.1.14", + "@backstage/cli-node": "0.2.7", + "@backstage/codemods": "0.1.49", + "@backstage/config": "1.2.0", + "@backstage/config-loader": "1.8.1", + "@backstage/core-app-api": "1.14.0", + "@backstage/core-compat-api": "0.2.7", + "@backstage/core-components": "0.14.9", + "@backstage/core-plugin-api": "1.9.3", + "@backstage/create-app": "0.5.17", + "@backstage/dev-utils": "1.0.35", + "e2e-test": "0.2.18", + "@backstage/e2e-test-utils": "0.1.1", + "@backstage/errors": "1.2.4", + "@backstage/eslint-plugin": "0.1.8", + "@backstage/frontend-app-api": "0.7.3", + "@backstage/frontend-plugin-api": "0.6.7", + "@backstage/frontend-test-utils": "0.1.10", + "@backstage/integration": "1.13.0", + "@backstage/integration-aws-node": "0.1.12", + "@backstage/integration-react": "1.1.29", + "@backstage/release-manifests": "0.0.11", + "@backstage/repo-tools": "0.9.4", + "@techdocs/cli": "1.8.16", + "techdocs-cli-embedded-app": "0.2.98", + "@backstage/test-utils": "1.5.8", + "@backstage/theme": "0.5.6", + "@backstage/types": "1.1.1", + "@backstage/version-bridge": "1.0.8", + "yarn-plugin-backstage": "0.0.1", + "@backstage/plugin-api-docs": "0.11.7", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.7", + "@backstage/plugin-app-backend": "0.3.71", + "@backstage/plugin-app-node": "0.1.22", + "@backstage/plugin-app-visualizer": "0.1.8", + "@backstage/plugin-auth-backend": "0.22.9", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.2.3", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.1.14", + "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.2.17", + "@backstage/plugin-auth-backend-module-github-provider": "0.1.19", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.1.19", + "@backstage/plugin-auth-backend-module-google-provider": "0.1.19", + "@backstage/plugin-auth-backend-module-guest-provider": "0.1.8", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.1.17", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.2.3", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.1.15", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.2.3", + "@backstage/plugin-auth-backend-module-okta-provider": "0.0.15", + "@backstage/plugin-auth-backend-module-onelogin-provider": "0.1.3", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.1.16", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.2.3", + "@backstage/plugin-auth-node": "0.4.17", + "@backstage/plugin-auth-react": "0.1.4", + "@backstage/plugin-bitbucket-cloud-common": "0.2.21", + "@backstage/plugin-catalog": "1.21.1", + "@backstage/plugin-catalog-backend": "1.24.0", + "@backstage/plugin-catalog-backend-module-aws": "0.3.17", + "@backstage/plugin-catalog-backend-module-azure": "0.1.42", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.2.5", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.2.9", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.36", + "@backstage/plugin-catalog-backend-module-gcp": "0.1.23", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.39", + "@backstage/plugin-catalog-backend-module-github": "0.6.5", + "@backstage/plugin-catalog-backend-module-github-org": "0.1.17", + "@backstage/plugin-catalog-backend-module-gitlab": "0.3.21", + "@backstage/plugin-catalog-backend-module-gitlab-org": "0.0.5", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.27", + "@backstage/plugin-catalog-backend-module-ldap": "0.7.0", + "@backstage/plugin-catalog-backend-module-logs": "0.0.1", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.30", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.40", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.28", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.1.20", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.4.9", + "@backstage/plugin-catalog-common": "1.0.25", + "@backstage/plugin-catalog-graph": "0.4.7", + "@backstage/plugin-catalog-import": "0.12.1", + "@backstage/plugin-catalog-node": "1.12.4", + "@backstage/plugin-catalog-react": "1.12.2", + "@backstage/plugin-catalog-unprocessed-entities": "0.2.6", + "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.3", + "@backstage/plugin-config-schema": "0.1.57", + "@backstage/plugin-devtools": "0.1.16", + "@backstage/plugin-devtools-backend": "0.3.8", + "@backstage/plugin-devtools-common": "0.1.11", + "@backstage/plugin-events-backend": "0.3.9", + "@backstage/plugin-events-backend-module-aws-sqs": "0.3.8", + "@backstage/plugin-events-backend-module-azure": "0.2.8", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.8", + "@backstage/plugin-events-backend-module-gerrit": "0.2.8", + "@backstage/plugin-events-backend-module-github": "0.2.8", + "@backstage/plugin-events-backend-module-gitlab": "0.2.8", + "@backstage/plugin-events-backend-test-utils": "0.1.32", + "@backstage/plugin-events-node": "0.3.8", + "@internal/plugin-todo-list": "1.0.29", + "@internal/plugin-todo-list-backend": "1.0.29", + "@internal/plugin-todo-list-common": "1.0.20", + "@backstage/plugin-home": "0.7.7", + "@backstage/plugin-home-react": "0.1.15", + "@backstage/plugin-kubernetes": "0.11.12", + "@backstage/plugin-kubernetes-backend": "0.18.3", + "@backstage/plugin-kubernetes-cluster": "0.0.13", + "@backstage/plugin-kubernetes-common": "0.8.1", + "@backstage/plugin-kubernetes-node": "0.1.16", + "@backstage/plugin-kubernetes-react": "0.4.1", + "@backstage/plugin-notifications": "0.2.3", + "@backstage/plugin-notifications-backend": "0.3.3", + "@backstage/plugin-notifications-backend-module-email": "0.1.3", + "@backstage/plugin-notifications-common": "0.0.5", + "@backstage/plugin-notifications-node": "0.2.3", + "@backstage/plugin-org": "0.6.27", + "@backstage/plugin-org-react": "0.1.26", + "@backstage/plugin-permission-backend": "0.5.46", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.1.19", + "@backstage/plugin-permission-common": "0.8.0", + "@backstage/plugin-permission-node": "0.8.0", + "@backstage/plugin-permission-react": "0.4.24", + "@backstage/plugin-proxy-backend": "0.5.3", + "@backstage/plugin-scaffolder": "1.23.0", + "@backstage/plugin-scaffolder-backend": "1.23.0", + "@backstage/plugin-scaffolder-backend-module-azure": "0.1.14", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.2.12", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.1.12", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.1.12", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.23", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.46", + "@backstage/plugin-scaffolder-backend-module-gcp": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.1.14", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.1.12", + "@backstage/plugin-scaffolder-backend-module-github": "0.4.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.4.4", + "@backstage/plugin-scaffolder-backend-module-notifications": "0.0.5", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.39", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.30", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.3.6", + "@backstage/plugin-scaffolder-common": "1.5.4", + "@backstage/plugin-scaffolder-node": "0.4.8", + "@backstage/plugin-scaffolder-node-test-utils": "0.1.9", + "@backstage/plugin-scaffolder-react": "1.10.0", + "@backstage/plugin-search": "1.4.14", + "@backstage/plugin-search-backend": "1.5.14", + "@backstage/plugin-search-backend-module-catalog": "0.1.28", + "@backstage/plugin-search-backend-module-elasticsearch": "1.5.3", + "@backstage/plugin-search-backend-module-explore": "0.1.28", + "@backstage/plugin-search-backend-module-pg": "0.5.32", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.1.15", + "@backstage/plugin-search-backend-module-techdocs": "0.1.27", + "@backstage/plugin-search-backend-node": "1.2.27", + "@backstage/plugin-search-common": "1.2.13", + "@backstage/plugin-search-react": "1.7.13", + "@backstage/plugin-signals": "0.0.8", + "@backstage/plugin-signals-backend": "0.1.8", + "@backstage/plugin-signals-node": "0.1.8", + "@backstage/plugin-signals-react": "0.0.4", + "@backstage/plugin-techdocs": "1.10.7", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.35", + "@backstage/plugin-techdocs-backend": "1.10.9", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.12", + "@backstage/plugin-techdocs-node": "1.12.8", + "@backstage/plugin-techdocs-react": "1.2.6", + "@backstage/plugin-user-settings": "0.8.9", + "@backstage/plugin-user-settings-backend": "0.2.21", + "@backstage/plugin-user-settings-common": "0.0.1" + }, + "changesets": [] +} From 669d742d38359fed15ab03bad494ed73e3e3c7ce Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 19:32:32 +0200 Subject: [PATCH 145/212] create-app: remove unnecessary yarn.lock seeds Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- packages/create-app/seed-yarn.lock | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index b09a0ce1a7..00a86105cc 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -16,13 +16,3 @@ // package: the name of the package, e.g. @testing-library/react // query: the version query to pin the version for, e.g. ^14.0.0 // version: the version to pin to, must be in range of the query, e.g. 14.11.0 - -ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0, ajv@^8.12.0, ajv@^8.14.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.8.2, ajv@^8.9.0: - version "8.14.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.14.0.tgz#f514ddfd4756abb200e1704414963620a625ebbb" - integrity sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA== - -"@mui/material@^5.12.2": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.16.1.tgz#6fcef9b5709df5864cf0b0bc0ea7b453a9d9e420" - integrity sha512-BGTgJRb0d/hX9tus5CEb6N/Fo8pE4tYA+s9r4/S0PCrtZ3urCLXlTH4qrAvggQbiF1cYRAbHCkVHoQ+4Pdxl+w== From 8e967da7a0c9cf380f458672340c6fe2e1399621 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 16 Jul 2024 21:28:51 +0200 Subject: [PATCH 146/212] backend-defaults: fixed routing of health endpoints Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/selfish-bees-think.md | 5 +++++ .../src/entrypoints/rootHttpRouter/createHealthRouter.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/selfish-bees-think.md diff --git a/.changeset/selfish-bees-think.md b/.changeset/selfish-bees-think.md new file mode 100644 index 0000000000..15598ea551 --- /dev/null +++ b/.changeset/selfish-bees-think.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Fixed the routing of the new health check service, the health endpoints should now properly be available at `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness`. diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts index 13691ede28..007d1dd9cd 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts @@ -23,7 +23,7 @@ export function createHealthRouter(options: { health: RootHealthService }) { const router = Router(); router.get( - '.backstage/health/v1/readiness', + '/.backstage/health/v1/readiness', async (_request: Request, response: Response) => { const { status, payload } = await options.health.getReadiness(); response.status(status).json(payload); @@ -31,7 +31,7 @@ export function createHealthRouter(options: { health: RootHealthService }) { ); router.get( - '.backstage/health/v1/liveness', + '/.backstage/health/v1/liveness', async (_request: Request, response: Response) => { const { status, payload } = await options.health.getLiveness(); response.status(status).json(payload); From 4670f06f72877aa20faf7f24021a977b22598754 Mon Sep 17 00:00:00 2001 From: Matt Benson <gudnabrsam@gmail.com> Date: Wed, 6 Mar 2024 17:31:38 -0600 Subject: [PATCH 147/212] support ajv-errors for scaffolder validation Signed-off-by: Matt Benson <gudnabrsam@gmail.com> --- .changeset/modern-parrots-protect.md | 5 +++++ .../config/vocabularies/Backstage/accept.txt | 1 + .../software-templates/input-examples.md | 19 +++++++++++++++++++ .../software-templates/writing-templates.md | 5 +++++ plugins/scaffolder-react/package.json | 1 + .../src/next/components/Stepper/Stepper.tsx | 6 +++++- yarn.lock | 1 + 7 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/modern-parrots-protect.md diff --git a/.changeset/modern-parrots-protect.md b/.changeset/modern-parrots-protect.md new file mode 100644 index 0000000000..7fccff1bef --- /dev/null +++ b/.changeset/modern-parrots-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +support ajv-errors for scaffolder validation diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 6ff0ef56da..1092947245 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -7,6 +7,7 @@ ADRs airbrake Airbrake Airbrakes +ajv Alaria Alef allowlisted diff --git a/docs/features/software-templates/input-examples.md b/docs/features/software-templates/input-examples.md index f49e76e469..dd1228290f 100644 --- a/docs/features/software-templates/input-examples.md +++ b/docs/features/software-templates/input-examples.md @@ -26,6 +26,25 @@ parameters: ui:help: 'Hint: additional description...' ``` +#### Custom validation error message + +```yaml +parameters: + - title: Fill in some steps + properties: + name: + title: Simple text input + type: string + description: Description about input + maxLength: 8 + pattern: '^([a-zA-Z][a-zA-Z0-9]*)(-[a-zA-Z0-9]+)*$' + ui:autofocus: true + ui:help: 'Hint: additional description...' + errorMessage: + properties: + name: '1-8 alphanumeric tokens (first starts with letter) delimited by -' +``` + ### Multi line text input ```yaml diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 15591eeb67..075fe0bb08 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -533,6 +533,11 @@ catalogFilter: metadata.annotations.github.com/team-slug: { exists: true } ``` +#### Custom validation messages + +You may specify custom JSON Schema validation messages as supported by the +[ajv-errors](https://github.com/ajv-validator/ajv-errors) plugin library to [ajv](https://github.com/ajv-validator/ajv). + ## `spec.steps` - `Action[]` The `steps` is an array of the things that you want to happen part of this diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index f2d7d44a01..9c50900c05 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -78,6 +78,7 @@ "@rjsf/validator-ajv8": "5.18.5", "@types/json-schema": "^7.0.9", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "ajv-errors": "^3.0.0", "classnames": "^2.2.6", "flatted": "3.3.1", "humanize-duration": "^3.25.1", diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 269b6a11d5..788c7cd715 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -35,7 +35,7 @@ import { } from './createAsyncValidators'; import { ReviewState, type ReviewStateProps } from '../ReviewState'; import { useTemplateSchema, useFormDataFromQuery } from '../../hooks'; -import validator from '@rjsf/validator-ajv8'; +import { customizeValidator } from '@rjsf/validator-ajv8'; import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps'; import { hasErrors } from './utils'; import * as FieldOverrides from './FieldOverrides'; @@ -50,6 +50,10 @@ import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; import { ErrorListTemplate } from './ErrorListTemplate'; import { makeStyles } from '@material-ui/core/styles'; import { PasswordWidget } from '../PasswordWidget/PasswordWidget'; +import ajvErrors from 'ajv-errors'; + +const validator = customizeValidator(); +ajvErrors(validator.ajv); const useStyles = makeStyles(theme => ({ backButton: { diff --git a/yarn.lock b/yarn.lock index c6a1d2139a..fe7bc4b2c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7110,6 +7110,7 @@ __metadata: "@types/json-schema": ^7.0.9 "@types/luxon": ^3.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + ajv-errors: ^3.0.0 classnames: ^2.2.6 flatted: 3.3.1 humanize-duration: ^3.25.1 From 8b808fd1a92389395668596a18942502816610aa Mon Sep 17 00:00:00 2001 From: Matt Benson <gudnabrsam@gmail.com> Date: Mon, 15 Jul 2024 10:48:22 -0500 Subject: [PATCH 148/212] add errorMessage test Signed-off-by: Matt Benson <gudnabrsam@gmail.com> --- .../next/components/Stepper/Stepper.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index d7e271f8b3..df58d971fb 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -392,6 +392,46 @@ describe('Stepper', () => { expect(getByText('invalid postcode')).toBeInTheDocument(); }); + it('should render ajv-errors message', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + properties: { + postcode: { + type: 'string', + pattern: '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]', + }, + }, + errorMessage: { + properties: { + postcode: 'invalid postcode', + }, + }, + }, + }, + ], + title: 'transformErrors Form Test', + }; + + const { getByText, getByRole } = await renderInTestApp( + <SecretsContextProvider> + <Stepper manifest={manifest} extensions={[]} onCreate={jest.fn()} /> + </SecretsContextProvider>, + ); + + await fireEvent.change(getByRole('textbox', { name: 'postcode' }), { + target: { value: 'invalid' }, + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + expect(getByText('invalid postcode')).toBeInTheDocument(); + }); + it('should grab the initial formData from the query', async () => { const manifest: TemplateParameterSchema = { steps: [ From 7ab4983ed28943636a913eaf7992e437a1e2ade4 Mon Sep 17 00:00:00 2001 From: parmar-abhinav <abhinav.parmar@infosys.com> Date: Wed, 17 Jul 2024 15:00:39 +0530 Subject: [PATCH 149/212] Chore:Add error handling for YAML parsing to improve robustness Signed-off-by: parmar-abhinav <abhinav.parmar@infosys.com> --- .../fetch/rails/index.examples.test.ts | 70 ++++++++++++++++--- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts index a80985321e..37e678ab6a 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts @@ -90,7 +90,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[0].description}`, async () => { - const input = yaml.parse(examples[0].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -110,7 +115,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[1].description}`, async () => { - const input = yaml.parse(examples[1].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -130,7 +140,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[2].description}`, async () => { - const input = yaml.parse(examples[2].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -150,7 +165,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[3].description}`, async () => { - const input = yaml.parse(examples[3].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -170,7 +190,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[4].description}`, async () => { - const input = yaml.parse(examples[4].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -190,7 +215,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[5].description}`, async () => { - const input = yaml.parse(examples[5].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -210,7 +240,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[6].description}`, async () => { - const input = yaml.parse(examples[6].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -230,7 +265,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[7].description}`, async () => { - const input = yaml.parse(examples[7].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -250,7 +290,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[8].description}`, async () => { - const input = yaml.parse(examples[8].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, @@ -270,7 +315,12 @@ describe('fetch:rails', () => { }); it(`should ${examples[9].description}`, async () => { - const input = yaml.parse(examples[9].example).steps[0].input; + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } await action.handler({ ...mockContext, From 31fd39b4dce1c39d258ed72c43dabe8de38e1c38 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Wed, 17 Jul 2024 12:22:36 +0200 Subject: [PATCH 150/212] chore: fixing discord link Signed-off-by: blam <ben@blam.sh> --- microsite/blog/2022-12-19-backstage-wrapped-2022.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx b/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx index e92c9f2265..c15b9fe208 100644 --- a/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx +++ b/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx @@ -20,7 +20,7 @@ Last year we celebrated 75 [public adopters](https://github.com/backstage/backst ![Accelerated growth of Public Backstage Adopters, from January 2021–December 2022.](assets/2022-12-19/public-backstage-adopters-2022.png) -Not only are our adopters growing at an exponential rate, but so too are our contributors. Our [Discord server](https://discord.gg/backstage-687207715902193673-687207715902193673) is a perfect example of the worlds of our adopters and contributors melding. On average, at any given hour of the day or night, there are more than 500 people online available to talk and provide support on any Backstage topic. Discord discussions run a full gamut of topics – from technical support questions to best practices for onboarding engineers to Backstage – basically, anything and everything an adopter may be interested to know and learn. At the very beginning of 2022, we “only” had 4k Discord members. Today, we have a growing membership of 8.2k. +Not only are our adopters growing at an exponential rate, but so too are our contributors. Our [Discord server](https://discord.gg/backstage-687207715902193673) is a perfect example of the worlds of our adopters and contributors melding. On average, at any given hour of the day or night, there are more than 500 people online available to talk and provide support on any Backstage topic. Discord discussions run a full gamut of topics – from technical support questions to best practices for onboarding engineers to Backstage – basically, anything and everything an adopter may be interested to know and learn. At the very beginning of 2022, we “only” had 4k Discord members. Today, we have a growing membership of 8.2k. Isn’t it what we can call a great and vibrant community of enthusiasts? From ee2b0e54330409df51f9ed2ac78090c77afd8f53 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:08:02 +0100 Subject: [PATCH 151/212] cli: add support for forcing use of react development when building Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .changeset/little-bulldogs-guess.md | 8 +++++ packages/cli/src/lib/bundler/config.ts | 44 +++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 .changeset/little-bulldogs-guess.md diff --git a/.changeset/little-bulldogs-guess.md b/.changeset/little-bulldogs-guess.md new file mode 100644 index 0000000000..8d247a182b --- /dev/null +++ b/.changeset/little-bulldogs-guess.md @@ -0,0 +1,8 @@ +--- +'@backstage/cli': patch +--- + +Add ability to force use of the development versions of `react` and `react-dom` +by setting the `FORCE_REACT_DEVELOPMENT` environment variable to `true` when +building, for example by using a command like `FORCE_REACT_DEVELOPMENT=true yarn +build:all`. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 60c8a7be66..44cdc4a475 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -15,7 +15,8 @@ */ import { BackendBundlingOptions, BundlingOptions } from './types'; -import { posix as posixPath, resolve as resolvePath } from 'path'; +import { posix as posixPath, resolve as resolvePath, dirname } from 'path'; +import chalk from 'chalk'; import webpack, { ProvidePlugin } from 'webpack'; import { BackstagePackage } from '@backstage/cli-node'; @@ -33,7 +34,7 @@ import fs from 'fs-extra'; import { getPackages } from '@manypkg/get-packages'; import { isChildPath } from '@backstage/cli-common'; import nodeExternals from 'webpack-node-externals'; -import { optimization } from './optimization'; +import { optimization as optimizationConfig } from './optimization'; import pickBy from 'lodash/pickBy'; import { readEntryPoints } from '../entryPoints'; import { runPlain } from '../run'; @@ -232,12 +233,47 @@ export async function createConfig( require.resolve('react-refresh'), ]; + const mode = isDev ? 'development' : 'production'; + const optimization = optimizationConfig(options); + + if (process.env.FORCE_REACT_DEVELOPMENT === 'true') { + console.log( + chalk.yellow( + `⚠️ WARNING: Forcing react and react-dom into development mode. This build should not be used in production.`, + ), + ); + + const reactPackageDirs = [ + `${dirname(require.resolve('react/package.json'))}/`, + `${dirname(require.resolve('react-dom/package.json'))}/`, + ]; + + // Don't define process.env.NODE_ENV with value matching config.mode. + optimization.nodeEnv = false; + + // Instead, provide a custom definition which always uses "development" if + // the module is part of `react` or `react-dom`, and `config.mode` otherwise. + plugins.push( + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': webpack.DefinePlugin.runtimeValue( + ({ module }) => { + if (reactPackageDirs.some(val => module.resource.startsWith(val))) { + return '"development"'; + } + + return `"${mode}"`; + }, + ), + }), + ); + } + const withCache = yn(process.env[BUILD_CACHE_ENV_VAR], { default: false }); return { - mode: isDev ? 'development' : 'production', + mode, profile: false, - optimization: optimization(options), + optimization, bail: false, performance: { hints: false, // we check the gzip size instead From 7eb08a69dd0cdc11fdf363a07149a8ca9438eadd Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:53:55 +0100 Subject: [PATCH 152/212] cli: allow linting packages with role dynamic-frontend-container Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .changeset/bright-trainers-brake.md | 5 +++++ packages/cli/config/eslint-factory.js | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/bright-trainers-brake.md diff --git a/.changeset/bright-trainers-brake.md b/.changeset/bright-trainers-brake.md new file mode 100644 index 0000000000..2299d83a32 --- /dev/null +++ b/.changeset/bright-trainers-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add frontend-dynamic-container role to eslint config factory diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index 6bc200cade..96d8478ec0 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -201,6 +201,7 @@ function createConfigForRole(dir, role, extraConfig = {}) { case 'frontend': case 'frontend-plugin': case 'frontend-plugin-module': + case 'frontend-dynamic-container': return createConfig(dir, { ...extraConfig, extends: [ From 46e949f5ed570aefcc6917f4e771f633aef714e3 Mon Sep 17 00:00:00 2001 From: Francisco Meneses <fmeneses@redhat.com> Date: Wed, 17 Jul 2024 17:16:29 -0400 Subject: [PATCH 153/212] fix Spanish welcomeTitle message Signed-off-by: Francisco Meneses <fmeneses@redhat.com> --- .../WelcomeTitle/locales/goodAfternoon.locales.json | 1 + .../WelcomeTitle/locales/goodEvening.locales.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json index 9010ba4359..ddb51654fa 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json +++ b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json @@ -103,6 +103,7 @@ "Sinhala": "සුභ දවාලක්", "Slovenian": "Dober dan", "Somali": "Galab wanaagsan", + "Spanish": "Buenas tardes", "Southern Sámi": "Buerie biejjie", "Sumerian": "𒄭𒌓𒁀", "Swahili": "Habari ya mchana", diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json index 5a98abafaa..c8d8c7334e 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json +++ b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json @@ -85,7 +85,7 @@ "Slovak": "Dober vecher", "Slovenian": "Dober vecher", "Somali": "Habeen wanaagsan", - "Spanish": "Buenas tardes", + "Spanish": "Buenas noches", "Sumerian": "𒄭𒈪", "Swedish": "God afton", "Tagalog": "Magandang gabi", From fdcc05947fc7447a28d3090235d882896e37d10a Mon Sep 17 00:00:00 2001 From: Francisco Meneses <fmeneses@redhat.com> Date: Wed, 17 Jul 2024 17:34:17 -0400 Subject: [PATCH 154/212] add changeset Signed-off-by: Francisco Meneses <fmeneses@redhat.com> --- .changeset/slow-toes-jog.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slow-toes-jog.md diff --git a/.changeset/slow-toes-jog.md b/.changeset/slow-toes-jog.md new file mode 100644 index 0000000000..245feab544 --- /dev/null +++ b/.changeset/slow-toes-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Fixed a bug on the WelcomeTitle component where the welcome message wasn't correct when the language was set to Spanish From 644d4696c28ca675efc0f240c428b5f43547f734 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:35:40 +0100 Subject: [PATCH 155/212] cli: only allow forcing react development mode when module federation is enabled Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .changeset/little-bulldogs-guess.md | 15 ++++++++++++--- packages/cli/src/lib/bundler/config.ts | 5 ++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.changeset/little-bulldogs-guess.md b/.changeset/little-bulldogs-guess.md index 8d247a182b..1f86f0a67f 100644 --- a/.changeset/little-bulldogs-guess.md +++ b/.changeset/little-bulldogs-guess.md @@ -3,6 +3,15 @@ --- Add ability to force use of the development versions of `react` and `react-dom` -by setting the `FORCE_REACT_DEVELOPMENT` environment variable to `true` when -building, for example by using a command like `FORCE_REACT_DEVELOPMENT=true yarn -build:all`. +to allow for detailed error messages and use of fast-refresh. Note that builds +with the development versions of `react` and `react-dom` should not be +deployed in production. + +This feature can be used by setting the `FORCE_REACT_DEVELOPMENT` environment +variable to `true`: + +```bash +EXPERIMENTAL_MODULE_FEDERATION=true \ +FORCE_REACT_DEVELOPMENT=true \ +yarn build:all +``` diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 44cdc4a475..8e3d7eee19 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -236,7 +236,10 @@ export async function createConfig( const mode = isDev ? 'development' : 'production'; const optimization = optimizationConfig(options); - if (process.env.FORCE_REACT_DEVELOPMENT === 'true') { + if ( + process.env.EXPERIMENTAL_MODULE_FEDERATION === 'true' && + process.env.FORCE_REACT_DEVELOPMENT === 'true' + ) { console.log( chalk.yellow( `⚠️ WARNING: Forcing react and react-dom into development mode. This build should not be used in production.`, From 1f5094f1b70495aba6ee8d54bb86ed5dcea6b410 Mon Sep 17 00:00:00 2001 From: Ben Lambert <blam@spotify.com> Date: Wed, 26 Jun 2024 16:20:47 +0200 Subject: [PATCH 156/212] Update dry-squids-tap.md Signed-off-by: Ben Lambert <blam@spotify.com> Signed-off-by: blam <ben@blam.sh> --- .changeset/dry-squids-tap.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md index 5193e1f204..9bd835d371 100644 --- a/.changeset/dry-squids-tap.md +++ b/.changeset/dry-squids-tap.md @@ -14,9 +14,9 @@ const TestExtensionKind = createExtensionKind({ output: { element: coreExtensionData.reactElement, }, - factory(_, props: { text: string }) { + factory(_, options: { text: string }) { return { - element: <h1>{props.text}</h1>, + element: <h1>{options.text}</h1>, }; }, }); From f749bb8fa989e5f4c29608a903cc9119cd745390 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Tue, 25 Jun 2024 14:25:22 +0200 Subject: [PATCH 157/212] chore: extension overrides spike Signed-off-by: blam <ben@blam.sh> --- .../core-compat-api/src/convertLegacyApp.ts | 72 ++++++++++- .../src/wiring/createExtension.test.ts | 33 +++++ .../src/wiring/createExtension.ts | 121 ++++++++++++++---- .../src/wiring/createExtensionKind.ts | 4 +- 4 files changed, 194 insertions(+), 36 deletions(-) diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index 72ce4b5e61..567e5efcad 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -26,6 +26,7 @@ import { coreExtensionData, createExtension, createExtensionInput, + createExtensionKind, createExtensionOverrides, } from '@backstage/frontend-plugin-api'; import { getComponentData } from '@backstage/core-plugin-api'; @@ -128,21 +129,80 @@ export function convertLegacyApp( }; }, }); - const CoreNavOverride = createExtension({ - namespace: 'app', - name: 'nav', - attachTo: { id: 'app/layout', input: 'nav' }, - output: {}, + + const CoreNavOverride = CurrentCoreNav.override({ + // namespace: 'app', + // name: 'nav', + // attachTo: { id: 'app/layout', input: 'nav' }, + // output: {}, factory: () => ({}), disabled: true, }); + createExtensionOverride({ + extension: CurrentCoreNav, + factory: () => null, + }); + const collectedRoutes = collectLegacyRoutes(routesEl); return [ ...collectedRoutes, createExtensionOverrides({ - extensions: [CoreLayoutOverride, CoreNavOverride], + extensions: [CoreNavOverride, CoreNavOverride], }), ]; } + +const EntityCardExtension = createExtensionKind({ + kind: 'entity-card', + attachTo: { id: 'entity-card', input: 'default' }, + inputs: { + loader: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }, props: { title: string }) { + console.log(inputs.loader); + return { + element: React.createElement('h1'), + }; + }, +}); + +const GithubCard = EntityCardExtension.new({ + props: { + title: 'GitHub Card', + }, + factory({ inputs: { loader } }) { + console.log(loader); + return { + element: React.createElement('h2'), + }; + }, +}); + +GithubCard.override({ + attachTo: { id: 'entity-card', input: 'github' }, + inputs: { + loader: createExtensionInput( + { + element: coreExtensionData.reactElement, + }, + { singleton: false }, + ), + loader2: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + factory({ originalFactory, inputs }) { + inputs.loader2; + inputs.loader; + return originalFactory({ + inputsOverride: inputs, + }); + }, +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 42ab21db80..cfef69b0fe 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -291,4 +291,37 @@ describe('createExtension', () => { 'ExtensionDefinition{namespace=test,attachTo=root@default}', ); }); + + describe('override', () => { + it('should create an extension override', () => { + const extension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: { + foo: stringData, + }, + factory() { + return { + foo: 'bar', + }; + }, + }); + + const override = extension.override({ + attachTo: { + id: 'root', + input: 'default2', + }, + factory() { + return { + foo: 'baz', + }; + }, + }); + + expect(String(override)).toBe( + 'ExtensionDefinition{namespace=test,attachTo=root@default2}', + ); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 28dc6a4e29..b56cc39fc9 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -99,7 +99,33 @@ export interface CreateExtensionOptions< } /** @public */ -export interface ExtensionDefinition<TConfig> { +export interface ExtensionDefinitionOverrides< + TOriginalConfig, + TOriginalInputs extends AnyExtensionInputMap, + TOverrideInputs extends AnyExtensionInputMap, +> { + readonly disabled?: boolean; + readonly attachTo?: { id: string; input: string }; + // TODO: inputs (only adding to the list? or redefine arity and superset of old type on existing input?) + // TODO: config (any merging needed on there?) + inputs?: TOverrideInputs; + factory?(options: { + node: AppNode; + config: TOriginalConfig; + inputs: Expand<ResolvedExtensionInputs<TOriginalInputs & TOverrideInputs>>; + originalFactory: (originalFactoryOptions?: { + inputsOverride?: ResolvedExtensionInputs< + TOriginalInputs & TOverrideInputs + >; + }) => ExtensionDataValues<any>; + }): ExtensionDataValues<any>; +} + +/** @public */ +export interface ExtensionDefinition< + TConfig, + TInputs extends AnyExtensionInputMap, +> { $$type: '@backstage/ExtensionDefinition'; readonly kind?: string; readonly namespace?: string; @@ -107,13 +133,18 @@ export interface ExtensionDefinition<TConfig> { readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema<TConfig>; + override<TOverrideInputs extends AnyExtensionInputMap>( + overrides: ExtensionDefinitionOverrides<TConfig, TInputs, TOverrideInputs>, + ): ExtensionDefinition<TConfig, TInputs>; } /** @internal */ -export interface InternalExtensionDefinition<TConfig> - extends ExtensionDefinition<TConfig> { +export interface InternalExtensionDefinition< + TConfig, + TInputs extends AnyExtensionInputMap, +> extends ExtensionDefinition<TConfig, TInputs> { readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; + readonly inputs: TInputs; readonly output: AnyExtensionDataMap; factory(context: { node: AppNode; @@ -123,10 +154,13 @@ export interface InternalExtensionDefinition<TConfig> } /** @internal */ -export function toInternalExtensionDefinition<TConfig>( - overrides: ExtensionDefinition<TConfig>, -): InternalExtensionDefinition<TConfig> { - const internal = overrides as InternalExtensionDefinition<TConfig>; +export function toInternalExtensionDefinition< + TConfig, + TInputs extends AnyExtensionInputMap, +>( + overrides: ExtensionDefinition<TConfig, TInputs>, +): InternalExtensionDefinition<TConfig, TInputs> { + const internal = overrides as InternalExtensionDefinition<TConfig, TInputs>; if (internal.$$type !== '@backstage/ExtensionDefinition') { throw new Error( `Invalid extension definition instance, bad type '${internal.$$type}'`, @@ -147,38 +181,69 @@ export function createExtension< TConfig = never, >( options: CreateExtensionOptions<TOutput, TInputs, TConfig>, -): ExtensionDefinition<TConfig> { +): ExtensionDefinition<TConfig, TInputs> { + const { + kind, + namespace, + name, + attachTo, + disabled = false, + inputs = {} as TInputs, + output, + configSchema, + factory, + } = options; + return { $$type: '@backstage/ExtensionDefinition', version: 'v1', - kind: options.kind, - namespace: options.namespace, - name: options.name, - attachTo: options.attachTo, - disabled: options.disabled ?? false, - inputs: options.inputs ?? {}, - output: options.output, - configSchema: options.configSchema, - factory({ inputs, ...rest }) { + kind, + namespace, + name, + attachTo, + disabled, + inputs, + output, + configSchema, + factory({ inputs: factoryInputs, ...rest }) { // TODO: Simplify this, but TS wouldn't infer the input type for some reason - return options.factory({ - inputs: inputs as Expand<ResolvedExtensionInputs<TInputs>>, + return factory({ + inputs: factoryInputs as Expand<ResolvedExtensionInputs<TInputs>>, ...rest, }); }, toString() { const parts: string[] = []; - if (options.kind) { - parts.push(`kind=${options.kind}`); + if (kind) { + parts.push(`kind=${kind}`); } - if (options.namespace) { - parts.push(`namespace=${options.namespace}`); + if (namespace) { + parts.push(`namespace=${namespace}`); } - if (options.name) { - parts.push(`name=${options.name}`); + if (name) { + parts.push(`name=${name}`); } - parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); + parts.push(`attachTo=${attachTo.id}@${attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - } as InternalExtensionDefinition<TConfig>; + override<TOverrideInputs extends AnyExtensionInputMap>( + overrides: ExtensionDefinitionOverrides< + TConfig, + TInputs, + TOverrideInputs + >, + ): ExtensionDefinition<TConfig, TInputs> { + return createExtension({ + kind, + namespace, + name, + attachTo: overrides.attachTo ?? attachTo, + disabled: overrides.disabled ?? disabled, + inputs: { ...inputs, ...overrides.inputs }, + output, + configSchema, + factory, + }); + }, + } as InternalExtensionDefinition<TConfig, TInputs>; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts index 52bf711822..353fb8ab70 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { AppNode } from '../apis'; import { PortableSchema } from '../schema'; import { Expand } from '../types'; @@ -21,6 +20,7 @@ import { AnyExtensionDataMap, AnyExtensionInputMap, ExtensionDataValues, + ExtensionDefinition, ResolvedExtensionInputs, createExtension, } from './createExtension'; @@ -107,7 +107,7 @@ export class ExtensionKind< }, options: TOptions, ): Expand<ExtensionDataValues<TOutput>>; - }) { + }): ExtensionDefinition<TConfig, TInputs> { return createExtension({ kind: this.options.kind, namespace: args.namespace ?? this.options.namespace, From 966cca176bc0506034499ce2cb622ebdffe2ca1b Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Wed, 26 Jun 2024 16:31:25 +0200 Subject: [PATCH 158/212] chore: tidying up Signed-off-by: blam <ben@blam.sh> Signed-off-by: blam <ben@blam.sh> --- .../core-compat-api/src/convertLegacyApp.ts | 72 +---- packages/frontend-plugin-api/api-report.md | 214 ++++++++++----- .../src/wiring/createExtension.test.ts | 33 --- .../src/wiring/createExtension.ts | 125 +++------ .../src/wiring/createExtensionKind.test.tsx | 164 ++++++++++++ .../src/wiring/createExtensionKind.ts | 245 ++++++++++++++---- .../frontend-plugin-api/src/wiring/index.ts | 4 +- 7 files changed, 550 insertions(+), 307 deletions(-) diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index 567e5efcad..72ce4b5e61 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -26,7 +26,6 @@ import { coreExtensionData, createExtension, createExtensionInput, - createExtensionKind, createExtensionOverrides, } from '@backstage/frontend-plugin-api'; import { getComponentData } from '@backstage/core-plugin-api'; @@ -129,80 +128,21 @@ export function convertLegacyApp( }; }, }); - - const CoreNavOverride = CurrentCoreNav.override({ - // namespace: 'app', - // name: 'nav', - // attachTo: { id: 'app/layout', input: 'nav' }, - // output: {}, + const CoreNavOverride = createExtension({ + namespace: 'app', + name: 'nav', + attachTo: { id: 'app/layout', input: 'nav' }, + output: {}, factory: () => ({}), disabled: true, }); - createExtensionOverride({ - extension: CurrentCoreNav, - factory: () => null, - }); - const collectedRoutes = collectLegacyRoutes(routesEl); return [ ...collectedRoutes, createExtensionOverrides({ - extensions: [CoreNavOverride, CoreNavOverride], + extensions: [CoreLayoutOverride, CoreNavOverride], }), ]; } - -const EntityCardExtension = createExtensionKind({ - kind: 'entity-card', - attachTo: { id: 'entity-card', input: 'default' }, - inputs: { - loader: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }, props: { title: string }) { - console.log(inputs.loader); - return { - element: React.createElement('h1'), - }; - }, -}); - -const GithubCard = EntityCardExtension.new({ - props: { - title: 'GitHub Card', - }, - factory({ inputs: { loader } }) { - console.log(loader); - return { - element: React.createElement('h2'), - }; - }, -}); - -GithubCard.override({ - attachTo: { id: 'entity-card', input: 'github' }, - inputs: { - loader: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: false }, - ), - loader2: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - factory({ originalFactory, inputs }) { - inputs.loader2; - inputs.loader; - return originalFactory({ - inputsOverride: inputs, - }); - }, -}); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 48718f5375..6a04d04cd6 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -529,9 +529,91 @@ export function createExtensionKind< TOutput extends AnyExtensionDataMap, TConfig, >( - options: ExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, + options: CreateExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, ): ExtensionKind<TOptions, TInputs, TOutput, TConfig>; +// @public (undocumented) +export interface CreateExtensionKindInstanceOptions< + TOptions, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + attachTo?: { + id: string; + input: string; + }; + // (undocumented) + configSchema?: PortableSchema<TConfig>; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + originalFactory( + context?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options?: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + // (undocumented) + inputs?: TInputs; + // (undocumented) + name?: string; + // (undocumented) + namespace?: string; + // (undocumented) + options: TOptions; + // (undocumented) + output?: TOutput; +} + +// @public (undocumented) +export interface CreateExtensionKindOptions< + TOptions, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + attachTo: { + id: string; + input: string; + }; + // (undocumented) + configSchema?: PortableSchema<TConfig>; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + // (undocumented) + inputs?: TInputs; + // (undocumented) + kind: string; + // (undocumented) + name?: string; + // (undocumented) + namespace?: string; + // (undocumented) + output: TOutput; +} + // @public (undocumented) export interface CreateExtensionOptions< TOutput extends AnyExtensionDataMap, @@ -548,7 +630,7 @@ export interface CreateExtensionOptions< // (undocumented) disabled?: boolean; // (undocumented) - factory(context: { + factory(options: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; @@ -565,6 +647,51 @@ export interface CreateExtensionOptions< output: TOutput; } +// @public (undocumented) +export interface CreateExtensionOverrideKindInstanceOptions< + TOptions, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + attachTo?: { + id: string; + input: string; + }; + // (undocumented) + configSchema?: PortableSchema<TConfig>; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + originalFactory( + context?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options?: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + // (undocumented) + inputs?: TInputs; + // (undocumented) + name?: string; + // (undocumented) + namespace?: string; + // (undocumented) + options?: TOptions; + // (undocumented) + output?: TOutput; +} + // @public (undocumented) export function createExtensionOverrides( options: ExtensionOverridesOptions, @@ -937,75 +1064,26 @@ export class ExtensionKind< TOutput extends AnyExtensionDataMap, TConfig, >( - options: ExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, + options: CreateExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, ): ExtensionKind<TOptions, TInputs, TOutput, TConfig>; // (undocumented) - new(args: { - namespace?: string; - name?: string; - attachTo?: { - id: string; - input: string; - }; - disabled?: boolean; - inputs?: TInputs; - output?: TOutput; - configSchema?: PortableSchema<TConfig>; - options: TOptions; - factory?( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - orignalFactory( - context?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options?: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }): ExtensionDefinition<TConfig>; -} - -// @public (undocumented) -export interface ExtensionKindOptions< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - // (undocumented) - attachTo: { - id: string; - input: string; + new( + instanceProperties: CreateExtensionKindInstanceOptions< + TOptions, + TInputs, + TOutput, + TConfig + >, + ): ExtensionDefinition<TConfig> & { + override: ( + overrides: CreateExtensionOverrideKindInstanceOptions< + TOptions, + TInputs, + TOutput, + TConfig + >, + ) => ExtensionDefinition<TConfig>; }; - // (undocumented) - configSchema?: PortableSchema<TConfig>; - // (undocumented) - disabled?: boolean; - // (undocumented) - factory( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - // (undocumented) - inputs?: TInputs; - // (undocumented) - kind: string; - // (undocumented) - name?: string; - // (undocumented) - namespace?: string; - // (undocumented) - output: TOutput; } // @public (undocumented) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index cfef69b0fe..42ab21db80 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -291,37 +291,4 @@ describe('createExtension', () => { 'ExtensionDefinition{namespace=test,attachTo=root@default}', ); }); - - describe('override', () => { - it('should create an extension override', () => { - const extension = createExtension({ - namespace: 'test', - attachTo: { id: 'root', input: 'default' }, - output: { - foo: stringData, - }, - factory() { - return { - foo: 'bar', - }; - }, - }); - - const override = extension.override({ - attachTo: { - id: 'root', - input: 'default2', - }, - factory() { - return { - foo: 'baz', - }; - }, - }); - - expect(String(override)).toBe( - 'ExtensionDefinition{namespace=test,attachTo=root@default2}', - ); - }); - }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index b56cc39fc9..be76fbc069 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -91,7 +91,7 @@ export interface CreateExtensionOptions< inputs?: TInputs; output: TOutput; configSchema?: PortableSchema<TConfig>; - factory(context: { + factory(options: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; @@ -99,33 +99,7 @@ export interface CreateExtensionOptions< } /** @public */ -export interface ExtensionDefinitionOverrides< - TOriginalConfig, - TOriginalInputs extends AnyExtensionInputMap, - TOverrideInputs extends AnyExtensionInputMap, -> { - readonly disabled?: boolean; - readonly attachTo?: { id: string; input: string }; - // TODO: inputs (only adding to the list? or redefine arity and superset of old type on existing input?) - // TODO: config (any merging needed on there?) - inputs?: TOverrideInputs; - factory?(options: { - node: AppNode; - config: TOriginalConfig; - inputs: Expand<ResolvedExtensionInputs<TOriginalInputs & TOverrideInputs>>; - originalFactory: (originalFactoryOptions?: { - inputsOverride?: ResolvedExtensionInputs< - TOriginalInputs & TOverrideInputs - >; - }) => ExtensionDataValues<any>; - }): ExtensionDataValues<any>; -} - -/** @public */ -export interface ExtensionDefinition< - TConfig, - TInputs extends AnyExtensionInputMap, -> { +export interface ExtensionDefinition<TConfig> { $$type: '@backstage/ExtensionDefinition'; readonly kind?: string; readonly namespace?: string; @@ -133,20 +107,15 @@ export interface ExtensionDefinition< readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema<TConfig>; - override<TOverrideInputs extends AnyExtensionInputMap>( - overrides: ExtensionDefinitionOverrides<TConfig, TInputs, TOverrideInputs>, - ): ExtensionDefinition<TConfig, TInputs>; } /** @internal */ -export interface InternalExtensionDefinition< - TConfig, - TInputs extends AnyExtensionInputMap, -> extends ExtensionDefinition<TConfig, TInputs> { +export interface InternalExtensionDefinition<TConfig> + extends ExtensionDefinition<TConfig> { readonly version: 'v1'; - readonly inputs: TInputs; + readonly inputs: AnyExtensionInputMap; readonly output: AnyExtensionDataMap; - factory(context: { + factory(options: { node: AppNode; config: TConfig; inputs: ResolvedExtensionInputs<any>; @@ -154,13 +123,10 @@ export interface InternalExtensionDefinition< } /** @internal */ -export function toInternalExtensionDefinition< - TConfig, - TInputs extends AnyExtensionInputMap, ->( - overrides: ExtensionDefinition<TConfig, TInputs>, -): InternalExtensionDefinition<TConfig, TInputs> { - const internal = overrides as InternalExtensionDefinition<TConfig, TInputs>; +export function toInternalExtensionDefinition<TConfig>( + overrides: ExtensionDefinition<TConfig>, +): InternalExtensionDefinition<TConfig> { + const internal = overrides as InternalExtensionDefinition<TConfig>; if (internal.$$type !== '@backstage/ExtensionDefinition') { throw new Error( `Invalid extension definition instance, bad type '${internal.$$type}'`, @@ -181,69 +147,38 @@ export function createExtension< TConfig = never, >( options: CreateExtensionOptions<TOutput, TInputs, TConfig>, -): ExtensionDefinition<TConfig, TInputs> { - const { - kind, - namespace, - name, - attachTo, - disabled = false, - inputs = {} as TInputs, - output, - configSchema, - factory, - } = options; - +): ExtensionDefinition<TConfig> { return { $$type: '@backstage/ExtensionDefinition', version: 'v1', - kind, - namespace, - name, - attachTo, - disabled, - inputs, - output, - configSchema, - factory({ inputs: factoryInputs, ...rest }) { + kind: options.kind, + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo, + disabled: options.disabled ?? false, + inputs: options.inputs ?? {}, + output: options.output, + configSchema: options.configSchema, + factory({ inputs, ...rest }) { // TODO: Simplify this, but TS wouldn't infer the input type for some reason - return factory({ - inputs: factoryInputs as Expand<ResolvedExtensionInputs<TInputs>>, + return options.factory({ + inputs: inputs as Expand<ResolvedExtensionInputs<TInputs>>, ...rest, }); }, toString() { const parts: string[] = []; - if (kind) { - parts.push(`kind=${kind}`); + if (options.kind) { + parts.push(`kind=${options.kind}`); } - if (namespace) { - parts.push(`namespace=${namespace}`); + if (options.namespace) { + parts.push(`namespace=${options.namespace}`); } - if (name) { - parts.push(`name=${name}`); + if (options.name) { + parts.push(`name=${options.name}`); } - parts.push(`attachTo=${attachTo.id}@${attachTo.input}`); + parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - override<TOverrideInputs extends AnyExtensionInputMap>( - overrides: ExtensionDefinitionOverrides< - TConfig, - TInputs, - TOverrideInputs - >, - ): ExtensionDefinition<TConfig, TInputs> { - return createExtension({ - kind, - namespace, - name, - attachTo: overrides.attachTo ?? attachTo, - disabled: overrides.disabled ?? disabled, - inputs: { ...inputs, ...overrides.inputs }, - output, - configSchema, - factory, - }); - }, - } as InternalExtensionDefinition<TConfig, TInputs>; + } as InternalExtensionDefinition<TConfig>; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx index 3bdcd3a7cc..30c3e6ca44 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx @@ -64,6 +64,7 @@ describe('createExtensionKind', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), version: 'v1', }); @@ -102,4 +103,167 @@ describe('createExtensionKind', () => { const { container } = createExtensionTester(extension).render(); expect(container.querySelector('h2')).toHaveTextContent('Hello, world!'); }); + + it('should allow calling of the default value from override', () => { + const TestExtension = createExtensionKind({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(_, options: { text: string }) { + return { + element: <h1>{options.text}</h1>, + }; + }, + }); + + const extension = TestExtension.new({ + name: 'my-extension', + options: { + text: 'Hello, world!', + }, + factory({ originalFactory }, options: { text: string }) { + const { element } = originalFactory(); + return { + element: ( + <h2> + {options.text} + {element} + </h2> + ), + }; + }, + }); + + expect(extension).toBeDefined(); + + const { container } = createExtensionTester(extension).render(); + + expect(container.querySelector('h2 h1')).toHaveTextContent('Hello, world!'); + }); + + it('should allow overriding options of the default value from override', () => { + const TestExtension = createExtensionKind({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(_, options: { text: string }) { + return { + element: <h1>{options.text}</h1>, + }; + }, + }); + + const extension = TestExtension.new({ + name: 'my-extension', + options: { + text: 'Hello, world!', + }, + factory({ originalFactory }, options: { text: string }) { + const { element } = originalFactory(undefined, { text: 'nothing!' }); + return { + element: ( + <h2> + {options.text} + {element} + </h2> + ), + }; + }, + }); + + expect(extension).toBeDefined(); + + const { container } = createExtensionTester(extension).render(); + + expect(container.querySelector('h2 h1')).toHaveTextContent('nothing!'); + }); + + describe('override', () => { + it('should allow overriding of the default factory', () => { + const TestExtension = createExtensionKind({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(_, options: { text: string }) { + return { + element: <h1>{options.text}</h1>, + }; + }, + }); + + const extension = TestExtension.new({ + name: 'my-extension', + options: { + text: 'Hello, world!', + }, + factory(_, options: { text: string }) { + return { + element: <h2>{options.text}</h2>, + }; + }, + }); + + const overridden = extension.override({ + factory({ originalFactory }, { text }) { + return { + element: ( + <div> + {originalFactory().element} + <h3>{text}</h3> + </div> + ), + }; + }, + }); + + const { container } = createExtensionTester(overridden).render(); + expect(container.querySelector('h2')).toHaveTextContent('Hello, world!'); + expect(container.querySelector('h3')).toHaveTextContent('Hello, world!'); + }); + }); + + it('should allow calling the kind factory if another factory is not defined in the instance', () => { + const TestExtension = createExtensionKind({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(_, options: { text: string }) { + return { + element: <h1>{options.text}</h1>, + }; + }, + }); + + const extension = TestExtension.new({ + name: 'my-extension', + options: { + text: 'Hello, world!', + }, + }); + + const overridden = extension.override({ + factory({ originalFactory }, { text }) { + return { + element: ( + <div> + {originalFactory().element} + <h3>{text}</h3> + </div> + ), + }; + }, + }); + + const { container } = createExtensionTester(overridden).render(); + expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); + expect(container.querySelector('h3')).toHaveTextContent('Hello, world!'); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts index 353fb8ab70..9813c9e50c 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { AppNode } from '../apis'; import { PortableSchema } from '../schema'; import { Expand } from '../types'; @@ -52,6 +53,76 @@ export interface CreateExtensionKindOptions< ): Expand<ExtensionDataValues<TOutput>>; } +/** + * @public + */ +export interface CreateExtensionKindInstanceOptions< + TOptions, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema<TConfig>; + options: TOptions; + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + originalFactory( + context?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options?: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; +} + +/** + * @public + */ +export interface CreateExtensionOverrideKindInstanceOptions< + TOptions, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema<TConfig>; + options?: TOptions; + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + originalFactory( + context?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options?: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; +} + /** * TODO: should we export an interface instead of a concrete class? * @public @@ -74,7 +145,7 @@ export class ExtensionKind< } private constructor( - private readonly options: CreateExtensionKindOptions< + private readonly kindProperties: CreateExtensionKindOptions< TOptions, TInputs, TOutput, @@ -82,49 +153,41 @@ export class ExtensionKind< >, ) {} - public new(args: { - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - output?: TOutput; - configSchema?: PortableSchema<TConfig>; - options: TOptions; - factory?( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - orignalFactory( - context?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options?: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }): ExtensionDefinition<TConfig, TInputs> { - return createExtension({ - kind: this.options.kind, - namespace: args.namespace ?? this.options.namespace, - name: args.name ?? this.options.name, - attachTo: args.attachTo ?? this.options.attachTo, - disabled: args.disabled ?? this.options.disabled, - inputs: args.inputs ?? this.options.inputs, - output: args.output ?? this.options.output, - configSchema: args.configSchema ?? this.options.configSchema, // TODO: some config merging or smth + public new( + instanceProperties: CreateExtensionKindInstanceOptions< + TOptions, + TInputs, + TOutput, + TConfig + >, + ): ExtensionDefinition<TConfig> & { + override: ( + overrides: CreateExtensionOverrideKindInstanceOptions< + TOptions, + TInputs, + TOutput, + TConfig + >, + ) => ExtensionDefinition<TConfig>; + } { + const extension = createExtension({ + kind: this.kindProperties.kind, + namespace: instanceProperties.namespace ?? this.kindProperties.namespace, + name: instanceProperties.name ?? this.kindProperties.name, + attachTo: instanceProperties.attachTo ?? this.kindProperties.attachTo, + disabled: instanceProperties.disabled ?? this.kindProperties.disabled, + inputs: instanceProperties.inputs ?? this.kindProperties.inputs, + output: instanceProperties.output ?? this.kindProperties.output, + configSchema: + instanceProperties.configSchema ?? this.kindProperties.configSchema, // TODO: some config merging or smth factory: ({ node, config, inputs }) => { - if (args.factory) { - return args.factory( + if (instanceProperties.factory) { + return instanceProperties.factory( { node, config, inputs, - orignalFactory: ( + originalFactory: ( innerContext?: { node?: AppNode; config?: TConfig; @@ -132,29 +195,123 @@ export class ExtensionKind< }, innerOptions?: TOptions, ) => - this.options.factory( + this.kindProperties.factory( { node: innerContext?.node ?? node, config: innerContext?.config ?? config, inputs: innerContext?.inputs ?? inputs, }, - innerOptions ?? args.options, + innerOptions ?? instanceProperties.options, ), }, - args.options, + instanceProperties.options, ); } - return this.options.factory( + return this.kindProperties.factory( { node, config, inputs, }, - args.options, + instanceProperties.options, ); }, }); + + return { + ...extension, + override: overrides => { + return createExtension({ + kind: this.kindProperties.kind, + namespace: + overrides.namespace ?? + instanceProperties.namespace ?? + this.kindProperties.namespace, + name: + overrides.name ?? + instanceProperties.name ?? + this.kindProperties.name, + attachTo: + overrides.attachTo ?? + instanceProperties.attachTo ?? + this.kindProperties.attachTo, + disabled: + overrides.disabled ?? + instanceProperties.disabled ?? + this.kindProperties.disabled, + inputs: + overrides.inputs ?? + instanceProperties.inputs ?? + this.kindProperties.inputs, + output: + overrides.output ?? + instanceProperties.output ?? + this.kindProperties.output, + configSchema: + overrides.configSchema ?? + instanceProperties.configSchema ?? + this.kindProperties.configSchema, // TODO: some config merging or smth + factory: ({ node, config, inputs }) => { + if (overrides.factory) { + return overrides.factory( + { + node, + config, + inputs, + originalFactory: ( + innerContext?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + innerOptions?: TOptions, + ) => + instanceProperties.factory?.( + { + node: innerContext?.node ?? node, + config: innerContext?.config ?? config, + inputs: innerContext?.inputs ?? inputs, + originalFactory: this.kindProperties.factory, + }, + innerOptions ?? instanceProperties.options, + ) ?? + this.kindProperties.factory( + { + node, + config, + inputs, + }, + innerOptions ?? instanceProperties.options, + ), + }, + instanceProperties.options, + ); + } + + return ( + instanceProperties.factory?.( + { + node, + config, + inputs, + originalFactory: this.kindProperties.factory, + }, + instanceProperties.options, + ) ?? + this.kindProperties.factory( + { + node, + config, + inputs, + }, + instanceProperties.options, + ) + ); + }, + }); + }, + }; } } diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 8fe9dc3a54..a312c0f06f 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -49,7 +49,9 @@ export { type FrontendFeature, } from './types'; export { - type CreateExtensionKindOptions as ExtensionKindOptions, + type CreateExtensionKindOptions, + type CreateExtensionKindInstanceOptions, + type CreateExtensionOverrideKindInstanceOptions, ExtensionKind, createExtensionKind, } from './createExtensionKind'; From 6b6cbdaac5cf05702210e9c24ccb4272c3ba3e88 Mon Sep 17 00:00:00 2001 From: Ben Lambert <ben@blam.sh> Date: Thu, 18 Jul 2024 13:07:02 +0200 Subject: [PATCH 159/212] Revert "NFE: Make it possible to override existing extension instances" Signed-off-by: blam <ben@blam.sh> --- packages/frontend-plugin-api/api-report.md | 214 +++++---------- .../src/wiring/createExtension.ts | 4 +- .../src/wiring/createExtensionKind.test.tsx | 164 ------------ .../src/wiring/createExtensionKind.ts | 245 ++++-------------- .../frontend-plugin-api/src/wiring/index.ts | 4 +- 5 files changed, 115 insertions(+), 516 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 6a04d04cd6..48718f5375 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -529,91 +529,9 @@ export function createExtensionKind< TOutput extends AnyExtensionDataMap, TConfig, >( - options: CreateExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, + options: ExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, ): ExtensionKind<TOptions, TInputs, TOutput, TConfig>; -// @public (undocumented) -export interface CreateExtensionKindInstanceOptions< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - // (undocumented) - attachTo?: { - id: string; - input: string; - }; - // (undocumented) - configSchema?: PortableSchema<TConfig>; - // (undocumented) - disabled?: boolean; - // (undocumented) - factory?( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - originalFactory( - context?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options?: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - // (undocumented) - inputs?: TInputs; - // (undocumented) - name?: string; - // (undocumented) - namespace?: string; - // (undocumented) - options: TOptions; - // (undocumented) - output?: TOutput; -} - -// @public (undocumented) -export interface CreateExtensionKindOptions< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - // (undocumented) - attachTo: { - id: string; - input: string; - }; - // (undocumented) - configSchema?: PortableSchema<TConfig>; - // (undocumented) - disabled?: boolean; - // (undocumented) - factory( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - // (undocumented) - inputs?: TInputs; - // (undocumented) - kind: string; - // (undocumented) - name?: string; - // (undocumented) - namespace?: string; - // (undocumented) - output: TOutput; -} - // @public (undocumented) export interface CreateExtensionOptions< TOutput extends AnyExtensionDataMap, @@ -630,7 +548,7 @@ export interface CreateExtensionOptions< // (undocumented) disabled?: boolean; // (undocumented) - factory(options: { + factory(context: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; @@ -647,51 +565,6 @@ export interface CreateExtensionOptions< output: TOutput; } -// @public (undocumented) -export interface CreateExtensionOverrideKindInstanceOptions< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - // (undocumented) - attachTo?: { - id: string; - input: string; - }; - // (undocumented) - configSchema?: PortableSchema<TConfig>; - // (undocumented) - disabled?: boolean; - // (undocumented) - factory?( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - originalFactory( - context?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options?: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - // (undocumented) - inputs?: TInputs; - // (undocumented) - name?: string; - // (undocumented) - namespace?: string; - // (undocumented) - options?: TOptions; - // (undocumented) - output?: TOutput; -} - // @public (undocumented) export function createExtensionOverrides( options: ExtensionOverridesOptions, @@ -1064,26 +937,75 @@ export class ExtensionKind< TOutput extends AnyExtensionDataMap, TConfig, >( - options: CreateExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, + options: ExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, ): ExtensionKind<TOptions, TInputs, TOutput, TConfig>; // (undocumented) - new( - instanceProperties: CreateExtensionKindInstanceOptions< - TOptions, - TInputs, - TOutput, - TConfig - >, - ): ExtensionDefinition<TConfig> & { - override: ( - overrides: CreateExtensionOverrideKindInstanceOptions< - TOptions, - TInputs, - TOutput, - TConfig - >, - ) => ExtensionDefinition<TConfig>; + new(args: { + namespace?: string; + name?: string; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema<TConfig>; + options: TOptions; + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + orignalFactory( + context?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options?: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }): ExtensionDefinition<TConfig>; +} + +// @public (undocumented) +export interface ExtensionKindOptions< + TOptions, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + attachTo: { + id: string; + input: string; }; + // (undocumented) + configSchema?: PortableSchema<TConfig>; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + // (undocumented) + inputs?: TInputs; + // (undocumented) + kind: string; + // (undocumented) + name?: string; + // (undocumented) + namespace?: string; + // (undocumented) + output: TOutput; } // @public (undocumented) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index be76fbc069..28dc6a4e29 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -91,7 +91,7 @@ export interface CreateExtensionOptions< inputs?: TInputs; output: TOutput; configSchema?: PortableSchema<TConfig>; - factory(options: { + factory(context: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; @@ -115,7 +115,7 @@ export interface InternalExtensionDefinition<TConfig> readonly version: 'v1'; readonly inputs: AnyExtensionInputMap; readonly output: AnyExtensionDataMap; - factory(options: { + factory(context: { node: AppNode; config: TConfig; inputs: ResolvedExtensionInputs<any>; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx index 30c3e6ca44..3bdcd3a7cc 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx @@ -64,7 +64,6 @@ describe('createExtensionKind', () => { }, factory: expect.any(Function), toString: expect.any(Function), - override: expect.any(Function), version: 'v1', }); @@ -103,167 +102,4 @@ describe('createExtensionKind', () => { const { container } = createExtensionTester(extension).render(); expect(container.querySelector('h2')).toHaveTextContent('Hello, world!'); }); - - it('should allow calling of the default value from override', () => { - const TestExtension = createExtensionKind({ - kind: 'test-extension', - attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, - factory(_, options: { text: string }) { - return { - element: <h1>{options.text}</h1>, - }; - }, - }); - - const extension = TestExtension.new({ - name: 'my-extension', - options: { - text: 'Hello, world!', - }, - factory({ originalFactory }, options: { text: string }) { - const { element } = originalFactory(); - return { - element: ( - <h2> - {options.text} - {element} - </h2> - ), - }; - }, - }); - - expect(extension).toBeDefined(); - - const { container } = createExtensionTester(extension).render(); - - expect(container.querySelector('h2 h1')).toHaveTextContent('Hello, world!'); - }); - - it('should allow overriding options of the default value from override', () => { - const TestExtension = createExtensionKind({ - kind: 'test-extension', - attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, - factory(_, options: { text: string }) { - return { - element: <h1>{options.text}</h1>, - }; - }, - }); - - const extension = TestExtension.new({ - name: 'my-extension', - options: { - text: 'Hello, world!', - }, - factory({ originalFactory }, options: { text: string }) { - const { element } = originalFactory(undefined, { text: 'nothing!' }); - return { - element: ( - <h2> - {options.text} - {element} - </h2> - ), - }; - }, - }); - - expect(extension).toBeDefined(); - - const { container } = createExtensionTester(extension).render(); - - expect(container.querySelector('h2 h1')).toHaveTextContent('nothing!'); - }); - - describe('override', () => { - it('should allow overriding of the default factory', () => { - const TestExtension = createExtensionKind({ - kind: 'test-extension', - attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, - factory(_, options: { text: string }) { - return { - element: <h1>{options.text}</h1>, - }; - }, - }); - - const extension = TestExtension.new({ - name: 'my-extension', - options: { - text: 'Hello, world!', - }, - factory(_, options: { text: string }) { - return { - element: <h2>{options.text}</h2>, - }; - }, - }); - - const overridden = extension.override({ - factory({ originalFactory }, { text }) { - return { - element: ( - <div> - {originalFactory().element} - <h3>{text}</h3> - </div> - ), - }; - }, - }); - - const { container } = createExtensionTester(overridden).render(); - expect(container.querySelector('h2')).toHaveTextContent('Hello, world!'); - expect(container.querySelector('h3')).toHaveTextContent('Hello, world!'); - }); - }); - - it('should allow calling the kind factory if another factory is not defined in the instance', () => { - const TestExtension = createExtensionKind({ - kind: 'test-extension', - attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, - factory(_, options: { text: string }) { - return { - element: <h1>{options.text}</h1>, - }; - }, - }); - - const extension = TestExtension.new({ - name: 'my-extension', - options: { - text: 'Hello, world!', - }, - }); - - const overridden = extension.override({ - factory({ originalFactory }, { text }) { - return { - element: ( - <div> - {originalFactory().element} - <h3>{text}</h3> - </div> - ), - }; - }, - }); - - const { container } = createExtensionTester(overridden).render(); - expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); - expect(container.querySelector('h3')).toHaveTextContent('Hello, world!'); - }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts index 9813c9e50c..52bf711822 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -21,7 +21,6 @@ import { AnyExtensionDataMap, AnyExtensionInputMap, ExtensionDataValues, - ExtensionDefinition, ResolvedExtensionInputs, createExtension, } from './createExtension'; @@ -53,76 +52,6 @@ export interface CreateExtensionKindOptions< ): Expand<ExtensionDataValues<TOutput>>; } -/** - * @public - */ -export interface CreateExtensionKindInstanceOptions< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - output?: TOutput; - configSchema?: PortableSchema<TConfig>; - options: TOptions; - factory?( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - originalFactory( - context?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options?: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; -} - -/** - * @public - */ -export interface CreateExtensionOverrideKindInstanceOptions< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - output?: TOutput; - configSchema?: PortableSchema<TConfig>; - options?: TOptions; - factory?( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - originalFactory( - context?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options?: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; -} - /** * TODO: should we export an interface instead of a concrete class? * @public @@ -145,7 +74,7 @@ export class ExtensionKind< } private constructor( - private readonly kindProperties: CreateExtensionKindOptions< + private readonly options: CreateExtensionKindOptions< TOptions, TInputs, TOutput, @@ -153,41 +82,49 @@ export class ExtensionKind< >, ) {} - public new( - instanceProperties: CreateExtensionKindInstanceOptions< - TOptions, - TInputs, - TOutput, - TConfig - >, - ): ExtensionDefinition<TConfig> & { - override: ( - overrides: CreateExtensionOverrideKindInstanceOptions< - TOptions, - TInputs, - TOutput, - TConfig - >, - ) => ExtensionDefinition<TConfig>; - } { - const extension = createExtension({ - kind: this.kindProperties.kind, - namespace: instanceProperties.namespace ?? this.kindProperties.namespace, - name: instanceProperties.name ?? this.kindProperties.name, - attachTo: instanceProperties.attachTo ?? this.kindProperties.attachTo, - disabled: instanceProperties.disabled ?? this.kindProperties.disabled, - inputs: instanceProperties.inputs ?? this.kindProperties.inputs, - output: instanceProperties.output ?? this.kindProperties.output, - configSchema: - instanceProperties.configSchema ?? this.kindProperties.configSchema, // TODO: some config merging or smth + public new(args: { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema<TConfig>; + options: TOptions; + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + orignalFactory( + context?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + options?: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }, + options: TOptions, + ): Expand<ExtensionDataValues<TOutput>>; + }) { + return createExtension({ + kind: this.options.kind, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, + attachTo: args.attachTo ?? this.options.attachTo, + disabled: args.disabled ?? this.options.disabled, + inputs: args.inputs ?? this.options.inputs, + output: args.output ?? this.options.output, + configSchema: args.configSchema ?? this.options.configSchema, // TODO: some config merging or smth factory: ({ node, config, inputs }) => { - if (instanceProperties.factory) { - return instanceProperties.factory( + if (args.factory) { + return args.factory( { node, config, inputs, - originalFactory: ( + orignalFactory: ( innerContext?: { node?: AppNode; config?: TConfig; @@ -195,123 +132,29 @@ export class ExtensionKind< }, innerOptions?: TOptions, ) => - this.kindProperties.factory( + this.options.factory( { node: innerContext?.node ?? node, config: innerContext?.config ?? config, inputs: innerContext?.inputs ?? inputs, }, - innerOptions ?? instanceProperties.options, + innerOptions ?? args.options, ), }, - instanceProperties.options, + args.options, ); } - return this.kindProperties.factory( + return this.options.factory( { node, config, inputs, }, - instanceProperties.options, + args.options, ); }, }); - - return { - ...extension, - override: overrides => { - return createExtension({ - kind: this.kindProperties.kind, - namespace: - overrides.namespace ?? - instanceProperties.namespace ?? - this.kindProperties.namespace, - name: - overrides.name ?? - instanceProperties.name ?? - this.kindProperties.name, - attachTo: - overrides.attachTo ?? - instanceProperties.attachTo ?? - this.kindProperties.attachTo, - disabled: - overrides.disabled ?? - instanceProperties.disabled ?? - this.kindProperties.disabled, - inputs: - overrides.inputs ?? - instanceProperties.inputs ?? - this.kindProperties.inputs, - output: - overrides.output ?? - instanceProperties.output ?? - this.kindProperties.output, - configSchema: - overrides.configSchema ?? - instanceProperties.configSchema ?? - this.kindProperties.configSchema, // TODO: some config merging or smth - factory: ({ node, config, inputs }) => { - if (overrides.factory) { - return overrides.factory( - { - node, - config, - inputs, - originalFactory: ( - innerContext?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - innerOptions?: TOptions, - ) => - instanceProperties.factory?.( - { - node: innerContext?.node ?? node, - config: innerContext?.config ?? config, - inputs: innerContext?.inputs ?? inputs, - originalFactory: this.kindProperties.factory, - }, - innerOptions ?? instanceProperties.options, - ) ?? - this.kindProperties.factory( - { - node, - config, - inputs, - }, - innerOptions ?? instanceProperties.options, - ), - }, - instanceProperties.options, - ); - } - - return ( - instanceProperties.factory?.( - { - node, - config, - inputs, - originalFactory: this.kindProperties.factory, - }, - instanceProperties.options, - ) ?? - this.kindProperties.factory( - { - node, - config, - inputs, - }, - instanceProperties.options, - ) - ); - }, - }); - }, - }; } } diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index a312c0f06f..8fe9dc3a54 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -49,9 +49,7 @@ export { type FrontendFeature, } from './types'; export { - type CreateExtensionKindOptions, - type CreateExtensionKindInstanceOptions, - type CreateExtensionOverrideKindInstanceOptions, + type CreateExtensionKindOptions as ExtensionKindOptions, ExtensionKind, createExtensionKind, } from './createExtensionKind'; From b20c067c61abcfff3f158b7f4b6c5857dfbf347a Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Thu, 18 Jul 2024 13:27:09 +0200 Subject: [PATCH 160/212] chore: fixing some code review comments Signed-off-by: blam <ben@blam.sh> --- .changeset/dry-squids-tap.md | 8 +- .../src/wiring/createExtensionKind.ts | 108 +++++++++++------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md index 9bd835d371..5a865395c0 100644 --- a/.changeset/dry-squids-tap.md +++ b/.changeset/dry-squids-tap.md @@ -14,17 +14,17 @@ const TestExtensionKind = createExtensionKind({ output: { element: coreExtensionData.reactElement, }, - factory(_, options: { text: string }) { + factory(_, params: { text: string }) { return { - element: <h1>{options.text}</h1>, + element: <h1>{params.text}</h1>, }; }, }); // create an instance of the extension kind with props -const testExtension = TestExtensionKind.new({ +const testExtension = TestExtensionKind.create({ name: 'foo', - options: { + params: { text: 'Hello World', }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts index 52bf711822..1212365712 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -21,6 +21,7 @@ import { AnyExtensionDataMap, AnyExtensionInputMap, ExtensionDataValues, + ExtensionDefinition, ResolvedExtensionInputs, createExtension, } from './createExtension'; @@ -29,14 +30,13 @@ import { * @public */ export interface CreateExtensionKindOptions< - TOptions, + TParams, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, > { kind: string; namespace?: string; - name?: string; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; @@ -48,41 +48,20 @@ export interface CreateExtensionKindOptions< config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; }, - options: TOptions, + params: TParams, ): Expand<ExtensionDataValues<TOutput>>; } /** - * TODO: should we export an interface instead of a concrete class? * @public */ -export class ExtensionKind< - TOptions, +export interface ExtensionKind< + TParams, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, > { - static create< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, - >( - options: CreateExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, - ): ExtensionKind<TOptions, TInputs, TOutput, TConfig> { - return new ExtensionKind(options); - } - - private constructor( - private readonly options: CreateExtensionKindOptions< - TOptions, - TInputs, - TOutput, - TConfig - >, - ) {} - - public new(args: { + create(args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; @@ -90,7 +69,7 @@ export class ExtensionKind< inputs?: TInputs; output?: TOutput; configSchema?: PortableSchema<TConfig>; - options: TOptions; + params: TParams; factory?( context: { node: AppNode; @@ -98,20 +77,64 @@ export class ExtensionKind< inputs: Expand<ResolvedExtensionInputs<TInputs>>; orignalFactory( context?: { - node?: AppNode; config?: TConfig; inputs?: Expand<ResolvedExtensionInputs<TInputs>>; }, - options?: TOptions, + params?: TParams, ): Expand<ExtensionDataValues<TOutput>>; }, - options: TOptions, + params: TParams, ): Expand<ExtensionDataValues<TOutput>>; - }) { + }): ExtensionDefinition<TConfig>; +} + +/** + * @internal + */ +class ExtensionKindImpl< + TParams, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + constructor( + private readonly options: CreateExtensionKindOptions< + TParams, + TInputs, + TOutput, + TConfig + >, + ) {} + + public create(args: { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema<TConfig>; + params: TParams; + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + orignalFactory( + context?: { + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + params?: TParams, + ): Expand<ExtensionDataValues<TOutput>>; + }, + params: TParams, + ): Expand<ExtensionDataValues<TOutput>>; + }): ExtensionDefinition<TConfig> { return createExtension({ kind: this.options.kind, namespace: args.namespace ?? this.options.namespace, - name: args.name ?? this.options.name, + name: args.name, attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, inputs: args.inputs ?? this.options.inputs, @@ -126,22 +149,21 @@ export class ExtensionKind< inputs, orignalFactory: ( innerContext?: { - node?: AppNode; config?: TConfig; inputs?: Expand<ResolvedExtensionInputs<TInputs>>; }, - innerOptions?: TOptions, + innerParams?: TParams, ) => this.options.factory( { - node: innerContext?.node ?? node, + node, config: innerContext?.config ?? config, inputs: innerContext?.inputs ?? inputs, }, - innerOptions ?? args.options, + innerParams ?? args.params, ), }, - args.options, + args.params, ); } @@ -151,7 +173,7 @@ export class ExtensionKind< config, inputs, }, - args.options, + args.params, ); }, }); @@ -165,10 +187,12 @@ export class ExtensionKind< * @public */ export function createExtensionKind< - TOptions, + TParams, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, ->(options: CreateExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>) { - return ExtensionKind.create(options); +>( + options: CreateExtensionKindOptions<TParams, TInputs, TOutput, TConfig>, +): ExtensionKind<TParams, TInputs, TOutput, TConfig> { + return new ExtensionKindImpl(options); } From 4e42e5112cd692ab40696b3970e016cba94b4456 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Thu, 18 Jul 2024 13:50:26 +0200 Subject: [PATCH 161/212] chore: smaller renames and code review Signed-off-by: blam <ben@blam.sh> --- .changeset/dry-squids-tap.md | 6 ++-- .../architecture/08-naming-patterns.md | 20 ++++++------- ....tsx => createExtensionBlueprint.test.tsx} | 30 +++++++++---------- ...ionKind.ts => createExtensionBlueprint.ts} | 22 +++++++------- .../frontend-plugin-api/src/wiring/index.ts | 8 ++--- 5 files changed, 43 insertions(+), 43 deletions(-) rename packages/frontend-plugin-api/src/wiring/{createExtensionKind.test.tsx => createExtensionBlueprint.test.tsx} (77%) rename packages/frontend-plugin-api/src/wiring/{createExtensionKind.ts => createExtensionBlueprint.ts} (90%) diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md index 5a865395c0..879ecb5d14 100644 --- a/.changeset/dry-squids-tap.md +++ b/.changeset/dry-squids-tap.md @@ -8,8 +8,8 @@ This allows the creation of extension with the following pattern: ```tsx // create the extension kind -const TestExtensionKind = createExtensionKind({ - kind: 'test-extension', +const EntityCardBlueprint = createExtensionBlueprint({ + kind: 'entity-card', attachTo: { id: 'test', input: 'default' }, output: { element: coreExtensionData.reactElement, @@ -22,7 +22,7 @@ const TestExtensionKind = createExtensionKind({ }); // create an instance of the extension kind with props -const testExtension = TestExtensionKind.create({ +const testExtension = EntityCardBlueprint.make({ name: 'foo', params: { text: 'Hello World', diff --git a/docs/frontend-system/architecture/08-naming-patterns.md b/docs/frontend-system/architecture/08-naming-patterns.md index 0916d9fd51..a17525f3ce 100644 --- a/docs/frontend-system/architecture/08-naming-patterns.md +++ b/docs/frontend-system/architecture/08-naming-patterns.md @@ -38,33 +38,31 @@ Note that while we use this naming pattern for the plugin instance this is only | Description | Pattern | Examples | | ----------- | ------------------------------- | ------------------------------------------------------------------- | -| Creator | `create<Kind>Extension` | `createPageExtension`, `createEntityCardExtension` | +| Blueprint | `<Kind>Blueprint` | `PageBlueprint`, `EntityCardBlueprint` | | ID | `[<kind>:]<namespace>[/<name>]` | `'core.nav'`, `'page:user-settings'`, `'entity-card:catalog/about'` | | Symbol | `<namespace>[<Name>][<Kind>]` | `coreNav`, `userSettingsPage`, `catalogAboutEntityCard` | -When you create a new extension you never provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the extension creator function used to create the extension, the only exception is if you use `createExtension` directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted. +When you create a new extension you never provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the blueprint creator, the only exception is if you use `createExtension` directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted. Example: ```ts -// This is an extension creator that is used to create an extension of the 'page' kind. -export function createPageExtension(options) { - return createExtension({ - kind: 'page', // Kinds are kebab-case - // ...options - }); -} +// This is an extension blueprint that is used to create an extension of the 'page' kind. +export const PageBlueprint = createExtensionBlueprint({ + kind: 'page', + // ... +}); // The namespace is inferred from the plugin ID, in this case 'catalog' // The final ID for this extension will be 'page:catalog/entity' -const catalogEntityPage = createPageExtension({ +const catalogEntityPage = PageBlueprint.make({ name: 'entity', // ... }); // The name is omitted, because the catalog plugin only provides a single extension of this kind // The final ID for this extension will be 'search-result-list-item:catalog' -const catalogSearchResultListItem = createSearchResultListItemExtension({ +const catalogSearchResultListItem = SearchResultListItemBlueprint.make({ // ... }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx similarity index 77% rename from packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx rename to packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 3bdcd3a7cc..b3e926e57c 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -16,27 +16,27 @@ import React from 'react'; import { coreExtensionData } from './coreExtensionData'; -import { createExtensionKind } from './createExtensionKind'; +import { createExtensionBlueprint } from './createExtensionBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; -describe('createExtensionKind', () => { - it('should allow creation of extension kinds', () => { - const TestExtension = createExtensionKind({ +describe('createExtensionBlueprint', () => { + it('should allow creation of extension blueprints', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, output: { element: coreExtensionData.reactElement, }, - factory(_, options: { text: string }) { + factory(_, params: { text: string }) { return { - element: <h1>{options.text}</h1>, + element: <h1>{params.text}</h1>, }; }, }); - const extension = TestExtension.new({ + const extension = TestExtensionBlueprint.make({ name: 'my-extension', - options: { + params: { text: 'Hello, world!', }, }); @@ -72,27 +72,27 @@ describe('createExtensionKind', () => { }); it('should allow overriding of the default factory', () => { - const TestExtension = createExtensionKind({ + const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, output: { element: coreExtensionData.reactElement, }, - factory(_, options: { text: string }) { + factory(_, params: { text: string }) { return { - element: <h1>{options.text}</h1>, + element: <h1>{params.text}</h1>, }; }, }); - const extension = TestExtension.new({ + const extension = TestExtensionBlueprint.make({ name: 'my-extension', - options: { + params: { text: 'Hello, world!', }, - factory(_, options: { text: string }) { + factory(_, params: { text: string }) { return { - element: <h2>{options.text}</h2>, + element: <h2>{params.text}</h2>, }; }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts similarity index 90% rename from packages/frontend-plugin-api/src/wiring/createExtensionKind.ts rename to packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 1212365712..74d2eeadf1 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -29,7 +29,7 @@ import { /** * @public */ -export interface CreateExtensionKindOptions< +export interface CreateExtensionBlueprintOptions< TParams, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, @@ -55,13 +55,13 @@ export interface CreateExtensionKindOptions< /** * @public */ -export interface ExtensionKind< +export interface ExtensionBlueprint< TParams, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, > { - create(args: { + make(args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; @@ -77,6 +77,7 @@ export interface ExtensionKind< inputs: Expand<ResolvedExtensionInputs<TInputs>>; orignalFactory( context?: { + node?: AppNode; config?: TConfig; inputs?: Expand<ResolvedExtensionInputs<TInputs>>; }, @@ -91,14 +92,14 @@ export interface ExtensionKind< /** * @internal */ -class ExtensionKindImpl< +class ExtensionBlueprintImpl< TParams, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, > { constructor( - private readonly options: CreateExtensionKindOptions< + private readonly options: CreateExtensionBlueprintOptions< TParams, TInputs, TOutput, @@ -106,7 +107,7 @@ class ExtensionKindImpl< >, ) {} - public create(args: { + public make(args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; @@ -122,6 +123,7 @@ class ExtensionKindImpl< inputs: Expand<ResolvedExtensionInputs<TInputs>>; orignalFactory( context?: { + node?: AppNode; config?: TConfig; inputs?: Expand<ResolvedExtensionInputs<TInputs>>; }, @@ -186,13 +188,13 @@ class ExtensionKindImpl< * * @public */ -export function createExtensionKind< +export function createExtensionBlueprint< TParams, TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, >( - options: CreateExtensionKindOptions<TParams, TInputs, TOutput, TConfig>, -): ExtensionKind<TParams, TInputs, TOutput, TConfig> { - return new ExtensionKindImpl(options); + options: CreateExtensionBlueprintOptions<TParams, TInputs, TOutput, TConfig>, +): ExtensionBlueprint<TParams, TInputs, TOutput, TConfig> { + return new ExtensionBlueprintImpl(options); } diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 8fe9dc3a54..6d20368225 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -49,7 +49,7 @@ export { type FrontendFeature, } from './types'; export { - type CreateExtensionKindOptions as ExtensionKindOptions, - ExtensionKind, - createExtensionKind, -} from './createExtensionKind'; + type CreateExtensionBlueprintOptions, + type ExtensionBlueprint, + createExtensionBlueprint, +} from './createExtensionBlueprint'; From 9b89b82f66b500dabc5493a51d2fd1144e4828a4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Thu, 18 Jul 2024 12:08:15 +0200 Subject: [PATCH 162/212] frontend-plugin-api: infer ExtensionBoundary routable prop from outputs Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/early-trees-dance.md | 5 ++ .changeset/mighty-dolls-retire.md | 5 ++ .../architecture/03-extensions.md | 4 +- packages/frontend-plugin-api/api-report.md | 1 - .../src/components/ExtensionBoundary.test.tsx | 72 ++++++++++++++++--- .../src/components/ExtensionBoundary.tsx | 14 +++- .../src/extensions/createPageExtension.tsx | 2 +- plugins/catalog-react/src/alpha.tsx | 2 +- 8 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 .changeset/early-trees-dance.md create mode 100644 .changeset/mighty-dolls-retire.md diff --git a/.changeset/early-trees-dance.md b/.changeset/early-trees-dance.md new file mode 100644 index 0000000000..5422bd2872 --- /dev/null +++ b/.changeset/early-trees-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +The `ExtensionBoundary` now by default infers whether its routable from whether it outputs a route path. diff --git a/.changeset/mighty-dolls-retire.md b/.changeset/mighty-dolls-retire.md new file mode 100644 index 0000000000..b31c443a38 --- /dev/null +++ b/.changeset/mighty-dolls-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Internal refactor to remove unnecessary `routable` prop in the implementation of the `createEntityContentExtension` alpha export. diff --git a/docs/frontend-system/architecture/03-extensions.md b/docs/frontend-system/architecture/03-extensions.md index 19345e0b26..cdc73a17ec 100644 --- a/docs/frontend-system/architecture/03-extensions.md +++ b/docs/frontend-system/architecture/03-extensions.md @@ -337,7 +337,7 @@ Similar to plugins the `ErrorBoundary` for extension allows to pass in a fallbac ### Analytics -Analytics information are provided through the `AnalyticsContext`, which will give `extensionId` & `pluginId` as context to analytics event fired inside of the extension. Additionally `RouteTracker` will capture an analytics event for routable extension to inform which extension metadata gets associated with a navigation event when the route navigated to is a gathered `mountPoint`. +Analytics information are provided through the `AnalyticsContext`, which will give `extensionId` & `pluginId` as context to analytics event fired inside of the extension. Additionally `RouteTracker` will capture an analytics event for routable extension to inform which extension metadata gets associated with a navigation event when the route navigated to is a gathered `mountPoint`. Whether an extension is routable is inferred from its outputs, but you can also explicitly control this behavior by passing the `routable` prop to `ExtensionBoundary`. The `ExtensionBoundary` can be used like the following in an extension creator: @@ -359,7 +359,7 @@ export function createSomeExtension< path: config.path, routeRef: options.routeRef, element: ( - <ExtensionBoundary node={node} routable> + <ExtensionBoundary node={node}> <ExtensionComponent /> </ExtensionBoundary> ), diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 3e19573d95..d50ef0dd03 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -844,7 +844,6 @@ export interface ExtensionBoundaryProps { children: ReactNode; // (undocumented) node: AppNode; - // (undocumented) routable?: boolean; } diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 66fb3901fc..1f12f127e8 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -15,15 +15,24 @@ */ import React, { useEffect } from 'react'; -import { screen, waitFor } from '@testing-library/react'; -import { MockAnalyticsApi, TestApiProvider } from '@backstage/test-utils'; +import { act, screen, waitFor } from '@testing-library/react'; +import { + MockAnalyticsApi, + TestApiProvider, + withLogCollector, +} from '@backstage/test-utils'; import { ExtensionBoundary } from './ExtensionBoundary'; import { coreExtensionData, createExtension } from '../wiring'; -import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; +import { + analyticsApiRef, + createApiFactory, + useAnalytics, +} from '@backstage/core-plugin-api'; import { createRouteRef } from '../routing'; import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createApiExtension } from '../extensions'; -const wrapInBoundaryExtension = (element: JSX.Element) => { +const wrapInBoundaryExtension = (element?: JSX.Element) => { const routeRef = createRouteRef(); return createExtension({ name: 'test', @@ -54,12 +63,25 @@ describe('ExtensionBoundary', () => { }); it('should show app error component when an error is thrown', async () => { - const error = 'Something went wrong'; + const errorMsg = 'Something went wrong'; const ErrorComponent = () => { - throw new Error(error); + throw new Error(errorMsg); }; - createExtensionTester(wrapInBoundaryExtension(<ErrorComponent />)).render(); - await waitFor(() => expect(screen.getByText(error)).toBeInTheDocument()); + const { error } = await withLogCollector(['error'], async () => { + createExtensionTester( + wrapInBoundaryExtension(<ErrorComponent />), + ).render(); + await waitFor(() => + expect(screen.getByText(errorMsg)).toBeInTheDocument(), + ); + }); + expect(error).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining(errorMsg), + }), + ]), + ); }); it('should wrap children with analytics context', async () => { @@ -97,4 +119,38 @@ describe('ExtensionBoundary', () => { }); }); }); + + // TODO(Rugvip): It's annoying to test the inverse of this currently, because the extension tester overrides the subject to always output a path + it('should emit analytics events if routable', async () => { + const Emitter = () => { + const analytics = useAnalytics(); + useEffect(() => { + analytics.captureEvent('dummy', 'dummy'); + }); + return null; + }; + const analyticsApiMock = new MockAnalyticsApi(); + + await act(async () => { + createExtensionTester(wrapInBoundaryExtension(<Emitter />)) + .add( + createApiExtension({ + factory: createApiFactory(analyticsApiRef, analyticsApiMock), + }), + ) + .render(); + }); + + expect(analyticsApiMock.getEvents()).toEqual([ + expect.objectContaining({ + action: 'navigate', + subject: '/', + context: expect.objectContaining({ + pluginId: 'root', + extensionId: 'test', + }), + }), + expect.objectContaining({ action: 'dummy' }), + ]); + }); }); diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index 768352b76e..b6269484b0 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -26,6 +26,7 @@ import { ErrorBoundary } from './ErrorBoundary'; import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; import { AppNode, useComponentRef } from '../apis'; import { coreComponentRefs } from './coreComponentRefs'; +import { coreExtensionData } from '../wiring'; type RouteTrackerProps = PropsWithChildren<{ disableTracking?: boolean; @@ -50,6 +51,11 @@ const RouteTracker = (props: RouteTrackerProps) => { /** @public */ export interface ExtensionBoundaryProps { node: AppNode; + /** + * This explicitly marks the extension as routable for the purpose of + * capturing analytics events. If not provided, the extension boundary will be + * marked as routable if it outputs a routePath. + */ routable?: boolean; children: ReactNode; } @@ -58,6 +64,10 @@ export interface ExtensionBoundaryProps { export function ExtensionBoundary(props: ExtensionBoundaryProps) { const { node, routable, children } = props; + const doesOutputRoutePath = Boolean( + node.instance?.getData(coreExtensionData.routePath), + ); + const plugin = node.spec.source; const Progress = useComponentRef(coreComponentRefs.progress); const fallback = useComponentRef(coreComponentRefs.errorBoundaryFallback); @@ -72,7 +82,9 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) { <Suspense fallback={<Progress />}> <ErrorBoundary plugin={plugin} Fallback={fallback}> <AnalyticsContext attributes={attributes}> - <RouteTracker disableTracking={!routable}>{children}</RouteTracker> + <RouteTracker disableTracking={!(routable ?? doesOutputRoutePath)}> + {children} + </RouteTracker> </AnalyticsContext> </ErrorBoundary> </Suspense> diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index dfe549f32b..cd434a3ffc 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -87,7 +87,7 @@ export function createPageExtension< path: config.path, routeRef: options.routeRef, element: ( - <ExtensionBoundary node={node} routable> + <ExtensionBoundary node={node}> <ExtensionComponent /> </ExtensionBoundary> ), diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index b1650d1f5b..85712b358a 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -166,7 +166,7 @@ export function createEntityContentExtension< title: config.title, routeRef: options.routeRef, element: ( - <ExtensionBoundary node={node} routable> + <ExtensionBoundary node={node}> <ExtensionComponent /> </ExtensionBoundary> ), From 5eaaf807fade6caae1a874744140ea469a46dbcc Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Thu, 18 Jul 2024 14:15:03 +0200 Subject: [PATCH 163/212] chore: fixing api-reports and changeset Signed-off-by: blam <ben@blam.sh> --- .changeset/dry-squids-tap.md | 8 +- packages/frontend-plugin-api/api-report.md | 179 ++++++++++----------- 2 files changed, 88 insertions(+), 99 deletions(-) diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md index 879ecb5d14..d0a737f4af 100644 --- a/.changeset/dry-squids-tap.md +++ b/.changeset/dry-squids-tap.md @@ -2,12 +2,12 @@ '@backstage/frontend-plugin-api': patch --- -Introduce a new way to create extension types and kinds, with `createExtensionKind`. +Introduce a new way to encapsulate extension kinds that replaces the extension creator pattern with `createExtensionBlueprint` -This allows the creation of extension with the following pattern: +This allows the creation of extension instances with the following pattern: ```tsx -// create the extension kind +// create the extension blueprint which is used to create instances const EntityCardBlueprint = createExtensionBlueprint({ kind: 'entity-card', attachTo: { id: 'test', input: 'default' }, @@ -21,7 +21,7 @@ const EntityCardBlueprint = createExtensionBlueprint({ }, }); -// create an instance of the extension kind with props +// create an instance of the extension blueprint with params const testExtension = EntityCardBlueprint.make({ name: 'foo', params: { diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 48718f5375..858350044c 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -499,6 +499,51 @@ export function createExtension< options: CreateExtensionOptions<TOutput, TInputs, TConfig>, ): ExtensionDefinition<TConfig>; +// @public +export function createExtensionBlueprint< + TParams, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +>( + options: CreateExtensionBlueprintOptions<TParams, TInputs, TOutput, TConfig>, +): ExtensionBlueprint<TParams, TInputs, TOutput, TConfig>; + +// @public (undocumented) +export interface CreateExtensionBlueprintOptions< + TParams, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + attachTo: { + id: string; + input: string; + }; + // (undocumented) + configSchema?: PortableSchema<TConfig>; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + }, + params: TParams, + ): Expand<ExtensionDataValues<TOutput>>; + // (undocumented) + inputs?: TInputs; + // (undocumented) + kind: string; + // (undocumented) + namespace?: string; + // (undocumented) + output: TOutput; +} + // @public (undocumented) export function createExtensionDataRef<TData>( id: string, @@ -522,16 +567,6 @@ export function createExtensionInput< } >; -// @public -export function createExtensionKind< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, ->( - options: ExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, -): ExtensionKind<TOptions, TInputs, TOutput, TConfig>; - // @public (undocumented) export interface CreateExtensionOptions< TOutput extends AnyExtensionDataMap, @@ -843,6 +878,45 @@ export interface Extension<TConfig> { readonly id: string; } +// @public (undocumented) +export interface ExtensionBlueprint< + TParams, + TInputs extends AnyExtensionInputMap, + TOutput extends AnyExtensionDataMap, + TConfig, +> { + // (undocumented) + make(args: { + namespace?: string; + name?: string; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + output?: TOutput; + configSchema?: PortableSchema<TConfig>; + params: TParams; + factory?( + context: { + node: AppNode; + config: TConfig; + inputs: Expand<ResolvedExtensionInputs<TInputs>>; + orignalFactory( + context?: { + node?: AppNode; + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + params?: TParams, + ): Expand<ExtensionDataValues<TOutput>>; + }, + params: TParams, + ): Expand<ExtensionDataValues<TOutput>>; + }): ExtensionDefinition<TConfig>; +} + // @public (undocumented) export function ExtensionBoundary( props: ExtensionBoundaryProps, @@ -923,91 +997,6 @@ export interface ExtensionInput< extensionData: TExtensionData; } -// @public -export class ExtensionKind< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - // (undocumented) - static create< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, - >( - options: ExtensionKindOptions<TOptions, TInputs, TOutput, TConfig>, - ): ExtensionKind<TOptions, TInputs, TOutput, TConfig>; - // (undocumented) - new(args: { - namespace?: string; - name?: string; - attachTo?: { - id: string; - input: string; - }; - disabled?: boolean; - inputs?: TInputs; - output?: TOutput; - configSchema?: PortableSchema<TConfig>; - options: TOptions; - factory?( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - orignalFactory( - context?: { - node?: AppNode; - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options?: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - }): ExtensionDefinition<TConfig>; -} - -// @public (undocumented) -export interface ExtensionKindOptions< - TOptions, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, - TConfig, -> { - // (undocumented) - attachTo: { - id: string; - input: string; - }; - // (undocumented) - configSchema?: PortableSchema<TConfig>; - // (undocumented) - disabled?: boolean; - // (undocumented) - factory( - context: { - node: AppNode; - config: TConfig; - inputs: Expand<ResolvedExtensionInputs<TInputs>>; - }, - options: TOptions, - ): Expand<ExtensionDataValues<TOutput>>; - // (undocumented) - inputs?: TInputs; - // (undocumented) - kind: string; - // (undocumented) - name?: string; - // (undocumented) - namespace?: string; - // (undocumented) - output: TOutput; -} - // @public (undocumented) export interface ExtensionOverrides { // (undocumented) From 3f257d25e26c51f21f6d63c3feecec59862eadb8 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Thu, 18 Jul 2024 14:59:02 +0200 Subject: [PATCH 164/212] chore: switch around the params to the factories Signed-off-by: blam <ben@blam.sh> --- .changeset/dry-squids-tap.md | 2 +- packages/frontend-plugin-api/api-report.md | 6 +- .../wiring/createExtensionBlueprint.test.tsx | 6 +- .../src/wiring/createExtensionBlueprint.ts | 61 ++++++++----------- 4 files changed, 33 insertions(+), 42 deletions(-) diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md index d0a737f4af..613ec4b7cb 100644 --- a/.changeset/dry-squids-tap.md +++ b/.changeset/dry-squids-tap.md @@ -14,7 +14,7 @@ const EntityCardBlueprint = createExtensionBlueprint({ output: { element: coreExtensionData.reactElement, }, - factory(_, params: { text: string }) { + factory(params: { text: string }) { return { element: <h1>{params.text}</h1>, }; diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 858350044c..342907e096 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -527,12 +527,12 @@ export interface CreateExtensionBlueprintOptions< disabled?: boolean; // (undocumented) factory( + params: TParams, context: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; }, - params: TParams, ): Expand<ExtensionDataValues<TOutput>>; // (undocumented) inputs?: TInputs; @@ -899,20 +899,20 @@ export interface ExtensionBlueprint< configSchema?: PortableSchema<TConfig>; params: TParams; factory?( + params: TParams, context: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; orignalFactory( + params?: TParams, context?: { node?: AppNode; config?: TConfig; inputs?: Expand<ResolvedExtensionInputs<TInputs>>; }, - params?: TParams, ): Expand<ExtensionDataValues<TOutput>>; }, - params: TParams, ): Expand<ExtensionDataValues<TOutput>>; }): ExtensionDefinition<TConfig>; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index b3e926e57c..c7b077bdbd 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -27,7 +27,7 @@ describe('createExtensionBlueprint', () => { output: { element: coreExtensionData.reactElement, }, - factory(_, params: { text: string }) { + factory(params: { text: string }) { return { element: <h1>{params.text}</h1>, }; @@ -78,7 +78,7 @@ describe('createExtensionBlueprint', () => { output: { element: coreExtensionData.reactElement, }, - factory(_, params: { text: string }) { + factory(params: { text: string }) { return { element: <h1>{params.text}</h1>, }; @@ -90,7 +90,7 @@ describe('createExtensionBlueprint', () => { params: { text: 'Hello, world!', }, - factory(_, params: { text: string }) { + factory(params: { text: string }) { return { element: <h2>{params.text}</h2>, }; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 74d2eeadf1..6c79017a0a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -43,12 +43,12 @@ export interface CreateExtensionBlueprintOptions< output: TOutput; configSchema?: PortableSchema<TConfig>; factory( + params: TParams, context: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; }, - params: TParams, ): Expand<ExtensionDataValues<TOutput>>; } @@ -71,20 +71,20 @@ export interface ExtensionBlueprint< configSchema?: PortableSchema<TConfig>; params: TParams; factory?( + params: TParams, context: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; orignalFactory( + params?: TParams, context?: { node?: AppNode; config?: TConfig; inputs?: Expand<ResolvedExtensionInputs<TInputs>>; }, - params?: TParams, ): Expand<ExtensionDataValues<TOutput>>; }, - params: TParams, ): Expand<ExtensionDataValues<TOutput>>; }): ExtensionDefinition<TConfig>; } @@ -117,20 +117,20 @@ class ExtensionBlueprintImpl< configSchema?: PortableSchema<TConfig>; params: TParams; factory?( + params: TParams, context: { node: AppNode; config: TConfig; inputs: Expand<ResolvedExtensionInputs<TInputs>>; orignalFactory( + params?: TParams, context?: { node?: AppNode; config?: TConfig; inputs?: Expand<ResolvedExtensionInputs<TInputs>>; }, - params?: TParams, ): Expand<ExtensionDataValues<TOutput>>; }, - params: TParams, ): Expand<ExtensionDataValues<TOutput>>; }): ExtensionDefinition<TConfig> { return createExtension({ @@ -144,39 +144,30 @@ class ExtensionBlueprintImpl< configSchema: args.configSchema ?? this.options.configSchema, // TODO: some config merging or smth factory: ({ node, config, inputs }) => { if (args.factory) { - return args.factory( - { - node, - config, - inputs, - orignalFactory: ( - innerContext?: { - config?: TConfig; - inputs?: Expand<ResolvedExtensionInputs<TInputs>>; - }, - innerParams?: TParams, - ) => - this.options.factory( - { - node, - config: innerContext?.config ?? config, - inputs: innerContext?.inputs ?? inputs, - }, - innerParams ?? args.params, - ), - }, - args.params, - ); - } - - return this.options.factory( - { + return args.factory(args.params, { node, config, inputs, - }, - args.params, - ); + orignalFactory: ( + innerParams?: TParams, + innerContext?: { + config?: TConfig; + inputs?: Expand<ResolvedExtensionInputs<TInputs>>; + }, + ) => + this.options.factory(innerParams ?? args.params, { + node, + config: innerContext?.config ?? config, + inputs: innerContext?.inputs ?? inputs, + }), + }); + } + + return this.options.factory(args.params, { + node, + config, + inputs, + }); }, }); } From 73184b37beea314d63196229f28c11bb14c89315 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Thu, 18 Jul 2024 18:46:25 +0200 Subject: [PATCH 165/212] ' Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/early-trees-dance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/early-trees-dance.md b/.changeset/early-trees-dance.md index 5422bd2872..55b2c08f89 100644 --- a/.changeset/early-trees-dance.md +++ b/.changeset/early-trees-dance.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': patch --- -The `ExtensionBoundary` now by default infers whether its routable from whether it outputs a route path. +The `ExtensionBoundary` now by default infers whether it's routable from whether it outputs a route path. From 6ecedfafbcdeedc529868b615d2597a6121a7f5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:03:48 +0000 Subject: [PATCH 166/212] fix(deps): update dependency stream-buffers to v3.0.3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6db9d6b8f4..b63d8d7d63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41326,9 +41326,9 @@ __metadata: linkType: hard "stream-buffers@npm:^3.0.2": - version: 3.0.2 - resolution: "stream-buffers@npm:3.0.2" - checksum: b09fdeea606e3113ebd0e07010ed0cf038608fa396130add9e45deaff5cc3ba845dc25c31ad24f8341f85907846344cb7c85f75ea52c6572e2ac646e9b6072d0 + version: 3.0.3 + resolution: "stream-buffers@npm:3.0.3" + checksum: 3f0bdc4b1fd3ff370cef5a2103dd930b8981d42d97741eeb087a660771e27f0fc35fa8a351bb36e15bbbbce0eea00fefed60d6cdff4c6c3f527580529f183807 languageName: node linkType: hard From ba8571efe953a343e08195cb586c2e1bd3b6adaa Mon Sep 17 00:00:00 2001 From: Yayun Feng <yayfen@amazon.com> Date: Sat, 1 Jun 2024 00:09:56 +0200 Subject: [PATCH 167/212] Setup user agent header for AWS sdk client Signed-off-by: Yayun Feng <yervant.feng@gmail.com> --- .changeset/chilly-trains-sleep.md | 7 +++++++ .../entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts | 1 + .../src/entrypoints/urlReader/lib/AwsS3UrlReader.ts | 1 + .../src/processors/AwsEKSClusterProcessor.ts | 1 + .../src/providers/AwsS3EntityProvider.ts | 1 + .../src/publisher/AwsSqsConsumingEventPublisher.ts | 1 + 6 files changed, 12 insertions(+) create mode 100644 .changeset/chilly-trains-sleep.md diff --git a/.changeset/chilly-trains-sleep.md b/.changeset/chilly-trains-sleep.md new file mode 100644 index 0000000000..cde7600c8b --- /dev/null +++ b/.changeset/chilly-trains-sleep.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/backend-common': patch +--- + +Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts index 7f02f1468f..5a2655ad1c 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts @@ -213,6 +213,7 @@ export class AwsCodeCommitUrlReader implements UrlReaderService { ); const codeCommit = new CodeCommitClient({ + customUserAgent: 'backstage-aws-codecommit-url-reader', region: region, credentials: credentials, }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts index 27f60f5b77..324538add4 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts @@ -222,6 +222,7 @@ export class AwsS3UrlReader implements UrlReaderService { ); const s3 = new S3Client({ + customUserAgent: 'backstage-aws-s3-url-reader', region: region, credentials: credentials, endpoint: integration.config.endpoint, diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts index a491dbab7d..a5f5ce2f6c 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts @@ -106,6 +106,7 @@ export class AwsEKSClusterProcessor implements CatalogProcessor { } const eksClient = new EKS({ + customUserAgent: 'backstage-aws-catalog-eks-cluster-processor', credentials, credentialDefaultProvider: providerFunction, region, diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index 91ba76b695..0c58fe3f3a 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -154,6 +154,7 @@ export class AwsS3EntityProvider implements EntityProvider { accountId ? { accountId } : undefined, ); this.s3 = new S3({ + customUserAgent: 'backstage-aws-catalog-s3-entity-provider', apiVersion: '2006-03-01', credentialDefaultProvider: () => credProvider.sdkCredentialProvider, endpoint: this.integration.config.endpoint, diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index 26eba88c18..de55dee017 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -76,6 +76,7 @@ export class AwsSqsConsumingEventPublisher { }; this.sqs = new SQSClient({ + customUserAgent: 'backstage-aws-events-sqs-publisher', region: config.region, endpoint: config.endpoint, }); From de4cd433c15a6cc2ba63448ff31542decedaf2bf Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:31:28 +0100 Subject: [PATCH 168/212] cli: simplify changeset Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .changeset/little-bulldogs-guess.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.changeset/little-bulldogs-guess.md b/.changeset/little-bulldogs-guess.md index 1f86f0a67f..3e646a033f 100644 --- a/.changeset/little-bulldogs-guess.md +++ b/.changeset/little-bulldogs-guess.md @@ -2,16 +2,4 @@ '@backstage/cli': patch --- -Add ability to force use of the development versions of `react` and `react-dom` -to allow for detailed error messages and use of fast-refresh. Note that builds -with the development versions of `react` and `react-dom` should not be -deployed in production. - -This feature can be used by setting the `FORCE_REACT_DEVELOPMENT` environment -variable to `true`: - -```bash -EXPERIMENTAL_MODULE_FEDERATION=true \ -FORCE_REACT_DEVELOPMENT=true \ -yarn build:all -``` +The experimental module federation build now has the ability to force the use of development versions of `react` and `react-dom` by setting the `FORCE_REACT_DEVELOPMENT` flag. From 2e1a7f10e5f29af3e4f253ad62b7d61148c5f995 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:48:20 +0100 Subject: [PATCH 169/212] cli: only check for truthiness of environment variables when forcing react mode Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 8e3d7eee19..3d3be4f485 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -237,8 +237,8 @@ export async function createConfig( const optimization = optimizationConfig(options); if ( - process.env.EXPERIMENTAL_MODULE_FEDERATION === 'true' && - process.env.FORCE_REACT_DEVELOPMENT === 'true' + !!process.env.EXPERIMENTAL_MODULE_FEDERATION && + !!process.env.FORCE_REACT_DEVELOPMENT ) { console.log( chalk.yellow( From 0983429447db996138e90c6168f82e47b405223e Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:05:28 +0100 Subject: [PATCH 170/212] cli: expand comment Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 3d3be4f485..e10ac0253e 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -251,7 +251,9 @@ export async function createConfig( `${dirname(require.resolve('react-dom/package.json'))}/`, ]; - // Don't define process.env.NODE_ENV with value matching config.mode. + // Don't define process.env.NODE_ENV with value matching config.mode. If we + // don't set this to false, webpack will define the value of + // process.env.NODE_ENV for us, and the definition below will be ignored. optimization.nodeEnv = false; // Instead, provide a custom definition which always uses "development" if From 232b134606d193c262d38892e86e41bc979305fd Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:07:35 +0100 Subject: [PATCH 171/212] cli: skip forcing react to development mode if mode is development anyway Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index e10ac0253e..1f29277676 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -237,6 +237,7 @@ export async function createConfig( const optimization = optimizationConfig(options); if ( + mode === 'production' && !!process.env.EXPERIMENTAL_MODULE_FEDERATION && !!process.env.FORCE_REACT_DEVELOPMENT ) { From 6c94522688dc49b850db6d23c57b1a794c4b0602 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:08:54 +0100 Subject: [PATCH 172/212] cli: remove explicit boolean coercions when forcing react mode Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 1f29277676..9a611a5954 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -238,8 +238,8 @@ export async function createConfig( if ( mode === 'production' && - !!process.env.EXPERIMENTAL_MODULE_FEDERATION && - !!process.env.FORCE_REACT_DEVELOPMENT + process.env.EXPERIMENTAL_MODULE_FEDERATION && + process.env.FORCE_REACT_DEVELOPMENT ) { console.log( chalk.yellow( From 0e706b3484998631a591b8a07a23b075781ffb25 Mon Sep 17 00:00:00 2001 From: Ben Lambert <blam@spotify.com> Date: Fri, 19 Jul 2024 15:40:42 +0200 Subject: [PATCH 173/212] Update modern-parrots-protect.md Signed-off-by: Ben Lambert <blam@spotify.com> --- .changeset/modern-parrots-protect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/modern-parrots-protect.md b/.changeset/modern-parrots-protect.md index 7fccff1bef..bb4df2c248 100644 --- a/.changeset/modern-parrots-protect.md +++ b/.changeset/modern-parrots-protect.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-react': patch --- -support ajv-errors for scaffolder validation +support `ajv-errors` for scaffolder validation to allow for customizing the error messages From b8600fe1650507c7b0e443ba58d8183e4a7413a8 Mon Sep 17 00:00:00 2001 From: Adam Letizia <LetiziaAdam@JohnDeere.com> Date: Fri, 19 Jul 2024 09:48:20 -0500 Subject: [PATCH 174/212] fix(scaffolder): refetch user credentials when selected host is changed in RepoUrlPicker Signed-off-by: Adam Letizia <LetiziaAdam@JohnDeere.com> --- .changeset/tough-lies-repair.md | 5 ++ .../RepoUrlPicker/RepoUrlPicker.test.tsx | 56 +++++++++++++++---- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 11 +++- 3 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 .changeset/tough-lies-repair.md diff --git a/.changeset/tough-lies-repair.md b/.changeset/tough-lies-repair.md new file mode 100644 index 0000000000..25b1884bbc --- /dev/null +++ b/.changeset/tough-lies-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +RepoUrlPicker: refetch user credentials when a new host is selected diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx index 002652cfab..fce9d9517c 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx @@ -33,6 +33,7 @@ import { ScaffolderRJSFField, } from '@backstage/plugin-scaffolder-react'; import { act, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; describe('RepoUrlPicker', () => { const mockScaffolderApi: Partial<ScaffolderApi> = { @@ -323,14 +324,29 @@ describe('RepoUrlPicker', () => { }); }); - it('should not call the scmAuthApi if secret is available in the state', async () => { + it('should call the scmAuthApi with the new host when the host is changed', async () => { const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); return ( <div data-testid="current-secrets">{JSON.stringify({ secrets })}</div> ); }; - const { getByTestId } = await renderInTestApp( + const allowedHosts = ['github.com', 'gitlab.example.com']; + + (mockScmAuthApi.getCredentials as jest.Mock).mockImplementation( + ({ url }) => { + let token = ''; + if (url === `https://${allowedHosts[0]}`) { + token = 'abc123'; + } else if (url === `https://${allowedHosts[1]}`) { + token = 'def456'; + } + return Promise.resolve({ token }); + }, + ); + const secondHost = allowedHosts[1]; + + const { getAllByRole, getByTestId } = await renderInTestApp( <TestApiProvider apis={[ [scmIntegrationsApiRef, mockIntegrationsApi], @@ -338,16 +354,16 @@ describe('RepoUrlPicker', () => { [scaffolderApiRef, mockScaffolderApi], ]} > - <SecretsContextProvider initialSecrets={{ testKey: 'abc123' }}> + <SecretsContextProvider> <Form validator={validator} schema={{ type: 'string' }} uiSchema={{ 'ui:field': 'RepoUrlPicker', 'ui:options': { + allowedHosts, requestUserCredentials: { secretsKey: 'testKey', - additionalScopes: { github: ['workflow'] }, }, }, }} @@ -361,19 +377,35 @@ describe('RepoUrlPicker', () => { ); await act(async () => { + // need to wait for the debounce to finish to fetch credentials for the first selected host + await new Promise(resolve => setTimeout(resolve, 600)); + }); + const firstHostSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + expect(firstHostSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + + await act(async () => { + // Select the second host + const hostInput = getAllByRole('combobox')[0]; + await userEvent.selectOptions(hostInput, secondHost); + // need to wait for the debounce to finish await new Promise(resolve => setTimeout(resolve, 600)); }); - - // as we already have a secret in the state, getCredentials should not be called again. - expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); - - const currentSecrets = JSON.parse( + const secondHostSecrets = JSON.parse( getByTestId('current-secrets').textContent!, ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, + expect(secondHostSecrets).toEqual({ + secrets: { testKey: 'def456' }, + }); + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: `https://${secondHost}`, + additionalScope: { + repoWrite: true, + }, }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index e0d714ffda..e653be9178 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -49,6 +49,9 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { const [state, setState] = useState<RepoUrlPickerState>( parseRepoPickerUrl(formData), ); + const [credentialsHost, setCredentialsHost] = useState<string | undefined>( + undefined, + ); const integrationApi = useApi(scmIntegrationsApiRef); const scmAuthApi = useApi(scmAuthApiRef); const { secrets, setSecrets } = useTemplateSecrets(); @@ -128,8 +131,11 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { return; } - // don't show login prompt if secret value is already in state - if (secrets[requestUserCredentials.secretsKey]) { + // don't show login prompt if secret value is already in state for selected host + if ( + secrets[requestUserCredentials.secretsKey] && + credentialsHost === state.host + ) { return; } @@ -147,6 +153,7 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { // set the secret using the key provided in the ui:options for use // in the templating the manifest with ${{ secrets[secretsKey] }} setSecrets({ [requestUserCredentials.secretsKey]: token }); + setCredentialsHost(state.host); }, 500, [state, uiSchema], From 924c0a6379fa9a2c521622259126125b0bc2ddc0 Mon Sep 17 00:00:00 2001 From: rghara <ramin.g01@infosys.com> Date: Fri, 19 Jul 2024 12:53:18 -0400 Subject: [PATCH 175/212] Updated document getting-started/logging-in with missing login screen screenshot Signed-off-by: rghara <ramin.g01@infosys.com> --- docs/assets/getting-started/login-screen.png | Bin 0 -> 108521 bytes docs/getting-started/logging-in.md | 2 ++ 2 files changed, 2 insertions(+) create mode 100755 docs/assets/getting-started/login-screen.png diff --git a/docs/assets/getting-started/login-screen.png b/docs/assets/getting-started/login-screen.png new file mode 100755 index 0000000000000000000000000000000000000000..70b56f399a22d19ad863f77db22739f67a352501 GIT binary patch literal 108521 zcmZs?1yCIAwk{k(aCd?Rw_pkG?(V?{BDe)-fCQHS!3pj%1PksmNN~3xgS)$Pn|%A* z=iL9lT?JHkO*2*fu3qc0H&jhU76X+8_1Uv$81iyb8qb~~20wcS{~j3;xZ+}UdIkJ} z`>Y`=@vL-+Y#TUv@kv}+{MoaLD6~5e0&tGvAouR`vuC)@Pd{*rA1U2|i{_SEdd`}5 zmJZ4voI&qF&LH60vu9q^<~EiNYW9|P&Y!6T47{k_sRcM*Q+oivy<h)5<K*Gx{C{8I z<Kh3$Xa2e4>D<U$a#Mi#*|SF;c`0!%cjLV@v`j+j>qknq;LmWF&yghgt-cb8DRU8M zJhx9m)uW~l+bF?jMIm5+nMD7Ckv^MA50x;P(2Us_SCNy8Ls1EVdpR-eK{+z|OWSfP z{G*5KrpIv8>CnND@#5verSR~l;m<9%doCYcU0q>mX|t}9KHynQ<V+*MFXU!Sq;$@$ zBUgb1X~Z?*DEnP6jNC%R(^P##;*d{Vnrc3cagAY)6JUh|fJv2Uh+pG0y<TWBTFbZ~ zKG@q15NRe>*c#skj}+wO<D{InDaM+uPO+BD>*=>ai@S&V+4FJ5MKnFP)~gdul<54x z`DbmbwH^jE;Ke!u9$p@S>9;}hct*vLtOm`zGvXle&UGRPvw(cD1ODN3PNwnUs^PRF zH+muj#Bu+CENOO(9pnab)b_Poc`zQp%*e2VwHV!r*k!Ee;BuuMooGi$%&$r;9dNos zf~Y8uLnSBNu^m@e-3)EO=huO-Q*?v8P5wg9rtGZ?a_ZLbcC;|wtbBh8*sB^ITYuKM z8u8DH;gy7TlRma@t-#r)3W*$@@>m4(B{R)Ajv;9j3PRbPvJtYWSDg_^(QA&OxEXcZ z^0WyQoVEVeq3?fdkk&kIIPw!xn6G0~d~<sKRyMhOu+S3CjqIqCk*II~j!}Bd-EK*l zSO<4mnRqo~KQIDxB0f(loL^-z+p<vfs-usZOXNYRqG(SKZox`Gkv;9mn=KZRS-^Qf z3s>81CoT*YBX1}fQ-?0?y_FXuXPDbf^@B^9noT}HG7|rXlLA(xA`fWQk3j0`n}oMW zw3t>*sG>q7s|KWJ`b7_2%KMJ!7+bk>8kKc5ucCKfyVQSDU21f$rQj!qLf(t}d|x(z zhb)_(*m8xo6S|PMeod%Wm$*E|*kGSL8bo2I6o=(<wPGve-}0fH^1i%Q=Ze_q=1nF< zjrN>^XDwq+9}W>oJJKIkJ?^oN8Zyet|D~bQwLKb-etfDpNF0s%2W54gNVC`PPUCI< zIUk;}4`=-A-&k+JpQ{wL3HA)<Fm}Ao0@95d1h8Jd%-35IelEPKQcS&a2KvAX;i=|= z2C*8LE_4qs`KyweE(8q+w7#t2<`wK82{(h7)t9lD6<^(=t9z5#bANI|WH-@Xu)+4~ zQ+Fv3T*+eofl(C)fy|oy5q(E&aa}lvt+>gE!820Yq}8R>Vy5RJoejAGzhFHTlJ1s` zN)%bh<(hfrADsu8&F)sL<xs0rpKK@mlLmRuqvo!yJxGZ979tV=cNu!wbS0s%*~eTv znV$LyKJcCP8`8U}0!yBdytfc6P^MA#&PRU|ER9LO%p(W{3-~@Y<&WXILKdFb_{Q3k zRLFlKAPuu(w^j?EFHF(83{=sQDPFJ^K&-?^a3MPh_CFiqSw3y~8DWkLI`W&Ix+V?E z>sZtUY6<lA)Ki6d@F$?P<oS!57U710V(25*DaTzI&+me<h|;E{A|gg+=4lJwDEYJp zztwx!%b_Gxu8kvdCIwBbJi7(AdN~`!)D6)&7AYwk<`Sm7k5t6nj+WwElSAiYo6V(T zlmG7Qx%4@db!Cw#Lo>ibMqOo-EJjRC1CJ7@u-*U~y1DNpgqr<XwkxTRdzHhcryGMK zV`I<B1)RSmCzEVVm5@4X7$@8CUL`By@u4qgwp`j&scqe!o~WO<$oBFUC9x(z&!H?h zYr$RCzC6h?sl3om)0}_<B2sRlq_U&IjEns^kssVZwawh{C~Wy8x0!}6y(^*|WSs-8 zP7Rh<Gu>lR_(w+hkfCKseIJgaa85-rX#KA6;#z6@t85v!8z?bT`9~NvSThZD0w*$X zFq#(*v={lX$bCUE0v&vQ81v5B-2-Nab92=5QZ}Ad3w)(kU1(-EjSAR-RjE2;^tUN$ zl<t+%={Bj<mN^U3WUcy5^H0e3Wov1Gr4N}@xm2BDgdbBnNRC;=aaCL@0tts##7gMw zD&Uy&?yN~wR0K4^(we+tS<GXa*smuZaM=<<Qp(tAlz~i&MLyq*ERzr<9wGblOl?Ko z?mvxcoZHS5_&{5=)SLH~_)Q&^<`2}ep~@*d?F<=U>SjaNMUPKg)V>+`xlYLN25f6I z%s8jojD1_!e1YQi--ABWD71HPRtcxn*txVwcP52;f<7eCF5UGME@6GRX&-<^na8{@ zNFGvqr%ojt^z>Lv^QNQ<$^U%v9J!<m^@1x?4!BN>?qrppM{<{=-lpQl>?KR2U5WWy z7h|}Iw2TF3jFWXs-fWAfc9wtW3uy5p<H(3|wW1<W{--REP=odP16YHmN{v{A?RR!} z(+H)=cY=M-_4!%Rlx)taniB1Es+tm4{67}t&u0?L$Cgn9yb_gnsB8!oM(v6=rE?Zl z4q3xnt{ewv*~8Y%Pku}cvh&643tm4zO(Av`4Z{_0p{z?48F2>Y(ap7NOGG{bIu?ce z3;)TRkKbo6$NRz}M~Wn6H(Ap*_F0{TKi@$aan|1EA(4OT_;(&Hjam=dE&?40`2@P+ zs;z|rjG#0pQW$y{<*;K-F9$cZDnE?G$;pDnwre_`aSA+jqJ8Hb@PS9?Xtn>{j4k70 zr8!;nX)^}&9vxEIq9u`Wz=k#CBCegPyu+4Q%$4Ee3t`@<Lqp}b!^hG_8NcHz+{Q*g zZ3gBTdDK%Cgrt<#dEtC}jp1+NnOP>$Qi>cz$)cb=c^VtBa&SBs0VzX-PU&)^`)uPN zaz8kRkYG{UfzMWnxkajcxxkhPhu)lwAd@f!RqzTq5w(?2NZqmeDh{29V^pw8Ob?v~ zIPSg&O;%zX=d)m4f?N>rJq^CC4u*mo&isf+u*Zk#qF1QTYbWbJZ>||&O7mN|m-izG zsjW*e$si0zzY%x=w#&tCwQ!Fc#njufvzlMI)BBJFUUG?T<G9XG{cPOtdO4`D=Nj3t zWJ+vMC|K{*kYS#r@uD%k7*gn<PvEjMqplJmgL`QpKmSYGTQC2AW)<zR{6^jHXkyyW zHvDYNmZ6HcTieUvsJCmieB(}r*h8OY<FpNNQDhZrfco=URx`kCzFhPg;tld^v&Y`A z=%E+^O@sW)x?n6rn6J4awz$K5fF7Y9Hx&iB!X`IR_yLx>JVl^5VGNS(>}4lRmN{B( z#S=og=CKNIX-bR=>&2H{xRbbMG)TLnLyQGViM1|rP^@2ORN{-}&IkHbXTrDY$I54@ z$Nu=R0Y@0&YfOBK|5w@k81wIY+0oC?1qb`tnLug8T%7ubEc}%XN*{-~b8(SlOQ^)s zJ?aJ%b!nOA-Zgrx&e0-iD`zmfB`8qThZMiksRy&~x+wm!5Y*On?38VVza^x}jasov zgFfu44aIhQ6mYCHSD5Cox)Elj4?hZtux9?aSy;q?`UxSFw9JMwkzwL$mjppm2>PY5 zYpIANu<rO(cDH{BDP`*_F1~20YEq;keqMTF>SmA%n8R!q5=3MW3UW&&$X4oXiujN( z0DH&&BHuw8&W*+O(1PrrTe&G5SMYc#q<{)i0I%dTMC*BW&SRF+#ZW0iGY>>n_|6g} zd6rrl+zv7KF`GAHEWB8$lRWwmgpTO5KL$yx5L{^FC}l5ypQy*Fc8HWP;LC6+53g4? zs68^+qKK2Y?ulbhy(K0E<~F~*-il;5hyA-Zw!v)L+9P?t^gYp`xi~h`0VSajf%=j# z=a@PM<&~P6@2!J(a<sxvP-itp$}k2wm@HJBzl#p#YUU#z9or2r-*`;E98*Ct=6?}R zgyf7vA#3o|&v!IEe%o`k>@{R0F{gu6;I`SDkN(BtK2!u5bU9<0w{@u|81;~@eC~-7 zcx8q&5^Ygs8u1=Y?T#r`3C=($W2vnD&6WtYA?LzNR=zre1rEJ8S6>w}qmJchcs9l7 zPsL4O{k%+rml^}?d_kgO6_2O+2_HB2$%IW1nf|sM3SsU`&+9HN>ffIfu$Vs~v$rMz zGwS+9E9mRGdm_ZDFkziPVLi}db}%=iFOp!Sq@nU@OtD;b5b7NHF3xNbFD9;j$n_sv z874GTH4Gw)@H-V0M>h_;VlsYAwI5*IaUtg}dhqJBe3|nly^)uQ9hR%k5%?C}ahhRb z6&nc{G(kD)Kv8J6P%~0`WDWH!n6cn#g+)O^BFwH9?A_M8S;7z(eASy!TF;REXPReP ziyhTg@qlU?9j~NW+;rVpvq3ZHp9^BK^Te%d<lBby^zhtC&7aY(0LAvVD^0j}hN=P= z%qU@XyJ&d5-}BT%8hqI&5+rx0%M#7o`B;N7e+cvvqoSMQ(h~G?&=Op5ePK|kSNjI6 z=L1XPot8|2bZ;t=^XMrW`vY<p8(R$zg751{P>0%8F*~$M<J)a3*B5(zyV(t&3}i}y zH?~+!_ItB=#iz|wK_CUZ9WxY@=VI0O!bzRg?4kXi=8^2>s2!t8g5QsEh1SvHq+HR) zh-$=8kR{rTC977PV=qpdg?kvppdxKX+w?KBh5iF)f~7j5o%^X?KtEVNO{ZpjrmS1b z=k^arZG}6N8et;^3`JLlvoe-#=MxE<4P{nTmKa%&V*P&@vu?X+KhM+N9G_{jpIsZb zWURnNW!H(U`)WG^rBjjmb_aWn0nToypF^)sbfI?C?2CPhp#6GCKnIq1K8~<Q)13tK zQ?X#hO9uQ)k}_A7+^=)5^7Br;vmwj{jnt`Ae)fia>pxBkY8tvN`ExhWbf&<}>ddF1 zT(^23SG}(j@@-YZsc(i-PlY3Y!X!&VCcDlI>6A`$zO;J9`kV@&))C66PI469MiE3N z?czC(6S%4NaEE!`FS@SAmv}b((6X1$=?Py^bialKiNBBxpfP}&hUwyG2bnPZ9WTd% z6!a$2=Z>u)141)vkEVhydk3>{3Gj6<yAV&vV95R_YhtD4Z;~@dyM@P$GT;>oEC~$X zIDmjb@py$p>UQz!YUConS6I*$UA@35ULR@K9AFK$!FFUOJ$jb+vTPvs!eP!2L8e}f zr$;JOCcUq;lj%IShcas>47v3~dzA+DY!s8`iOXqs75R}YS2cY{g0#}LA1k^mWSbhm zGkq9fF$LDJ@@z&n`FS%bZ{)(5&Sn+;g9O}AMe6^8+urf1DAo)26=x`uiH$O~@OKRr zJo5hmFTu1OAZnHVtP3Y0E_Vk`k@tH3N~_4%6S^gd3ccYm2-g-@9}!<tFvi;FT)$yg z>fe#+p9KowS|(t^Q*29_7<j+KEQ6P+Xfp6Qm1XJ1@6CueEO|}B-I>O53|P79=3eEj z+r)+J1Z(g4<;y0r4H;~Pq>{<^VvDxof9sVIZEd<UV}AGD1hxBpPC@8XHK3iKeV7qe z2Uy0|WHssDpJpWI!}+eO<<I$5?Y!2FS~`kRpd_akCTlk~rys9|KRx=pkn-}eyKo~p zQ!%r_zno-6UkQr=co0IFtg!++?+p`M35!J7h?ggi!YxDg4P>YPYPq^)pNDZ@TN}y1 z!mY3&upJ+3uI?57M{UdUudaE_KOzToUTRT7zlj7IX=}EeX`t7Z%8})@BZk#NAc4}# zNWTDzq@CwTI&r@$Y=h;<K3CoFXB(jl5w#THH<>~HTHFSV2aqvS_<aE3*c^u;Dc1&e zL_jcWMh>*-D!*RjK?G(bj;F@`^5-P_Wfu@xivY&9^B}LSNK>JY8Hmuo6PGXf>O?^E zWCWp-EM)ADTxyupPrf)sw4z^qvIyT4l1{K-zh|pl6qm46f-2}rc&U}<zMPt{O?<$q zVOFrN&Hg4VIO!a;qjJzsuC%QoQ(LD5*Z#Rx-zS4HgVvErdF9dpclwO>02Kfp)Q<Ah zWKOxi<1{@T41WPsD~kQzcd>5rnnFEkKT$Usz%Ks_p~we76ZBwYe?H8ZZl*}8d^rWb zwMn7%%m`SUvuJ7CFGg8gPr;|mHP5olkqB5hM%JPJt=qm0ikPY#mGHB)e!B$9zKht& zuS|QwutAjV;7Hc+REMzkH6jU1F&i)-&4^~0js<ZFJs&zQAK`P}8)@I3CaRUbPbES_ z_xBWWzYhF6Tqp$&faW3hS7B-2#WB7_W)}@}wTW5>%WmmwvsT59cOX|{8IS)Mdu|Z4 zOj_j=0^L;{JDp9WiD7KfPbKRAi*j`;SDJ4&Z0AxBBo~Mp|A-^0cklM&<nJe?V=cKJ zd5Z728FAi=;K#Jri;jW~(_fiPGS+1cPBC_3sLr3GaHp+)X(VcQ;6n8|t~lOa><Eq8 z*tHrmsWL7O>wTHJ(0dg+`;zW#8zR%rE^Ncm^%nKN%*kil@&tvfCqc@cGCZ8Cx+=tS z{X0cT54Q^VzhmbA4@W|;Xr+JJV|st+!y3augZuc)2~M>R92<3~8{$CpTu8kCD^~`d zF;RdMf7EzfFpvG*j<dn?gWUV?Rfb=F)SzsN(q}BWh1PJ@!x%aa2{^a&f0-kk?fuaP zfCvZMj1iXzSJ#x3dW&jYXNk-Nk~_2f{3)&DbX<a0+50h?&{b+#vo^<GdSJweGZo01 zYsQYp6slhbIq{R5{9g?PC)k}exP|9Rzqwmc`)NY>5O;m*;|c2@PvLAZ+vp4)A4<VC z^N=c=owx^oF}Qq#{<ZPZRGEhQN0;OG$S;8EhU)D_57SAzt2EkRQD$nDR;T`b34i)k z#I(j?Dk{^mZ5LcMTJ>d_>d5_LA7%6qy=dvm-1^n(ulhU${mvTODVitU?e9qW4-#n> zx;P0hlKP&ZQ@+k9qO_->y)UHJ>0nNWlfl0YZ{W4en|tIX2|NboD=$-n%G*r*M$E3n zrlVUjJ-b)#f&n?e5<1_9`)!M`q1O?1D`|H`RPYX+^gS#jRF#N8DpR8bc0<p!<MVqa z84G*p`i|Qd!WKSR;y*2B%J52e&tYBWVXy$q*-%T`2H#DUwZ-w&ksw1fl=IUq&sCl* z@x)cn*N(53lI~dHo39odYxs^)>FvNuJ#SI#3@W^2SFyP%q~@;*k3;aDB)Co$tPl4V z*HGa`EkFQtSWAhq=YOA)nRK=OJl%9%>?L_jjQ?=(8P0lAKy&62)<A?oty~mnCAx-` zB;OY2GFZmoxv#0YBJ~{wIdd##xfvr2Mhqg(2e-k5w6g~aA%$Od!ukg6x2xtO=^Twz zRu<ocuF27og?7?~Isok;cRjr|b<BS?CY*wB@BT%GNx(K-V3aaX5iod`<$8Gl=o;!k ztNb%Tvbhx~gKsezR5i&mXUWYRBm~~xu~Ea&97elfefq61hS!VEJ1w5nNY|jYYCrVL zB|Fike4OZ?D||DS6>ay$g)OlrYoug$d`7Tdb~C?TRS6V!m}{HO@=yD$KpBjJX|ew3 zvFlGcas`p(SrU5v7YS;-?*d<pQ%12YuM=s6zRF<oo;XD~K0yN(e>hG=FEzxB&Oeb- zhqG+yGV~~Cle?qb1!rj0ZZk|a5yIR<90cn4A`E9LE$W?td?<Z<D<z(Ez}``~ef>4* zbt#>DmSg-uxN=ylptA|x+-L8KS@gAH5+rW0`p7Dgt6;u!<nsfay~R;JV1ei;j1`hL z1h2)rX<|hgjgvLN_f_W}0hNkbeAFN17+V4o%dCj+R_x&qF#U6^{)HWd+o8j3M|32H zWUaFJya2mJ``TErDnSxdxSjPxRG#$R|64YRK~>EPopjo2qf{OUrK2~i|AO?3tPPJV zqz*wrq#VVKNsWG%v20Va1$Cgt88aAe*v%6!xYy*toS1aX{r<xr#2IazYQ_%7e2yiz zpq+*(g(wg)Jw^OXOrd7M&nYP%TF1GR&Spj2OPKt;I2~?e++Bj)|01+Zy=I}gNVq)8 zM<N6nMun_P{23o&lj~@K586mtk;MpnI#F)2$HX87mz<Tf-x6?J{?_F_{TRxVHd3&u zi6h$OFqXpJkU@B~mJ3G{^h5J9?F@5-D|=e))>7;2ycsOo^*tGXJpRP%J1S0C58CAp z?ivG%oi1;(U5;g^vaW)OFEBAIm2UB7fpt~g{js<wseMWV84oLz3?4Axt7?h(tH-N7 zym~?!e>n;vWj1l|5+KcnzG}s9YgstXG6T@YW;?a_ZIA)}I@VpcSQMMlwII+BXb}@; z4N{U*!+h>!f^TiJFVd&kN$(C?J4|oxu@MXnq-e|-e?dVv_t|DAljlk>+U#{d{BfM9 z2#BXZvBC@1U=qY{B8ZBU99ze-5fT$eyZ`8BK0WZ>1frB~yO*rgoidzv`*V$WzkA$q zYFZ%batBuj4*JdJiOB@y^Tui&^Tjvhl%yOtNkV|cUj)#Q{9=+QxnuPUN_H}oJlTQ$ zCH<sZZ9wjLq+QW`jcd=3PpEW9%d`fP6WHr|{?>v23xZ}vs0K$q)x11V3WYX9NCxGz z_xatd<@FWnDxf3_sb<diNwkckw?|rI6JvWK7V|Y2Bq=FtTJ$JKf|4`Ulu!<(mNwWM zy=4>a(O%0U;zZhmLmP?&gnLBAm9GZ4wWt82hz5uUCO@u!RVN)vACvGHE)gx|hu@nK z5h{w*dsgFwBA<TA)Bcxw<PQBX!#+)s3=Xp6&-Irl>OMl@qOUuM)lg28A4d*i9)Hsb zWe=eCQ3xs&3BrSh7oI97+jHm!kaNJz-{JiPbW^<MZjo=eH7yZlAuEjQ3`S4XBl=;e z(Ek9S9>Y@>9#1AulIacg)hf;{OHT?5gD(;GW?lbkxck5Kbl~8G%i-wnTmS@By+sZF zZveW`0b3Io;DhL7l=E&Qs9zr@*UmjY`{cB(uZ>|8z=;lBBi(!XtkL8TJ<C-^HFd&a z0PNllUuhaoVu9W4#1bf4<zXTRR(9?OAFYek0HjNr%~;$U=TFzjv^C_Mo%|wo2H(;l z07C`zEs@jksE9+Kx3dvQ-LmH{lTjJ+Y4&zZTgTZ~xW+yrV^BoRrfh8Bt&el4gVA)9 za?O2ckclLrm}kwS0cJ}+0}ijzYPalyZ<7OU+`p#G7(jVcsYNX5bo#TzbxjPvYqi6x zVaDmd-dakK-AU9GVUBgblA<YDx8;9N)q46Y<<n|yNR5s4TuDgw;xkSq+_>#~dQIx9 z`=$22ESZ8`>e^fCZVGUC>nEazA#5l|7aYs|u{g79e*l(tv)l>=ZRqV5Ekz5RqhP!o zA3!~DD?(MNuW9JE+Z}tcy5#VFqCXnmroREG=Myja?|8@!1!(fg)VrGW-*dJ0Zu!yC z_8R(&xpnB-x9V}Pf9qnvOh+-MIj(t<?P-)wo6svJCCmlr8>m!ntVCAUiS&<^3Eto+ z<k;tQQE-sE;Q2;U*L};tZOBmNu<*%emjqt>8Xv7L-aHQWANam}D=CA3za)5XNfXxs z8N*&n+!;y80!uuV!mn4YS|zNK)BI1O7eILOtbCkpVC~l;>cebCS#Grl<*{`-rv2ki z9@q3HrjJ*`0&au3_Ih+<Z!%0<tGv_9hCiL4J+yQn6=c}`LEaUG#a$N)QV>qs;OKsP z;tYLQ5XeU~)=-E6v01R>y9}-Yp3az-uo1LupRh(6=@_+1n_gMZLVwmOL9K-j)39J$ zLu}Ma2-l2Qqei@vY%86nH~QNf{ENb{;aKdm(bP>vr}-bHUH$#K+T?z}_hhYlF7~qw zlraDxH)(loSBW82dQjMFqYlkU!T5i=E|Y58%atO^r~V0Q+PtJq7pmP=8s!O+pi$r4 zgt4!yIvU$lMds>Tce<D!A$CrRX<TGs9J8tz_NT5k(p7;l%ui<Rm&!^NODPu}%RcA} z_aXr0SgEo{IP7&T-QUo{2Hug}P}EKM!?a9@Cd1YU+aIr1!Jm|s9<iO=G<$?fqN6c& zPIS15QUIoWLKAVChB2A*V>H6Yy^DxCKK54g$=f>PL9}DJKZh2C$PyW8WAl#ziekwy z#T)EO;TZx5TYY@u`iw_yMH@&D7SIniaU=H<{<pg7-snU%-`*5aWPq82ext<a*#i#C zCR#<Jn50rmDY3rMc*y>-G+omjUl<C$w4?W?GNFHzX<qEkCMnJv6X=(^Pm=C~yfb0H z?+Mkt3W{nPVJoX-c62vS&K>f}lK@KaUuD#k|7VNP6UM+9Fh~>7;lm;pbc6VXMUr=M z*%XDb#?SH0^g%QX6<-E{#t>`hJM3L-7HS5I)_QKJ?hu()v=@WBoPBW-3`xseA-E;F z;zY4!LGZq-e8Splb-82U*qvd`5r8-`SCM7etD1<sc8tP0pztrSB8~}DoxhU?=EzEE zB54Lk*f5c^G@~(Or-xN8=anB_M|24YU>fN1UH-_8zCW)H4Bs&gonNRV{+^Lk;Li;v zd0;$#wGzZ&=ZOtSW)8N|WK`y(3L3{uE@s}Jx4uyNw7%EG!1Cl*`|VEa5fdkQlPHVH zoMxW2(R%V*G!HMB(9wTx$^fT&)awO3UDafY7Cm|AHO_7oV+@Qj+s=Frbw--Sb+V7c zHuTMP=)%XM&M+Sv<u&J*%nS-Kj2$_~s#<`|sjy-d_fNA}{!SyEpE{N^A8CtwEbmut zDFM@3taGVMjZsRD(>7M7C1aWmn&8W&D<et<XlI8~E>=iy?P%fFuGckW1+ItW{*`nS zeqx38uUDc!mkn4Tqg9L~@8!+@h$M?dfk|1p>U6olGvccaQ6o@nBTK;J2i)0F4x#r+ z3F_Rie6!@J{9ziH3|a&-Xh{r^HkQ|W)c35iYZ?$6bSB?3S2NBkDG;tD#&CW-JMHOb z&wXkT@4vl_7H}Hq0#2V=$rv^PxLj|c2ybtq{w+Amz-?=>9&N%I;;f&WAb4d<|C2a7 zxcKK!Tj}kXE`1V(?@>nnJ~iu~EoQ3Vc<dHdoZm*A9$ltW-c<X6wa-7?WqMI+L+9~E zHGsAeZu$5gl)0W`C;2)phii+SEE)WV0CIrGv&qX8lm?cH3NI;xw0xr^wm15#TtKWq zqpLXdY{-EI+9b7KT??3hc%~|i982BB84sOnqF0v6;VSrJSk)b;SJpI;c)IM<AIP(G zaeYc4*l~46gAXLtCb!92C{p+czptqMWF?mrAOPA$LR15Wz6Rj-PTa>rx&sOp5`C|P zi{r-G)w*QJocK3X|8$TeF#l9ca|%J~%8Uz3Ir%NT9^)Gm>Ti3!DGg){#)H{gP=+`_ z%cN}iNu8{ZN$=atp0%n<v4$O$3^`?M_Cy{9+6l@7&@<n;-R?(u45b0COPtZrY)mIN zZ|H3|ZRwq3hj^<b-v1?S#ZGwJd62{QY)ujB=4cd>4sr76*qmq)ujR)7Z5rzm1NNBY zJK)cZ$Vzqw(9H~Yuro|%JmmCa5V6e!p|=M+W&B-X81LH|4Q4r%vq?Oq%N)PCa!&7e zKq>&XWZZEOIyNkWhH#*|r{=jR5{L%lddrlLP#PbAYur*oMGuoXjrcwK6<&sT-00H9 zWz0m=;7_f6#<o><QQkWhRx(fb=6}`BblsY1n8aV19E*@4Tki>i0u03jiSmt(jus!Q zP5B!HN-;AZ3{OGf{iWL1uG@=}Ps}_IBcb2DH3~-O<O{8b(RFMaU()bgEvN>=P2-BA z)?XUlb@;c0_3BPTVG1{{TCm(RL+Nwvv$H&B20)S|$3e%z)Xgys`}DVSxMQoYe&yR` zIu6Os-()iTMar(s)FMt2%(wkDY2_uN%OeOnS|N#4Sk$SrN1o~7RT-P0{@3mO+8Fg2 zJn`hQU7c~^xc%2;15oF<PRRLS)_vJ8YkvF}>Ce`mrHABUrVG{NZquUS9aL#xI*OqZ zgy%Y$@aK9~oM1lab&7zG&1-BiT6#^x<R?;mg5>?cguZjhDXG?|HLsbwto!YRch`sk z*y-WN9tyI~Rv~9zi+eA(vPd6#drHgZ_`mQbqvNPAtmgb|M1H4MtiFo@U5~TMv7d0$ zOA0^mSKp_9wQkxD^`TBH{xgLB1W9?nX=I^%h#gFY+izARi#<cNi(#;kFHA;+DX70V zKLZ#heh48jvHtHn+!K&|9Nvt&YZ^J*(ZtIW+qi^F4MWUUoV?o|R|}DBJEGBlBn+Hq zZjXTX#-T;+tfSB9;LB{>g2xtJx<e`pF>6QxSPZWngqfL)vJokJ`!5sq^f>$vK@~2) z!tRno{Kw7z_Ohwec}H&<oKo13ji=?jx0Tv>Md<P;wyZ@q?Q`l&`qxQ4_l#%Y3q|O3 z{O?Qf9;$Qr9xndjeg$3OwvW=JQa^DQQOs8#6YLtF2^c{gp`G&oXy8_!vT$xuy~WI5 zpXz0Gw8h)CA+w=>wwMAdj1siFJ6D0Q8r81OjqZ7;p7Vq|0k-LLEN;@ZR(zb9u(DRX z^V&WOACo#Vs${03gEZqS*SnUu_o%+aou!=*mE5T6mt~UMUr7*L?iuIW^NuYF0h4ao zVmCK?T}`jkQ6)}d{YNg;=yjnf+lTUUIkcKUo#YO(_+KIZ6B@yh(i%j{VCd~>cg<{A zNT|wu%DbRY_{Wco<VX6_C827^w{DitS%S(H`GY`7HHfYlVU(U_dls-5#?aeVS|2E_ zBkA`9r0>1Cvba6ck)2;wV<@k#KIR0Q_t)5#JMJc^!E{uVlQ*b@v%e2iM5rbYHEK`+ z2-!H8a<7U^ftn@=E~>I*ImeLK{Hpk7ME`R91tOZiZB9TUrL6QXX$>XapYgoca=6?u zo+S|*MExlllm&MBq!v37H!7G+PIj67Ne(lhX(((foZ138GUX*h13Xga+D}=35eO0Q zHxW}thnJ-LBa8Q@toa~K9Lhh-*9AJJUjMR@`?K?Zsfhc1+nc0+h>3m2wMw*&c7VNS zcGNn3OS7@kjq~BQA$(CP!wmaBPOE0A>C10enzj5e>IFCga)FOSNE3`#Mxr)tdB-+d ze^<pkOqDn<K^G!07tg6VK9~C=ohojd*{mi-4Q|QF?Crb1w(3)hm^fk}h~i6H!IRm( zf-uuJ9*=27@j@2yVk?{cNJTkmo^cDW<c0$&kPwo{Fii4Pdyv^KFF1WU)gI@f%)gO0 zub8z<0gL+J=0#~>z1$=tEdd36iXLn5MdL~K1O>tX?YdZzH2iSClbq*kXT`J=ex$wC z=ma!L{#@@=;-x|S8fw(jDP?}_%kF4UcE@F>5+~C|H4&xKilqFFu{sq?c%fMch&afx z{Xnc@&T<dJ6pB<VAKh8a9GV0u4pRR{H%@Mw=f`qVS2Vx9d0|j!&O&`hj}x8D^<OlR zhtA}(QblG}dl*l2Fk;Pd&&A&j&Ecg$^v)wn=(bgUrc7!&*kcck^?jfjFlrv!ztwop z3@!}k?u3{i)7jjc3M2ZQuf{HoFxOL8oRinBH@tJma0Wm`I&y~d3qT1csp!4*e5gG~ z%Ssw9`@w~#vP0*Vj!il(^xWijG9xdMEUq<l=`>S&{8?b|5n5TPJOJ{}hlMU>>T>O? z-!As~E@5ffec44{?qG918l;UidtGZ|9iVJbE6{(lE=m2nHvcj3X!_8bz%|hh(?F*~ zwX_arLoTtGa%Cj^j+*@IzqZ;D%OQ5P5K97}ftVv1zb*F*`Pu6cD=Z4wD#7EPp}?c; z%`->3<FPClxRgJ?_0`_K4u{;Fzk$fWO?NaR0KPmg*^{&C90`A5`~o`4_*5DZ*OJ{V zr843#6a!d18-TI#KUVy>BT7#9T~;6i#7wuFw8q9U-Fk*v2X9waRaa9e2S^)XI{p(K z{0~e0_Jc28a>&{#4ix=L87)4IIrG<NRpOCDj=H@@&7~-q@XHqZd~+Gea<MFQT}C<^ zZx?|;gK4g8>7HYzx_W)ARh0(x^ahJ2L8`z)glgM|+GEQ`58eAOxn$$lOYx86?jlJY zs`$)zHy1x^*r7$iM8fRS<&Vb)X^ngfkFSiczayX6@`Hb;FA_+~of~tU@bM3=5eR#E zTthT?SmOSZ<V;B{V~+nEr~!D|du?8fd!D7%+T3S*W0Ra@J(f4eQBXF)bB6|%4%=T9 zz_CtJv(lOV;^Fq1erA3Knf2tWNH&XzFICsAuOVe9L@srg(JpW*@>hJ_GT=M2Q#c87 zBHiRuDBwfw8s;63hi6sz2Ky+fg*lYuw4}2ihNL+3ob|HFD8h(hI*%jJ5RkaTeni?6 zLsu1cezLEAD7JbKb1-m>k3?G(z1Y!bIbg6(J1m}k#k-2l<jZ&*o~RoCrBh5fim@;& z<QKuXBWO(8ONh<lD!;DeDfw9CA{T?YHGXJub)8oLRda;;RS#I~EZ!k@>YT!*lQr-! zyLGQiFb_GqiLeFFFqg*0#Zb-kFuhdy)6$GY=62?`6kVj-j)irNdsZ~Ixp~8rtO^w( z>2ly;qNdBaes-D2ZZUTwa<=VAaLTZe#|SAayLsP`fdxIJpG|6c4TMxasGz<uH=Kr+ zH>UmBA?ZOKJs*lBHwi@XLf{T8{&IhlvGpzV6`>)JqO7sXL*MWqcuEuyuGMO@e2_w4 zO9}@nJ&)?Qs#P2Bv^6ses9-29OXaToP-lGhTEsYYSN+g#Ar0w8oQd8$;AkemU<sY_ zZRy?4M=@}&Nf@j5r!=0MW<}PQ&|bL9^e(je^~&O<xA3G5wqm!2=R+nlruZ<!-Nbuw zHSjr?+8|=Zdl_X(>?%@ims9_aB1o?vw&I5gdt^UX0_|gu<WfHNOOF&mm`w8kR)v(z zeb6fl(VG_S@$J_Kbgi!r9s%Pvrl={aOHFwra7HrH6T?T1@4S4M@C9p5SwS)-m#%iK zhxnZCWPg$~?v5^j&V_mxT|{w9#71nNad7+DX=HTz%hxZ?zUI$aWF0MJ$PKva4>pxR zxNB+@7|Kfn4lW;e-W@ekDhF=MVm+^RW0_BAFnaRe{o6+(fP0F>ufNAVsjgjgwEfcr zrx}T=TRvIscZwV9a~7QK9HuPU6lskDl(@qT`z@Ztd%7&-l+3$G?eP*!9w%8^P1}Fd zhFKZ*#y(V9tp_{PbAY@qe6|~(XoB3e%bpFS8#m#>l?08v9f<^hk}SY#a14dcJpWAy zP@t)Do=qwi;3zJ3�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 literal 0 HcmV?d00001 diff --git a/docs/getting-started/logging-in.md b/docs/getting-started/logging-in.md index fef8907e07..5386e98798 100644 --- a/docs/getting-started/logging-in.md +++ b/docs/getting-started/logging-in.md @@ -20,6 +20,8 @@ Run your Backstage app with `yarn dev`. Navigate to `http://localhost:3000`. If you're not already logged in, you should see a login screen like this, +![Screenshot of the the login screen](../assets/getting-started/login-screen.png) + To login, you should choose the "Github" provider and click the "Sign in" button. This will redirect you to a Github OAuth page. Verify that the scopes mentioned on that page match the setup you did in [the authentication tutorial](./config/authentication.md). Once you click "Confirm", you will be brought back to the Backstage interface and signed in! If you are already logged in, you will be automatically brought to your Backstage instance. From 708294012180f435dd61c87a827b752b6f3fb974 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:01:01 +0100 Subject: [PATCH 176/212] cli: add support for frontend-dynamic-container role in yarn start Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .../cli/src/commands/build/buildFrontend.ts | 27 +----------- packages/cli/src/commands/start/command.ts | 8 ++++ .../cli/src/commands/start/startFrontend.ts | 11 ++++- packages/cli/src/lib/bundler/index.ts | 1 + .../cli/src/lib/bundler/moduleFederation.ts | 44 +++++++++++++++++++ packages/cli/src/lib/bundler/types.ts | 1 + 6 files changed, 65 insertions(+), 27 deletions(-) create mode 100644 packages/cli/src/lib/bundler/moduleFederation.ts diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 62da74e6d3..f29e0bdd38 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -16,11 +16,9 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { buildBundle } from '../../lib/bundler'; +import { buildBundle, getModuleFederationOptions } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; -import chalk from 'chalk'; -import { BuildOptions } from '../../lib/bundler/types'; interface BuildAppOptions { targetDir: string; @@ -29,29 +27,6 @@ interface BuildAppOptions { isModuleFederationRemote?: true; } -function getModuleFederationOptions( - name: string, - isRemote?: boolean, -): BuildOptions['moduleFederation'] { - if (!isRemote && !process.env.EXPERIMENTAL_MODULE_FEDERATION) { - return undefined; - } - - console.log( - chalk.yellow( - `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, - ), - ); - - return { - mode: isRemote ? 'remote' : 'host', - // The default output mode requires the name to be a usable as a code - // symbol, there might be better options here but for now we need to - // sanitize the name. - name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), - }; -} - export async function buildFrontend(options: BuildAppOptions) { const { targetDir, writeStats, configPaths } = options; const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index 2f2af95d85..aa46b717bb 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -15,6 +15,7 @@ */ import { OptionValues } from 'commander'; +import { PackageRole } from '@backstage/cli-node'; import { findRoleFromCommand } from '../../lib/role'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; @@ -47,6 +48,13 @@ export async function command(opts: OptionValues): Promise<void> { case 'frontend-plugin': case 'frontend-plugin-module': return startFrontend({ entry: 'dev/index', ...options }); + case 'frontend-dynamic-container' as PackageRole: // experimental + return startFrontend({ + entry: 'src/index', + ...options, + skipOpenBrowser: true, + isModuleFederationRemote: true, + }); default: throw new Error( `Start command is not supported for package role '${role}'`, diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index a809473788..627da1bd1a 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { serveBundle } from '../../lib/bundler'; +import { readJson } from 'fs-extra'; +import { getModuleFederationOptions, serveBundle } from '../../lib/bundler'; +import { paths } from '../../lib/paths'; interface StartAppOptions { verifyVersions?: boolean; @@ -22,14 +24,21 @@ interface StartAppOptions { checksEnabled: boolean; configPaths: string[]; + isModuleFederationRemote?: boolean; } export async function startFrontend(options: StartAppOptions) { + const { name } = await readJson(paths.resolveTarget('package.json')); + const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, configPaths: options.configPaths, verifyVersions: options.verifyVersions, + moduleFederation: getModuleFederationOptions( + name, + options.isModuleFederationRemote, + ), }); await waitForExit(); diff --git a/packages/cli/src/lib/bundler/index.ts b/packages/cli/src/lib/bundler/index.ts index a3b584efc4..2219784dc9 100644 --- a/packages/cli/src/lib/bundler/index.ts +++ b/packages/cli/src/lib/bundler/index.ts @@ -16,4 +16,5 @@ export { serveBackend } from './backend'; export { buildBundle } from './bundle'; +export { getModuleFederationOptions } from './moduleFederation'; export { serveBundle } from './server'; diff --git a/packages/cli/src/lib/bundler/moduleFederation.ts b/packages/cli/src/lib/bundler/moduleFederation.ts new file mode 100644 index 0000000000..ae3f9ab048 --- /dev/null +++ b/packages/cli/src/lib/bundler/moduleFederation.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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. + */ + +import chalk from 'chalk'; +import { ModuleFederationOptions } from './types'; + +export function getModuleFederationOptions( + name: string, + isModuleFederationRemote?: boolean, +): ModuleFederationOptions | undefined { + if ( + !isModuleFederationRemote && + !process.env.EXPERIMENTAL_MODULE_FEDERATION + ) { + return undefined; + } + + console.log( + chalk.yellow( + `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, + ), + ); + + return { + mode: isModuleFederationRemote ? 'remote' : 'host', + // The default output mode requires the name to be a usable as a code + // symbol, there might be better options here but for now we need to + // sanitize the name. + name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), + }; +} diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 7291ac0818..60c59f757b 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -43,6 +43,7 @@ export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; configPaths: string[]; verifyVersions?: boolean; + moduleFederation?: ModuleFederationOptions; }; export type BuildOptions = BundlingPathsOptions & { From 94ab20ebfa48ae8197be8180dfa86b7ee4ea6bfa Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:04:36 +0100 Subject: [PATCH 177/212] cli: skip opening browser when serving bundle for packages with frontend-dynamic-container role Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/commands/start/startFrontend.ts | 2 ++ packages/cli/src/lib/bundler/server.ts | 5 ++++- packages/cli/src/lib/bundler/types.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 627da1bd1a..20faf4a9ff 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -24,6 +24,7 @@ interface StartAppOptions { checksEnabled: boolean; configPaths: string[]; + skipOpenBrowser?: boolean; isModuleFederationRemote?: boolean; } @@ -35,6 +36,7 @@ export async function startFrontend(options: StartAppOptions) { checksEnabled: options.checksEnabled, configPaths: options.configPaths, verifyVersions: options.verifyVersions, + skipOpenBrowser: options.skipOpenBrowser, moduleFederation: getModuleFederationOptions( name, options.isModuleFederationRemote, diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 1da15cd37e..cf35f65518 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -162,6 +162,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be const config = await createConfig(paths, { ...commonConfigOptions, additionalEntryPoints: detectedModulesEntryPoint, + moduleFederation: options.moduleFederation, }); if (process.env.EXPERIMENTAL_VITE) { @@ -278,7 +279,9 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be } }); - openBrowser(url.href); + if (!options.skipOpenBrowser) { + openBrowser(url.href); + } const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 60c59f757b..f8e228c5c4 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -43,6 +43,7 @@ export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; configPaths: string[]; verifyVersions?: boolean; + skipOpenBrowser?: boolean; moduleFederation?: ModuleFederationOptions; }; From b7709255e8b3540176b7db675e9ad77e658920cc Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:12:16 +0100 Subject: [PATCH 178/212] cli: pass websocket host and port explicitly in configuration When configuring the ReactRefreshPlugin and the WebpackDevServer, we previously inferred the host and port from the browser location. This is no longer sufficient, since dynamic plugin containers may be served from separate dev servers. https://webpack.js.org/configuration/dev-server/#websocketurl https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#sockhost Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/config.ts | 30 ++++++++++++++++++++++ packages/cli/src/lib/bundler/server.ts | 12 +++------ packages/cli/src/lib/bundler/transforms.ts | 11 +------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 9a611a5954..1f08f74a85 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -29,6 +29,7 @@ import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack'; import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin'; +import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import { paths as cliPaths } from '../../lib/paths'; import fs from 'fs-extra'; import { getPackages } from '@manypkg/get-packages'; @@ -54,6 +55,21 @@ export function resolveBaseUrl(config: Config): URL { } } +export function resolveEndpoint(config: Config): { + host: string; + port: number; +} { + const url = resolveBaseUrl(config); + + return { + host: config.getOptionalString('app.listen.host') ?? url.hostname, + port: + config.getOptionalNumber('app.listen.port') ?? + Number(url.port) ?? + (url.protocol === 'https:' ? 443 : 80), + }; +} + async function readBuildInfo() { const timestamp = Date.now(); @@ -108,6 +124,20 @@ export async function createConfig( publicPath = `${publicPath}${publicSubPath}`.replace('//', '/'); } + if (isDev) { + const { host, port } = resolveEndpoint(options.frontendConfig); + + plugins.push( + new ReactRefreshPlugin({ + overlay: { + sockProtocol: 'ws', + sockHost: host, + sockPort: port, + }, + }), + ); + } + if (checksEnabled) { plugins.push( new ForkTsCheckerWebpackPlugin({ diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index cf35f65518..cbd76b4ead 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -30,7 +30,7 @@ import { import { paths as libPaths } from '../../lib/paths'; import { loadCliConfig } from '../config'; import { Lockfile } from '../versioning'; -import { createConfig, resolveBaseUrl } from './config'; +import { createConfig, resolveBaseUrl, resolveEndpoint } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths'; import { ServeOptions } from './types'; @@ -131,13 +131,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be const { frontendConfig, fullConfig } = cliConfig; const url = resolveBaseUrl(frontendConfig); - - const host = - frontendConfig.getOptionalString('app.listen.host') || url.hostname; - const port = - frontendConfig.getOptionalNumber('app.listen.port') || - Number(url.port) || - (url.protocol === 'https:' ? 443 : 80); + const { host, port } = resolveEndpoint(frontendConfig); const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ config: fullConfig, @@ -257,7 +251,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be // When the dev server is behind a proxy, the host and public hostname differ allowedHosts: [url.hostname], client: { - webSocketURL: 'auto://0.0.0.0:0/ws', + webSocketURL: { hostname: host, port }, }, }, compiler, diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index da1eec4e11..0900b8e48e 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -17,7 +17,6 @@ import { ModuleOptions, WebpackPluginInstance } from 'webpack'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import { svgrTemplate } from '../svgrTemplate'; -import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; type Transforms = { loaders: ModuleOptions['rules']; @@ -193,15 +192,7 @@ export const transforms = (options: TransformOptions): Transforms => { const plugins = new Array<WebpackPluginInstance>(); - if (isDev) { - if (!isBackend) { - plugins.push( - new ReactRefreshPlugin({ - overlay: { sockProtocol: 'ws' }, - }), - ); - } - } else { + if (!isDev) { plugins.push( new MiniCssExtractPlugin({ filename: 'static/[name].[contenthash:8].css', From be1ee471cf2b09d455256ad6edfa84031ca16489 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:18:20 +0100 Subject: [PATCH 179/212] cli: disable historyApiFallback for module federation remotes Module federation remotes only serve static assets, and aren't intended to be loaded directly in a browser. As such, the historyApiFallback isn't useful and should be disabled in this context. Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/server.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index cbd76b4ead..a3cbaeacc7 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -227,14 +227,17 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be directory: paths.targetPublic, } : undefined, - historyApiFallback: { - // Paths with dots should still use the history fallback. - // See https://github.com/facebookincubator/create-react-app/issues/387. - disableDotRule: true, + historyApiFallback: + options.moduleFederation?.mode === 'remote' + ? false + : { + // Paths with dots should still use the history fallback. + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, - // The index needs to be rewritten relative to the new public path, including subroutes. - index: `${config.output?.publicPath}index.html`, - }, + // The index needs to be rewritten relative to the new public path, including subroutes. + index: `${config.output?.publicPath}index.html`, + }, server: url.protocol === 'https:' ? { From f93ae496ce956598c56254d5420438be92faf0c0 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:19:32 +0100 Subject: [PATCH 180/212] cli: add CORS headers for webpack-dev-server content This allows loading static assets from dev-servers for dynamic plugin containers. Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/server.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index a3cbaeacc7..0421f4d019 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -256,6 +256,12 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be client: { webSocketURL: { hostname: host, port }, }, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': + 'X-Requested-With, content-type, Authorization', + }, }, compiler, ); From e20fe546952f78e8f82faed76c03daf930d821ec Mon Sep 17 00:00:00 2001 From: Ramin G <rghara@outlook.com> Date: Fri, 19 Jul 2024 15:56:09 -0400 Subject: [PATCH 181/212] Corrected typo in logging-in.md Corrected typo in the description of the image Signed-off-by: Ramin G <rghara@outlook.com> --- docs/getting-started/logging-in.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/logging-in.md b/docs/getting-started/logging-in.md index 5386e98798..7d6fb5d55b 100644 --- a/docs/getting-started/logging-in.md +++ b/docs/getting-started/logging-in.md @@ -20,7 +20,7 @@ Run your Backstage app with `yarn dev`. Navigate to `http://localhost:3000`. If you're not already logged in, you should see a login screen like this, -![Screenshot of the the login screen](../assets/getting-started/login-screen.png) +![Screenshot of the login screen](../assets/getting-started/login-screen.png) To login, you should choose the "Github" provider and click the "Sign in" button. This will redirect you to a Github OAuth page. Verify that the scopes mentioned on that page match the setup you did in [the authentication tutorial](./config/authentication.md). Once you click "Confirm", you will be brought back to the Backstage interface and signed in! From c761199ce774a272e303f4493ba4a9c505ee5131 Mon Sep 17 00:00:00 2001 From: rghara <ramin.g01@infosys.com> Date: Fri, 19 Jul 2024 18:51:44 -0400 Subject: [PATCH 182/212] Replaced login screen image Signed-off-by: rghara <ramin.g01@infosys.com> --- docs/assets/getting-started/login-screen.png | Bin 108521 -> 53116 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/getting-started/login-screen.png b/docs/assets/getting-started/login-screen.png index 70b56f399a22d19ad863f77db22739f67a352501..1af52cb2c9b0b95cac2b6e715077bf9095ec038c 100755 GIT binary patch literal 53116 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 literal 108521 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�)jB)zjg-BkI2qs<r4ZhZ^Ns@B3VpNXNDrO~^+IH~`pMiOOq z)f9w}qbbQ$jjmyUOqT^}=lee9j(>I)C>@1lMVynsHA}KMv8q775kN5y{sWPCbNovk zy;YvmMm@e8`US+0M}?{m{i-;nqYGtQ@Dc;kCl#fs6mN*}?X)e0&^?7ZI;EvyDtZ<e zMz-pgsw8w|O2-amLJ3RNux^WYq{_*qUKqjKULJ?TXNZBNH%?EnVJe{3YAJ=<%J*m3 zI!t+*EGxB1(RxN5W<w-R#dI%aYr`z(=yN{UR;g7TOjl3wvi$Pa>RhAY!x+7v*-=<2 z-25t!#`y|D$u)AqqtD3kI79N3BQAT&5hpJyT--5+1+gX_QAc5|97T}csKB$y5U$P5 zAO!Cr2sReqXx1~3xx{#o%D?pA{+@;nECUcIW_K~~Jf7&YW(U%slHW~sB$>XH_%>*1 zivt?=&xQ<5mLR~?NZPWQtr@hG4_FwSov$2uL$6KA==o%f<I1|A`IGoe+Ne)6U{qpM zypM$Zf0QWs?X1R(l~QMIDihrfE|Xdg_dFzJtmmsG#R4zH8`S{`iLuF(?6(chQ^eSr z3?)|eR$|h-L(A^_iUuUAWZ293;CXX{>*oVf+Z?}TI&O@btz}ud%65D2<P9X&EQlSC z`+mkZCh|WRO)DKu<eVM{9u9~?uB)ICUC++$NwnqSR<MpIy{kV6X&<eJ%#-iiotY{} zbL+$MZf*PYp|62wNkDJP1Be~Jr%hOt0^a}JzTe)1^rN90=|F5LO0yU%nJlzS31obU zyXV_~KUU$7dZ-~2AtBP=ZOz^c-@VoBVC#)!S8qo+&tPQG_i8pg=`ohZ>Ou6hY1^Ub z&->wJ(WBp!<4XeDbSN#{SM^uwNn{KitF_+(%PazK8LVOckQ?Dw1>&QBK7oEmv<6+~ z_2=TX@V^fWz4D;$Nve6?5>Q!^_Y~b0*HlE~LWA0)&t7m3b12Q_k@@%&k|=TRfu1$q zq2G`vbaL~D-uBTRp#R5}=Q);NaTR3+vK)rkA}zDIn^zz0;T!ws>kt|TY@#*&<dxqg z$R&3(ggS5>&v5!ilfmm%evBYP%eRNMTn$o1=bgU4f({H1EHxZN2~@UVDLy=KDb;^Q zRl3<Ma7weeITPr7r&G(3G*!#3{};rT-HNk{+{I1tGO_t@`4oGu9%Q*(XTljM&LqW7 zzm8YBJpB+iy>Ms!N##b*N~BF0>1A3J{ok0pC@kzc79EKckW%<fDXKXK&>+dtD6vT8 z1URm{u`}?s(f+2bsrrw@OXr&=e!h0mr3&|ANqULoqGhLf-=7*f0SklFxBoVDdlpIl ztPGWZGV{X)%TOaQUX`_W#5nm*n@FQzNw@=vOFcAzX6vg*)@G#%MN6qv#YWDUH(GJ? z3bA*rg`%eU#*H<J-|s)A!LwqDMG$@LD$1v>y-^JG_)d+NdE%GnsQj_2jbV_RLyz-I zQIbK1v%2WAzde1uP~=H|)vkWU_!E*mfX2xy+l)wRp{ovT#E|UK)ecyBpF@8tF)*lJ z){gZj7(zeZp0L^Mwu8A~W!35ON{}q~W(jFuQactwynk!A=p#I9;L~Igh3s_4@niuh zcl3H*3*(!PaeMy?wt@Za!c5ch-@pGOYk~0XNzPiVTwLs4cYa}G&bG#`>7Nzj6{!JY z;hgPFKS`7;N6oShC}oS-jg$8l3QA}y;z|X@6}CkD=BNNIYyA0!3CVBW>JcsHt!F?A z#I@lfG(~56ul18%_z1>h5JRFcg71TAWoqUx7IgCJGBP(50s9rkg{v2TwTWP#bdT$6 zwX)sP(!W9TUwnN_>zk5ASEv90V#jX`p}f7~@zR!N*wQz`5N>cDG^{NAh1~DP<148& z-is=naes2;!g>Loat~vl07X5|uzesS0W7p_kb14`s(L23dXAybgv0kB&T|7PND%jZ z3bz@Al;T7Aahi1+@ta{6!-}bS*r}70+3qe0VHlRBmc=C`h|}{Sy5u6CLRUkeMLj4t z(tg)bMaH(hd{<!=6oQATB-kX{$rb{ngZRg@1K>jOFbQ1l3+Kcv1`7=UtA+l)YS;G~ zL68Hw3h!U|Qk3LVTXqU2uQSUf2YPz!LJ8SNF>q!=G0oDVHHJ}=_Z=?!-L?I%H*6)c zds`K}sh`)5IjqVSvg~#DG7N`aC8wCKZf11sU>OGn4hv(ViE!-03~qftaHZbH!-I}W zg|n<bAZRg9Iz-IXv~@qWVD;<XV;p$zNV^;0x9Q<U?NTwCz4bF6IG#J<nFrh2)jFL+ zeV?JJ%pf%=m>@Lnl6qZMmh0S<>>)Bxx}krYFJ?!duP1D99lHU+7GFd}7%V{_Lyk)C z@CFP{c)*NM*<~r=-NxFs_4~6go~sMqrmDn3d+vOX{kJayK4A#m_GxnL#$&Ca+v}fR z9&p$V$UFQIX_TZC+Hh7%t`&m<Jg1}eb-<hnbjo6*^mwZ2MEI)v`L;X9($c8ILwj(@ zYH1+(>3DZ_x!Gi~11#Lv??Z-}JEP%47%$2Ub!ToItf?VBh3cfYtP5-ynF$6^0;X_J z+aVi22_i8`FjvbX7kG83XIXN4{AT~pJ`%4=3%1G@)2Bl41JAtE^u9xe&D|#3;fC)3 zd@!aU<`B%+o_Hx9sgwU~xyO1*wM+=3_EDHu_Y<)Ijxkxi+Xa7?bM5rz7hvdq_pgjf zoO$Su$moE$8Gm_+X7Z(W@{X-9qB4>KWA&mmSR~B=I$7re559Upy*gikwbR65)2pOE zFZ0f;b*3dw&lvDCF>h*hB?Dh(dV|DNaVQk5za1v1E?39<2AZ@SF}Tmpo4$d*gVoQD zl+5=4b3ioE`a??xrJeg}Y#Pkf-=5;uh4^!uNO_bFy_qL3xC-&%*N*B@O6#3s9L<jB z?4MuWf;N8|2!QVYDYDq)q3^^i4I#XYAMs+`*x+hy(4-yCjSY@(C-~^17yff&g25$j zs?)wvxmLp>7|Hmm%V9>*lPv|b4JB%0@<p8SKEzu>>cI*&cilx0)Me?Zb$r|OxCmSN zT0jvvd-yX}b5QB3o$Ts(k$Wum?i0%1Nvug|wD8gyUMorgxe)e6SqYq@r2|fnJ2WWD zbo->sVb@%734_00E9*||zrm{4D&<Y||H)|f`V&Z!3%+ROo|+g#2lk?nV~pn^Lpjh_ zF<Qie2u^u~(F<EiCGLH&GoxDVJ3ma;7eitE5c`~x)+l}Nc{cH7>Lzfk4Ap__-9fNz z+X7$+Sdy6)1mi|ADgbR%*ayUo>KMfWdQu+TX$dm;uDW`<dp6DSF+Gj*3o0`{4*7QV z92VKm{CY`}vd_{mx!(=^F9%jfy-F_horl;EEKTASXO`)#>E&R<aLjbI!1aEIn~;U9 z#<LLHUETJwhXYEKfmDl;&wPf?QW>d8ff$rRXs5=va-M8MCov!fr65?UtI)T?GJoSq zf|1b)ABZ<n;m@7t&2EcmkwR;Xd@Xh?O-J3WApg{T`Fn@DZ9zQH&JtX?TNIm`<|v$i zmHb?VB*KJqk$-dR*#|zCt@iSsDc+@7EaiK;^FVtt&SyK-pH@fin3~S+ob;%SUa$lm zh}uzZ+3|fNh4~VeCf&biV)M@S`OWX}=%y0L>2?0q6RZZ1%6XN5)@<B{!>YG?t<%_i zpSKdTZXb2pO&(T*5@XKW%E089yIDvRq(VcNcanlb@ZD|qL|~*Lm_2X!(OFO#`W;DX z{o_#iwF%1-hUN*=8IEI`?_$wW1pAcuC9~teq_Y)!q-@E|;)CR}feGX*_oGFCc#yNn z<7n*BlEU^@NE4rTN0-BS7JTNRN>I7E599#TYmeud3YpN07L8N>yZf{#zth-&AyuPL z8Z1&2cmc&=3#%`Q3GX*!f7`r6V0R%#E;!rAyeaYSvGX=9v-J(`S?m`<_U|!#27?tV z4xN~pADygNIy9fWD<8P#U!uJlyx#1%zCA$nzB>_<?Sq$GMsF1=zcuBvx8I+8zjAlI zOUmjnP}_>di?zUe=^nbjS`RB2ZgE(l@)BEx=q_a#z}--~P~Cl;x481mI8d8o5;RnC z_dI};CF8=sur`KMZ8?mlLc;V%bK9pT55-GY;M>hcTS78fguihv8h|LduFRo8X-<Z+ z{6U9<Wl+k9^}v2OByzN1m+9dy<)f*Lhv$lnkBsY7W1o&@9bg_Xzu1dC3=@vHP`!_n zVQ)-Zn-QTi{ucp%hGBt;59U@~s}ID$=8!L#EBQY?yuv@5f9O9jR|%%vFV(g8MeI*+ zKqEBEn|qw>ILf&QUO3-!DfMHsen5`mmV?FmFQrDC1nk}P(YyNs(H^G>uDZkAb_dhp z(vf#@bIjy|I%31=CIK`-WC*Z7>z~~>Cb_A#K4Mb5A&J~7LO43TTuOL6fR8e9f=jJD zEG@T=kTZ87Qc0gfx%hMX>gr;;1>H#G-E&2e=u-vkX7HQq9SmV8ZKzJuOYaBoah|R5 zoPyJK5UB${IOH8usmUZkTBZd4*p&mEuS#%zB;fCIl$c(JvVW_tvT@U=b;fTEJr)_^ z`;Aruc@F~nb^na7p+Q&lb}<MkDN~I?wPP59e-};S^qK@8LwhypioJH+bG{?FQpA?w zXWw}q%bhz(ypm8r{`ysS{h<0P-gN5z$gu_@3uW!s)zZ0!X5<~MkPye86Ws?JM?NDS z$CE3UEVc}6_>qO37=h^&N|p}$N#pCpZ8zt;>v|jujc&mg?(LXw$ZX;f7lz5_!%I<I zn8v?Ux6QeoeW_GgB-&XQD6#nxU&fx;zvdHbzQr`%Hn8lSHFQv$t$1s-<u%gFacgkf zi$Z}e1W#zhg(-7hV=oRr*$i5f*M&AiJq4D>2gAv!MM}dyAg02(Q8Y$zszhWoA?x0m zQN50y%eK9Ec>%BIFh=!#yzN!U&cafaW((5vdCyTOsmLr+QR5`X`?A*NM|XFdJ5Gl$ zMYFM3My(FWN+P%DS0q90P>#~e3=6zz)WTgiu6Y4V*n9#CX8rYo`p=QNEdz%RqSttp z=D>h=78(%Uwb-IzxL!W!^*!oNlkI;21o(dkhi`W57c0l(PI6rRK6Xer>GYNzj$QiY z@44xUMQ@>3jXiX)cu?!)dL~N#;BYgFbJEuCF#E0mI^D^)Zm;#r(6a*tMzDOJea%>` z<vK^y!d~OO<Cxf97CB3F^P>4|s)DeEXGP-bSftLL&NnvM51#v|+CJlj>@EQ+z0@NA z4XJKBduz}-LigM0CWmqUxlT%S0!ObcQY1-}+I8nw@e7wh_8H)p^Hd#TORmlk0qr)G zG(kb5>lse$()rK2{H5i~u=1l1{Ftg8V^K=>2WNi%QG7~~;YYvkcJl-lE;7(XMG6x+ zTR&>THBveg3Z43|g4~v8?I{0T`oS_BpOcZLu0ON73_#gb4mP;xDTzN+>)X0s_$S<9 zJE`#UzG35^ZMk3)9rRJs%qkd17#0U09%TVL4X#TmT-iHdEM~+(q1`nJA7{!xgrAPO z*<^+CwLW%cGG{0D1Qa##{2=0z)n<Q6avx^AM%I@B)FW)gwL(ik3G!DU>>9paC}!&F zQB>|R;T7$~<+md7%UsC3c?WER6~=^IQziUXA74VSHP&W7ZLDs0SOKHjYiUQZCx2e- zcRbPlOG7M=o?m7aCvH#tqh9b`25^~}$kzTNPu*qKKhcsuEfC}GDvWR1wC*RxGUHD% zh|WS7mJB&R=7ymsx8bPjZiDQ|)u>b513`uF2Qe~ocQ&PScT;jDgfrnSsU))<YP4++ zY9WW6<q_VxuBxv<Ao2XL{Tx1z<+#|zZnJ5SvC8hnyjLG+WVjsG?c(cSbb1g>)LEy0 zisnEz&($%1pGrXP1?^?<#Zn>6@d2I5VhsB@oNg6bBB{;pawNHD_k4ZnfH=Rd?zkcS z;n3Uki>xq~G;fXzK-JBozKVMmr-?CV+O=8KS=)#Onv+2;zxMJjqLjg|(A=RKy%eO@ z0;L%p-e?(q>luo=(tEppoLaM!c=YK4^D_wK;2Yk$jh4h;!-}$({@Xd2;qgc<q7A`s zDcs?TcTnIP-f4BpIn3!|-NxKsQx8RP_N4FRN?&*c@5u3JpF3Ev-yWHIIBtcYE&qWV zixAHM;VpM_IMTeQo2kAxnkHt4YYB~jxozo;^Vb<<JkTteHd{-z-yVvfpsn*c%|R|R z)gz1dntDKDhw^J1Wj2HPh!-%gX|?G<JFkrAr^9@&ti4vI<Rf2habpKrVRVCwW(ZV7 zSFP$szEdb2kDfLrnlMTi&Wwk*{#DU#+1Ojv!bV(lTOSawugxwSj8%5EJ9#J|G0{;3 z5;NJzt?B^;(pvA<s$#nA<(8amo{q0ICG~)oxqaGQ@`W8jW#wV5L2coKT~!5HuK6B8 z63LjbRvYq$1k`3tR`iw)4<UY_Ter2@-!!SCa8AE=5(}-nJr|U<kP{;<9$|tX?ggG) zRRDk)FK_Bc%~t8$8{&Kp2!%;cXC}bKtiqWLx$Gom9n&wcFMS*LCg80Ev_DQ^Rbmk6 zv+wZyFBR?D6#PDN+ER2p5wOPmxBeWy*K--Su}``3uDiaXG1WVq52Lz^?6qz6n7J;E zJk@D>lj+-z<DV(Q(5+m4QBq*qXNzaLL_j3*ICt=6F2VO1#w@`!+rAgHt*j2G-L}Sx zTuEr3nD~96n#G+BhS1#vyXgP{o@pC_Xdqg;=gX-i5)<b=jGRbvc<)r!27gkdw=+%_ zJ(1qa44<Nc+VjIuq6sSxnu!RxLu{8^wMboFq4#<GXhk7;xTl`y@ueqY8G{=)Tl#6z zR<&u&9bCKX<M1c6q$F))sD8p|Gb|WXZ+We19Cykd;9b^)j5z!*^&L^92xcVb#6FkL z4dTG{0>;n@d|4eF8~BD!uUdU*Vykg(*-0sn!O&-qY6CTR1EYLpy=SRojvaR{19elR z=|w^r)`23vzhW5A#|h4jGuRc}>$}x74xIq+R>6aScaK-ZJR56FKOM;6MQ2&O8lxxT zU*Pm~bAfMO8&<B_a*zj=-bV)S1|57c3XTXKce=U9<?=;GBN_}On%EzGb<#FU;jwgp zAnJc#io5MTIlPoDdb9TR2AXP1mHS|t?u+vA?u2CK5pvjGOZfWhv=V^{vw=tuLzm-y zf}YQ#;6X6#VJ^qKjU9~A9qKRX(KNXR#v+-1q<jswA|zv|d0c;4`G44Y>!>Kdxa(U4 z>5y&^R2mt&K}0~hLmCn3ZWuZR0g>+RR=S1|knWD5Q@REinCHUZeZSB1$NML<F4uA~ zoa>ykzk7f7>O#^=0P6KtvrYBWeb}i(u}k-Qy8DS{qgU~jD1&VGLelD$5p=h3u!=T} z(TxAc$8b=(3w%$B>?8^lyk5l0*WVi>tj0<oOOO%(pNw7@w2X=y7V^07;oth<<gu^m zv8*AiaVI6PRyN>76U3o-u$n+h>t8dQiPQ#h$N1)B%*l42Ria<)--eon+hB|}kZo%P zz8Z%bZ^M~C-F>2+H1CMw@cVp}TRHeP+2s~vf|>mSd<0D3AMp%443rIE>qkG&qrJz% z@~P3gjtIK<!6V~fe?0B#pT*8*3C^~QCC;SgR7Go&VxcBvy;&8Ci|KN?07Ju!<!fuU zb(HUY&R+f4MCnB1!-uKO$p;lzsC0w<s2zEbSaez8`c@fW0Q^ls{x5P=`eXj>nx-a@ z5*jMLHI^6V9DAcEH#&4r)LmYNdDBc#cmrS(PNe~@bnHmQ^;bYw8Nf(T(b1en0~|PM zM4X!OyAkqv>uoC|b<5f2NtH_Ozxf;`6z;VDn`GFiTq~DSOuBu&!0L@R-}EFbzR5kE z)Ly$4whL)GGd;Y0Sg5;0#fI`;2e@wBjdlrMi(z7kyJ%ZnuTg^T*0Q={v{GqqwcZ-U z2sy6z6Ml868V3zrfZ&r!CIf%dj0c>nx>?M-&`!FXue<(yRPKzh9SLS*aTk5=`~AMa zh4w1wfJ2m4X=T0|ItmR<J5ElK#maDYtB5nj%&PRCJWj`Rez>VuZ`!WD!tq!wX)&3C zlbR_0`cTFWOkEcmOis^=j2D6+{>(9`zN_8gy1T|m!ZirHCi_EsJ>>JFCc-cG%Q%WH zZq`x@!(laeRZk`jS%t5M4nH|m5w4MSbN0zagzXqT+o1KOFm&KUFthpH&<R}kl7j~4 zY<2lkR<A{fl@|NPl;b}j$bb2v&Wc>4wua2h<!>xQ?tnj~1(lMCc7AM1FC8u4g2S-s zcc*Ju4V~$JDH9kiNiC>HLK9Og6&N$+I_R^FjPGy>uQ_)D?v98(R{@O$MZ|BryMg3( z3(1p4{&J(+4^9xB?jyd`upp|c)pjN<uu;Mvwdgi?<>zosJ=>iy+P0TDYHbuq`DHZB zTv7RIaaIB`V>nW7p?|I2cu}agx<I-T5Cf6~-{P`?!6a0d9Yz`F9ASocT_25NH|RRg zD@S+NPIx7U#~zjPZ@^M*(Qe*rx7wO?m>!~BBT;8}th!%aL~B~a8pvu?07f81WrBZG z!yE?3Um9=PEiYlPWIa%Bx4-V=7Qq~?<kg&W2@1lCQcan-z!z~CeGS?VU^LkiO0ULE zZ1<r3c0X==(&2}FXANPca-PDUsM+}CPUDX+-iBibKY%P_99Q|LTpJ+0r1@xB{$js= z$K>P}fsJT>qIq~y6)Qn}OzwZV&qQ9p9#e_Fob{>#kQm>i8Hvr+wJZiUjyag&1EwF3 z6N*N&w7X9Ca<;RlRg)wB6y4S3ZuL8G_XKQ=vkWcLcaC}<m(o%;36*HOrc0JWfOTau zFR&(O=cB4h5dM~%bPS#qQC=diBkFI__D|nWG0Uv5C>8Lq;~v+R;bix_Q@SX*A>+;8 zXuRL{*o$&;r`F|cn~0<?&yXQN96P+4SI~A5&(&M?2w@4ySN%$fTL`vPCVvVD;6d0> z3Ov(K#5p5&KTNy{ZEQ;Y+l>~6=j-QbLi(>q0uB&xdN#v*r1A$FZXP@6lR-l-BC+S^ zr%}X31=0Gx{fK*UkS5-=oA8*beyGdT+I<vM%xg{gx{Lm!gyf(w_?C%H=Vkp0TUXnI zsZQOkrU$GEZSBm{dG*Y%-|Q)wM`x3RlYLA?3>1oaZ5&Bw=AcK{6ogOy*2_6;>f?ja zxyj}<;c5r15yngNWlV7o7-u7b)-Ht9wo3C}HK`Bjj->6d0pkBPM(i0L;@no}vLAJB z4twn6=p6Jb=dwkWT(61Z+a*75ox?YD?l&8ovN>Tix39Y$k#t(lOp=?`{u;0)*&cWQ zeKtzTdZhm|>DYNn4D@10XbgqbFdgH%lF(%A>>h1&H0)Vu*n(>d?Njsq^QR4w-$@Ah zMVdl~?x(v?0!yAL@2vFN3-kKI`C)s3{Q0xZ)Dw#|t%MAsSG8$qGyNjfUKmRZ)>czn z?&Z20DQM9oSgUVumVYJB*Q3%rEK`m+eb|P3Y}uaIKclVm!v5mJk~D~I-w}ZtL%dKn zIKJ9>g=f6h3hf}^t&(4?aT0@HD5@*kvt!R)v(hVq+V|7Uo$8goi0R_NjtoP8nJywa zTa6wt#Nyx7n93jzdBa}oBskkmb!8*RsMV)ghnE7L8ln5kbsWuY$u$}3FL0XCJ46Fn z;(%j9O4)<$U){<@ny`*nIZp26F3{fx94pvK@JZ^TEdv9jB>0j2NjvxJv<!D)vR?FI z!<8T$*42d+Wue8+tv}zdNuW>MEBQ3v`rzZzB({MC^_t$^092>Q5&%x5_tQz@w{&#V zqeq=05WJXEUx?~8-ONEYR>M}fK5A#f8A<$p`S&Gmi`LcvJ+LKl&jo&QHhl@Mzj3y( zvEoWB8!=9<wA#K2$t@iRb68ZCGGM)17B8{m8*?fxB?;DHk+}qh7a^seOJn}*3_s>w zBg8@kOV^JLP<(3tLLp73r)QeHIELW{o5e{t0VdPapC!TzoHS_R*#?h;a;9>}y61hf zcuQfG4C<YjFTu<fNg2(;*PiJAcbxr9;trS)hN?RH=IoNcG$May&8TP1LM<D5MXWOG zEl!50U^U(gjx%XYFaPCsS>}LHhXh2LBZ#DxT8MBP&oCQTkDUr%6WL(;wYa&0;8>wW zsd^8zY~E7QSQjuc@G&uHbw6@#EXx4PbbBa(Sy>b>ax?8VK;sxW4;!i%c!*PN@lXdc zGdp_VS|jcr7{&21pQc8~(|18lZz#G;^|BuJ(7BW*lK3e17k0s}17FVrnLSj1zVs41 z>;enP`kc!s+r55S2qr0?ja$ZD`#lg<r>?c`2Yr|_ZBP%bhsfVq0o&OXCk2y5v}*WA z!8bWzXytMdXagaZs7_q!9?)qNyH@B@gq-*k#8lceX&4wuQu(LkJ!k@s1i#`xH)(Q- zojLq)f4q3|S3agQhPDw$scDM^wjUq48h-bX*pjZn;gsn9Iw_Rbq2akc?z!ohTaD{{ zsrE|Z#{7_;{a+%5#7Z}wN*_Ys(kX=TLjjWKO-h4F6nCQH^2l8il<&vjJU10;a`WjO z%$CvM8@TzoAs<iKe)G9lcEhaY@VQ;1g6?vO+LD5+qsv5g+lz+fF(Hqws$3BF=@h2( ztW$zfuxWJPp}bM<ogmKx*2Be}Ys!|>MFq5b0@1kfaq23-^nGjzhy7DX#)ZGi{h8du zkT$~1Mzq%3$I^{`76@4=dEDHn1s`p+0<O4}2}uae<InFQ(DPOV)`@NLfiBi^PaVp2 zOwetp1yP;oC_Mff7>xhU{$nt^Wf_Z56O>JD#>CXr)VeG*fj_N*x2v#1FQ=qnK1F(~ zIO<EL(T8bHW!sQ#U{)lmuR7ikMA4gmkeX|PW+fX}K+j}h=b<}1It%$2WFpbU(u>@$ zKpGTLQB}t7)bJ4)9f5i{BDT(9p`1dMmcs2Or<h4Nuy@bhc&`CBvfwD+bA9n4K6G8l z+3NRS(I)GecMq&#V>#2Czu%YmH~YR^-NtzNja^zp0q_0(9W^>(yC>4nT_=yn4a}!Q zWh)ND+ZwA_LdCqeC?E<4q>cV2?E8eBZG3NqB@_q11aTdtV%s-AG=mkbXC2AkFZi4~ zG{OT9Otx?uI=BfLJ8hu?xY0+8lQ%V)mLKWRmhGv3`!3-y^hRfTcXP^QVm25!@QR#o z5Smoym|?DkKDm6pYkr$Nkf?0Z1JO<5=DdD68r?%AQV*SopIk5J9mTqXe(ApUvdWpQ z)TRA>HjH6l*<RVZYvW02mk9O9wHKY=FNQLkkSrF?C|BGVeuD_2RU%|mba^`?Ub}ZI z9xPqb>&5u2?!HtC(SzJcdU)+yjEa2!m6=}o%W*Jth%3BO&<2Wa&8~OZq#;?Au~qyG z2Zv*TlQdR$zPrf2TUTA4-G-A9B}w8#(46BKX?9Z11$3r%Ue(7t|Dc|XMM-$J>xr$P z*JNM*KDbqTPxq1?=GB%%yxHv6dH<90oyeQdl?Wy_J~wu4q`PBpxtX(O^7(4qBYw6; ztAkaJtmINVq3)48%fSJ`d7IE{=y~xj{R7<I1FD$G);xX0(l>x6a>erBKv_AD^C4X0 zhv}QgA46Y=M@iD{P1mKJs~Rs=IUC5n-J=>PT1O`PThw0obHY+Kl06bcGq=SulD8!% z`?YomuwL3470sQebd9yP^_0B0ZFMYTN^6V3pytrj{1uY;9$*ky31~=Npw%(-Y`Gfs z%xD8={ofOUa*|{gNu{Ca(hNhTp;1`x*4#|Liv|a{m9f&}rB!`ubd?H>2Mq(K_=u{~ z+vlhYuS>1!i%(zFJwB=lSo1kUf~H(Dy_hRQIFK$^N`x<Vm!>^$`uN_U|04(#o|N^1 zB#3IA{)FNjzw!5BFk00?3^HymT<EQ>qaBB&aI_<_@~6_^KApM}#{Uw<S4Ux4U{Lh{ zx`J_=j7ZIP{hD3DMYLMtiU_YgO?H6=fb!dMt%L`j;S@7cb-WoH+>xkLS7DNywB->Y z23ed4kg<v?uQZWXA{P64?NX?OX5RYTi-Fcp>^lw#3p-0GeR1VET2So79vsN%^(g;1 zf$*j}zjs3YEq4r&B7I@bI*#2oI<8x<K?i^?@E)?9-TrHh?&te9^x7c1b(wk>jn4%c z$CS@{F{!9y4z-Vwlb|l7<rhE43y)8qxEwCYQ(e!Tn6IQiM^<8#YMqMAzHE&@IXCh+ zq4R(do_mJd=-&(I-wYL{U=>k|nw;b?b}5f!V8<A_V!=0x+`H1W;ZN*@?L3TWs<(cP z8YY;MWzE3<k`MI`uR!VL{Cf{+Xwe5x_$}sGWHf~|-r8!lH^}d}@e(&dJqTP5`B4oa zr=%ynQSKD3PU4a#v+bx}U!4L*ATN5*9Eg;r3}o<vkre~>T**YhJ75@$^n6YHUH>11 ziMX>^@zTbVMy+VQ$)Ce>w&??1ilHP)xkD9PZB)j&Ql*TDpizYoCn<9>7Ma0kZ6YV< zZ39JMz+J^%5N4=!dsDxqI{^$?ZHZfD_LnPK)SKRINinl(p_>fSf5X!vGC^GGSO!B? z_g}Gov#D2C_<xgXn1l7(U!ceM<ELu85vt^odS6X7nqn3sa;+NjJRq`@7<#n4Blo3g z1aBOVgh2vC-<Ud8HzX-~a_h^-!)7)PK3EixXpoU8;Ax!Q$Ey^{2)2`<X>J5|evKV= zTK`JXyW3bgI-H9=;^y#t(W#zLmD5jPr-JNQOC8y#`P$`Z&&IJ~o#6Gy>64RVO$N-v zY5wg$&t03dddY`sNBVp==bAN7`m_@}Gh&7U?G5=Uj}U+0SFnx%#x7$=wN)Vxr|(BX zfNtqJKe|@I$`N2GLyg-FnOgLm(@zPr;#x8y%5!}=?kb+_u<a{&aPV-Bw~Whwe{VWx z?4d3i7R?JBIcUjTH&Z~@XIb63&dQ;&qM&m6EGBl@5BJ%|V`rPkyPiz&)y3JaGs>dQ zrMQ@|xV!5R-(~9|Vi3*P)d|i^?9e3_+#|SFH=W+_L9={g5cDc<tYYzn*<!^_M&S*P z?-<ypDf{xTCo+lO-|>pnY<1TP0S3EtmHt06QdEbY==$Jo-s->Rv)yfsv}laSlccw9 zRk*;gTe{%8(2wr=L!1$=+Vn{Rxl~f&O|ZU{!HPGIyPz)3wEai(_N@M`pSv2!qFeA8 zxJ|F3eL9DRl02%a820zP#KxSC9JKVyp5@J{U<-s4nU~zvAd*XZki{_nm*Ik^&8`g} zN2Cm**#_N3a2%bRAV0x+Kh>my4gdFi39lE4XSr?q4_FC~Gnu8HvqjS?96MvF2VRjw zqwdYQ2jK?eO%;)2>Bl5rEvXmX9TLsRLYdvGMV5ZrQray2>?vWJ7?d}--`+TWp_X1) zCaldy`_A=T5-;YLa?t-6VS;qH@(TasK;B5KX=vg=DoRHQ9a59j(ru=!Pb5(p<rTM| z^#I!Xxg=YxDg<~Gr!?umNBzdYmyJKxMdIj*7{xCs1G61+yizb%`Y!)Y5W5|&Og8!D z{a;laq-s$$1N!lmEkW~qw+MI%!}@diu?_Mu_T7TU-&K>4NmPih6Tp@&89Qp)L=7+P zR<Nzb4Uy#&#WBQn;aW=9#y3wURe^V<0wE9IplZl%0Mx(C%dD!(9uMN@j0l?T%4tBL zeu31W&iEcJ-@c5z;vK6(peH+?COB-p#e^i6U-0ffOHePi@^HJLLOOpVM9T1YmeNWw z!DTVcxcfH7q(_gV>+dt=POcW^rzw_3Yc7J}EsT;*Q^d*^O`qu0gt=i7=a*Z7GrzW1 zk2`BQyc;IUmRtpf#5`#?9ysyF(r6A3qC>nnMEd%X4Mfj6q=%60At=r?z7&bxfcjv} z<Tp>ncz{(rr*q)8?0D9uk~E5?C&uj?`3D6xATYLQEVyXr6eNJxR(J@R{V-<`{jz?q zineNXqk;V4yk^*mKC6FGR21@T8={!iNiR4MkE+vN0(b<}Ud18u|Hl~p{hx%Lj>Uoq z9FVm6_{;fFN$DAW&JkM8C2)n=B=kM)<5#O;6|zyBDj2wTQ=Kifnr|-7<IVyI1F}<R zrE@bsO<T@I>C2gN<!I6?S;+z0wV>?&Lrf)b64Tb1y4*-!ID4mnA!;mC%JM3H4_fLt z%^Q&gocb7<P9=UP(z8Z|NuV;y*m45LxRSlMk5163je~EI#vwIKecBnyeX5$~?PTAX z<RpnM92V3xRo7alg4tJ2dl+=NZJ2;CTPOxvH_AATUpNZ+r!pX^5lMaAie(*z7ttbd z55o7o$u{j<G4ERLy9^V$?{s!XZf%CE!j6WHyTr5&p300S<L)3HRAFuA&w`f>rNskX z3(bVJx}Vck45mz1p{84Pwpn=Ws0v&bTsyaf9W+v=gU>ie+@2l=6*wFK?!t>02;SY2 zoA8kSEeqRSHWD8hOUBRLB~m5CZU2~zjijwqz&6?ux88MtrFjOrq|>dgbzmLhntmw# zPJP#JtH)s%XSS=++ZUNTsRjBTW1HKwhuq}|^=0ttk!XcpRLucD%1&8oHzP-vKPv+Y zg<EX#ZP<_@8~<wVv5fW(4kPDxuV-O)nQieiJj^&N4<?{H*nI~Cxes&qF2T{^{;h6{ zYqm(cPoLpQoc*J6E^=~Ax>oPW-{D{OCbUz$1y`wMXVziyY_w;A#@Op~N_r-W;9??! zd0NNK$9?!{LH#+M(@>;jwsrzl6gq0a&;gIZ`G8b4RP(5zNFf56H!wZ8x&&!F7_`tm z$a#4c(0*j`a|$o8?3OY*VXkV)m5*1rGdbPtm#u>Nx%@L>-JeO^>d3;bNqLzrVHpa` z2OnKLwXMSA|DEIL5<;tenXwi4Zaw$^N7k*)-vu}?8rW;dn68Vr?Q-Lc7qmALzI{M$ zCGkD1jo#*CFtN1NL2mWp`I+nM2;QuOggQ`ZlDg%XE&DK#86p9+vecsxOCK&u3ufS8 z*JF*a5@1@!h^wk1)!Li?vA^N*rO97s9IOe@a+`$zIcde0ZL&dSrMMDfb{el=W{u&R zC&sKvWpz<&PH#kN?QXET#}zcsf5)g&iJPZS98smP$@VGPCq~`4Vd+yH?;}lrG)z)| zGx)G=*YnL1QOc>HwyXJCnrSjet@m0DfD=n+%<}uC=BWD8zf0!>c>%r<Bc*E%oRgs{ zKa>q$#H-e7K4Ioo$>aL+cltiWGbv(HHR{yZqq<6yZHygRcFo9!kRw!&Gqj`*>LM_q z5@Ns?r1EhD56(AP;?5DO86%1*|G>(~GIivMvJ_2;)s1G~Vc?gHW4?U#Qed+b%Q%bD zJ|r>=Op^$%?u7Z|4j5bqVclD!I{oSAM~m@)>9yl4{P|FPr-Re|0rg82{H+qk;9zy{ z#ai}$ro-5Z(^8Q0?H@c=@@$*s%H>b6v8goOSs?+L=-Zqu8pC$kJ&Noy40BIT59alP z;3U5hcjZtM!#WEVV8~cZI1+3!F}#+I4_Zn_I|?;@)REuQWb=#Y*75x5KORBo@9$qZ z>ehPxqP*a8{dhND1X7L;%~_sMHp!sU-kYXoP0;I(w1*Y%Z&IdEuOEmxEYPg%=!$rC z(itslN9(C9VufD)*r53fHDwem{7JKdh@-+WzFt=PZc2l_LfmfLNbgtw0Cg${k3r|o z&J_1G4SO`{%K>zRMms+6f1kcvdj-VA7g=DE&6U;_+|hjj-|lT_arE%lki#pdr<KfN z7pM);ybiQd+uM_>tZR0D{rp*PD^<_O+X>uhGb*=RRgz+yL<{OPqdwVTVRJ^a;9wj~ z@_(JWF^ShTevwS`X>h`M+=n_fhGNH82bGbJsH(X5ln#fxnB6*E!X|WRKC>X}&RaOO zw2!nCRI=NSJ1i6B1U3V4Q4sSQIu1=uFb%Dib4-6Uny-&KuCI4GM+-!ZpJcktW_Sry zKx|bSMnO0Eq8V2VuO%&(EK6QsPtRD~*tqgpqwFkxR2FdmD>0^AnbdQI=xnK~eIuMd za<7`wlZXoIORIpos{%U6az}M&2abT<<h1JvoG}y^nEl~Ow(l7@b8UWK&T02~rr}l@ z<lAx?W{Q^eL|p9f6Kty~Y~VF`?u=wf@QxhzHrNcU6&Gq`wg0pVH=5d&0lV<TD#G!- zOO`_+f9o?p_&Kt}gKbiG#c`_cUzecUfbGK_`t5L@?)%S{kj&6T#x#>d2qNVpZa*9J z9E&C);1lGN*c-_!xF5x)NP-ZV!*2V>L9>seaV8fuG|RXVH&$3BeF}?JD>$J(iox1L zq>*S=L|8$V_b>7;!I#!a9U<Q26D6M9Hatst+DBjk1?q;Z3*Cl|YV!<1yw>Yxfg}2i z-`@*Fy(KgH>WFsCOU}hfMP{NS==9ngCC8e<2gcIDTEUXsf{&7YXH0wG>EsArNlRuU z9yUmWiU)qpZr*N{`I-HitK5*Ih}C7;)kr|^O_1l*=ZZxKoSJDiTrmn<p1Q)D-H%05 zKl*PCooFr|R>-T*LpHeG&cqd_^`Ep{QzNZw)aK)TBl8ZXB3(<6;mvCs39L$KhAaZA zJ?3~4|M;PNSa)4p?IDZM!*`CZk?5r1J6d+P?MeS)Up>kJq<irHN{+vr-o}JHX2-f| zyIc)gLT<C|^)EL5ZWVp(e-37nSROYCQEk|#*j4-Z?9mjY9u}<>@~GG|zcU#20RZ>9 z^(~N;sWWBjS=#bau{QzjE%^t1fD1Y&G);qF+@k$af@Dj`yZsN07nogLTG}f?%};HF z$yj<MqX``M$fww*hw*q<^8c^BIt2JvfnBAg6k#uVc3w5Fix@DA@$K`@WrvP_W`WU< z0yD6s%59-9ev;~%%VZ2$0uXejw8yLpL$wEkWGfFr%OPG~m@lEP<Bdk6m2wQ)4bE{l zSa4s~m740aR}&aIxkifim!6v21?im+cu%kQm<C7WWYM9r#JAWEMSlv)V~Si3zBd!3 z2_*H!!4l&Fb6f0mI`q#j&#o;3bX{tJ6BPmH&3T8rVCIvlY3{Ww{*eHPvIuk8R$#Np zgrYBupVB5Hp^eHCcUpK0j4VgwIO6e!R|V4dFp~T$oe6#U@+yed&%3&bYpmlZq=J(s z>DH&OOF62d^eQMT(;EeQ$kOD?mwBhjD{JFFs!1J9XKBh-Vx5Q3(>%eGh1#s{NNs&I zyiE5u3ZnZeWaFbP1U27{pPM<{6GQM_r^I?IuV)_44U=2fO-<#0-3Ik`vZWl=irRzl z{+iDrd_k8(lA`BN+EQXwcl_Qw@P|Km>?{gQbydg4tuAHSHBO(S>??<U4Ewtco9F5H zNs3_T>ptTXIms+>a;8mo=;YkiQa}c#S9a;ostoz5NesLMhiLe58Ngd_$(AM+m0x|( zRC+i&>V1QuU17-7=Tvf}fdRPE14@-Dw#JPT9~PGmbw}G)yE>hg0y7{YG&D4^dWKKI z_?5!PALgLnQ&#-$WIc2&uFhyQ!1yCAyI=w7Z|hE0?Th!{<yU|B;IPM0AfCl={28&o z>GxBh8y`jE5YLhm2mPlvb`L+R00c*k0V%_X)fltCdDM_9^$)uubgl4OCW0$?Pt2>0 zAdFjId@Zsy%JMl;>N*f${b43f)zvkgoM9iE`zwa0Iw;E}ZghXYtKclcDP9;Vg`-V$ zarx3-ruXeMQ&3!{TE-zdhUpQ%m5b}}awbHXU8Z;|tmL(<4iG@%(jmve>XFyhV_GVe zCZF0!Rjui-E~GWunfu_}MVh_usHtf~_wzB<bTU8I^poJR5&#Hc30!~qkbFtbAiG4B zjCQgUmPD5?ROg-f6w$nu^uT9H4~OT+I@kb%AUcb{1bflg`_&cLfG;(b_k!~3j?9%i zZRk<GxW3DP=E+h+OmIM<y*QHqJ6W(GJJHGwvdC3tj#HttU*kU7Y$r7%b_ks)XKzdQ zNnSgSA8dsg^n2=TEzYk?l%s_??j~mk&i~^2_|5DSaqnAKNhzskR05x=(px1ho2qA< z9PXNSI(dO>c;8VI<;8*OOJDQ|7w86x$7ZR#@q%iQu|r0=WmqN%=D%UGaBUard=1jT zwY@=qxOE3>l;9$`{SJx_j(}r~-KPgTkj*atYHHP>eYpouL9zI%h{sv2X?nH50hKF* zh{q)g8KkV_M=7zsP~Nc!W=vYNe4l~w-v;|oG-Yxm$@HSh7Qw!)&Jsf7zatNeqg~c_ z8P@@<j1OdAsQt*WN&2Yu8gYO=fMn;bXP{i+w`OKIQ-@2GsF&hIy|f(BeIL!c1YKHr zU$=sNyhx_$srW}j8`x~86`KvPW`)_Mewz$fC<~bXJq7)~AUM7SX6ev&Bo4S8lE}kX zI)mRgeCw2?9iSTOC9PiFO`E7&jx4N5CF(T|vA=V4XC1NTUG{z+Na;s$XzAx_*u~NP zdqNd^J0GMYBmVFwMS`6tWA7yVzDbR?nGBsmgUsIup`yC{7-Zt{t$k{Dm-G#7`}#W@ zVgqX2!?(xea_G(_0^Ykh3F`b=){=SGxF)diwxQ0{mHpyynKYD|hl(N7OKi#QY~0Rs zE#LDfO?dNV{L!L2n7S9-KgQ>LhgJ=a^#VH)+ov~cXbIcD73NnhBk2|wcl~1h7sW^n zMfh3+P26hL8rq7Kn0b#Q;Dx7Oj~j|B`iutFU*`~80=K^9XY+zb4JGQK*|T`lA~_cs z;D}QSsp#XNcw-03_d=g2XZVC4ScA{5oE^%j?B&N!0YmvC=QT^qv~^q^0I3xIp!4<B zWk)a1@8D+vWPY<V>mQGxmB81}#Fjcqq+x5H>$=eO$jsVJUL~0=Fi7-4#?I$Q87hFF z&%`-?TIIGzF}5bJLX(_)soa;K*@ynFT)Qw0KjXuqPgR2LfW*&&!>1CjIX#T3E>!#l z>k)`zlWkBUSNJkcG-%&6dAz8)%t|QQFk+1X`d+0K*qZ?2(y11p(*l@r5>!n7M?~wL zdV+$od7QHMyTkDU*2mDa`p5T+#>wv{E1?BYX0;JvtOt)ll>@C8CKm9|%wWG;pOe;$ z*xR_mE@w%_80fpRHSX%!Q9lmqkY>}hSwiK2D(S<U+#5vav7Zl){(a#E^>4nSueSTd zvU)3lltoKVuRkOS9Cs~#V=vKyfU&5q$Mhx$2Mt~%E-sdR8ct-;X@k27JX}{SzOHP( zuoIY_dKT9e>h6b!HfqQ=amWMfCYI^yXq=8;6!-V8N=BV5NHaMGV(dFKcCZx<4=u#u zQ-L+Qz2~l`Yq9+=%k?O?CQw*2+i-jD$ETiUQhS`j_K2O0%S)6?b>QMUpc`FS?8W!x z=vE?<=%v1~t#wqLvliCXr&fQsP82-W6`Y&-%i%cK8F#rCD6SjEYg;m}exPj|-c0@# z0sSTL@N}{B(1vg!N#838ef;L%G@FsG=h?Dl+ql=)(51ik<+Q@uWV*AhU^2pymS!-t z@W#}`<G~f=T5v%;(zLqc;3+Qp^wSON@3V=Q`!Ys2F@CoE=*3vqUlu8neGU{I(zFB# zVKc*=N!?|ASHtu68+Xc`0q!CUZN?|4b^!I^B*9)eEDY#d;j!q(Om^8)bbz=Zjf9aN z3q5@*Cj3Q$u=RXaZ|%gW>boy1slgd*;~TddmbnWCIIJ+MD`VU6oD2gh!rc?>Y;<N^ zfb#I+%LGQd$K=npp1rQG-v&(pu9LJ>STs;B5$j0_jjW9~k`1N@*r-3Ff{&tq2=MF) zw#!mcJ`(Y$jKu2aiyh}10p0ode3YA=j+_Boc$?#g=@aXj@>tW(i^E0I1$$Ed%WzrF zxP37*eTo>fkwk*+^M=)(>m@Y5#6@>Kf+wwz6MR@(4Q~*sVs-r*w_xR%sk?wSx*hi* zMP>jutuAKSp`ph39JZg2SE4#+@Z!4<-43J2NnLW?4|miqnEC}5P?ynkqfR)UBJ}xB z^JQT_|6{maLR@=z>G>>~u;)N-{mfy#eP1)$>I?)SZp#1JkcW?F(^moFqfB<fTM$At zzj(eXten=~zbQpneplRS>NzC8Fm~b%doV@#*TqJ+{4uR!_m|Ilc*VaY7P=E%`XL@p zklKt7I%HDsdEOD~o*8g%M6yTMwK<mt3Eitojj>+th%?C)&_aKMg@Y$OfGjv|sYK(T z-ZfvDD=4t`+lgI!`19!OEH{1OhRCu)xxszC56yBrQsVt7E^Iu_1MZB2#$9nP3-z<X zUAh<zRk6&<X+k0qceGdPdA@G9?43LZ)u5Y3DO>TJB3Dm(4b4YyH+i^arR_kQ7|nIF z7r3zyJnC!aU*kyIY<!kg?uD*&cZumJV&=g!Y{uRCFcNmPw{$@_gt+8^^)LdfZt}j~ zt{|Mo>M?{K3cImgmczeqJw1!enX5qcElz?vX|z7jKjQYUZ4X|~MHH*|VeZ^67L+b< z{=J&^dA``Ww-A!}rB0om{T$=!7c=kU6bVaKUH;eP^wLbBZOR$eAQuosZ*t@<6S-6g z?lR2-&c2Dj2>t)pMI2s`vQY@z&%O<(p*qvN{PlvV-HW1lJ4}aOMlp!A)>9M6p<@=& z*ez>wHVnlcyv`pv=kI>IyQbEyxNpWXXVsz{lR0u<O~Vn|&v^KZXZ^Fn&ifw`yJETK ztF_f@8(R*C<$9}`p)*f&HKG2rE9Yn)4-gXK_ro|R@JaqI{L$7dJq*=0Ebyg@VCDI? z&&j1QjenmQuIlN}Ekx%Ao)I3@cf@B(v1BQAfzARZI^?&gk~8h-F0d7ES`gHVJ9`iJ zYCJlo_cdYVt+x+N;JC+BYV+m<GfgDke%Ew8q@@Xg`cZu!nO<J9fBtnVNyJ5Xsd-`> z$(69&=K$}dBI_P$Wr>N=tT;gQnw+Iiuk+9k1|MBZ!(wM~c)z5D@Igwym21=zu{KJ@ zk-7KcWc$^x7yQR`Fq8D)ep_(EJr;fcPJ`OQtA0C8DA{F!e`uM`&J*iMw^2u^g3a?! znRgQfnbU=tgHnb!_&WrCL<*V|!V|&VC=~xPg6!Sm`Bog^+@Bd!n&doRJZM&xgc5~l zBkpAb^9sb+-H?&ouQ3SR-7JYjNMK;%4%<(odKf<tB{yy!st~6~9Y8~y8dr?EGgyJF zT<3|GB){#VnE%#jBT`=LN-E^OZli^xL+>C%HHt#OWsHBT5s&@Acu4Wwc)Z!)StlO< zm@NyfZ`5nzW+XNf4}R8-c<>ZVZ-W_~eAVOFm)$6`@5+FrW5+%0-QHgERm>Xo(ugKK zCcFkTy^b&+=kOdwj}b554OH=g#frjT(`N<cE)c3sQZ+~DW(&f=^(az*b~kRldG0l} zn|PQ(u1bST25VWm-Ci|425Y>9=ZyZpxsrIA+PQ0lD1?j$P%*Z9rHg<lXfvoVF(x0q zXHv^j)La-aYkknik^g&X_Dv&-t8s6Yg&dW+yaZaV3X%D2eSt%-$!y3Mu_mpN)r&7E z90ag7J`#a&^Lc1cF<VMaP$M9vBaL(VWRo$hsf09~s!@91t7wg=z8A~gbYm@WNVJ#U zav2&o14DpEV0jLtt-sbFJyG(r$D}e|riK2cZLP75ePFM&Nfv*{fO}Fm2Mrv9%43pa zjsHNmTT)t#M+A9CF%2{6%>!ENEp&Ju+Ka!qcruR&g1qs5qSSg8=CHzjj0C@D_i&F) z&&r_$yY4cpRM}}yUn8)SR^Z5F_p5{_b1ewG8C!glDH>EhKD$snF%K+X$S#S%x{UhT zk0grCv}g2WPQshsYrk=(UAZ?P-ZspwRvul3EjzIeeeD&4hyJLrVdU9v?W$H9Us%!B z$t>QGi4>^yK<p#VD962My5K#U!+Q7Nnoo@%b{?UU+0rEfL%j7yy3uZ4ulm;c(8T(e z!j7hWr%=;TH1)x1rnfJ8->~1)%o_;m+>7WkxsAk8<I5KLwdNGk&R7WesaBVh&Q;bD zW}_i}v=tWf@8t(v*lJJ9>Yp_G07^tV#Y_aK<P@0cbS^&}#;rD@J^a=CD7nh_OXtQL zc7+2CvKPC^4-dL_G`c?3JAq~S;XqsWtzcKVvs>x<7I*X)BiWFWx4!LqxNBnH#TX5Z zAC8E4I?Lq!kgs`E{!BSQaJ4yf?d%k1J!u<HHuFrhzlDV#Tv2Crv|T@-M?+{QN8`~> z$lB1wn-1?BE~+wv6NHC6v+YJ-u43*8D_4AB@Fv$CvG?TUHWxhJ>i%_A<-D4#cN8*j z4}O9}kNE5`P#9w}e}=Y}Z(SMEPeTkKANumEB+o7Nr(Z|fh2U!=hzj91nmVWbq3IXy z19~E;Wk#X%kUM57hwci8Nx9?08_WAKUr>jG{WNU+z@bd%30e`=smiNaKP(zpNBWzD z7HagQ)w7AAaL+6_{z#?}`5TXQGY_A(MzLbE-`j{zVS8~jZ6$F$+_XCx59N8o%KikG z-`{%hjF6FwMni7w{m`>YWZ5RJe<>rLMP@c4<KB()G)ik>*J)n7x_I<|g=ZPD^wtLM z)I|K(i6Gy3LdpM_Fy~FM^HxhwbOU-4<i~%Yv^eii)#N#UtC=WHWcenf*-R`=Eu<sP z1dx5cV!DNVC4N6gT&E}~Ggxf)5~zZ+)R?x(xi6t$pyE3?z^le^ZPt02_kjTDYE!A! z)MQ78T)Y6>#*tPO00l5-{|j=+tUUfbEH%1p|0Up@Y%eYBB*EkZkPXQb692OC`$0f$ zfop-IoO{<`Uy|L)Rtbm8Vt-TCr)2QNk$l|9<?xo5G&^quv9Hi+p30f!n>r0kemIxM z*6#=1YfHtG{q^I{62EvmxlTYM;n27AU-`|X`7Wyr+Dx({^!vmo7ozAN+iU+~D+Wx= z%^4wP)D=n!3Gx?z+F+w@JvbVc^o2I%Za60<E2!D}I8jODP4gyjNfEo{598}L^7W%h zzhK<s^2~FQ6ECFt>U=y)GG9{Yywc905#5(N<wxrg&==8e6wKk%Xyu_71x%$FU{~T= zI;LAL&t>&xCY)Z&y?TRTpdII-75iRg!YNBdX|J=7FYQ&<;9A@G?I^!sFUaus?e=ps zZ4TF!F_JH8T+4X6%G2Pq1+IY^3sK6;p90DCq`elDeET6{kIgyJf_?nC^oIKnfR*+1 zynEp)qlLV^>=(J19%FP1dYB!Vx!!PtAbSM%q$YqYjbeyG4{$uOVh%xmCnhR#qNXcG z8xcf^8_QP=Ky=KU?)7zN)p@f*q4GaDYl`b~=^F3za}F0N9$?-4@il4mU`avjPnT@1 zV2(?Ai#E`|6U#85_rZY^%Rh_Zquv(=5YB3(`bX-71&Jkh<Hq5y!0)RRPXA3rpz`>< znG8L5im%E6NS@El4Z53sG?eX~b4y{FTjd&ThOM3bzy&La<8A-eBVQ+v>7n=040Fy- zKM4j}0AFjomi!Pqqqg+S2XF~~pt=MmDL@zKBY*+OM707w!hJJez}L?E@%g6`9l2Tl zX>GJK4ABn&Y0ivKCH>On#eaGs08@DP$i^`NCdMuMW!H)yW_;qw#$;ZLGf#)T>h#`e zuB=mm=8ZIoe@f1e6&0vAtOlT{dIvyr(O{e)W4_9(50g(xo_~HMb9Gb~15v3dET8tI ztl;(JMgEd9bmkHFvm}k*@dypBhw8yqT7D>o{lljDN#9^qpizb|iN25v2<)R=aoohV zzq+vO7Q2BNg;RZY?4<9X^HS1=mtA1*O%y;ICAxiZ$)X`pfmRvrZ<p>yjD;9z)BowS zt;03+IEEy9Y$0<1RBAHg=VK>1Y7MCrI}++)L&T%BKFl0Y`8IT2W$j?6xc{&V5<79e zD_fG&w7LxcIxe!oKtfxUcyQgI<@(P>PV{7d#MXiC-y27bHa}^b`+Q=AspqQKICzP~ zuzT8`+_j=IAXO>hr*(6*%D99U)wu8D_rva@=ge)hGJ`(M&aOoMZnB#!QMGh^rvLj3 zBMyF9M?C<Xlb3DJ!DffK$r89B5fm4lDzx#5+eG31i(*+Qe{=o!^|y{S<3p!Y;CIfV zJ^-$?%imFjm#xqv^gnHjS&y4qTc}>iBlyS7jZ1r3A1S`Gsbx;_(Nup^Om9;82~D`k zk1k8WP4oKXnb(pw31j!*An^~a-?qzAJqh2QV^m$9<yaIZDt-<ZGPu@c4>egR3FRF8 zh6VhRvEcP8cbLV0_0jIVu7h5-a}m7}+7mvB#PUN$Bzp*@*Bl=?Nk|y@!EVRwyExut zVVYmWP7<AXGl?f{pdSqnJ_kB%>URgdZy0)Nb8C5-Kb6HFF#xa8ZrwuSXdcdKUCS|_ zDmsj*Lr;6vLV14zUp+Yj_1iqbJ;px#3w#a;Swuz3RH7oxp+rwn#=#!p)X5tM5?2_! zBR36=<_1u}k5!+w6dO^QV*k0!IBH#i=9)cz(Jb*Vj^cpzEET}@DF52R0;2@kk7~K> zuIkU8(qYu`bjjtFMkpy2sP<h^bK!vjUXOw?zar3~SP^cM{ZkRBkjZ(i`5NH#;7+Kl zo#@a;(G(N<w(E4Cs`fK#(x#_vdzT2!(dWe$d<MEC&;YVCpq7gPwz99Ksq9uFE(IGq zMP!D{+BKJ&(_tW$B~@UQ8ISv4w`bV9AP&gP_3oa__z*}|<$KE)Ax#a}$Q1oYdU`VE zkD;_DYR*uUKbbMf^>NCti{GI;V!Ck};bl>be98-ha<sOuA@0OihohYI^W0)yt}Qqh zb+t6WC+cVoQ6U1BufS~$4;9ZhxnT!`%70Fce@Y(T5;ec5lQLQv)GktQ4AiKAbza^R zodRr;7=U&wanqe_6!FbV9sfdFE(fYI-rqdlcR>=AdX&QN5bweV%q&@QZplX03#sBz zBzCxOEC>C<kASMf9mt9CY$mWl9%<lirB(O!Nw-Z18UaH91y+w*g#>8>vuhW9%io}H zq*23)0Qi2qgzc=`#UMPeg*Zu|pn1=WxVKtZnPPSpZ-a6A>?!Yvq#HR~7*m)`ly>}v zb%b(y&ZVa*PaX-MRclxAtvAgia(6xuiUVQ?oe|EI?@!-NIDq5GPRv+sB&LeIm3N+z zhkljxu6zAecJDVQ^T+S@T;M0v(++s`sT|#hVVtf8zqR6iyHK?bVf^9&3TflF87q=$ z9rZQ2>=Dldu9Xq~uEQe?^b;t@?IL(1dMgwNrqDf8Ycf;dZ`hFJ*`%8dHs=i=&;w|= zpC4AuBzFL?sU$2s&n%ZV&C|GrNzKS@TnT%B>(0mlJbd0&H^ss?d3Z_}>~$)ujw+{T zZ9JritvsfW-$%C#Wtnrr=Ko8_+SsMW6L^KUf%SSwsDXsv^3AxFT$WmOeK3t4hxo8u zX!bfX9ULa2MWN}skHR~^AJsS!&P)V*F&gu3)LeYA56JbAsG6#)7cR|KJu98hPLu?$ zuUCWs8x1}8RzOKwEGPOU@NPE;eB@mFyfBFtmFdrhDrB0MT)_R7yr2^(<w1S#GvXg4 zi_ffO#l^vtS?ZNS`|0bqTO44ZuJZ3XTWSTHcBC$Pst{U3M0{+1d$Ei04=?3T1{lgt zqmTqYX_T5=mRaJQZTR0!KXT7FJD(>5{P<4+cYQqhE-rT{!&i!O=jR3CM!xDv?teps z*W%H#f)cWX<GRO}p7{A3DZy@oss=G`Lb}SM0sI;XAvRJ-V6P@r8beQ<xd48cP$i2$ zWM5(EJ1EWpDhGBIpb;zNeXx*qwk2OG|AOnE1xXdJy}eTFHxGl0EClgT%veM+JeS7) z_aH|teL7+wYbNh-9e}O@h^`E~&66;e?U(fzl*@R}fck+nxb)*j-Ds;W|FSJo2+U2Y z_u7QXW0h{-zqnnD$V=?*3oS&dwyvb#6BI+-YoSC=)cJD&=#g3bHZxexK~$r8?@4om zqR7|^*6hz+?d{T}z(YRNng(rO52u2pJp<3sfQyvb3nOic=OUH)Bdg6d(<I-5DuD}E zM9ep-&43A{X>!su9Mi&$vfi(~ilN1QnwnJK)l_V9AbK%EyS)a(ocIp9xN9+<datKW z!GU)eT)HCw4i!&+>}sr!*=`sDp|9V6Xi(?+J$lWxRMZc$12D-Oy3Y$;R}gn#GTtpY zGDM3MaA`X3Zb2+!M@1<*;e;G2e!BevTMcShtuf!}+-0o{JdL@ZObJ=+=l5!C!|puC zuK{S90^3yPC7*9ye)CX2(x_}3#W}hgW7mI%Cy`(>ve6WdGD4;QfqE4qkn0$@m_ME2 zr-$yg%UXhN1jEzBTfcUpy;maJNOq(>a@jbe`{p=4`M=oz&)*_5fz}1i$Aj6qSpqR} z^`Md{=0ueK#IEP^pmN-gkW2Y~OboFvsx8H31UKZYkpN^(up%0`51)HT_2G^<!Jb5z zB%kFp74+<V!EH$+eEmJ00y{yoG?cmfKmSOr-%rrk*)1Q?hx|#_ND5BlvSH`)=nkI# zT(8o4S)08lq0*|U8TP`0zx00&-=(0_f7os+;8UhX;L{)qzqkYn*t<c}&)Uu&kS<Vt z?V`{6yr5vF(ydy)Mpq>%&+CkBHThUICHGfWut?XO4X`2AMB<U}P1T%94?k*y%_6EI zd(Ca$XEg=pm0j*F$)=rlyJ_CBU)B}pGmNI8gV_Kxpak8h(8cJ}e7jHBxj$BdNirZ5 z2b{Kenp9fK#-CcU@Dczm50?Mal<S1UQFO6tnkEjQ6)^ztWp9UlDd$#a^iAX@Sw||b z^`;KU=k6-RtwmA9I!v9ABQ&2VZFNntreEe{Eo+~Y5*v>M1?WV!>B?|{T^<5G3q5J9 zaHN7ehMslu1`4UKFT~jl@3<`~vRxQjqa$S}{h9UWxG>Xt4*+<xk;3J`@oo{my4ckO z&$=s>!akbTbKCiuT`E>;AD>5^?y({{C(G~gtNjn+PaX(vt)F(SaBbeAZ-kq;oa{Di z(|C(30#C9eA75g=R0FhKoO;}R`?zsewvkKh*)taHuI$j45vjh)|J%EcocPtTW7+gX z14nWRW(>RB$yL?xquCbY2la83htdIfeJl7n(ApGi1Rhw1&P-q<mTU3(Z06aDKwEO< z19U)_bp6vzo&q`9Fas+BB9`++J1b|gQN~-TMvXPNEvkKQO(6OmjdqNy?g!K4<a*>C zvCxh;4b`8}X)`$$m}5f^m_nb>4y=o!g`%OAimVTcbmyb}wIZl;mPK5B$fc4iE&lL| z?dxww>JTFWGY8#?m+X$RfwlQEB{hx*Utq_Diq&<NY3W_@l#VQ63&Q-Fo;1((*k9>1 zrYK|q4a5K1GidPz?9_b3ZkpG@Ji)z+z7wJ1zHn?w<Fb&a*IQ=c6+qnv&;Awj@TKY7 z2#<frI0Cut9aFJ?Z{2pq9v<lD+VUt8%%!v>fRvseT#*(0N!R4zLhX0H04+|A<8oh? zFKi4T(KDnR<G;4~MlNdhI3^-r-zl0W<GyX$`*)@SQ+6L?9xk-W6n;W4Dv58din{<m z?naN(q5`DW*=&2NHP7H{N?68|R)ocUud0kW&6^GIPO*V7)0+DavcgxER+f%lBpvny zQUL>0n%(h|wfR7`xyPXqFK`_Jj9pS}hb40pD5vU62}fmfKtmu&C^#SOu`gW%C_m?= zX;$kgj{4NtFT+Fw+=9<FADaMwm6p~?HET_G9Eo}ha=5JdpmY}tED!Xb2EKjf!JJEC zck^PXr#lfGCYTlPGFQNL6L#*g)-O|h?n)=eReq7@pywP1{PvM;FRqc|2+kajwb4(I z+(s)6a)7XLdKXcg+OUj*$Vu@=P2ZaX5LV-cFqJFn2nlu^Y4y)U!w7Uq0yEcCrGxkD zaUW9uYu@?PE9EmXuotUH;K<Am^!GHDHF3StYH;#tM;ubm#jx-h$@CDSWohsG61@(D zzi6$u2mEA_U(XqW$ZU9UR*|jiUN9PI!tn#4KPHL@Z!g}lTxCSE%h<qqvcsUtkGN|x z%prC@fI_DeSYLEJt9&WuqlqI#;|r`ClhcJPU|lK-T%PRvass`kEN<MhOyvLr-=ks@ z!{_GHzd~x6v76j1?H-kX<**{YKqHIfW5<eV81BbD(NY2vpTffc8Ax62uj|SD_usQn zJlX|9gXS7_zrLRY<P6mM^|JC}2VZV_4?|A_D1f(a8;ArYU(xpT?YzEqj&GzuWy`zR zF;ndTy4Qp$!hqleeBE?o;~XObO|<HTcxf+P)}D#v#HnSO@z!%K0uwWQ_1oX2R0A@D zIj@uyL^|b(*_Pyp*;eI=F}<g-G;)~tY7=!MHiehthWxf1#1iT2>H6dVl(cFjY&@O> z)}GAFP~dcLmK%WA<>j)6378rZ)qjMYKi`mIhHB>v8f_p;V`p)P8}Ac;!0z@48uh6T zM6Zguc~SE7<=2W=_=)f9V}Cd?f0c|&y!arm|HHpZZ>=+w4rq~ig185(TSdPli~=ez zoWwksqahUwIt0n}^8YqCtbh;9*V1}GVvmkC0US4RcB4X}0lk-4ElnB+E}{8rdh8ar zE{wj3z*i;K;WJ0ZZk-ogK?^WStc${B9fvOUFVEummKw55sBEVnyE5ACYSW`07jvV} z^u_rPu*PqB#}qJgN`K$RfmtI8vm0Z8{g}3UsSAznTJ0ywmlG0HwEx55O-kjg=u7#} zKh$nzP+FSZE<^27R@u*%geO%@;LqFssZ{#(yyOEBtz#vmOHT39BVYCu$Hj(vti}he zi1&pX`B%J_Q#~0Ezo2aeLalKmi1(Cgc@R|8%7^St?jH)te|ejvmjrP!=KQ`()DX$w zon~OXTWV*LU7!kN<FAT<Y<%%SEU-1|OlaeKkuNGcy5|F$mhN*RBTe9e^{D?>&eA-& ztzZoy*1X;*6#}hw(sX-GyE(VZOx2a@Dw!=%C^a}ai8!An(!Zv1*~vAduLw!g_K**l z7}*HHX*tA+Bz>JRqb48aGyruxH)<d6j;si)w_8Nj`;GU$@Iu!6!E_*vXG^>srElh4 z-A}b}%SDIzKgk{X0iNBQRKHOs1-j$8bsQ<Sgx|kc+p;FmJH?7TJRW|v9`}cj;6v;y zTt(gWA1LWGTf^pQ{KD*C>v>xYcDV~A%k8$@?jKTw(<xjz-)n-M(e)zcm`I*_NT^~3 zy%4u-y)u7X${47~LH2n)@1T%<)AmmOy(#;a?+cp^`SI14wmdj053dz&7UUlnX;4?$ z==IroP9b<X6~ZarWZ%AihW=nLs@vq-o^wBM@!v)pB#VWlMPO@Z6Ks3t{XV=Mk?lb8 z?y*r%bXS1ze|FRLpXxg!V|l;sZ0Yp=eVQ7Ne!y*uCvscSB-jiX$pYbE|D709f$)r! zUh=~D&Z8|MSDSm>Sj_EHfmwY0!2@u;#4;p_!1r8rPwzckF;^?MHwe_z$Ld>nb9DZU zpXLG{0DtTe=u0?>6p!;eN$D+0;}e3u{3I8qlOSm|m{YylZznnRl#X^TR4^*uVJzcm zo6h^D??l|Kj!cJegD#UKEBdGg&Vtm5f_Rx!-Bgo<`oeO#+)v*N4>%^TR^i&I8k#PF znFcNIfD$;t0+BU%e=V;ZUEhZ19vLou2hY*S;1xMT)T(nZ*f;FM>#qko1m@~MkJYO; zdrXuB;c`}wJF7`V_qJ`65^FAiXdhr!Nd2lVIF)mdW7Mv7o8xFd?1YvoQ#uPP1<Kix zUXd8{J=P#{7-_anSF7sNK=94f^cBJrfNFM}Mu8Oc!e8x2fKHVSmxpcH8%+Ag(Yk$x ztPuhQsrZ`xe;9kV+S+B+*&L1yNr>zLfPCoFZpH8at87TYi6&!pzwB9gQ>lD?0mZUm zjx+!|V!KHk#Z)uRBJilq|GxsG-D_mvOC%bs$TYAv0fM`dQen6ygIN-b%AeY4Z^2jH zcgeRr)1xw)ditajj2Acg!D6B%fRpOgVZ>gvXHF1P%#uA_9qF|i&;mg3`h1HZQIUG9 zA;y6`J=#pW5BIQ#Z$WUQk$1pPAS$8md&I0d_<&|qcql7nZ~a#Ch5!&FqJng3-qYE6 ze5&F=l`|55EXIu4!U<?!*S!O(?*5&i9(yl?nZ-)!eIwS~2amO~qDi}aO?;kK)h(9_ z3!D8P_TDlq%C>789z;SKX(U8Sx|KmfKtPc04w3GX1|_9Ix}}?;n;}LzrMtVk<2&)X zyq^2H@B7F1<Nft+?`*RngJ&G)I*zs0zSf#!?P~gV<;O|k;w<B#V69NNLY*e<P=w(| zSI24%@a8(Y0u2LRZnFe@6+tkkFBpxQ*xbJ=yu$)eT)J6cWjMbxWh?D-H<4N`l9K92 zGCS;eq2Q94b)!E;bK%W2Z(o%El}xLSyU33)JI>m<hw8~HO|MGm;&U2#{$Y1N@uxd6 zT>Xx~Hp0#f@s*>BBnE(c;?eZ`=is5Iu~hFrxUhB&H#8y;zq0&tnrii3A+7?uK%;h! zAh|Td-PH|;{2HSzLCfviX3EH=a_d<2FU~J`2&&w`#JBiQs6L<0CAb1m4tf5Xkye{| zX;Pxjx90-$gLi<Z1{B%+$T97>e*TUvb@26|eU@A1^`-3ZYrI{gFmFxFKR|^I{=R}k zW6^XKxUU==Vo&Gud)LO}=%$qi=EKx3feV`FRrp)VTFYwMKc2RwgfR&w<CEID5WAyT zgoo)`vKT#7r-wtyJRZ>2<4cpr1N!nt;c%}m(ezRIy$`N1u!Y9MYA#X6L+w)X76)Pa z%LVgf5?dPYY?|#}^}AC8i~*D%VBv~xl84`x-kg)GyDb6qRT7xl7v`ojk5VmX;+(Tv zG9T2~*BJb%RS{p8LLp!Gt46-=qGsUFC!b4;MUpyDWr&Zf$NOaSQi9%h#ZbzlKB*#0 zk9p_oXZ17wWBQ$}LEwNimsZl4(1gL`NM6l3c8ln$@5hHEImaXrEBo#@!v^#VoXn_% zH_7j2r3eD4j<lYFUq@I|J&m<b@)yTNG7%4}_~}cB@j%{&{!GA*o-E!TYdcXm8MECA zL$1%FBpc2RF`%XN7w3?=9m|js+A9x!JT^LXLX0hqp_Kyl_XmWDXbObK1$*0DnUHJ~ zkoV$a1tF+75D&jFc6S%ZeKA=$(nlU8Q#RTq@Uf(8{#`ivW9IGGgY2&o5R>d-J~4pu znt)(wP2w>>6Ak2)f(h{yCd3Uac#8cmRZ@ZKoMnd4&D)<yWi>f#G5BDAy2o*As-Zyh z@4vzsVHW0w8#hy_9El5|>OJ6V574e1CH}P!zzplzvpRk=PipCrDF>!L(xasJ1&j=- zr1CMbGHAi^dy*A3c>cX@U-BMt6-}DM%&E<rer3{BIwr4BqJGD{^R|ypH*IpSw8@uE z2}f)LI3MU;a2($Vo)6Qn6a?1e)jN(@V9#ucjdJI1V((tK<oKhT@-F@Cz{OO;9vd3z zk5&p@ERSekqcI{@&;-cEkn5u}yfp%&Ob0iGaAQk+-AKNp*s;AF<xM%$4cdN-Z*l9! zh?tdFz3l&HN6J?5D7js#uMaS(7DDkuY2o8Ke!l|bZx&$~J3_@k%Nw<EzWyFbjV#%7 zY&0B%-sC7TxxB(`bF+l@O!7jK{EfkuJd$_cYY=JWVyGj0hvc(qBFV%_2>xcMl>g2b z$9vG!mzcDVeRv$a^z=GWctiJV+Py62Wv&wiEQYeqN}jRzpe12%8i20;ZDyV{Qj~;@ z*t`r<@KG{1xMqo?9!@4a*cBf9(MRQ7GL)|_d896%c~f}lXZh@PP#a&na4WXCeYjLn zX>!8@&@4GV<TjoYePTtQPhi`F?GPaS00@tMl#0j6a@|^lseJz(r&5-sc6ttB|J{pq z=Ye$6LCY-J(!#u(Qn}o)cpuJ#O(}^+nvQg(yv$Fj+ei>v?3*OB&d?`DF1L<jKlgNX z0f)PI4zgHSc*$K+hw*A!CK95s1JLXC>0&-om0LmBl^Ny!sWC0U`^rNdD(>SvzwIbP zXnp>IP>tBn?Wh0XvJuBT6M5fZV7}xuKw>qS_gww{k_PNc#~GlA;R}Zf$qY~@ocbQd zN)Y%Iqs}gk4q+o*H9yMqj(M~p*<M@DYV5n;vBP4(Gs{A%R;#CaLFq!_12-M4f(Kz8 zH}Yz7ixs%}Ma2;cakf<RO&NqVeb#$nn9&qO(RmUgLF4u`WD{P<^(2g(;+h=aJ0JaK zddj2}rnS+|qUQVl4OJll=%<S?W^sd7WzKMO9zD%dO79P~{&n$FM#GIT03k3W8{4hK z-#0&G4b}Zxo1gwnt--xMIk2tG#Zr${1>oE2Fmnaw^7njc^g#rHB&x40<R<tS8@E&y zah8R%!t<FF{HsgVfdK#!3tlV0vZ@qEM`=7olmHVe=jCQD0(8;mvRb!#iuW|)=EZFm z<pccv-78yr6)r9Ina5PQnaajw*4TO9O3C0EkTiTqEudw1*Jf}zPG6fm5Qz|y(u+N* zY{;}}c<rdzf0WQ)F;pt-x-pU_B%U!g|N82bq1X>D_KaSj70cnC$EG^t&LcRzhzU=_ zebKO4ck%)VX2>_O{W6zfPB>9e-xRBDfaRu;D$sS0<1Dk-_B{e>f;<AM2F-nxEqko# zUBAj)zgUY-_qtvlVq;X#uKaWJly;OC=Her*(eLv7_FVy6s_j{?sPv}D*OZ=NHEQUq z=Ua9QqNbSvM?*>Qk9C{M-W;1#3a-9>_5O27I;jI{jKb$b;xCHDHB}<tA6972M~a70 zg$9uX2$~LYMv$~dQBJrR&-xAME{`$UqD0CpDPlNmxE;ho+SV1f5-*TrIJS-%jxt`( z^@+-n06o7c3gdG&^JNBI#JzkqqrQ$+0uFIAPg}L~HPF>kkIh1c?+dWU#^z0wr#G$& z&c`gD)Uc;xtm^$&_z5hm_adam?kr04Z^hyFY;u-aj=j%l@GpI!ZX|8+M4~o)y^vQP z(B`6PVnks?0ojL8*3<)iD6@GN*wL7I;a!@a9!Ur*89bem>Tvtr1RW`w5@44H;Hw+W zvzruI$dqMCIqMFP$>U>ntWF7;^T1VV>ia(MkY=60JX4bN7`pz@Iv)Lveg4)xOuq=K zu*bmPpR<k7$C4QsQF!ZmS!md1KZHloX7y`<?={^V{Bi@vQi7jBaKUbJ@x;vKo_*39 zBdwx&-=;|`k#YS*#uq~C1T^V<T$mqx`!v3S3$PgQ>j#EjzME|drQ^5ZLW{piGsd;9 z{&*hRPGQQ6+;lN<Vd*S-*mdTRbrF;P)FfUeDk!W;tZvlaFEqWdU0d#;_F1GrF{xc0 zz!K2p(Fm{vxSsD_KF|d-e#5G?mUx4^CJ&E#DNj8w*wM;JC&3*Lwjv-z`TYHoMJLJ> z%6!Tj@>(%Os!!T1-ECGgS_lYrVsiz&rrA~bPd0`ST<rJMqpS#6EpYf%;;Ycs?~dLw zb(FP7Q%Xjjva^s66qK+RrVER0;`vi`pHa!v{C2@BVRpCxgjUN!QgGqq0(y~WqaVym zJzK@4G5P4gFmBcg)c3@w@o@oH$x8TwTN%L5i&XgeGynyY(92Wjf&zYWcb}sVr3XDt zb=lZxF~}*=e``Io$k*!oM6M1$YQej3$Z{oLWHwx)Tj9FVLmC4On}h+lgxkwimDzE; zb;>9m<k<m&376c`$y*w1zew@WAIIVX`+;e{mHO(mqldPrsMrR|v{J>)=>9Slh<*}) zTq2PulAV~D<@wogA#flok76j0KPHZD%}Ku}8#FXxf-naSD3?%|plI(WuQSCgmX2~S zOxg5r*DMCq^QX<JU~jp{?7E$v>&pX{H`AZY3gE#D(Wv5{f6>ak9`{K+(>52E?w9Tx zUvr88jUNO&4dSiN^(7n5)P1jUvJxO*nQ5HEl$y+bfkLWutEX(6FSf$i{Yv4QXnmM` z>y%Y`=p+>{G!3TMldj{dq=_ML7p|D~`mKR4-eFSSwJYar%9Y%rr@Wh4wxWo)rV?b| zjA#ae(9nuZI#^tD_lRG}vm}WZ{RLgv^|tS?pt=-dm<op<v#N3BG@^0?k%-p`gq~;s zYy%)f;l~{k4>}bm^YZ?2-f!Icglknl@kZ+LrFh2L_;V?EK;%hiK=r*;_B`y;M?G8? z`s4^T%_kOsK3|R)7GqN#ejh`^@K70C(U14Bnm0GI#D{9RQ2%9FT`vD7>?YY5DSZSi zwnwwRhnJmH=xZ3KM=QVvrQdibmjobp<peq<&`J%Sq4G6px|s|543J=*nAC;j4={~f zQzTglr4};FOjGx2u<EM|1U9+5+Gt4Yxj4;wPJOAzsqY(l`7;DbFA$T#EY0D#N>sIa zkz=C9A*eTAk_1H_oayB&%l1wE0FR-;=3#0U0<;4!R-<zP9kGToGq&{)JHep~gHyas zsHFrgEhqOXs9Gh`sl+udJ1Ur3V?0-pV>Tz;H3yh0`^B`tQiOgGDKwbT+solR;f|3T ze)i~EfZVa&pp9zyNZX*+Isg1f@BOD|DHM#1Pu@{Soan~o-kcwYlvu(LjZMYRh(t9g zl~-kFeTJX1ARHhu;zt3Ba_}+8Hx0<<=opXOX6TL!?M~}+_j#~I4Z5Eul4hSO{_$-( zl9HDbAAT20GG4A}bT<jo;}dLGt`R5!`F3XQ>(@ZO{YE10s5CYku9w!Or}23cp@ghQ z)@B{V^!q}H5>Vz_ihdW&omzR^0+bIQf(v5%f;}{V62f;&di78%O<t4*()fEiID#>N zd9rN#3yH!uWWj*f6@OXvyw#Q4D;sB{fXU|sUs#R{!}KEKOF_VBxndiGTZPWLGtO1( zd+fx6+@6ap8A2sje48qU$IBu>iq=n%kIW_%6}A$T9fN=I33ji0yPmj$OT(B~K=Psl zi&3ehnNh^tD>-}nZ2*m3*V-=U$~Nok7g<hZwkgbvddKfeEq#KbV{5;^)EdMT@+(dI zU{I+*H<FbvA+6Vq=kXp^;c$VbOL%VvS4F%xk>-7JBfgaI{U@Ip5c!KQ4yxxZlRs~& zt*|_J)v!(*@qjG-x(EQmv(HcwAFVZx#cxXBkz+5LHwS5W_*fp8kdsSokEaRF!-B^S zZI{TSojAb!7~cca)e!0}9^_l+iR*H#eG02$4ER@jmDGMCnS1&AedFM|>!o-u9r^Yp zk-H|DV9O@3SN+*kYRH38ORBn&w7SdV*G8h72TEOu)z!^o*)+xg_A)sPPJb|A{4cJ* zxlui^!o3F)GV)n=23f~aX_j2cB5psO{jno2lGg!tv*H2PxM%8i)s3k%)U8(lTv?@z ziS&t0HGJ@b#HxZLY3|pmn<jOs)n7w7eqCLU02?UTr-x~?VmR}+nqI$`Iv!p3ilbqg zwo4`OQ+grcA&Qxi$TvzRp`!%*(e6D&heqKTD?D+)9!wtRS(*qo7ymgCITa%;*X-3B zQ-Yw)kBl?N`IR6^o=U2vs`f70nB5M_%qr)l6@1OoKj2HL<6K*tg#aYBlg8fJyoa5} zn$$;a?+*~G(Ty}_2PJ*it^p>DG3bI<myjP71ywl;;6HiUcoHQnK0`o-9ZU5c21Fft zAD$Bi$V!aoQNa?zu1B6qbeHJ?YJRa&e&5nwq_KkA9tOmt{?d(~kodh?-BRS(yolzH zf8jE1h@`An!(QXABp$1NS;=LefBzM>PbBUAv<`w((Ey$0&Tsk^Vbb+Oren1Y>ig|< z#`-xSJy-3?rMU2P&g~j-0y1!op<@oX@AN||RK|Puibl?Qob!*R@yq$dKSIkqjcl;_ zX`6nq(0uBbNr2GzcZ{BHW<KeoHRch#9It?SYYHrH529#O@d~UroY39R;Q8ROy7AzW zdCh5dzw}i>6J<PuKxC#JW!H>$V-+WxOrTbmG_9k>jT3jD)A~Ko=+|*LlLyz=pT`ii z9Q^!zzws<$+G-9uYa&unq*g%n{g`j@61@3gbN@##CJ+B*lFXpJP-?2TD*x`LPUtZM z1N{$xWqjo02a2$xS-I4s)b;9+%jwm`U!2KvxMS-DR#-+Ajj~~vg4_P_skJHHmm8E< zZc4KVKR3TlQ|L4w5Az8G@%9c(Ht`A|Lk@nOFlHUUPnAv>bOQ{4-a5eNaT4Em>>GXU z*5E0Or+Yai7?l@)L)(2$O18?-H$UgKTKUpnguh2F$F1Fs(Tk#0aF_%7DivsXO_Fpe z;gl)bQsFrR-CsjHPCodC4yIr7Uk#$oK5YvRz_jpp(~>$ix#uK554)zY_FT!J@0kC( zmJxXEY6K;yNFtzSO&tpRVQ&veF6)g+)N+RD&r2f4C8IYwJEA)>!??H59+jK>>P@=~ z`(eZ!UtNqR=1rPwO<ek+Z(bm4flp$aZVbfq^(i@Dzkc(2hY9#@RXfdy4bYPB-`+Hy zx(HNXmq~f-t+lVVCQ2z4m6e5*l%n;ocdJu1>4jDYk5+peb=$CrmC*F4v6ocXenyJ| z-S+?O{Y#eNtGzLqGKw4mwlW6rL;e2lHrXJ2Fk1NjW?jIMlZy-ZDxWGHt|D;0(7&Aw zeC%3K63<k|O4CD-6A|&%Pv&v3rFZ@YoNd)~TQzKp%;2888lLy)8-wq^KY*X!k$2vA z<Q%yk3Pn>sAG~+<yH$z52i|mlyH7e|icCRWBmcnA`M=-UtQ*nqCV*BAb+NERt<RHf z@u5D&Bv%_5VV^f<yKH>1{^J=XZPH?dzfCogm}+SybC5m(q})ZD%FQKsRqa>f^<VD5 zEt?gt4)&2<9h82vyK?b<qdV<%d)!C~W7C=%*rNPzQjpXetdFM5wf&}!GdecL!~e9- z))?l8{^kVb`RIB##lH<7;7@D)p^UK!^xtOlvELkOlcTa<9n6+WWUmy^r2F~r7e`ZY z^P&$hH8ss7NV7FQUh9))!cswepiTJS?@XLVuiUyi_0YCl5@6Hm{}$Op30mo?l9CcQ z5D?W=nE|Q_#(!QS3hSYI+w<Sn+Ny33MPpEHNyT)X14H$Hxd9yoJp;ok;eY!tJ-3Db zCWKdy|JxWUC~9o~%Y>k}L<|4#=Y4p53*_SeG87YG>(&3V2A+8_`2VL|rm!@@|0(lf zzyCMnf5iQNM9RBWma}!jp&Wl*HA}h&QT4VPwPeTFpyVP%WpQ@3AWg8}ix06S9PYaH zhaOD#Bm$PE;|Ok)b!$MTA!8#-i71BsL|QBgLKM$=Y@&*2=UI&{s>AtcYbCSlJq!m& zZ^J-M44;PY1YeT-Ezhbncy0H)V3f1W3_{OY&C~XMTH3s*-BOb;B9VTrD>+%)){MPQ z9{9mqOj>{GO7>1(VPVw2Y@&<R_*+GyP|<tVTYutU_(m8>`EBp3fqF&)DOMWsN_IA{ z_0C2KnmZvFzU5Y$j|*P+I7@P?6YEa$;Vj0b8Np5*iC5qG>7jJ_c`|uz8YP1$SrSp7 zX{-Q$`)er{{#j>n-*d2)`|3eZ=P`SyH9jz1IHKw+EB0Vs$5(|5DCx2_(bmF?JBg`Z zD{YBktO%cArmFVKlh&;pU967I%6Qy=%t6ebwJ#Ykd~sgCAoiNM%aZz2PhkAP?KZv* zwLcR%i&v^ozK=d3jfrwE`IE?XrXc0DKgM9W`H!Aa_o+L%^_#2G@ulx|K4D!>Gg@37 zpYy7ov)!%Cl4(x*;J@znENAi8GrbnuJ>rl6>Df-TcsJ28L+ERt6EcMIii_j(2eQcD zZ*h)4TBgwLR`W>PNiY<rqwfpSe!}Anj9mYzF)IZj11$kUk2?~l16tTBZaSQyKqK0# z_hJ8I$KsB%XUB>(96SHEPuU|*7OzeO9J1{-{pJrCdx;cS_~1i&bg$ryV49nCl3$SK z6COh?NP+2S8xDgl2i+roNYSRKPcQHn^vC=PFBbacumjEiO5G?uK8&E{CnXJxW5^-y z;)f?{P|fwoWC?x@ud4u~$49(|t21`h-IEVWNVkalPmuOrqlhNCF{14!*0=c4(zFit zlnL#1W%_@tNKk7cj^V9tellNg7TXdidG26IczsIx7#2!`+JRBv&-&}k5WZ0B?h&KM z6_IM0DK(=;nmw!PIHupuFy<90E1F*jdk(6eL~66WY7+`Ri{|st-KS3NlMwFW4s`!t z`u$&jv3t}`CsQ45`s}!wcB;g?4<CA*AUJtkn&vz!t84UY#)gTu<Z_6?!sjG*O-LAz zt90C6HLwgVrnQx9_^#_sHYJ4z>U%s!yKaBUZMA}x8-zE4$69~SE&Cx*-%1UoTy(6~ z<YYqA$?1^&!aq`U&qK`9j4_zc5^KaEoZ)%JPWq1i*=a-$tc&tWZN1BH9KU7R(+TnJ zVu&N2S6jds8SOHV8%YMw*}`!+#&&t3%Z5XDvsccNL400+7#Y2XpjoTQomT<MeYxy} zeQp)vCFsKOK_JTRb-g(l*$?dvzG%>qschU$Oyt;=%^zi*ZPN<%B^ns;l55QB`OED^ z@kK`$oyYyN<2Ck=ZI?(;veWAoA1<$terETk!3~C-id6Ybx68r56`?7**!y0%4c#*p zRiPN;VoZ7#i+kN)L2szjL)YywahdJQ2zNtcpHWnZKWecy)`ff472B|Ge^lupL{u7U zrOCFF-|hYEg55&SYt1LH>&_=~7uIZ&O;<eIm{;TEukkFK^mx%^hkTJWz!jelPgyUQ zuJwFR9JWFmo_aX<fCsm0H=>0z_~S7YiW5<Lcr#RxV2~^7ULM!CZ|6#%ORo+lX6VlL zDJ<|bS#6A<`!jfgQTvIoxH7Z50p$ynKEKYEn_UpM?$CPC*v4jsYtZ*|U)rBqyQf>< zu$!D{3<7M5mh#(_xn4cuA+$YHVs%)`Ewp1j>`p6ZSiCD>@!RHu$z$pp^?RFiTq;&j zGMVWe4nX`j&l#UEYaEbV1#tU6GW|8iPkXednA%X^p&Ivj94yOaM)k)}SJ(h~SDR|v zziqhWJRGZPsG+J8sbqr<cA+}UEs3e#MOfv2M#JjuTbtcs1>N3@FA~I=dPKdA=2S@z z>nPu@5i&lVPa-qy@uG^VD>oEsaU=BCSw@VOiI<)QkhiGCED96H4EUllC!!am)0}h! z=^f<}XsDo3n59C=powqm3f(`l2OsAV3K6FyV$3(1!!=AZ()xT_g|ESE3Z%${i<!C5 zio29&YpSk%x~9X2!F*N#92?VovfuAxkeHPUGV{bq5=HMZYCVT&pcUvOq8lZzXtDWl zckB@KGz`En31Cq+%a7>&{>A>jXN9WWI1=^ppHrm<QhZ`!jN>FSY+8&Icm=_l#_kwM z|A^dTw~?uk-Moak`WU|d(njy?oVyjA<mFkIu7~Vp=ig9`{~y~-r{MhD&3)<ZCe|Ko zG!n{#E2{0p*Q;4Ws9b3Br5zvq2CZPNbw&Iv8ox78ZQegICFl?#!{>bR;3RC}y8I^p zg{_H`J-cK|wS<fF+OmlCutCrb1CnH6+!Ivo!0-JbB|FvQEiWAWz1v|G{wcoWbD7<( z;|r?-ecs|?hUW#oo9wPOz4~TE<9_Se%`1ub6phxr$GH|RubO9^!#au&0{b~iHspFJ z=UEBX5S0rj%5B?F>X#2T+E|Z9;-S&b?_hj(i-+e~k!|#Gu9Iyg&m|4~I_Kw`oW(t$ zz5we<Zw38R<>p=;rnC1sl_eW>`><Xtyh4n9rY}J($GI0So^YC08u*-rK$V(4{VyjG zcjWUwIxInGf5yeyHNl|)U+A&t5$Ar|#=7T|X59?EU5v9)(7n15DKQV}qZ~(6<ur2X z7hV)J>ir^ica@+Ka4^+#(8;u1X@$qtd|OWDXFR}!GiS8S9w#8>IiZdOV@XCDxD!_P zX{f?o)8d)g^^(wQgWxwmoxIAoTw!#aTsLYi-OE%iL1oAhYO5^K>MnA*n%iN9lxP$M ztyTEROqhAPU}bwyp2NR9hm`HV`Vq}lvb4B^3qGr`)7^nPm5slIN>Y!U5k11OjEBQP zM8`g+ZLXMJZ=v*bA<7Odxh)IsM^wE@oY4=``XuFUOxF+-U}l!3C@kEGw6A8|aR~`) z4(33k?MCg<RZ5zr5}dV;v)3_Oo#_ed{c`B{gINtK(bHq|Ysh$$#HL}jkYr15Ckjb# z(+s!Wgq5E51M^n{20I`z@>7eIZ#|O?lY`7zjqH3p*v9f=*W$o>EtBjqPLjdA7ZPo^ zILbuy4;&=UE&2Xf7w)s89b4Ug5n<bLSAZ#CSXT2DuMLi5f8K=M@XPYP*E)W*d(fw$ zv%>u}B})uB(Mptzu1?wS1w(jj$mZ@IH!vQzo5h6<yaM{K|23gPt$(0ImUQ3qIO`sg zW!R_WN(TJq8I#Dx9RN<HWt8G|n(n_op7$if|KvXK=*M0K*!nz5j`c_V&Uj53K4NeZ z3P-k?5>3J0R{`d*K`oWm>zRYr7jZ>=O{AQM7;z4FSh+d-@rq+D;m1ZT!N(OHMd;_S zC6%KgONt;kqdWW<e&CyvwFdM_9EQjz&)A-1OOebG^9vfJ3bNk0M(Tb$;V8Uj6zV~V z5LHhobhj#Ue#0fKJLG+=I~GRUGJy^7aHmdf%&%WwAUL_Dk<f05wgkK8v|PY%u}>HM zl*oIt=bZ;z@%e^!AMjq`l-rRUl#zWySWNQ#7R^j|mj|ql>j$eqT5I-cDT=nMgcofY z0mu|w65l!*F;NkXd?heh7n-%Mv0efXbs1jBthH2dAPOe)%2vX+w`K-_!B4BWETi>* zsylh;eLkff3GoLzO8LsczZ6iavw@00+_avY^X?8SX`!+5m-qKDBl*cBAx7FB&6=v7 z3xE^!@ObYSn50>e0Z{A{DH?C_IoH<%*8f%QyuUN=eUFL3=mkAve1^)a0E@ei7?Q-l zFij2Sa(}wHjgr*pr;B_Y_z$xM9yH>XZ1kk-O=KtY+qTMc9Xx=DupTc(ShxDx=|!gn zWmH0-pq`bQp10il8nBiT%gno)VT$VrTX;q%Cbxo^Pv^C8+U_P_NoYfTIoi?K6|EIp z!!!E7czB2T9ui3T5;+PW%P@uvOVB(frB5r~e@@76%d6=O<8@j(E7iSu?2(f{13)9} zsxCQK`ZW21=W5RDe}Q24#gD3|i+&Bf!Sf|sN=3d=+;!_oNEuh}#j=D22wmrV2#YBD zzuH0dB--Gt7b0vqVG_n@so%P6=D@5jj^JYUg}0q~IjW;^3#CMlay&WCHFIb5=V!)j zSn=CKJEaD$+-({BQ!Zr-7A~w28V=G)V$pbw01U`X8qH6+aB{p-lexHvhn3}?B%pnn zL3Y-gXQzgUesw;rt?}sGxDR)`Kk@N8Nw`KC_X!cg_>EJc{Wt0o<M<(|g(l_ZWnef& z1TN;~PZ5(pr*PB%9eUxA{t0jC2zY>A*BsfcNdi#B59GC^*;<@rp`@P*4Le&e!*3jd zD)x&xTc;dP#4b+<drsaG<jvrm@kxh%AXYBMm_$$>y1v00+t5-&=f=7^jalZ=i3iEd zyhhEICc(_`L-+D<U{mj+5?Mwx#)?pFu;!7B=j6z-r6z{g{TUBibz)Fe<30zN5i&C{ zec`Xj;g@M@=L^(N_1H(T$)V@@B$QhV6D{CACw4^);zt>)PR61;sHbE|kTQBLQd`n` z$3`*!0-qa12jm@-{HShwrd;cb=Y$ImWQc<obBW-~DySn1I9uU~E5=BEHnDQ+R@kbO z{KZOGUEK@{OLC<ZOAp<&D7nWED`z>^1#3d;jsOEst6iEBz1@27G>O9jbZ&ZwALJJt zzWhgSaR12d4SkwrGcw^D8~R?C12w1ZzVRf>9ss933Kp;oFIAF&WlXdsO_ffF8ehnU z8ag4;kpCjTI)6PW8&~Lngd_t&zf8N&1OQ9OmDsnl4Fv75EQOY<(UOmckK&wX-*LO1 z$ZmKxr?H;+*jOKq+Ud0fTIX$w-NxlvUlTS3!YWIaOwL`EXi44QS`+x-b6RTCb$s*w zF$X!yC?mTfW$u8<D?CxMRm@RI^<Ig4&9KM0`nr#1bG|H4G)H0}T<EsNmXjPU_oS$| zGu<rO$OW;~#=T!0eL$1usESU?7rjvuBH@BKb|)4}Bci3EZ}u)T-_F{s#m{f&EM($^ z4bW67Pi-9aEGmO&#PX4bZ6aj1Hfa9I5bBm>wCmrp(tq*?-9^1An=ZrFr)e39$80TU z5zxC~z4xNke!rsk8t1E@0qc2L1nWrwrQS@Ng6^4B>Q{sN?)6`}{@Aa1;Rfd1se`8& zYMdox34AWoQ1a`#f0gX~D)_`dv9ALcE;ghsmmuTfa9l`2|FX31Nuf`EzirgtZsNs1 z5ocC+N04s%-P&y*KDiZS*XWM*et6%{d=p01>K~+g1oY6_lrf$?7(Ml-03sm<s~XZZ zz9Ytlo8Y_ByBbBI+Y&q7_T>+YmFwg-8Zo4k1J&|vn_dS5_|A;jRgWq<<^1TxFd#uH z0h5f;|9NkTeHX%90W~*!gblvD_)Ubyv+xX}vF54XYCayp@g%F@hj`Kjb*`-HN%%B> zS6Jq`+LLH7p00=1Y?0SwMn}t}ij4nts_G+#G0tYchH<Y#UjzV+i<lP4&c2L0>+Crg z8=}>nLsw}9>x(vr12|z?yng^bx)G^{sb|mSQB3|U7DmsmOYvd~!hw*Fs8lrRX)p1h zmg=&)lU*r^kwlC%SM6>j&5RX~W1~eYoNZfF__Y?$TcwW+&bO_@OmJTn`}2OZNy{gR zoap<rf?@)L$)=T;f2RfBefbAl#Xwo#-amJSxjEf{6Ioz|-VXa+ztG%&Fe`u+P;U`8 zFyE>)*YEfIY6w5!7r7##5#wMyo}0V2YAy3D`eJ~(-g+9V1zMISMDf?Ic-~Z$H>ZOb zq#eHeodO`4n1J(>hE!REoc58MjaVY@dpuf8&QaN7^ct;Ajd+qdpsf?DU&MPH=%{44 z9B6P*L&C!8&CISM|7o&Uj(XTZi!E>WHORJqYcy+CwA3S&0^D(TNT73SAvb<-ZuIs( zVd&uY+JN5Qw6sho6lZ`@XVtwQqJz*=bM*IO3V{1(F*8=OuGBo467EK<@!XQpm1~Aw zSBA~OpKlDT`k#8ctR`-qUSp1vDKmS=V={G_A&8V+7y5klZtu2vRBg0{Uxcm9=<>F= z&v0S<%y*`W<4GBm!YK9GE6XT-dO|1yxJNVt`xR%QA6^ha^Gsq+^W?H<>q%?_nVE`E zHQAdU8=;jN3|r2}mGBt7oA_3%Gp@bP4#y~QuG{8!h|1uSYbdLxSxdF~*lkJXbFO6P z^!w+81)~^TGaNW;&@~$}_Zno&8#U=stYB_cI<147L9-<MIe1LMYPxRk`56S%%#Oc( zv)n)Vqo>)n2jh#pjr&!8BCMAq<$B#W)FpL-&LtB=$mnJ<&9~!8=*z{LQO67JJ9Hg} zDn4k0a5<Qf%isN6nZF$8Fnz%zuePkVxiwXF+vesrOqy8l1MJxn-p8hAXn3snR1E`D zdKtPx{lYWli-jgZx}z?&_R9c&&4EiC?}z;p6YeCBs58ZSN4}Hj45B$#f;p}I&{wYt z{VHvF=^3n`7{#6+@yx^Lr%i6G5g2O@A6?uP;_dmw0TCcJ1}V?eb}e3n?D=v~Opz+O z9i#@79pwJXJO-5_al92Jf_Z$lFsH|uup*WOU3g!Q)g_pOmKCD?nU7Ijq@x)7RiXOh zVU`>IUP+od$zMbuO6h0J9n|P{i(bbNe*_3uMN0V+uV$kYP4IQybmj7lZxCPo=fdsC z>(ip>g7LcPr_*ZJ(6U)KS@7d_>5-+!47zK1lzK~iJ-STEVoWcjS$cf~)=jm<P!OdX zutWiu?=Q%D^TtJwx$3eJRd;P^v}m>EN~y$nN{w}|KS_G)@2nRM?mtPdA+#^UIg;JC zRWdMkRo2}{gy0j<#(Yzg*)<tRSf&P4d_P+@gHPmL>~lzYQrx?xcY=UpTgRi$=7|#g zCf%VP$<2XElh+8LP>bTsdB8r-_K>@D9%li-#k=-T=Zsddxb-D0&vh8qWc(tb^wo~d zY1TudwX+@Qb@t!S4)F7Q=-u@T3}KTx(Tl`9Pw{;>5%)j9>o$;B7$D4ePKpDaY;6<8 z!o2B@(K;AkoOWm-<D6Kr1>CT!?AUDx?(vhp*&rlzJiUFvIf+(x@{U^;Xdx=Y6wH0x zJ=0LP<N>tjNVEP@J`cYz7)b-0S)DP=P0A%V=(Jm^kpVm8^6s_%bml%<nk-r3t2XP{ zRMFY7-*svYsbxYcptlofKJ6Xtr~xJcytdxcJaWe@zSYF`gl6^ATIE4+pAn4Bo@|$4 z$^OpXNp!!}Zjh>GZAUPjv&*o#nx+Np0&ubRO%jBeGhsqqo=T<+aOM2v*d*Ql)Dg0O z=FM0=nsMLvnepGNLL<hsvjiiZ7*)StRbloqWA3|!ieh=w%)zOUm|R$fB8cyo@6`Y; zwsBxt(hu2|Q6hL-vbiP4V8rAd^0H_%VMX&14F6IUHNPTD<Y3Z_N@b_3*@O9a`9zNi z=B%mFFcX`->L?*T<j;oNzQe6I2W#v=M>&j_(BfQ@?Bj+Ik7p_)b>DA9x;&k+xjg7~ zbpBTNRrsw{b!6MH+3|G|Yo;wovc`-|5?SMbXSL`h#4Hmc&+(Y%wpDi0DicH0ow6DM z?{BL<@lS+h@OD=(asf(f)C~59@wylJFRu4}q1{VH0kTx$V8zxS$Lr1R)k*r7y47T% z$w$aD?s9t?hi6ocGNp|{d)~B`gbXoghJ!>L3($IAvbjJD^vf|N)@~NfK8@9Hr11UF zXKPxxvP7?pd3!HTN#Z`QVHGKjq8{Ym?Y=~%M@oXXBdg<k=aJ`4VH9NFBJobYZbCue zD92i-xs=epog|K8C^PXSekkYY%M;f1@iPO(TN{PNh<d($*sK`L7m5d6>~HC`Cw~;V z=ZSAAU{xbBxs)P&w_21aea^>@2gIDxtn$BeWj81GjUm*39v}86z%&ptn3uC&b^oxq zO1s_?Ot;=V0rP*`RJdN^Flcpv#eG)sg!MGW`QWEcsxLS;%**2|<~2jwd?$n6Txv|K zceUn|<*;Xuyho5?fsT_D9U=Y<R?b*4&@;wf9{sodiFb?QE~y_~^|M>bS%SFdf8>ND zGV2c{BHE=m6Oko74X1Gj!a~~AFVjF&L*ZIUV}1MF@fj`o%m?YxEPOx7BqSMr+c%od zQ>7BT+j`;OmPhJSzr^mRqGVht3+;(*tF-b3#fV%(B~r23n|i;R5QO;apWes=Sz!`> z{Qh`$crv#;8P<LJ#D<UFy!ap1TPlvDIDg)T#6RXBd)ncK!f&pFw0*Kyg8{)23!+n+ zVc(J|IY}du)+T3vUnGxxd3&I*ce5HdukCGQ6zUI`Ld&Im5omW0%}X~Y98qWUcJ8Dj zfTmnql(_v$BNNr+M@qTf#$lC-QJOSBZR@%t$tPhYGvo^}n5QCqQ=>yO@W!C`ClTv@ zJgr{;B%Aud-=%}#AO9;m4^%m$N*)=WlOu6tty|BasqzdLYw-Ljd&Jr1l$D)Er*WB$ zb(ZR0o$dB8{ZpHx0|6C^u8aY{fbl+`T2um9nUa|v3^DbU%zhIpr27||>$qLpKlMGy zM}LxPfZku`IP#6$n-TFIPfxPm0ujqjWaw96Og>QiOvB?b%b1?$Od*3MhHSk_JUB6( z;y*9o5VjeE{|1AE*@cUitAE>WKROVQL>tRk+ySExVT#W_jyum0XY~=NX#LyqqAfSi zcK@lGMB38pbygR9$wgfG7WhA@*6hUlpK9fke<@w!`tfJzwhLFoz}~_mSi5W_G_&<s zH0Wyy{}hSiG8g`35Ujv(w}ABiA-mIJ<!H&(u0e_VrbV&GAQmCFE*9DwTTT*JCxP7E z=SF{q1!E;zvS;Y{qZxmr{NEZwjp%9jpQTm5(*IW%{YQBJf2z;D_eT4k3mNqL4fpp& z6Yc6D1=D&4)E|5+Dt$gv1AOy6Op8@ig}y(&OBLu(BzSQ-9?+OuvITjTjU>pMp(qc{ z2qoxH8y2w@(WrJWls%3mFE=LA?D;X;9N4nNUVlJ+_d(qnIvx*>1mH}U<Q+&)d!R#~ z-Kak)$u8Nh*sj>De?4jY5eNhde}{|#2mJf{MTPAp{O5O&xX%LCpAT=Jf+6MQ!&aF3 zk1ub{4M8B$Zs+x6Bg_!SZad%u#wr*D%9!7zAHa_K{{17SI1(mVg+Jmeo27SN!jw#k zn`jU>d29rb?nWy^QrcofzK?tgJcyP3<p3Or@K)N$IrRNw|9+vx=zbagODaQZTo4FN z$|>vk=6HboOycFk)hWY{NI+kHS!MRv{S>v{VANM)6=o?|G9g7vy&B1}Z$TiiW(9#* zgA2>fR=OgumdWoYwAR!OEH=1sqt-#7B(jH}!5)-qDH&G6PcV{!M~U;r@MF_&-$)N) zMp5JkBZEMM+RBrr(WT02X3@3ElV;H^FyDu}Mu<6MfSNa653Pdz$Xn9%3=N#N4MfD4 zQZIXr9_@obx}SWzH4D%%qC31r9*&F`{OkCnRj?>VOB%MWfeQos)uxQ+6d61SB#l(C zQ`x;RDDori2QLz6q4(jZ?#Z+MBN9sQg+W#n5y7I^=tJ1_HE=NH0KIDwJ9mSmwBp76 zgV3z6<Q_zKg)ZhC8X;nB?32Xf5cnz>4#eG8ST;`Y?T`8(tsL^dX2d#nozUa4zbGg| z76j@~TBP@<14Pbk_m|*b`VUw>_V-hJSsu9w{FTEmgE9N}h|+(LD1(u|9-HK0k^FwW zfAQG=&+nk`$So9qK764PvId0r*Xwxz*8f$f@c274u16Q-$*%?_b;t9|;m}S&Ho>;c z^eP6CAGA~X*gzm|REMtW`K<QQRa+iAPCmsLnWQYY0!Gg=k(Beg9TS_+$GLT`_iB)j zl?_IoqqM2&F@E<0@09i{n;wCJ>2LVS<JJ#Lc0X#jCD!P|ftugH;I?PpI>`4>3oTA% z(`jQc9jzHIsyg6_hDhxthvaL?6<!YK^jz}APRUHpQ%7S|R*&UZ^qYAif);ju9U2DF zCy$$5HGGVQSVc|GW~VGQXpt2#>L}&k5RE66HHcV{PwIr==eQklD&-Hg`I3X0!NK!@ zax5^4SZ|OFS2ih&*sT?ORJcIMyt`N_W>CIMH+2@B7RhSi8Kr;}sJE3veif!mRJwJN zj)T6Gf4E+=?Mv0jS9j#is#S`KrOzo5D)QfzGb(3}oTKeoO=8o!1RmiI$(0ub0#;)x zJa7_GOPXERBL1waSe81H4Sex{R@M@drj4?y*z5T<SFWZSiDKb1U`_SE&~h!PbTi4A z+8wZ&)_|msG6mevo2;n`X!R?F1TyR)fSQfl_5mAY9Ye`&U%YXY@1mBco^BczTKK3a ze>#5_0xkAN!bH@DG}Q^C;~{ic<UBd#JOA;j&N&bf^o3*1sepbz|9i(B;fk#CFH^3@ zlYH&!o+Iz;&t_(`606cBv`;2)N=&OACdfgcW}=pdTd|^Gk?@FJR23^IDI&GSx1qa~ z>Q$nY53jzLD(#S<%QNY|svkAI5K&V^7|Ge{v$PR^0^A99Z63Wv$xLOCZ!H{2yvHKD zcA@AHyG8(xN@-N(YBH6dRCLw+B6B)WX`2HqDkH2WKvSEI-&$`ZEhru1FuP8CURE!v zrTvh1)}bn&9b+tIP<7iiR`OvkH!+eoOlIU~03T#Ys+0wM`3g@=^7Y|i=XDAt$$57r zRHzNr$U#QHy;xzj;Mu5)@s};g=-1P1btOgzde6@jN7?J0wYi$nGF6>8lmTfcQabpR zMPn=Hy%|lMfT>C6HNDLnC&RFvQOk~gojIBMvF4>h(7j9mdpSx)&BvlG?TIv%f}@p^ zCJZNtNL!jM^Fe;73p?G<jID2*fulz$w@TcpVob_J(3d*pt=ZMxHtT^BqgF?q1R0dO zvhFWDL-<<P)L%8alDUtuomJ(Yef<btKa-r+vf>_;Y1FfFI6qlD-gg?#+QO4wm8@<` zOokkFXqlp>Dv2d(hQ*#U4(A&c=EiLIl^?Tfb)+eC6~EeaSTlB+;2BWgP&|ZJGBvjd zTYjV;>$WGV<Y$!_P#)D(AynzUGyWsDp}azbznpX6>S(V{UDuOukH-4R^C!VO`^U(n zyQUWTiq7xiNj}d>FSKL`##S!6?dhuFDu2f3lbyePCUBJ?=%5H<q7bgl25jFWWQ=6g zv-;77c`^*^+@J9U9?aAZA9`;60OqwUuQw_t7PB<FPL+~se6QI;zI|57%46(TIoxuo zJv%!IUT_^&q#BPcFg_yzH?7ON7NTPLYti3N*{`w`DBc=Y^Df>ZUQVsdP3BH1Wp$_W ze^^Z%IMXHofso7RGnlYd`Z+47SPIrn()cq!Md#(|RVv?UrIgX;(Crwm05p)bn&sK+ z1tat@N0%2@ugkh^92JV_V^+<s#=BQ}%sts~J7)83rN>Wvv+~WYyKW5!5Vwk7_tuJ* z$Bgwn8*um$xodixqF}d%Uy$EbR5iGR;XG24tqQi282vazy7QJ-tj&zrHO6F=BOzbW ze2djcg*4+s3URq~d5|J>E_0l8a?|-*yIK={l%6Fa3*7I)!@UpKH<QrpH$6(vpccK_ zXXeE!2D(l71$Whl<{LP+0@|53qam{nTxOom3i6cju^?+^9;T*&`jN3u-798>dB!3| z#FX7DCC6u{(n@uRFU$S%3N7Y8jbbOYl)Wt6F}KJ?zG-L4KTa6Zb3)rp0G!wcfEq{^ zTwPON>~m=UAd0uqCaPmpDX8K!c5qHRD&vTH^<5<@_)IFRo28ia2aaj#j6++u4)$o? z=O43cAG!RHe6B`{+p~I<ZZ)Qi5kbKiP<Yq$?qhQU=_;gwM_NrqV@Fvb5e9ZT7+m>I zY7X~~Z!lM^gCy2@lx97%D1Nxx`L4~&rH#Qol1~|JqtD-7-^5E*GhAd`?cIjJ+Z2S# zF^$_?FB7dzyFmgy3FmRZ?w_siBE&`=dsW)r3POaLl^9>jpU%O3E}SIKGD}a9v0E`Q z(e}HA1NCcB-d{I-Na;xKR>KyH-?1yG;ugS)3vgZ|%Pt$Mp1kST8PjobGriT0==OYd z@<z<hUJ8JG@1AqpV{p54T3}Av<`~~}wmTuNH{vTXmnRUFg3FY~N`xUz297^3<-NNt zH)>Ny&NcJ$wfL%O9CplujjO?~&fcGQ%HOP2-FZH6Ab@W?6vOhzI2~0GNsrAQ4exp| zj+5zY%#HAcc{_?Z!lQtiH*HR=N7_ziSdv6!)a*NFRq@71v_27e94u_RocfM-qsko2 z(JNVO)lx+|J6ca=ubv@m?CICn_bet{rV%+QJsXXvrZg)ZgC68-?`zMgGxeirEX=v! z746->2p=#q+~|}SpS5)5hnQ{)%bj`&?|yaubT_MRF@+<k0&!5t@F4B*t%z7xI+khO zQjSE0RYf>Ap4NRHiGFvY_p9FfnAzDw9b2vgrMP}p?P?ohI;`)jcg%WPfV7pt?AE>{ ztG!CoNZd_XKTdy+a?JqZ_76GPKMu8srVmAK&Y`R3-Kjo=lA6B6^px*H>5Vd*wMvHx z!xYCnR>u64^b8^vYtkgq>xEW3ExBeQEhG?9d0mm1`L4KrnxTL|&z}Mw@$Gn5ZV--s zChBg>d;UtsU6c80$awhF?XG{dfwzmg_Ge=or4Wg-p{S2TX5vJtS>HP(S2+szCNG@y z&)fXZURO^D$1wIm**=&MOp=F0^{^`*45~*<R+7W4Ch}*uQI=fX$N^8pba!+kX&mcV zz-Jr}@Vo07)X@1x^VGf-$lcn!&s}Vr&98N~&a(C|(2ia_jmR^|pDdAgj#St+JS$vX zHCgo|j+R0XMxJ_bi>IF0FVF8P>3cp;9E&Tt+`VAJK=bd?F5<#-?yh0H_@!PgGLMGg zUqQ0Lg65ELfnlcOG%PY6e>>C|63j*?(Zf8P(5-1Or<(g+%(P-Ni=dLTAtZgd$W#*t z*lO=OyK5pZAI;{e^htp5nqic?`Lpt!<LRuG;mgW5VW79Rgp>2@+IIcLn5aLwqs}K3 z@@qMOIS?`Ft}7Rn;lrOpN>dq~4@29*R8gBgg!JX3>lm2vhef~K4wMBi$tv4~n8sk< ziuR^yD**O80vThG^RRpNsW!F7kI8XgtvKY`mmln4EYO{KX2@)?<`&*!w0JeWWsltE zGLx;pW2nhp(+<Iz{)O76tq(#N01tGt=$RS_=hP_HXXKTvYzvqQohzf_`EJP#q#llV zx~Q<7E$YghyU;IW&`v+h(yh3djBth&By-AKeTAX1vYFk4QUCFixcKNq?%H*kZJEog z=F)}R2#%-`ygwy-vJ5_5PHw9?ywy9}6%(La`EZ>Qeb0bW`$fq@#IME4tb-y_vsoq+ z4lDT!{9MnhFq%N~TD8dQT_;iyE3&IbCB;qlm#z;LpFDP+u??*y?zi?-H}=^&-tSvS zSCSs>1SiStRik|Dr4@<ytbdtKB0e(+p#y=wJj7;jbg3bxO}D!lJf@f_T<QDfN<~RJ zq3%YCcgtr~aF&&oNGyDr2K~wtd<BLn7N%+n^y^7xoHn1K-74b+23GZT8}?5D8_v3_ zmc!??mSwVqj-6HFW>uAI8m|BzmmVqfUCnm`05j~?7j<c5G}}FKckzp6-$f4;hL%Uf z@q*<Q*RDb$-^4UQp_4>ZDe<+l>tIa$+_liOFTn)7Q8ZfL%TUd<voRMX+=1~E78#ZP z?<h)KmHE3)^VifzUFHhM>R<-LTl1yUEImiFXV=r$MOM<6ZaYEIAnW31UGwX^P1AU1 zBjFK=CK6uIPcM%VU0It8_?pU7T}Z3S&U`{eA8V7W-OLyKK(G7wy_i%&ClF@++lm5c z9pil>)~Qp~W$E6tA6a3;dx|Pej<IJsw>6FII4BkQ5W}ShhY1zRLN(g#;qa2dbxu!N z10p~382sB$f!jr$sBhgjjg{IojSgr6lp#}ZMAsg@SHGhlUFm=>%tNc?Dhni{J#S;V z_zvDhh(TNC3T<WxMy&>ML-rw%gu~@N+{G#T3qDutaAsb!1fgxT$4{<f(hI*6NBf>5 z{p!o9vN+Axmdi{sYbsP5!x1UkZA{7E8oiNg_<VKMl}f{BWWb6J5}kX0$A5nL;+mtI z^f^{)44GQBHEGqr3`}_}p-k6E*M~{L#Qmq=$JyYB@Wq-3#Pb4x!AP0eMlXBFF!IOo z9Sgg|gq^o{r0MH*>MPGgueT|-cug>ae^fkEF&jSPDY7-}z<8>*LmPOMGa!GtR{lc( zxX981p#XKE73f0&FScW6``BUk=h3E3j5#VfPMz}Nyzj2zDl%7|@^n(QVdg`jz=H4H zyZ)$n5RmRBb+2Bl>AxF6xwGTknlFF>Hk|T?Bdf$Dj6KZbTe3->nIo8G?pEw-Cm1<V zjX#1;tmD-#;E0Hnb9SgP4Tzm}Vh_)^34*w*lL27C1iO6ZZXZ<%SYm8oxg+3|RXi`t zBH${92%rnxP+nq)X*LqRjZb(%-{Rh6x+bQr^*%YgsMUX~V!3F%a@m)ss7ST&QGqAL z^)=b9siu0G=oA3}5)dOfl(6LDoGa7X_?orKcBh79tuUvPIHIsKp4`DTI@YAFO$f<H z$7tm$IlQsAp2x7r*C<eO#DB@4EW0F|5lC(xT*m>%PzhG4<0Gtq+|z!=5X*7>QlB~y zhw7mBE{Ko6^4{aioA@_8F0Vy!w*!*cFbw)xeoZmepI)UDndYk%JvAz3r=6JhKddcK z)SPvMcDtK9_Fk#(s|5JXRdR!pyhRVrYxUhmBHvn_>vUhuh2DhB-DdtOiLRuR2r#BE zZhA%@y0tNVwopQN!-W6>VZ8$alJM->dF1`)Llb5m7XpLnXI~TS51lJnT2Rz?P>f^i z_$!d~Yd?O^emByAnCsTAFBv#QW5lq(uWaf7Q{MI1cG&eO6{S|#-G^7NysZ>`75fMP z2T|X%YUe|Dz|GZAiRkGo=)v%vW^}&#E+!mC^h{h9)D^wtL=Mr>;mp*wP=_Fkj`EF+ z?`pwli9t2S-gHqBmmHk;*zQ!KwR37V%ext_-JxR7{Wk`rb6OGZi`*RP%udNnB*gdB zu7j@z+gi^o241qC#PaJdFszo@8?52y?xYcOSqS8g>&&KQ2R3|5G<M@6^Q}9PPK+|b zL~ur|8AJvVc6Q$K%UrvSD6|)+96MWSJ6BD5!x7bemfvXdFDR5!aQV4!l`8H@-qbYz z<_Dh*FZOIs#Dk{1{SL&Yq3`>cM!LUA#OKBEspPBaFen!><llb{;~9Zgi&QcsC-67E zuY^||Bnj>GUn<i0>Z0@vv;L&l?#x<pv%41)(p6h=s3!B#0?~6tVOl$ucw8~pNzZ1V z)~h|UlZ{TLJY#R@5lFY9vr{9#;TtMB*(gg_?6GUe5y>7MSAD4K=n-GeQIGnIYxgc@ z1wrKzec@Oz?j-Vz9x0z8=I8D2IazWMd_?cIjw-vBO%+Gs0qje>9PlHEL}DPhX!5M1 z#m?CD^C!JIBdk@*a`&;<Q{;??k30I4PP%;0!Y!<#0h$=4eM=Q5rM{`LA}$fbdr`jV z-G*262cub`Fzrgqe3ey{NOF&f>wq6oBb)|ncLV&XM>d~&WiQ6g_$uX?a`M1$LEI`K z6Z-%;cJR5?pr2H!_tkD<ujArzH?yMUxdJ~-X)K%G&hXRo-ct?*qfIVyg#jlQ;ZI-a z^!_%*0HE#f99-%c6V;Ms$Ea0QGd~qLFpx^(X1hqR4KZY5R9YRFjUb3(3^fNcSNmJ& zcZkik6=oxyE(|nomF&B0P0XhU8f2Mnp2H>M8sJ5VXu2H~(g8R@TsZgD_$bAGHVRn( zbN;T9MT`9^Bct-=@~Q3r$KHEJHQ9abzCJc=fDMsu1C%Dc7ZnwyNDaLsz4s3CsGuV7 zAcS5-q$PwN0)$XRS|FhY1VRErLJ1IRC?Uy?&;NbT8RMM2&)DD2m!0n!W8L>!_gZsa zziZC59$axu79Fo^FD{b~0}YsfNYJeZ)WA2ID}{-zPtyZN%p?0KpUk``TCy1Xd}SWz zPbj{v>cd1E>W7<$&bN?|z}RSg3p&Ab3EUvj!aHO#wSC4g?$%xB*3r91EnA@NI1y-@ zYi78O3tg^M*LLB2OtNz<WV>wXetVNOhE*b5KGX9MbR)cL0F^RL2oEThpB6a9yAojS z!aif9&SO#S7p|NCgIjX%4I3P*pKDiIx0&cM!8fTR(uJf|J)#u{ysQOL&OSe_=+jmm z>bEBLMY;Y~075L12QZ&k;@RdkIaW#YYz!V@w<@Oik~_>2z)vQvHSC@Zh?vYQW|Wo6 z8o)f*;rAzlTlK1rl2SPr>-f~dy?&e&Z<Z13J+w6OAi8>!{;BHn0X&qV#=5@;<qXm_ z=8PvFg|?r1WW-a1UwU9+dEHQdv%-05UBp%(WJ7^a8yQ{eV|OFw)W?duSUwA_n^kr4 za*=V`KPF{1(@mW8`8vOs1?#1L5+GB#$__5={mJh>gxI>emf`t(E+5ijqPP2nm4jE) z)eq)QCGjYr>BQX?<>B&A-GsNRuDz*%ZvvHaK*;h1uS_ezqzYixceB?|adGo7Lgec} zZqVk&Ce;MKXk+*N8%9b55$%U<HPr)z$7y>_;yjc%O)Cm@%n*t`RU;Px&8eJIlt~YI zEAm$g9^YgNpL}MQe(J<$A+2QIk#0`A%0B`#Q^ubBBbUcsE37Y4^~GCP%{Zy$j|nGi z^Lvg?A0nP;{<W_6EC+h$8?E`KuF{4iAuRNEJ{Su3Nau9xsr)yyBR_mIo`@NtT(pdI zdR;MH3wbgr_HkAC!M(~2dyM-A0uuEmSO15uxUq5cN9l)k8dkcq4&N$Nc59A=KWvEh zmSvQT0a~W$Zt1k&_9ik=7m!|ePMet6&rxu}H36`?agZ4hyH%AbJ|<S(DfUo$7gLo{ z7X?($>>HE@Oklr$seXrAQOiX7CT*F76b`D!I1r`eWZ6?d!bGquvx<lB#Lc|GB2|G@ z{L<<_)P!dN6F;ql_HDFoX4Fm8ASp_vUgTK$k-g720%U3HV`-H`tYl@(<{j!q?rG+! z(y^j2zRi0|=jF<wx9c91^C3xyqkvbr_q(ph@G22vX_f8_vfc83&EW><@<EJWX6pXm zU%Rg`#Eng3W%4Y`TUM#xBpRRU8201`gTQ@BV8@J*1rdzexba7wY|C#gJwRmANOORg z`ym*!X<%aCCmkM_fy4G9gMvU~{HIUM>Ar;fL*>0Kz}>VPZH?!fEvF6~3*&C<{2PSr zn;j{qN5|&J_`zwlnb)0hHf5#T2S~sAryQ!IUj?3?i5HV9EI5ByGtg$GWL{=LP0VR( zi~|sTll*H}y{?xJ9UJFS<!|XVbL5&#ib}qIX^hik-I}EGT8K=Ub8_9RvrYwL_JX%c z`Ujo1;^iFTZl8E$_k3}`*$5b{pD*9N16TILk>Sab*3RCmtYBs|^H^i0-()_Xdp0zm z=06-C@^2veyT8aGNFJMgeLgG~t1>%HueMr@MqO%m$6GDDFq_Df2KskhLLqLOWiKoo z`;&Ld2}?;4M6;Q<J9WfBn+%sFiQ&I_WC)wk^04Rb`t^deSwl}0so8}VkzmCinj1Zt zqPb$Rf;>i^MA2rNkp<&gDSt6lWcp5soUdoHb#)j6??{}F@yM4~MLdJVF<^8BV~hG5 z?ko9QX4t<@xE2P^zgpn8*=FvThnejY6dIL)^VHCh?M%0rbk5m^Jql!KwHWoD*4nSu zT!)}Q-X5kT7yY@4?+q0eF6yP&R!n;CHKXE<U+W7i?1g4ke5h&MZ{Sj>4>XvwD};8( zx9WmVpJ{|iV89jS>BWdTQ=XWq`}%V}tFq4MK_epm2FNSsdG)Oqy>X`A;US4RcK)Hh zr+PW{`0ho%k!FZR1(YXD32c})%jZT%ZoZjrA!$CpGc!8j-v=?Byg`d7Goyx{-S>S1 zG?SM8dk@NMY)67kyAo2xL3pfK%G5+>w9_R;_5$9MlE-MsHumNQekntuP}M3IvnJg> zf(lnL7s{31>+wP*ky`Oi!_MAW$(j(4s_5q^6`M}!pXmZ;U}8p{3Ong5A0&MFL_Bej zFV(*UlHv+^FUX`bC4x6&@xlquaRvpvcNAqmqg#-|Cr-Rv&Vy&2KJn=4H&u<v25?DO zhBlmBKll9Pi5F*?)Q6vWvkEiBW=i`F+ukB(^lt$G^{H<Y2=DFScibP6MuW|%KlnR< zqjCiuOw#9iz4O12&b`5{Ls=-<%VZs7#34R7vg_Ga`TK0O@~f$9H*M6bEltfTCg@)N z*M|(^)h;nb`b+;!4PRi*woNj;*=MZT2Pft1vTnLn^+ZI6A2BzQmD+abYjEW)u$jPH z=74tZD4zazXm1|BFn$bpt6j%SS^7({&ZN#CazW@<AyfM1()@3#^fu5U1IcYWx?xv| z(RQt>s~hFGB9ik@MJ3Ng=yt8pdQ8P*XBp%5hs+c_JLv!G6gbNSffE1oomt=y$V99D zi!<AcGL~+dYisLkLjD6XUF2xUwEq<uz5QQL!hD?nU6}fR{_^kK|G(Z?>W;FC3XLKe z-$&jjrRl4WR2Y}-cCrX8v&SC1rLi(mn~gecMW6IL@!>?4lv5D>aEa8qgRXD?$o_|S zy7K>rdKwG_UJ?@&?B@T%#;@we&g%91=SL>zG!7yJVw?Oa3e5Evq!h5&S7WRDhjh;E z7XOF9dW0v(u?i|atq=I`;r%}#`2U%=|9&z5`zu`*c_7c>$(#ZIRuG|<A?(2C?GauF zP^aEKn%^mRj(-1%6TOO~j4_4s<3|u{s><3&wDkEkyP<{7U=YqT<pTk6gTsnMKlmER zd*U7+GVtD=bh}RqdoTqxP~-OCXhywnAsNC(T>8-RcRtSiUpNafi1vh|N2g8xv0+(_ zZO_wN^Nwj*^^!tA?rd?cwZzqH-E$oG_Pv*utY8DOoi{-TQB~zt?9d#T00h^9m`mZc zq1U#_q)?oN<BO#vhe@We#6(AA(jRoh1Z8?Yhb?RUXz5OG#dperC+6~)96lY{)cjO* zz>Knk{HLoxUP>KWIcT^d>bj@DRsSfnRsV+P#4d9ioUN*r(J>ht{u&>zJR9m6Z)7|5 z^>_H>{a65adYz%DQWN{voY#|Zw%1@uWjsEwLc$jz^2x=WeqqwWH1kQ6BFk+H^Y)90 zvaUY0AlnrqB8n0&reI@JZBh}o!3&g!=2l3q^7#Nt0+U@h02NSC&G-A0*?0!4NBIN8 ziFd^T5I>Kjy=S1=Askbk;Afr@6<bq@4F~9MT5BE9ML%(VvTend0}{}0Xk9kTur31( zTujdemQ+@H5ooj8klF2TIT@}<r<nUD0ikzKd}U8x>VR5a&L<C;f!(QKkNaqoNaNa! z$*|^{h~&Oj>Q^@YN<$n6<(z$k%6KIR02xJjLY*ShOfwVmdt;yguy2N?shAlpCNg4R z{AwUCuL<gydBI<*7y8pqj}|}Rp{S9!RfcIU{>Vzes^Z7t6mdT`qb}g03B@2kq_x2` z+yg-PWEp<HfoVTE-Ff?#AsJd8V38O#1%?s}yp(!=E!_Q{qrD~q8AOhk_`;samN5^+ zH*w!ZNCk41`QqU*<|RZ;irWdr!~@%zD$|{|WlAefA<#O#>TlnNhViP?HA!a?&#V8C zfg%usO}y$T?m}CZM@ovcx;~!fQ#9Jkhnd%W%JfAPpXw}nec5fS$y_`^WxTP%=viPH z%XCk^`eX#C1X3ox|40S*Cj!6ZD}f0D)3BTJ{rZQCF?HXl3`j`&fIw))`dupZpW$t` zFhIMQu%rNK#B<-DN;Urz%d}dfYuX~239M;%S^>@*Ma6<*zm~(3?7%67Su4w$<!Z~v zQ=JU<hAUoBPe$%R4r0>>cPo=u#G=mr4ZK;Vp{eL4n2cU^zuM<xr))Zdj)tD%)%9(2 zIWwwkqoi~6g7j#{`72GOQr9*R0B((prPL>BN!$I^(Y)v-F|^+HNT@2LH|$5TCWJkU zOvE5PQ)(p|8kn{NELWcI*SrrHg|}*WbDH#N>b%s-#l6S&`c|~u5NMUeh4kF9r3~;^ z#|ch1rUuc}(ge4)_a6ErJiDD=*UnxN8xh2{ZaiQ_ip}%*e8D=5M9kgd@PL{Mr%M=9 zs92YLP3L0h>}0}2*LU{}d_PsA9jamd#_Kb>>J5!EySzXJXi~?RntKj4>EsR$v|ioq z4^GO5CBk<A0wk3W(WAk)yk-X3_ya#yRS(%ze-aFt9+Vf2_-b3Qx#0fQNdo6I8Gs$D z$D``BCwt3{6F$DMS`PcH9uhy#d%d3gPuRYd?)h=0xGO=sPX%1x^=BjH+s{5!n;r@5 zvY#{mWZ1~(v#vB`zVrbj?m=3fH8Y=6w4DAH0=9c-j5{+#o=}1tF>f_d{AAYJ8&<fs zecKctDmAckkc_uoN_w2vHDeW-F@{iYKfjUi7u9Cf4;-FU(fK;F!q4TCnsnnh7BJo7 z0Nif7DL4BjI7#xO?u<j}y2Jt`FcgAqqqOa7rZ-i~F=G~s(+OXmC(xbsQu8#(!)pXj zi~-Cvag@D$;&@XGRR0iTol#Q4DkqZd9<Ld_jLqUdMn`;(7*SO+gNhJH`xWRYvmHnf zR0B|%_roJRg65}EnIuZjY*f5KVsR-*bVV8TI8)3<l3^gn$H|`xl|i4a%%;~VRi+zF zG>{z-pQb(#DFH+Wddzm#iX2zX_6|)N6KtJEYa9GK<X2QIMP*}sYUTNd3?4V{F<jL6 z`+;2^lWiRy0>27s^w+l=Emtg<IF@F1DM6D;+_sOv>rMXVPQ$fO8*X)bRsXFdGJCzT zamy={P9fK<eu4Fv7$oup|1kLqX&p7FH)meleR|Y}34RjBUJ#j3PB_1WaL6&$@&Mmj zjmN<s02h3`-T#r^u1mjib3|}x;T1|``YYx9nU-p_gfMQl+>vy$uQ5)0hwjaw2LN~r zih!mTVTmuv-(!%ZE^(236+42&gZl415*o(iEy#-SPz;ji7k>pZD?{{<>6ft8U-lSN zC;f(DrwOd>$BvU6KCPx)4ho#)NRY{n>4xDn%ru)Y?glXZ)L1P)aM(McDAB}fbaJn> ziNt#w0<@$fLQs->6)n_@vQ7al&%e2(M+BGF4Z;Uh3?zHNM~`rn)lmx_y%^|(10>^h zj;h1N<RJ|=qlAkd-J9`HTDlHieS`28A!=)cf(~4%YY)yk5)PyELQTu%oFCsBnw<6B z9-p{Cz%#GnIBiW;BwketfzZ?y>^Q7Stik%9q0G!{Isj#Xc&4@fG9s%1lNHRSW3|7f z*jurR7_o}}u=LgTD&AM^YI)$vyFa%uQ%HI8-u{h>I;Ia`VdPN4*qJ5NlcZ_-q1=~= zz=44dsUTbb@;RwMEZx3BnoYGbPelYeTOjnLx4nFot*l@r3sMIq=57DFqvrvZ>y1%u zcc)(Kl~Nark2&rMWm6ciAe*#txCxkjg%Wx9-+s9SjUK8T+IRAqvTd=QDT&xA0H(<` zmBuW9Jbzuyp2#B&aAvu~LLThIT8veVpY%=*#07Xq%@mXhoX@15coS&M)PxErH(S-| z2TqIxftY3T^71BwzTdu|@~&7s^su^R74(-KNtL2uu1flQ!PjC5zf+_pVP6>AHHkW? z_LbDUtwlo#x>&!g=US~Lmt91u5)TEUOUC$}Jbfv6XQJAAsttX^_sIa<HIea74WvPC zz>@5;)P~wSJyVt`8L>`9*+UJ%vn!EHnG;8Ng0Bxu>lZHkh>fua6Wh@kb4mHI!WX-v zA+~ELP}|nnm_(nc+&g$#?fY$QtGa#yvdJX_dt3w4`$?}|Kdj9r16%~A3TeHKRDrx3 zSkBae)m`%8h^ovwAKewlmi#dm{u9?8eZKTTNCcekJTVR&!>fJB&nTW)5(ut-Ih+7L zW_KOh!c&S1TBW}nUU;-!hqMQaH6mYwy^;Yg`}^NiVS-^ewORtMW0#%bx!C%4Arg6r zwn;fE%U-#W6L8lNnO2Hz?-KK!eoJ5PUGop&3N+Ip8;Lft>P?0I<i8@4;t1T4LM+4z z)wDVzkVDsI1lUTq$|!lb7$F1w4=)m?)W@oFh=ndd44!?)XT+VY1Uwn=>Hvmy>ala4 z2z|rKi+otysL2jbzS=mnP$h8BU}+g`<v<O3RMa|dV^;HPg|(Xc^XhcWDU6LO&oPYf zY6lM&jBpA$f-#DrdCqk4BCdN4`fE|!ds@ZGD#q29yb|IliSx5_g|4N!uB*?lgyR;$ zDwSLMzYZ%4oh0Z^u(5Vhs=LedGL3&a(}g}y?fmZTP}%T)!gsouZd3H|&uLI;70(BI z`^#5tMun~{{Jd<$puh@uh(XXvkXE<$QUZ?HPNK8Yi8j86`ahBHD7E8hnN+2rV??kj zld~qz4Er|N65N)puZh5quIz-`KIsh;qa9E1{@9u!wWjtdsRk7!srP0FE=hy2GWb2J z)#N!~=g3~}!99b?7jDxr=sk@_<VBgn8ro%S{9E|Mi4&S~<J)<Y9o2p;Y`h<+&h!06 zh9V2Zs}M1}sU^{cfXE+`7O}ZygNbYGe{msOJ6qSuMvQ$z*!a;qtrq`}H)w!ywW5VU z=<=(6voY$cd2D6>YOx&docj*sRf2;1*6YZpupnn^S6tKtO?{rt6HmS)@Xsr%OMb|) zBQryvV%~OPZ0npB(pvMYEl_Fj`z4g5Z(G_aY|(CkMdMyUM3Ka$&o`CY@`uWCO9oT0 z;+g7KhD7oANF#90?0o3%r4ab%e@@v#-{V)q4KqxA@Su2t1tq`xZf^Ands)Ry<j#0y zvxqDis-otwviwh^RdKpaIrfo~uexuQ{4=T}a5kIk@Oyt&5XW_K`H$t3I~#G_PgZ(a zvk?P_<`lRI-lK78Tbhsr>zy*vix_ap5vM%$ta;FSo-4<DlpHZN6Ri+HC5QAYgnvY~ zIb~4tw7e+?Y-Ijs-3Mw^u;Km({4p!xxtK5@tv&!nH4NU3aP(AUwPxN6igxHXb*XNd zIoIUaT5+YvVbxL2YYKvC9%+_)#+5x0BHybOl%~41mZs?=XLYzp`MZ2ULa!Mwdnm_- z<Kxj)M`FHM6>lHEn`&9<ku3?+%kSrRUWpzQ^jixFZ$R{{kySXKL9E>Do^N~2*c18h zdKc~O06eLpslx9&3*S249E?enx6i+J)52Y$Y9e`7&k~N!6Bf$qBI+>>RzQ(p;&RTz zA)~DmDCLk$Eh)52c$|8oW53a3+i`y$w;$-dKy}@N7oR07dk-GP2ORhKv+etU6$H&Q zF8F?&HeJ|hSe)_4+!&`fif+cNxU%>^fBC&CwXcEC$9sij9ZS7x!CFUg^&_aCn6rM$ z%zC&B5ib(S*4QP{858Z|5)}z(!VhbEniQ~%?g2J^^s}Y(pNJ>kJ}~T+hbhzgmKswC z4|ZP_?2Ew9&;%yuzm5MZCA&-~i3B#Om9-m~{Dq$yt@AM|=mJj*j=lB1Q0<fM>6@#3 z_Gsj03An4Q5g|mYdXA~8FxdJ*0M)gV%$85Sx3G1!f2s&qTeht(55X*4uY6Yu-g&Vr zuTafcK4&fK*B-5C@K~T~U0PLIcE3Q^J@9=%)%iv%Yk0qjBR=uyfq%<3$BDddbj7x+ zoK7HFmOQUFz!&NnnU)^yRQ@0%Bpj9l!#>~^Sm4&tKDus^8EnaRiT?M~i6%Pt#>#wT zq}|*}9hZp8=Z>T4{7uZJ5NxL#wc@1%QBkmXiQm))1_z9{IoooxTWrTavo$Uzp=B|; z#5PCOip!8H;pF8}ebI*VEM<nt;cgr#uKk7x+%sBQmY0Fxg}%fm^~2`l(*DX{HR^S! zei;e2ET{w!E|lfrFWUY}yfLJ{;wG~rO|IR3dB2@9RCX%Pg+;HR{f=U30+Yjsl&{qO z{o#apwUb1u)BYn#M6(??-g8#=wwj-e340+M%gKby)t|9XhbF~SvhD2A33qvld3Ke` zHO{ZW`28o8;>J2tD_Y@@Tz>Y|y;&=aNxfy_j*OoBsKn%I6bB}Zda3dqvY5;6lOcbS zer7*G>bs}cXBXR33X=Tm%NjT3-ds`YiR3!kQrT3(<}I|wiGz1<?o~9rtc7{W_8!b- zx5@PQlI$4?QdNKbihe3+H2I?3HlPnIP83(T`#h~GGs*1ego5uS5SHacUfMY}!PMGJ zEqp5HXrQZ}u9oimi3mpc`@B~>n&PVfTUXla&;ib1hr4tYPvu1myGjeYw&!C@i@D)9 zz^j@3!ATDdIu+JmK4F5lY4O~@L!JAJd-ez-wpqoVZI^rbEHW1OF%tPD53M+c-wV;Y z0UpPCnWH{(19ieHg_`cB)Rn4eWh}UK(ct+wAO1bFf7n|`nmG(N3$mS#%vDJgc}rca zdDw*3oRx@vK|SusR1I1yuZT&|F?{a-He?)PG|$R>)r}DV3DMDGF1v{!*AmMo`8+9M zZ71IZj!jt=l-&<B{rG^@6J990xgKSkgv98{XmTnKhLJ3HZ&FWAoO?m@Vt%+Bx(n}x z#JBpgS~|YZZiJSD^&$(>h?caqs1-{>_BxRjd$b1Z{Y+l5tLQyYf9qu%yazIV8t}Q$ zk$tbYxNGsveSZx5)ylwkdGTfCr&^TX4&=+mys<Yi`+6$4tisnikQS9jxdWtoB~BZ= zfai$IOU{4)o)WfUO4A;#hp!cPmH&i~dOskEi7mGZ5Mx!8N9`6#pbt`ZOP{76*`wF% z*{1p5s<U*bAH<NZxaMC|L>cii;kEcMQY)unp#+MQ5q>zt&O_KBX=U5+cc<roez}5b zC(LU3$Tvry(dN~9sM5O&3RQt&vALA(HoC^BxmFF^f;%aCK^u86b)}tQc4O&T2IJ$n z+|_K#>l$T_8S^AstJ=-6TLaNdAN$9-%V%9gn9=C11-hPc*rt79^_Pmy{%Zt?s1e;G zleb%Lx`hfr_z<CyI&V!g9~!t+Myxn<sf9Wt8eRsc03VPO^W#+Nv~!Q(W52-D-G?CT zn!!sH)3dM_oVGDLJzZldy5#y?4nLA9>pa*1ibPFw%x)wfN~o+Rlh643rbz@xyRn~D zlvN)d*C|@HO+p|}RR@QwacZAM*c?d-g)s|aV)d>?1>NzxawjzEKrr`ui8H4M=4^ew zT`a&V*030W_Oe;70<3tG78_j@ejHvnahV{S7iui+THbM;rL&X}6#%z3qU#|yVjO>@ zi<<gh**TB>85E=VQ|Cqq7boe}oVih(a#uRicKtB_fiZqJE9X&F6-pAy()${f^#Id8 zA+)~lmN-<@JlQ>IqKB0@=hK+XPMz{7nllKx?A&qGd25xoER_shzo2^l+Rm$0e!dwI zg0(5QF`EgVzEtS9Ighp?UdpkHoBFhK_8(;i7;gS{U@nfQ_X5^Rzn^}nTkv`5T;P3F z&ekGu`nprhY&CoV4v&$HZ^QrolS2^FwJ?EYp1)5)qV1jv6qeNyOuyFK;!brMB7PAJ zb!}{v9oUmSiXO$jOVx-$J*d^U5$J(A8%tNQVqdJ{R!w)SSH~;PFjoR6Ctd$p!*<3C zo4cv*>k*H=@>kbjpMpaEg#BZQ%!NRd)Zj(sP6poUnSEdrP+d^P%lzz8W{R)=22wL8 z?oJ9?hU-_2^3A8j3jpB3p*{+QmhDR>=pWOMB!7I7R@a=`(P^N#?w^T~h4HUY>0Ilf z;PC43_Q(=s0MqKv3VzTwiBlVjd%Y+wHLzXL(un~F+^oiRW-JHG<woc2-F4Ji6S$JV zO~}iy{&rRrqluNE`9;NO&z;oV!L`AXC&xVK%CdwV%ClVj#6YuOhDhU-+-SN<i9QN2 zuNiDvk2QH1?Y(Ug?)-Fz<HXm@F@Kz1|EM*XH5TmN+{M2>Eyh7~c|ZFmwe&OZ4sp8p zl$3;Z5On<_7LudzK>G5q%Kg!jE?=wLZ#ypn{UO!eA`8<P%BS!^-I4DG_M&o{lQ_38 zK8=OFo_h*G5L!NFJn;d(F?@V`I>h+K!<+QBFrn9ORpEhTERPfZOmLR0#Y~m(hpVx) zTXf~#6(V3~s1hnNtS3W-KgHbE_e5AMSkJ;*4YC*XqU)$=QV{_-zWe!+hMLq)e`?4X z9=6pK&4u>YCDF`-C+Gg>MGGgsP(Z+bs6x!0-i`0}5s;{9(&hS`vU(O~_XGzyXak-% zMQU^cT5t4680G?vvBGa}z`{s*9WS^R{#h788BWza+lbk=)cPvkXkD`B&wUZ71+UCy z|2C!fELnsvAK`Oi#lttzI%O>2IA_QAx%~i0e0bOMdn3)?XKJ7hP=g)p&8x07)w-I> zTtP(<A8j7`h0hF5Nh7yt+iML8R6k0_pOus}ciHh`d$EF`aUcXaOk|zDNfSG!M*txE z73)Y(AXhPJA+AJ3Zg?t0t6FPZ#Tp8_VB+!WSIS!z*_GAn&~@*LjAiYq1mTk!Z)A=< zkBIdy78CG_JS}=?f;E(=)hu`5z?<)7n_31;A@tWan+%vWkMr-K3nUJ)7|Z>1n$%)| z)*K5wvK{k6E2Rn{?(rGaQQ{oib^LC#Vzn&EaA5PySY)}5OsOTN_pw*zCT*s$-y*K8 zFv?Q|K+7$U_6_258;>u{(OwuAM6%7g)4PM5`F+f2fe^#adWm4(dI|T>01&2g<pWba zdGzSukS?1aD&rzy5T663RoHlpJEHQr=w3dcI-iD|0%oHFVMtlxk30`!CmRUY{bBs; zDYIdTi?-K8BPlG%dY=TF2%=JCSdGxq*__xt_1eecsq4j{VvHBNv4m@sii%&6P+w7n zjNSuWjUlP=ES2X%bf$W^j~|b^gU2;A7jn5wS^9oOcFwx&%<G-5h=b;voa`xZs6%w| zOUI(N5x%D%+hZ=r<}b)96O2jhq9)B|$kuj)W*TwedP&qo1bO&lIbdCy>f9CKS+1FX z%5>W?OVSG8BxS1ZZZ=@dgSx?OPGmJr*p~2)k{20@U7W}$wCbI`?))@}&M%niq#*0v zrHmo7+F53*4*6V?=NK1%mfh?e*WevC%@_VTe;X8|2Z{16U75~G+3u~tD=%H%w`vW3 zW&iWLdsY2#o;T0l+&(vln~kh?Z!U#g!ozrOaU~`FUG2c#edDC<r^iC_OWB1!AE-h- zG7kZ4o_wJZ4H|c9#v~>KQIwi>S-y5Ih3g_S$D`G-90;uU46o1mjm)O3KmyFH!V6@E z5$p!hyg&1KD{TM<t{E;(JzeGgsO!16WA25d?zx0t8OsTz8T05Sui-5$Hcp*DpZi<0 zczk|>Su|!hLk7$B%u1cA7Y(XP^1v=;sQ6j=nmhL?*>$JD$}#I1r2YUUa(Q}hY8jNF zqHr_prFo7E0}@R3Ky28UmSmXL4^G^`R0VmU-+Jfpxp}rCs8vM`JekwLx;sFPl?y>x zzgj9RyAwqhL#~6?h%q9Pl$1!d@IS>2)(I1OP?1pd$Xgay(V<nH`{<D1F(Ts;Ys&S$ z{nxcFs6N9AOlRQNQ(E9_v)Vv3!geel;DNYrCd$(WZXIS8^<2CJPBgjx;%#<Jbn~Z; z>8h=fbx<fv5faG6F?&^WjTN>z)xXacN@rFZ6-B!S9M1?&(AV|jZ)mtc)7A|Eoq3P6 ziU4uPfRaywN-Z(3nk{vMBMF-CJGDWAf;wmRS>C$B9zxhQjA1%^qKQG|w23nXC!JZO z2P6q#1m;S9%*hV7fQ|Ge2PSbAqUuhQ2=@yhgXNE2YEKwp#D>yc^=dzvPMjS$Z-zi} zDgj1vO8*WPREv8v{C-17X8qIABMXuT$i#oXYg89Zm%Q#`ftPd6d(f|aF%dBJ8dz9r z9n)&Qlju=WDGg~+tFuj|V5Ul2JprDcHSI<Jj6bviTN~TCD^Nj_D!5XQYpI~M*m5Z< zx-6s4A_rPy4Gf`|Tc=iAOezBq3dZk&i9o&^Pd~zA{&|$|bg*t1XdVNz1+ZTiWq6JO zOco3+W0EGWKif#aJ$cLq6EXd@q7a<YgXu<Qn^o5yn%b_+7Me!!B1Osl!kApkb?-p9 zcBm!3$04A|G<fOIUY%_%p;`stNmSkR&+Mz^$(b0R-MFy6Z?9H)P(iU>(aUa8{70V5 z)LGdvGbekUQbqQ2f@8(j!gOD#2BVpKcoQkFCsJIbaWzrL>i31^zqYgy4kaENQtb-` zqGo70CSjA^jjR|wMh}kUH52zo51wFvxvd@yeP4IWnpWLxDj3MvdtBg(zEI@UGLkMs zky`gKDX~aN06HW4B|q}EYf3r9AvbE#{)fog%xj8_8yPwEIw!3t-*kcF^5yqt!N@a{ zZl;FsJs`B#=<W$+Q#FIv$lr$|(RDT!&pe@f<R>V9A!a)AdYb70jF~)s6<QHg6xb0M z{m`}HPcj1d!>C*1m-e=L1*jUwo0_p&S_zE(b_OE$9<O)vKr9Ae*1c!BRLkhEm)H)> zQqyQGMGWAhe)2oG<2Patl1XoO4yoewVu~B1pD8eDL=MMlZ8Xe<ju8*YO|iwXLsCo- z6qCVeRmOm1Bt#N(5yP?M6xab?*`e)lG6B`(PS*+zJ}V5(3md>6)8NM!>)(>N)M;<j z8|eFNj6C;F27;auNh3A35;Pn}Ye>`yWwljyyYV;42m+qI)<uu>&0Q#TDd~PSWV5fa z8tVejCclhq@e^XTifJ8u)g{Zw>t?6s&M2=(!_n=%1!9Xo$&Ck-v-QKV2sMU5PNbj= zZhdr;zMhi3pCQb@iw{nu#S-Pj_Zwqi)Dat=AB~C70`?<?jZd`=MMTlaV}M37F5tu$ zAmFT(b1gu~$~mx78s5%HXcnK}ZtYdk$Ggx&Ej1XPNV+&F0(m|yerAIy#BRgQ-y_d6 z_(c&Rhc3XkJhoC}XGS?2(ONmQ&}=f<<0JSr$=@p_c7diwSlWYO39*~5@MA3};CyMi zE$97qRKU!-;a}8ao;IHpc05Z)%qRzv#oyP-6W?7K>b2V|;&|$6xk9(ulH#hbgcLqg zw%077=BF4K)!2|ZMsgkvn^9w@7On)AII3)Ys)&5IDiYGhHu!tp^%$1zAF4`y<@cj= zE$xwda~Dp2lFEsz?`&U!{@k{HI(vV@Utw5^lU(ch(E^X<#}cJhb+tmhcQu+6K{yva z*ZDt^qGwAH^J`emqcDWhef@iq;<r^7f6mODa_;voS{Oqn!e&Xa2U9l*AEU*dEfLU- zHGwt3Vo~i=j0y7J;roqc>kA#{__YwR3L%AN3w=wS;ZOO7KJqU_D{g&KDtwn54IT`4 zOaXjh8I{jkfKAq2Q%{%)W}~Xgh%?tnm$>gC{-{K5-J2;JHwq^v9?ovKQ+&^X6@tj5 zLOJR_@5+MXlRLpl@5)$h@aHo0;K>^oh;PZ{Rf%>gQpVrT^TeCmH$B49o^xhRr&!p2 z((3YDQ60(G96k(E#)S)`z8ROjT6xQ^KHTKLu;$A&=BzON7FTSdwz|1xTt1^U7*YA5 zxNwTX7Sl7R>^mEUVh~PxM>zNorInmGaqrDG_S`PRNTRo8)-rNoIXvD?){W6@TyLsu zQXdHk7%I`5A(#>}^F7*J(?U3nFP`!vvO-mRJB*AXa!86{n_tyrCBcG1IY4FweaGfu zU$J=mt_ant`E)$9#{a`m$>@}6pe=&hvE}2LZZ(yY`q<u0GAdU`&eNfGb${k=OE@87 zLfm<1c+s%g9RVuHzq_<LVT<}5x`zGcqGaNyr@sR`abnkbkhYI!q%iF&alxTq_Kvy) z=wEdtnt$?NWES~zk~Ze_V~@sWLj_rh=zGYWpUg7t)hWmf=HTG&(bW9=h|SJ3N5`&f z5C1yx!eBIgsSshZM58cPYYUrpdwL5oWj#TCq4w3*q;~M9!q#u#ObrlFX(1?1P3iak zln#2Vy*gG`zx5zqz4gGjW$v9mi*4`<BL?<_u@3|TOrb`0JPO>fwUVQ?-uBVEDU2r! zdf`&G>R5@B7G+@8poG9U(8+IYHUhWhJCvS$Bjc-x4BRFEeK7)Ax6r$uOkMDsa?bqd zLWoZVtbFI<W^zWcoFXNes2S#&``s|#Zg6Z=fcD60&`F!wZ2<N1>0^`kX_5TiEGKRm zz62B$Zuis1mA$>(fu6o_u6cta)<A%HgUNdfRXN#v>af+=Rzs%kZJSrRkZ%QvelY%p zVN*VWa90^8g3Q)?T)abJ{jNRV^~4Q2^AiDCxmDwHPfz&uj8tO#c(m&Sm|mVMoM}uq z(9WOU6_Wqe5#b6iclfT-e_Uluo64J}|6dvrJWrAc_uQ6aCi<vYtdsg4gapGN6~^?e znP4~)JB4BzafJ}qz=YafH{W1;8}R;gq2^dt@b;7w`g9@OGN~P^vD>x7l<Dmk5tH8> zMe(@{cdg;zf9(X@V?U>$+Z3tU;cyea$tb7Aef4gYA&1eC(hOm%yUDfowi2cTk(6^f zpI0vH+@W&!|AbX2hQ0c<#38aD0ZgRTl!@blZU~f6nh%1$*P2bKITAYDhduE<<pP3V z%${qv5`MZ5vYg*(2Fxx(HS*r0iOl(Awzn|3r6}w1{r&>k6!f}o(Wd?tDq(E|(EtwD z1F9k?RwK95T~fLwe_26R&?1MgrVITkk@dC%iJk;KtM57SXXTE=;zrl2FEgJVs3a0y z(nZH+hLxQf&P?B`ht@?o{{lY^FOAB40Ka&*C)bP;W_+sSt6@mtUrSGv<_-6P%0S7x zp(;22IFCLv+f_VIOO%E-MAh<o05B;wQskap?<ykzI6XWzn`s=pI~2i2fT)zeWzk7@ zPx<R=$g=_uww$P-P<@G2H{Xk@wT7GdR|zi84@jbHB|oz^cDZd1)pmSDA#zCg5B|8* zem<IB6~-6;IAR>CpB)tz!rGcmFWzB}E2h-w6a)CCLQn|LXM&1KP&;`A9i}9Cdd=$3 z$T}o%E6O6j4_bo+6#2IQG#FU>+>*O>7baink?QM_ZK@=*milOW^=R2`j`l+;j_E=B z_xI+ppWFCL03~$8xGD-{V=U?$8da`?69$`*227?jBut;1^DwglZxh036sm4|vQUxf z*AOuwC&z5uf1bN{+rPmbR|~l#NnWO`Ytlp%WBD=E5xONxVL`OfLNEE(0Dne|(ptC$ z?c4ztWOK!mY=N{h1lxlDWDpChj)bAc(%kZuz(kX+?^bQbl>O&l@xb?hxOvd~i6F!1 zn%!=Xz}Vo_fKn@llQ&fx+ByZ#2L8u1wf0K$7D5LVbENmHfvIxH;!)L!7<x3EJ8_VK zk_rF8@3hrS0@U8q8iT_gL#<hU``!_(3hEvun3l-VPC-`HoHMZ+4M!(H2A;n%JDr~q z#%u>xrrOtML;$Z#JR_2kxzpnuBMDDystZ;Itm7CLHe}$8_iaZxl2qhgSh$J$=Fb=H z{@CIpmNlA3hFwcr%8KLa-F9Kcp(cv=SN!=jGxpGOZ)1JD&O{5q$Agp|YoDuYw&Ehw z+AOQ6A4z?>t2EvlRJ714t!(ejtiF4H=tDnxdgQ%4HDtr`WBdJ-#51tfI3+dvdlxUP zXx(|DeL3!VIAqK4xBA&vf1Eo{y3xZ86CnYhV}CQh@Y^Bu+U+kxHsI~|^(V((F-4#4 zz0{>7bE<NU8iYRgTU8;80|=}57Kuk$npar{-tlDj+EE<RRD|Vm&xJMau4eYs?1Y61 zcoD+_gbak978FBT&9O^L0WX~OgOnjoqDP9CKIX_Dwa&Uu&6+7~8p16nGY7a3Ys<jT z`&jNYGw9A>Ya|9%=`vU;Yl`Hntxz66dc!3kf_GY<>C_HWYZ=FRpd)9OUPj{M3*ko( z^p8THcvpD<r`>DN!59hS#u@zc7vEcx9XeWhe@Pr(Sj>~_q^sX;49z>d7u4oEW9M6) z`2al86q%9M5%Dzref?XiF#0aDbos=IBPI!w<t{7(0;h<*o<LAR{acBI9KfDtiu8tU zMYr9`$4gLO5lr40LcjxKK{=)w4X%ID$dXxLQua-mTtlnTMK#G^u9>b=I6mo@Io;Zs zxU*qK(Xu}69_tNnC2CZmUNqI_9US`<YcByk^{4MZJlqWQo@E$?m>`v^pFn!o-Nh2D z2H7cxA`1P3Viks(n)w^lh)<SgN^<0?>5W9xib9U}zQJSrrn%cP*cuay%roi~C%6c? z&k!^33EgF31x0$L3PCYB1dfW3cD0WqBbG&nfr-$qu=oo|NO<n9`WMjzm@UT{hp|_E zqbKWU5bZ%)r{A1pYsO$joM#Kbb23gg+v8cmemQ%37GJ5WgHfJ0L}{~0A_WU3d$_p$ zDD287K|$mCL_VXgknnHS0YQIY-(tMVlE?s#ww^oib|6B*Cb4VBy^|N{^XY<^rKKqJ zIC4G5VH6M=8|R|?DrGh8u1&LP1Q~rH>cU3-S;E;I?@#kJI)sY>CdVf5(RPjk_HDVc z%4b&|gbg`U9NI)0wVU@RD;&JDOIsVQ15JGmH<d~rzi=z~xG}><8_2n%6pJgG8ko%S zhW~393bA-K$9vh&A|pe7y!>@3SAoUVwZfI2n+TabseC7*UR`qXbbf*DQaaoI4NR_e zvpw-vfs1VK76JzFu$xn?8e2VN3!ltH9!3Xlz>8m48TpPPp~03`sxNWz0UXg9syDOy zLYM-|ALmhc@JeOLQ5@9PMMb<{R%<KAvAz}e7#*z-^uQt2w5by!X?7kBBBIY`%yJ1d zr^+{O>B+HDE1|2|$x|X$GyBb*7bKcZ2Pcm?tj2-OvNZ@Z;!CT+!^p!SwwbY8Q(lai z)PR`rDo>r5%ha&sex#023gf$;lP}F@F3LQVak8Y%$T>N|QI0Z|;$Sf83kJMKk0c3X zt9tV32n{`?8znTFFZE$IY8GNXI1e#>h%Cw7rX^7{bxd~9M3kt_x;}?!S&e1E=804} z@6O#CcT_m_Y+zKDEOXzjT#ziPo5``87vrEc*to-<ic{I@)e&sXifD+G^L?4g^pxEF zKAxC5#qm;`xKBp)s=jA?P6vlW!H)wLU8V_Czr;|r!M12@g~c<TarL5G0eH*DN!}qZ zxqQQ~5;ljBgebqv9truw>EqaWMHuK);9})R5Ln2_w)e7`Ej_A=t6jvPY9)6<sqL{& zb-k$AM`9XG0o3|h#}6?`$)Qbhb-LWSX3j2(dKH*w%2z{r!4X(er~){ykESsDZZX^2 zyvWbbkFo9AIXYNKp$Wts6ty$q5rc|n;56zehYHvLAOqyiegqRxna9U4WMz(PVkZ+Q zlNqqWExbPlKzEPDuf_KAbG$BPv}Myl@9C_zk?D-`_U501)!(NsJyRN8YabX~s>-m% zzYFhW=sN5&$XnI9E*Tmq%Z!&+Woj*xb!$Ur5E+KUPNhs?^>+2nufj;0zTK0Z9HQOC zRp^A(2J7HXXRNwIij;d>m#&I;c6;+Izq3j$&4D?d^&f>B^}RO{qvJ4U?mcQxXY@uM z&w{<Td^U$<7M6mC_W#Rp!`NeWNqO!*v)qo-RaqBS!?*6N@wv3^{GeD3|4B-F@bj@m z@5px+=qH{^8lNoNpsJo!-AGxl*q>j_TO3!gdv5kT6fg2ubZqpm@4-2Y5rdP--uoJE z+aIC6clXW9Vuw<Q&#KJ!>FWsACx^Qt^(f8cMyzvb(zGk0v_7Q}ys-*oSGEh6t!du~ zlTWXE#i-J9)Z@|iT-<g_M3c{>Wm2O(m!`lzQUez>6wH@Ws&lulP!%XrHwFiUytUS@ zNR=_?ARtA~=D^>lwzn6lMO#%$7QkLD;HIiNt*4cB1tw0#<6%9yI}Z2Zf(`ld2fhRO z#P(2~*S1h1oF)J{l-LOszMCE!k+1M%=9U=iENLXrCy{}*&+rk@45dl0_DNDPmasnB z-+rcCUcheCY%d_H7P&^ZErY4TcG04UdjYC5V<DL{>mf?~z|50-@7CVaorl_-Q$BbP zrBpVkaRcQ^O>msXl6;`p`_Z&Mf6ii`e~nZDe~eUu+wIIsNlIVU#^FZf4`mHX0VThz z7};;`99kW4D~R9Ltu)cH@xji?;0h`%w-}o-+zf>oDBUb5eH8Azsxu)KYjtpl67#e? zXc(c&Pbeu(DEEHph<JzALZEGg@!P(a#!D)X%GD%HbB?Y~b4npQY~orx!uW2bZYwu7 z?bKyE<^!ufNUPOGT~)^KOITgCV24*71*Z`|M86g=cA2J=mO7T@kN%W}T=p&}C{@SV z&9m_;Ufv)0k)~N}8h?RN^pvYD0%Bzd0-dd>38<)(sB4#Roms_z#DFxiYRS~X-$uY_ z&+NmVVpf$SI9%?EJF*Y`q4k=)AE#G*2Dbj^clotfzu2lNUKw>RuP}YVWv!3!eIG4J zpSh57WG#(4^5a>u5n`_a^~cY`o_ygYO2TAHw}0oUC%G)>cSgzIt%H5SmO`!`RpC4Q z>ow$edjDn2Y1-%OOp})FYCU{K!OG;y?+Dei*~&4j!a&mm+*(H6l8EOMzt65SXX`@) zp^Fpcgj@n4Ca^Z!9opD_)&HSS`p;1g9qkD(acPyNHo4Z1=@Q`~6WYlqQGJ(62Vpy0 z7%Hy_H6%z~?~k#^MYIt##TWh1uTfff=B?(QEx5pav%fa#Wp&g)+h%7(WDmbWyZKrZ zVbqGUXUL>1`Sgj$FZ>GLPQm`QDb)mO&Y|~s250D1eJXV<;sA^0_e6wzz*O(-H10KJ z0`^3|LC_<P=#o$<IGP@lzgGaxb%|ffxSqr?np-wapuK+lWXApf^5W=Am)^fSmQy|T z>I$lxOCk2IM{J2lfaz=QqZ?H0Ki;@jf%rzd1f?o4Va!pyMQXJ#%d%YXkiv2=KdZMM zxJSutRp|ID`XH3$POf^M!B^$Lf2vRd2mNdndbkgz?8fPUHccxY-RK{$hK7;+DVBws zFBBw3zO{ck_DveXd{aTSPkLnvZ<1}H^zrn!Ql)wW@`S~A*Vkw8E-4>16RfOGF3n4e z7jQ$@UY@aZ6o=0r+Ukw5QLs7N7%q{|VbjwwqsnEv&gi}}2qF@eHGihdjMy!?8L^v} zg53B#4Raa=7`$GeiBJ<j#9f`czhQIM<;*I`A^6QxBq@e9FVl(JM7<yLdAV%ynnlWn zaM#6el+<Y5Keb>TmoM{jcQavlg(+_3A6F}IvU2R#I-Vb~QAa_)JZ*ydOj`IPKS<tI z3yZ@?ex;uLY2d7zFPhNEECcOWniYgORZ05}N@{FGGwrT)YjT9Q+1Km}W<R;sIi*UW z6oAJ#ImG62l*-yg`Edf%8gpwiEt_AIkMhh7ug-TfsE~h!d&X=>E+N|Cue7gKY=zU5 z7D9*|C`6B01tP)T7%H&w$!j*fy04T_d67k>1e@!cYhpFn#>j`7J;Z!uNbUq`wW+#9 zR}Q?vb#2N0eZ#E95otn!$$XDzc4W!fu0MI3B-p!N4|FCMQeco$mN88D_fX4O#>f@7 zm~I+nzVD*9%vk)xny~LSao6*9_iWp!Z<H%fuX&9*i2lXW8juUaK1@n!*>wNaYx3i2 z&e9ykyJf*B^1kj|m?9{7`Lp7S3YjHji!KMQ!Mb-~22<{ZLip759+haXKn=xKxCMeA zuRoz4uyHR+`GIu!5>RwM7>Lks38?Zz9wO|=vPPtPN-5KJowB?WBTjKCv=7lfO~Xfl z4PELA4Fk;PVY7<QRuGMkK|OYAO@2Jj+p~HDAHk#MQKj)bg(bN@jXBO@qq`5yX+KRs zBLqV$yD_c)1IuisVz`OaevW<neUjC93!anX1efE~4$fh{p02j{Zu%R2a-C96dp*~V z@d5Ue`OdxaMCT%qe~WILTnp_O2<h|mco?0RfXdpvBziRQt+8CF>uj`-9%>k>kOSR% zw`%Xbt?J4=ozs)JEJgz($8Y><BB~C1Xh$x6{?K+WVWxxRP?*B(Ar<vG==*nlL;H(- zHx<RkG6%uMxe0Q*Tv_foY^fvHi|2O;!2-Vr2BL48s7vhumvwVGB7z#ukj~Yvf{NDJ z{=^<{nUypq^)VkTHF8lYJ?<eqM2D7>?Lnt9dGu5Ivn{_4=hxVHL_(e+`wfnst+Y^x z1p@mx1j=Rl43fJOZohX}%k9bjoYW@6FxmW*X2Y&XnpTXc2>!Rh$4<sZ>0*As4YP_Z z1siQR^wuw2^g9#hS^J(1-;9}!-0!pA@u+kK)Fqo}H#0cqN&`1wUUnxj22u_T)_~4% zw%)^(y&L7Q>KQzPlwA2m57-p3m=v7>c=m`j-_G)V%XtkF3Ik<6FByyA0GyThx_hCt z#8%NuX(m+hmrFZ!>o;y7^hSkrjre+E74hqZ(*%i|Jqsd5;cPzmWJGKA=bJ;u68;zV zWT5InuI>7eT<#Jc-_bSmzgEMrSuQN9#jFjGtFy0&QP2t_G}A~VH#wv*cD-WP)oQ#z z+Ukr#Zw$XzYw?iG=je@feT}8~g`%<xD0~dtNNsZY^35;L@x6huyv)0(40HKKjXO>H z$4tQ6zoQ7&Du&EXZ%CO(|J)~t3VMW9yaMM<wd{t0SIsg(Zn&W6@D`a$r{mihYLqj9 zwAOF?j2yFYo4IxZkK}>!IpOs#@y<Z%+z^CKFW((4UQ`BQ4?m@EX{XX()bN>$XcfBo zJa?bsL48$W@8VLejPv6TQ(N0ft$4`lw8EL$vAwqA_~idy87t9gns-Pedxnx($^pTE zKdYku^W3nK*nv}njHK#4n|wq;z{dxInlgD`_JOB}!rsdg=Av$d;YCv2_jomzN;CdU z-_t6luR5~ks`t>##BZ-et35cEzybA|f^t?+DrPcxbI!-x+Z+Piak9Iljk5jPO=RSR zTHZbbf7yII<&f{ClI}9AtsGt=&!_4}oa`lS7`ze6_<gwupVSb3bVYE5tsfhkSPW zEFZT<f5bzRS+LaT%?QYVjB)a%-ln-(!h0U3_h+pG{5;$&>*L^Kjy{-h!hF+(mu>}J zwZ2-~pQK%HWL=-u8$c{gR4eG5*{Rp^!taUKu8-G3jL^<dkKAT$y@v?UBwiZt?Q4Y6 zOaxL7JMOu@QPS9}pI6rSOsc%}?j@k-_$q|n&Blj?Y38m@Fw2`;loiZG+ABxWg=jil zh5xpv3EpQmS9=y1WpZ7FdI#o_sRTuP+ZYM_KI~7u5q9AY(<fES7>WVvvc6KUl&E2c z&8x!LZ>@vtQO)hPkvNl~v<cIxgJ-1YJk^xrDjPnWT7U)W3ytt1KK?iJqt}gI+4a6% z4Av2@+}}!mgt%L@-6C*miJ*STSpDT|H6w`&xDS3VB+#E(L<5+Obu8GDhH4~~U`Qto zhpU~d?0v0D(n4oqm*aziC`a>n4DN>8sH6SA<rU48<qli{++N(>u5RO<ocq)wCM2!k zjPa?=uA%$AOJ%1ekg&EJVLo@Y;nTKKzR3@AWILY4-<~3y?%3|c>}vxFr2|)RH*k^b zpMZ74*SK*vV67o(0q>hF9RA9!4KUaDSW}>Odc+88PolN;3P~n}#uy0+b|D+7k_Vy> ztvsd$hax+L2+}_NpUz!U?y=B!*_GA0Id4#$SHA-ozs$ILjVS3Z>ucpm(I2VmdBy~n z-_@JYX_Z1(^=|C{(!;*1&v{zMa_!5oYJvkk`D=P*v!%4suLf&sJNb*w`<&chAqfwm zx_sqDcSxpwbBk|yk#ujFNC}l~U4k!0rL7*nsbl;+Ieeyd$j4U1I)M_2HffRMKCC>k zjE%y?ub$$xpq(aGEY_Z#4J|&zu9B7>%T=#(5<DkPK`2Th&nnWSfz(j2$$qi4aU*q{ zVmG0nsj8vG8z+1wGRN}bq~QMQ3ew*cgozMY{KQPa0S{wB6?+=^0eVBJfk|*i*`ptN zedl&rZtkdh5w9+@zX;bBK(kOP)z6`n8`_8~;iJ>sg<q;3xD+<2Mq-Y%5wd}erOB_= z?nzJ%`#|{aU(q(+)RX$r1U+uAYu{X|5qx)5-4)<Dd*N^{7&A`E`i)|q1XWSYX#IOB z3+bE`38Xcx=krH!e$&b9m%}#DRAewkEF;7hHa?xbf-RaT0}rxqToTXbXwfYMJraGU zqBbos!m9<h9+I2sIu#fd+<17!syZpfcjbSu_nuKrZtdDAYoVedU`MHT6a=J8M_Gu{ zJ5mBF(h>sFYlsyT1uP&Pq(cG(gwRPu6qMc)Ae2ZAAwX!MBqTWxxYoPhv-kPN_`Y+_ z82itjKO7_Y<jE}eyyrdda$Pmtc-f!y8_;mz+DW)tWg>?WIO%cSyeQxH^F``0!TzJT zSS?*h10o<9y16j_QY*swE_H0%1AMKZ?+92=JV(3479x6D^j4q;R5P;-UN@IfnjB|W zR3q0U{UwF!+h$M;A}m$Vz8NK&yNDJQ##1}wK`BFBI*c2+5|tH{ndGp$C`nCEtKy%+ zFO>fB?5UXdx@@gs$*eG4I5Ldqx5sBnb)5QnZ{qn7y*b*&9imtuBQCmWkG>>YfuyJ~ z?o`DYbbve~1n`C7UYmr#pR4oD_deWyr3a{wQZKHf)U<J#{5BJ-bejZ{!Ps`yEplg# z%K5U)=auGBW*QJ!Pv$Yx)3p$IgJMxtgi8NuV;51Y!q(cdzwjUK3M%QQH`c8u`zZ{G zKE8gvw=aGrPq(8aU8C`-(d={o#|DZmHy-_Uh^)s3wv}MIDWqTdGv!;4)+3jB9Xy<G zdHc8uzvtMe=*5@pkL_i5(PR0Q5-3ZTUsr7EjO#iCai_e;<lPB1I?))#T*J<nck{#B z7W=Pupp(DP`->>@%J1vq+=6Epy{vPO5Z6vpN>RB@QGw2e_Yx+$a}3Ho?T{AE&8`n! z_dIoRwVDmHK=~Wrn$9$bf{Ty}oO|ghvpmPWf*)+A6HZz3L#*43l@gPkN-*!BUQhlO zsryC*>c{a`u~$Wr>QHJeVo1X!g(ln&AXfo(pGSmL5)3bQBU)B+NP7MZGvum!)kv`< z!a>DRXNJoIua2}B9IdyW%`(c4;!i-1CF5}gt+`c{`_6llLteE#Kh&YP>H&o=0P^)- z<<3BIQiUiuA+=l~4l>XrX1JFKLaLz)DoXFh8%pg}$%5}Db#&|#??VhXNy*qR?jLCw z+VpF%9d+2sD&am9#msgz9z#xuRwi!M;%W=6raP5GmVS9mcgPcUHmaO*9(Lf*McPGc z2DX`cRJ!(~-^3NZMuUU%>SI+E;O=N!+hV2-YDGr}F+wpJbGis%oSY4>x{)VwNe-3L zUM~P2yX-7&V&%t{vz~I#{h1%rG2QC;kkp{1(P8cV%s8p7X_b8Y+Hm&B6lWyhMobfj ze-;y+;vZ;JQcx33oSYg)S}5YO5p_}X?jVGtP0mC<p(AzQ`LbZ%VB-2u-otf@2=5O` zu0_D+qi0BUcg3Ns(8ekjvJ!F=6GZ^ZOY(K^EJSgB@2$Jr!&m$keUT4dq_NxAz3f3n zslhaX6t6Vqt%8gXf8;!r><#n^_+j}`n$#Ln)9yVR^4VS|5Ng5rB`f$I_k6Fdz;%%* z%ZH@gU%~6ch(WT+_{!68se=5=6g=Zk(rsi>(O5q?=7_5Fn4PpCsPL)a(--vaPUB2p z0RORbOFUfd?)u#}?=jOWvFD8;?7$LXEg4esaCTQp<7z<XIrDI0z178<^*HbNQ5Ru- zaL7!RhR0BqWdW`~AN=*Bx|&&>Hz>2B+_Lz-v8|mfCRuWefb(pY-lSkQ+5y-yw1t8T zF$8}%xIoc^sAAazo-?O>-GJ6l-{C`M<SuTX_>oeDTvJX`E0--wss_aL?nGA`{<Dx3 zh(r>!^ups9>G9qX5*Zghi>y}!fD|4u<|{DKD-N%t43$;A-MOEf-#I-w0i3}Y{S>T~ zv|@=_r5Zvipw-4AcVVUv5y9VYA{0EX=nQ@(&|dK(q1N)gVO_1Mup=m&A9>O}O1<NY zY+RDyc42ag@|AfmpX5s*{_V_R-@tjBdtvE#PxoF#())bE+^Oo@mc?cPI?o&i?`8pY zL*%Q+PP#ekq)Uz3WS)})O{T1Go|Vz0iw+^sfiAO;%nOrH<|i~E=7g+$4TGh6#L}nd zQ1vIWZLTm%+*jFFx;WFav*&e^0+rmWl5_*C%SEJQ8RbD~YWy8IL^D4+fTgu@mQN8> zc0EoVd*&KYY+==|`iXPPPe1x3Xef`Mr6FlK`R-~w&cm{4v<&Z+XtVX}doiL$-4LqD zJ@c&BksgPxzhy_sUTM@x5-+UE^Z&7W3_{?tq_%a#*lV1UpT+Lny}2qhhgr~%`s|77 zoAAg$BqRm1zKyoL$o(U)oRRe2>79#f^kzCm7ZA{(MZ3a3CA1z+V|&G+TaO~FsAHM9 zi}=2`z3H5sv>&oZcMWI56Ta~iHu(>Hg{&%Xmk(e*p{wkO`9Ea}w9dSb^;he6r#NxP z(d!6W4hQ?Wq{K}+KJudxjHf}OMZQzqJhs80$YiYb!%kBp_02WYTsCUof~URE$-4r8 zJ*L91xM-!VQ&}5Md-Q4k3hR~b6=h_9g>&)B*A&V@L&kzUT9bM2S3PqHRco*{`fyG% zSl!vM-Z^k1c(9!cgwQlM{pxx>m8P{;!P61ap@}&z_7Gy&htbs>^z`ciM0W@u{7=T4 z@^$F5O8iwbLT2+X46M{)!CFc-*zJQA@f5~uQqnl&;h5J9bHklr4)ZGIU3yoDY;Ki% z6*o}!EiIw1;!{}sij>^+Q=5S-()^`gzoiym0l3r>1QL?uxnf3dD&WRk!2rYy;7AT4 z`3D5h7w#W=N2Ct?*QAg$HbIuEUYO)nMfbim^@O3PI}s~$Xq9L0MET59lG{TXcEwpa zL!vKl^deJj`CWaCkaE^*Y0O=x<~KcCqzu$*r)n4LlEyB-L-22Y2W`nNnbNqp^ptYG zUFeT?@Mxr1BnL-R9!1Hnzh2YsMUBkOlaY3`?M@G8gU_1Tj$_F->cUHrs*CuKqzk&? zF2;RnJDmqm`v64mKR6-^K*j!OATnE$^&TwjFV#CTi^)Ait%UasGPwP2*`)~T^tztB zi~}J<6Fwpp`;~5)=SF#5F>6@yBNp2{F#k=TaPQjgyJl||7CXepdL>X%)`g3rpPkkv zJ}w7B&?^yt(1mVP#i8N~q`LX7>jV}sGEqqN+O>}YP?!vYed2eI&it{aEdR!>?-T&y z8><0?-cB)p6FJQKx=r<v01ThM+r)nhA;Z_9NsBsr<m)&&Sk@(@)B+ny!#e-@ZzL=K zzWvfp$CuPRq0}7?gxK5PfCNBA1|sM~mx#<;UNjV8O?eFD&^s628R$JJ_dIhJ4F~9! z-#uMLer#clpc-&boN7W)%+tSz_r>PPufv@JP-98!<q%Xe9C?d}hlhU0p_bWkd#NC# z*4$FJcE0Pna9GNsW#?zmT&X6I2DICZG@oi;*Y}%!ltR0D^U24PBykA|{nOStd$3<$ zw*DJhMXLEA_V2G`9TEC!lcg`*QoO;<neq1@IllY=QX=O33OX!{cjCRUV1v}_`dj)E z7VMN$JpQo$5<G3%r!!3?H2^@s1+X0?!M$3d=k)FR!*>GKH({*At&)QHmV<+-va7-M z_FLt}%YE_XMYfAgjqK>|o=esne#aDGp6|rd*A9(5zx&DYJz0Fz16E)bey`yxU0!+m z1LNnx6FR6C-Sv&}dHL&gRb77pab=;Tm@g|U9q->LY&~&*%0#X8bs4@1x4k)B423u7 zak{7EW+^N#PW#o9&1g|!9S3uoJgP3j=JRK1De31)QQ#WhZ#`3>d!Q(sPqlV>X>j{a zs5;bUp;&#(nGRqYQ$6_J@R5;SnLpSPX483!{@ZydjfX1Jz60x1`Vi6Lz_!3v7GbQi zFQKNrR6kd)$hmpItK$`?i>g#x&sujF*WkthlBT{Ua|ytNN!fbgC90aC=4nO+5X(bF zxnfTTz9|U{TMPks2?93qPe4Sg*dy+k6cmBE{_;_zfaOD$S2)O6)TVKJdftJE>D&uj zAi6DgqEcX({Y7;)8ChI&&q%MYe}j4ay-ue9EZ1A{KtfJbocTz>u_0#!#LYyz<3X+{ z0QC4&8Gx@iX|VYtd+w`^INcr4oi)<epdD4cIF?13&ticI#P3o5q@xA8`mUW(ht-g+ z9CYj9R<d}!qg5{A#Fp(qdgjG=m$2_1xt@skzBa`nQs#mHYQN*#Y`{{A7nzkSFxr5r z>l7n*!?N$jUtZ4WQs^51Vym=FEo2c5{KN87%UOu}$&L>I>V_Fc1TFFcxAL6p006ac z?rlVNYqh`gCt_AaGOks)%8_<SSSOR4@?^SLR$mrVBYthPmtw~0cyWFqT3mW4tL!!W z@F0IVTSXh6N>ECGutpw0!+?k~#6V?kvG3{e4;tkedP*<6crxrI+Ud;S!O%03{u|OJ za}t)~>E=bv+UpOQbKTgYD`v}hsSXDWKF2Bdk|@N&`9>iEA1BUU4saf+A-r+S9m;^3 z;<Bd8I1i5+XxLFL!cF5xCJ$CrmfX<1{L^H3!^dKcxv(z(j+gCGGZUXoT`5{{$hNe( zLckCTgV}>%E?2X7ns7OFu~W9mM%;$KGM3>4B@T=9OSL>;ej#jBsZ2Q>8`&6J8XPiN zA>L#D6Zg;3N-r+_Vb&U>r%Xzkl(>3(0@mxn<@%zYzXRVZdY1yqM-w~a0uo_icFr3W z?l#<U&=|xF{Pp$Ee-cJJzzV~~OA&9rkYeyr_gE_FnNNKn=`LHbQ?290O2I{LkD0@E z6RX1|tw7nAx`wtyv0A8cWo@O^Ww>XdJ?Eax2~EYn#p+FBXQz=SYCIUWz1+g$)7<VR z_VkRinN&DcG(2=W*1;c$kcuvpX%1@uNvYREW7w6V3oC*9Zu!vz)nz+?1YvF_MXgb0 zl%PnbURzBJU<a@dg!ZS4rUWN@b7u^?#+D@phiP69Cv0f1@`RAc!|j{FJ5ki<Nqy^# z;_1;{6oDZiaM-We)jk>HN-S$xo8Aeq+K#D|1713&-~D?wv6!Q1k%bOc9T0@;{ZJWb zyGWC6=QXzeVaJMo;WE~T-wMS)iiM|bu>p_(jJzt(#SWLtt?z!VEP}~fmnudVM~@QT zS2_(d`HZ+b#e9lYnW5qGUE5tdNCTC+Llo};>Fmm`;jT^U4~JRD^?AGXml{F%y}gfc zl7ia|Bf{hxAS`+v7(^2bBuY3h>4K?BT3!tosJ!?m<^KBUk<jO84~W2ru5*>OcPO%u zhW0=JhT(c>oxK9orXQoZ8`oR_KnCuoklflp0Mdc^WzDrQMSQKH++^rV|FtRXDTY!` zV6wJ*%1%^RnRpBc3+HS#!Yi%mQzGn$;hD|+(ehkI5H}eBqqs+K#vYVh6=ur?CT@j~ z&g+L7ygltTO;k-UTkTV2&A{)@Vle}L5z3fiCJ+l|&J!IdDv4X)e~1QP4rpG|-YQqc zUhwhY(7qSnXl3IO|I=zn2-&_!Ran`<96PoDU&vz)oY=3z*B_l(On3p{^<{I8{m2`a zYVB=Zu)~$A>0iN<c&jS=H*-|mDuTfG3}S%*kWc;;BuE3Xg}i!VYF~!lem&EWTb}m{ zenHm5pzh?P8I(0BQ%q?Lf|KLXQ2r=wIu;a~DmjfhXmQ;f4E5%lAoKiN1j;scxyJTk zv2I5|@VsA6hhuTxLP|xD&ysKKZZ&^9Jwnq%R?SD@{P~r_^TbY93#xCy{BW>ddCT71 z%c~!6mgm~x=3_<#BH@8&iVi3`-Fz}vspJl2J?_HqdB}v8$Tkm(zKL7Aa>=<_i6l<R z`cm-e0Qum210Q4T=opmNm~Y|v^UWX?$OgJd<n20t-;)*LbABC$kYz{4KUEZtE-x%g z6uM~Z=hSbNYE$reNloFwme(2Xwdm9z6}c|$`#m<YMTdq14f0MZdEgdxeK?KdUg+1* z312EeqYQP!axX=vb)`08R!-;Hs^mbuAtjfgtt*DX@rH!uuMcPh!M_8Zm)`er$zkWm zww%5`{Z(^uKJkrWqSN1Iz?Y@-*X0op-4@l8dS?z!+aH_H&kit0)>Cyruhge-Cs#i* za+lxndvV^pk#Q2OBQKeN;0`tRn406Z9t2pUr7E~QbP3M13m?LJ#j1vdX)2$1DTvKZ zMPZ|<5cG6I6Tx+FshM#>@j#e5Skry4Q9U@;vl)TZ#&C+rMgM!PJ5Qawve5dcV&=jZ zyb0;utbED<+;7j*J&RR*T%4AZ<$iq~Kt>I}JDtBs10s%_T`7=ke5^O;n!Wx?a%0u> zmD~g4(-VF7QMjU^L%GKLV-6%J{yZp2`zWKRaLb{*dRo<mkGI+1(If<QI{yj`5j;%B z{ULE%#SED64=v&fDp3ACL|#VBKCZ8R{uOPI-u~J`2Q;#6dK=&L#4pmfz5dQ#t3Dr> z(VIn=rblM3*Sotsy#BoxHxA%Wm#6=$4!;povF{3N3nX|Rp5wNiP~`jC4k+i8={c(M zFMW9mC_@6A-cf=T*`DzSy{aw5DuHTAn|Cu^o1gPkC2&XQMp!xiuZY#3*w?gjfLeRu zJqOFAgEqbg208v3Wd@a{8oBl(Z^x$;dks(PuN^qb$ijv@9C5-J#Sfs9q??}c39l4M z<BOynv&p_I9rE81^juxgB)7$qVd(q&frV!0{@kM>%u1szTG4vP+yhQp?i(PNb}hbc z$s9rOqse8mCk*1%fEp#~PU}weF$IV_bVPXD_%u;<|M9mbBKzmU&s~k^Z)+iFziU=~ z5GyM4L=DKm6^*{f68u4GL7>pQa9Jcp3t-;rqfJ9x+%b2|dp_Gjl~c2Z_HsX@eK|h< z`D#a}5}P<t$yfs?gqGX2V($!%w~)MJ!3mwRtEnZaFI?1LBl|Z~q;f~WE4Au(ZbrC= z1GoVCgXS-e9993<Odbbj(kQC;GdCK%1EtQigfexat(43CGwZB!tBOAi*Io{Na>D1D zQGhL6H`U3$lwAIm2Q~Mr@foZh95-OSR45%&p;c~odc!M92AgjEigx_f_<yR7#tb*) z-axkTUzV#iME&LO-yGaX$XxzVv~d&HAfVdtWT3YGm&U={|I6@$_nu^JisQpl3A2Gn zvH487!nT~I5aSP%pO3V|D?9lj4<oLRAzFv^EQdelTDq_?wOx8`yb8s3x!;w(fn~2* z4h6psP5xM(5+_l8R$|G*@U1oa^{QpfQ0999Uy&f1zg*WpmDmx6{HXC1$47R~KfQyO zd~+Iw1phvwZ~fVr*)vr|^N~u;je@R!pZoX1+Ou1Pr^dPlKf3K#XI6w>9_gMnOBtxu z)HEiWGRqGopO;+P^X(%O|3R&XfU9=<xRTI6Y<%;=m+}2;`V$J2BR%S7S_vb_e2EsD zLj0FwXB}$bWEtu|<-{GS!j_>yqEejXSqs0r#mELj&(NSVG2Ty#kJPyrYe)NZc3PWi zKZ0ylTDB@HIifSyKyi6*=GEfW@Shl-@)>q889eNQvb;vqT(C<5fp>4qbI7nL)#}tG zR7K1F81&D&zWNS>fgp$&pA(C+SvrtDFkZ!l?G#R5Kq#qRtExA6Vcr)ffM=UK28&PE z4vBjQ;kLaTfY8>>j*f%eJ&CaH`hj|4gPC&;z*|xU-Eat<Xc_u6Q)L{Po#HF+n!*%; zM9aqEb<Y5}5&vk+A3%0<a+nM#DRS77Z4c?+ySh_4dUL<iGyV+oa`~#kvkFD#%&~&? zG+udd6M)jhR%#U}8D14@$18cph3z=FvqaC!by^}3X$y0;QssfJ8=QB3_K!@@U=_W( zZv|Q;0`6)Ap3xsLjc%=P7kG#0|I&9*_(@*#SN}b|_juA~M@MjX80u#%c_r>u_GL4V z7`{cf(d^6B)k^=(yPiM)?hlO`Fmg(xKlilxGrOjD=_PSW>fnIjc85cOH^qu9(^GqO z9W~C2O-9^wZYvH!-q8rP|1y3Ox=FqUs*W98WG+Gbn+@O9G{E}yK?kL$Sqt-8OONiZ z5S<&fa)!$_Wj}=s=DMbrM{=G09)1WpnO*Q^<DE6lAN{1!;g2gHsIF70Q_x~-G%jZ( z&G&?eV6Us8L)Dn7O4f>j?lrj0GtmGY07>~qM^|oi*R2_#qL)VKez?-LQqwwrzV8xX zVEk^Uly@@S<F1|<$q;LqF&o8H*h&^#=`Kp)P*-~!jP;T>_%EwIidWLHxu_tXGG~ed z>4-ZLv#hG@N&wEi`r@0##|~PoVttQ+r4Vr5>3Ec7dQ<Mo3nQRXQ<e<2Y@7TJ&|D?# zB&LsQBywY}v~I79my0-9s*;eK%cZpHY%>wvbbJ6xcc%svw;!}q1A1aa5%86R$o;8Y z!}1Il%BvSk;jThH?ebol#DWmH;th3*Y?Y0JXOe7x!Nm7zL|{^IwSZ;KT7Y^{k=C(5 z@0xSQ5~Ahn<~1w)L#s0G|5md({xFnqfV;vEaBaKTohU9!Xq!vaytYtfS=N{Nb$ahB zE?h)wPT{<WgdtGVw`xPl1E1jk*sA_*AJ$B|{N^>Fn78)()Vs<bS_W5LyceCTh8Y@( z`JRsWbEEZE5~Rpp*+W~>$6tELGVbd<(yUGv?@-aa+VLi|YvaAosC|4AY$5#9h*5$% z5!GeQBpv)SxOL%mxMI5y*3s@lOXn?xWHl4OsV%GJ#?`bo)gaEgzm(`UYJv1238@D? zs$3}wOS_F#f0mq~y<kk6vY{kitwrjIK<wbE;tk_Rl@+gsB6sRR48ul3#Q*lue0j86 zF3{nK<QK3Krvw4c(a$PAa1$EFPZBy<oR?L~{fpljkrR#tdm5WFq(!aX^%ZhucmlwC z3IsyEZA}v{$NtBrS}{zfy1p{wM2g>uLV(V~SyHJDDQ)U<*Vpzy89v*)N<jQGj)wdd za-97wJ1*4{O!#aSe4jFYZP<n4^g6z0tiTL1Ttq0S^nTsH^x&D*s0UOqc8ng|UC!%n zYl6`WPN=cGpC=h2HF&|Mwg0Gse2Lk3u1i>1Qp?;e|1ke(ha%gYD^IRWI4$J!SM|H~ z#6uD28FLFn^LZO_<PajFu1OleHd$R%`LMWGj|nP{;uK!Bc4z49MGUQES&d!`J6pe* zdpW!Mh8?6e&i_KSB(-=fy8^4^F5M+h+`#3xe=&18iCFCP*aGVrir9#4XhCN{ff++^ zXT4Bp+9J<Xp_H?=d=m2=e;D}k!yj8+1U?=x;<Ck@#EB<NlP$l<66W}8+tAQ+p&EKo z{z8iG&X_3&=Uk+PWr<Wr!AP-iY@A!=iJ+ljd>y&IaLD$--aA5jlY1yKLI5=XKYnia zUx@{qHYvR)6=uIjSkfbCa8!M6<qusLT_$l$Qg*BSR=KevvRC7hFuvwu3{;q1wv%WD zw0k+J14BdPsz{l?!m*ASVJokU(o}N!G&Z#+o1@FIe{}LJXa<7udXeA&l;z?h1^xXT zP4hqAov+=Tc&*^leku*l+ESqcIjLR~+Pk!}f78(JK;?7$L+&g8UFe1u0Z_*eH&)eH zl%G6_49BWxrC67^j+(6>GQ4!{pL(e5lK{H+5uqe_c3~t4arugsvw3Mo>OE0;AbONi z__>b%FGVr{kG<PYSpwe5mwq}Wcue;vOSy~2Xq<73xegK1-}6czS6X-HqB;4*KM&uM zqYDY#s&GF9fN$%H0&%uC;lEw6fUk08*7sk;upHk1gP#2N{Qhs_g#Saj{Bk=StFbk8 zEKS8<Pc&@lNprNID;zkRjS|l5#?1a#H0&<&<t`%JKU;*J4@>qL=Ct+exVS#(7Fhw+ zLEAevSn`c%Si%hbZ`GF@O5kg9Qje;ypbs>9i>-qvn~WijOXfM6X`j}RC3t_hx}iCH zqR93b#}^y0{LaFDg8%JfKOTA;fY<>_ElXb=6I+?u(QFYt8OAss$Bg{o`zS%o=4t=! z&fNTgVjO)T(yuptH@lo8_4FRVAJ1_uOd}BO(q8K@5ZU14CDqHW*<fGRGzT+ix6~N~ z#`?G2x%y>rW(QsU@w*LjS#m&IaV_ip2D&OZfe;k7D_D=^B6diszqqZ0rv#{5=j)2v zW}c9+n#~j~w(irU_Vs82)JI#J4KM=!ZZXcK_Jo8;Z?<NrL){qb`lj<sob}tS@ZsT> zwPw`ZpM4r@*_Rr^{PtuiWf%?gQ&94hrO&*~F_E2=1YoXJcTw)MzrTWoge-(WRg)2? zm57C^So54<#|9Iqozh%Sz5`w+RJN^bAbD*tRuMd~tl39<-Cm)yDo=Z=w6<+5g6l&? z7@uBi<~F8mO_GycJ5~vIYJKMSt}8WBx>#ZFw-^{+A#&R9z4a($w}SE1#TelwH!8zi z9(Bf1E-yy{){9G~qjhS1$8ojW*_>ciu<_=&U`XMXvoK?>SbQr+eF10g-?YHiz38Zq z8-s^9NVBWdix;|oq6(JQDGJ|{NkxUVy&{?;Qn$AZel;w>=)${UieCo1Tl5-2XrNB_ z9H&Hrv9#G%ubl$aV`-J))ldYYI%bVf^5<55T^`<>{0RE7mlojweI4TCy7K%8_kNw~ zDIdc!u3ha1aA5DC_uq@F4l~Qkb66>Hpbu%=E<Xb+Fz1{XLP-T0tZ(UG0_L&nNmzK^ z2ewxVf%mV5u(aClu;F#bmg8ec6MAmKp!WHBrqgG$-z(55w7UZNLl+ZTmfv)?h$5|< zAPGi2#c-@a)r>*k9^u|L)#M8A(QL$J$Kq&JUh?FZzo?YKP<&`gr8?SylJPPG*Kqhy zrx2W&t}**H&Ui@sJmsPFqe62jO~b~^yUX0oy~Bd=xUZxCOHuc2*8$OI8BXNwyTW+J zU>j#&0AoHLnESB!5yne#_Wh(N&o*4Y1gijsMd16eWyw)>e<g4wC!}O%S;o%3)1?QE zYAE<%@_*Jvt|wy?W?Q>-6odX2C(+-78P0GhL?u>LR`JrpRer25*Kz%6ylHS+@L*Ow zN=dPJAe-XQTeqIbEmdb`?dbc=leu26rK#byjcu#}`MB3^G3**B++lClh|&-yAHtHv z+c7OqU*cKtPmyPBT?H}u*RSu6>k(m>%KQ4LhTIKnSJ1hJ^`{O?ca0f;ZIv2IqcK-O zvf6!^!yy>b(P-hoCo!0EYsMHaRr1Fy{chUKLTeoNpQ6%Ox{6^B!>3{GbBxZK?iyyj zVajY6o?$_Mem%KT&`4Ai*OrowE7_3ld6P=<OHSBL>E!tOriRU8uzN(BuPrf{PX<I; zOI#qIpGj^-dpRCYt?$75_hqmu{;3InVx4shqsETYP$I9YV4?W|EOsEci)u8>W^Q8F z#xk|(3vJS~3C8T#>lulkPFHpGwM%F^v18Ygx<H;R*A#~l*ljVX+dcX0!6W-mLOtp6 zW7YQ}SnD6%=_C0a&-r7_lP9rjRQ9%X$h4->tT}(UEvSXpo%%7K)}}5df7bP#C#y~K zz}UrpS~`pAxN*lZDHmIR4yao%J;u)Dchopzn4G74jD6vHXL?tUOh#{hZ470@1;jgq zB4Xyv+Vc3<S~HL5vOCVfoCu_9?E@U$sGc3p_Q$M40B1W3^ytRYCDXQLpvpS)2`FI5 zSp`*I-sc5xK5P@;X{@pJSH4l&!H3)4lg&fK9YLJ_4~rmi@u1r|BS<joCj-1X-;E3% z>!xtU>`O^x^c_fE`Cgd3j6SUw>n}{1%qB3T?J6KZ(ly;{b;v+-fnZb1Vkc#lse9)< zjwKG~gQOR_C9ujDC|fyg><7suZ5W60yf>DCwB>IO(6CK+awS*AZ1Q#K)Q1225wRXt zl(&=x^3kciWbcobEidVdlgK)HfJ5t0HUc0~;E?&J4&QS{?O%t(Q6T{%3k`r_eziJr z5673;-*$}dBe$s?wuRRsy8^oN&z<Kx+WdFZI{M#WFLYAo=hudtPMyONQGc&RFPCW8 zr~jwb_;>qEu6H`v-yf|GjIJg-C+C}pfQV}-V6$=TQ~XVAbf+vb2jC$mXmh4q_deH{ zIsVu5v7IRH>pTBpFabBdF_Wj-@)Ex_1TYQ%`z#Cpn0f#6m7PHQ-|fPYT^;-T(|@~c z{ZGgKPuJ!D?QZn7ZCyPUie4ViM^lkib5Dlz=iYz$p-rgFvMu@YC{DjwoQd!=+(J?m zXmf3})1iJDH&?4P*S(@V8@%4-Ie_8wU8=(s57P9_Jwvr#mmJ8W&-<7~vbGMWqk_e^ z2zx;HVui0zMF;Z+%ssm+me*f)2XtNOIq$ON%A*RJ8}q^kAn@F^cZkY7!v{F_Ia%zQ z&5r*|v-!)=G`M4vuGYZ~cMf-ehXp32qwI=X1UATt>dqM&u$q#XWB}RNdLO1%MZ#yh z9_3ivrYXD$?pNJfS5&8B%xY}%&;DBOR8(Jku1|$OfQwj4bw4k%2a&^XsPbl1wCrw( z_Xl<joATB@hH3r{C46w0y5q%-lHRm(vDrZ{#X0f<6+zUeXC!@cH(gEira3=BU9uNc zX3=f><nMpceRu0wAe!<5T6=*M0%F>P_LBq17Lp-T@5(8KnV7J3FepK?rZQw7ui~yL z*$McsfaSjRqU$o#%hQsD>&<kmo7(aUmH$&?j2#tFFK5`7XYU!?s8zB(cgQffDHr6m zytb?SFQmw95aDc1Ler^zR&~vI$`1LD6u?B`@fu*S_D#W|zT|5%$&2f6xcc&zUnY=1 zR5Fq6pAo|vvX-YHCy#ra-F(iP{BG^1MNU?9&0<}+CPi@d+sZd8pb^<1lJO>lIekJM z%FQ>$M`hMI&=*W|jZ?B;HPHE03Z1;>`%&S`Md3Z0_2IM$k%r)UeSRkSdUV64`MP}b zD|Yv5Y0>UMRQsIGx>!raMv^YtWl|%J{Q~7Sk+EjLW<3GX{oCY1)-%4&kT<!bnI418 zJ67=Pd?h__n0t&6oAzFj+1E$6F@No_R$q85U&@-`XkQZdM_}firo4RG9LcL&(!53d z>h~L>7-WKDm_wK5z^A;A{4_5tbYR(7swaN(JP9_NVO_AoPDe?1p2{9b_v-F5RxAXd zoj?mMz;S-g5dJ_3%b&PN=6o{FDUB`QcT~&jGGqXur-LQ>O~H=+sju#bF7W}^o=!({ z0(SdFdhnukqd%qoc7~vmRR)i75iY;4NtITErDrt*S2$M|`LsGTud0{9axJkSoXCi_ zuZoQY6Js1HTOE=i%>)-|Oo2L9HH%H3%y9eaxP7OqA|Tv+GrZh>Q%t~jB==)wo8Lme z1lMBQK@OvA(_L#Qoab+A$T)khC04X5avxv|B`ojUBZ2E&k~cilZxurwWzS>XYpT=w zGvVPkk(7YhK~kQ(Lr+?*Zx4-LK3P|>tm_{V4u?m_2=|ROgVh&m?3J!w>E+4bd%@qX z2GFMy#L@=@`3<5|N!iA3!>SGDti_UibR`)I0_q`0x6*WKuQ1I+nAR>L9bU6ep~)J| zz?AidvxZv}&&U3VnGB_^#fq5sX}EvodEH+BTAqsJpN)2rqee@UM>Plg{6n>+x7i3Z zh0j`IAg6BDs|5cO)`g_&8bgS$ejRthg7Kso_b21-N-5*ikxy%w<j;(zvrs#%t-rKz z-}FGIc~9VCe^wGa&uK9_WG&x?v4*G%oa3=hOCKC9MEZK-(sZi8&4bw?<O~tZQ7i^x z>=d>ck<MPuCd@8&XRv8%AND~6Ns!izZHKyK<oW!CKG!u8*}PpGo2k8C`+Mhe?(Y1a zsi(%|n-u0IgOkf};?c?%Md(P#?PESC3*E+P$szu=z4`4XtoPK}JS3m{fV4xepeW&( z?&e)6HeBWKspREiyLiTn($aQnAo+!3)fF1#B3-kJ*e%yZjhn@zMBY<=Lcy=2_!dI- zlLJZTMjv(H^fKgi<W8QPo58*x3k3SZSv1?i-U@BV)c2bvWl;}m<|Sl=+J-7vRwV~_ zF_2eW#;hT811H^WdT6~Y{<ZDJ*)y$Yg&8BowS7h#IJkmwAPW@+VzM06tB{h1-!j_o zmTOq=V?3&z*OOBt>w$WG%dHYWfQKthO9C%l-k?UsX+jVs>2W1j*v6Q}?M}m5dKxIC zQbA=Cm)F4dN1q&hhdOQEUXMyg2zp8b*?~$|hJCd%`$a+_8K*I3s~FHjR029W-+gRB zr<Tm^Vk<`s`l_7}tKdGMAsiBT-RIcOiFU7I*GF+J**>I()h;4b8;e^fC^n!d@hR#z zjx0y7wtxxjYKIVb!n#NhQRMVW886c}4etea?DNE0DjCMilSthxVU?UKwc#QCYW_7C z+0kT@V#BhiX-{T5Wi9?*{dX5lTox~EghEcg-NIU}nxXCii3khQnHn|3x4phT3JMR# zTo$dnOg!7q=+*j=3>2>LtmY@xhJU=Xz77NHUx--oPFYWa=GfFE)A&t$vdj9OuKH0e z*Jcc{o`_3>hJ$-Sg$F)#3{%;I%!<M3fhpwFm~$<wYvIUf^IR*kJtfJNN-BQ<T=2`M zbBHWgxbe$f(dtzAt>!-*Ygx-!|J5zrSI1}{j+5{Yek0~6bt*KsXsLZH``wdgld2g! zYS0}0OS5wa-aR=hA?`h|E<x~qr@=xgqJ>|EHNd_ebi9q##2-minQHCt`fKm|9OJ!U zL<=pvT`b|I(cENg(R7mk%Q<~AlO3XTcr$6qwa2}}zC@FIkLeh$=jA2bC2|+5Ut~Vu z32R;tqQ7lLWd%MmuxBwqtD-KeF?IC=wX}9$6iW`_UQ?FvZI&rkDmgaL($iS@L6pMG zj%=?S%})rdUfVXug@hoPAT^E9J7*5BvqUEkUi>{)Aen6ENc->sZjn#ZSoh&+fGm8l zsy}OTe%s>FU-n}#QerQ=<&a@%vMqy#`EoTxsIP3GXd{8PeT;(EpyM{*$0U0VkHv1I z+NAEFBeNQitjUwWZ)4~q+f%Vms<@99E*p`(>XPN*5BQp!3<U4CtsWby?Y+p~vY&VF zb4?vu8Slbi0Zu|<jg1bJUNXf6ve?8sZ$D{W?!94l?A}?Oi}x<N$!yyk03SeKbW3Rl z_1#O^3pvR8l6!(x;Os7;S>gNwn^X41#k#jz!4SFU^&aG&kFpaN-Q&;XK8bp@FG)wQ zI`Q%Ww(lje`!2FI#<yKMDdfrPBt_TGRJrk0b&@ZJNHq>mhYAgMh=)@OI{6yzC>fSn zkza-<D6IGSZ98SG?z-jnJ73tDs;585t<MnG9K;T7SPSNr(|@KNaDOY%cUkz@2HF4y zSs(BxS?iMh$g3gKk6rygHrDnQ9bSEbg7h3So_&Rn8186p|8v}jXWd=4Bq=^lnV9sv zGn{?reb>lvwQa&y;eDKVcFuy4yv-ei7X268C!Wb*3aB}igfGqN70bquv)LgTX4sFP zOc#v`Rc5zcqhTU@FAV+e;K|<|Y~~UlcB<suaK0EObGT{+6<6n6&eZiEF?LiT#lp|5 z_FdY#khSm7D>KbWERvn^a8!1)Vx6^q!cu;~?N#r+a+^v%;;MT|anBYOQ;@OE*w-){ zZ!#AXUb}s8^={awSeS;_5($OJHB%b(s17d#U`4Skm-<5(+op!(Y)Fl;6N=`I4!+Pl z$L*35B&;wGHk5Z*7gZlie7x-AAX+oLmXd?C)%QftaS8+Ty&RH)Vc`xey39(=6oJ>4 zBk^v!sp?Rn1C!VM#3?%^E*$#<eh=(Mle=&jd2hoeMjjlHb?H#WOkmq|R2@r^2k+3Z z(vt0R-j`oNXn?{lA3nA9`BDt@;t`ytQ5DP=u`gq+7{Eeoz-HVsHo)Tus8NP^{fBcr zqHbQxo@p8Zxwm4$#pH_4x)pJ`y}lNr46CiJuX`>!NWe@!+c|3(cENlep`zI0AXpoE zaZj%f1HHkM;pDyTzcA)pr0Sb>i!o(FD2n$BPQBEAEhcUC!~%?%4^@Mt)Ou&xs@50R z*>jGN*vDdBW&~iL;Z=;#q&AiQ=<dSU(F0#zg)^LY&CBQF@3n!^!dgw^7YN0pBHGKV zDe=tmjLU%}Q&8}vJ#$qvBwwyJn>aWuA2@Sac(lx>20L2(`Pm7?AlCYYAiR*D*P&1v z&cG`kGfjxa8aiSf=)d@+!PfEdA_B^wK!M@yUwncUi`J+h4>@aTjON`Bi|(N*Y5rTd zZ224cv_|i8-Zx@SY%1N=&@71++T_x(ITLOvq*3r$(*s{SGTJ-%a5QN)^ePv{DL?q? zI+#6vaDnZVskpteS6IlsD73BMwSwP1UaQpQ6*?r;e`$$JWkKmkpRspUhvmwTZ1M0* z)-UQ>-}tLnD$lniGvW_K!&rW;@wJPW%3B8<m@`8Y&6A;}26gbd8=7!db&3A9jd`au z4T?jJp{TW}B1`i|b_uWj=Uo?E_r^ORVA^KQdBIewWB+TBCW%NR%4c}BP9ZVWqOJGR z<x+n&x_kcuv5#-Y2p7+0)H<|}e&Xq(;##!x!HZZavpL!VjBwA84n~yVOiy4-t)qFW zlZNd->0$%(OVl0l)g2wE*@hplYF8PW-I~P1zih>|C*+qy+}TgEB>|Ral{@Th6t^Gg zI(b$i1wEcnBd&27q)O6|Ooyl3qE+D`F5%Rg+RCv(s-tK3jJhbf^;-?S5vD=m)$bmd zG3cF!!*0|xD>`brbO4q>oM#=CD>)@7S0kLF<21?>5>9N19^Te0OztLMnik-}jEgXT zrQ7+@T2K^h$7G!|Duyo!L1q5Z7**clZWhq>--Zt(Rs3vq*|uj#6F)&3lJUE#4$o^# z2?PQ+{(3F>>kDqMo0|y?*3g-%oIN}1206gNQEh=hA{E@a&*=R1*JU*|`}_Cr<KM@e zP3ICTm(FI_Emyx!aG7g!$<PHoTI1up5~}<8q?&iiEt}PwgjWOcjG(d-))*T(RR-V` ze)2jCI0X-H*9vy^V1E3#4J7LNOubaVV*ti*lijdcBZ|l`=1%|d%xBT7sP6eXtdv;z zY~T{*($O8+=eVc3yVVc>S%U9J5djulZ7$XBftZCf^#Y{;{hw80d#aOe?mVb<?aU4= z;^Vj7UTe@=xn<6V6*?CfRV5s%ipuVM0DBvhpBI~h-@SYd9)a=hJq#7#<mhJ1W&>aP zr3<+A&TRIOOuDkW&hET!xB2b=`5zwy&;AGO;=S{`z54x>$MfB=(C&xF(QkKz@OLXc zvb%qOe^5MXWoH-KD4?qZs(y3>$mK_%Py<PFuE&Lip_0JSo1h1{*7orT<h*>j|3ze^ z#MP_qw!Jdy!d>O%<;5}_95-8@?&U5j`!Y*e7UJNjK-Db^Exdm7rlL6qh>qQLS`d_T zIrKAAD{BdGQ&J~gj&g7`#s=|nXf2&r?q99_Ae3bTWN`j&kI%SV8du43SO2%}-I$M( zC@F0w;44J_W6Y1wC_emg7*Yf^r0njk-(TPVfXUbxSQ>9CzQS&wo(Z}1@2~Of{&XYi z)vF#L7b}HvQr5A<-4|0+f8gKVdH5dwkk54?0R?be^PAI?;R^XF;6n=kA1C*w&+du0 z)4?ydZvTgKZ1Dfj75wl0k=u8Hn#{crp;5o_uX`2b<qI$v2f$=8_4BJh1A=caj>F*u z<lWw$J9qB5h)79k={3lmmcv~~z@6#p6qe)w4p##w?gs(5NblagTZ996wnA<ld!HTy z#Od*`fzhCt^`vQb$vNfYTE_t|_AG3~;7=));FX*=E0~Bwr?c9mv8k`i4!g1#w4Y)b zv;BqU0@v<6j^GsrOaAecZ}%m-J_RIyG3DRP!dm-fA%~J)Wga<yYZef#UpL$LwflOc zLsnB$NMqXo%Q3L+G<SE=<HHX%u6W^3oxAc-b4FRIt6}SRejEJvI-dP8@ASh%k9YlH zdg$tJL`l}~O?LC`5OE(mV!lmnV+KIq1={xixfs0zbfIm?6`sT8O#=}T`)~sTl=dXK zsF;|_kAQ7A>NuvES;Z$hP<G7Kf~Yk+?|x+bzJQF!+Y(V#`++w?E*%Srb8}!G0r}fU zjvP72$4B*x-2UeKJStl^7TjN8ia$R8lF0G&G%!D3{DX+yDFY-o$A=Gd6!5_pBL95( zRgB|#-Y7iW(Xz#uGP5b|Hf5>ZFa0#CN_`;=_H2wj8|NUwHj!!KqI{H&uJ~9Hn{r3% z{87$dN{{c|0yI|Wq)I(?wu9nZfl~c>mIVM1xINp-d2e&Zu#Tn@z4g`PU{1oz;M9&u zs7T=IW(sN7!aq!}=B3(IYw&@#o&VtI-dBFGZY$^Li=MIj|M{U5;34(x7rPHZoyrfY zhQtPMcdYKt^nRFq<Oaw-Bl6=*U6J4^Vv+Zr$3rD$%3*YJzJOX#sRBR8;i6RFnCyQg zgFhak9Tb$vP#Y0~qZRU@F4#W-4dUh5RV@J(IM#LK;7-O!7i`fNZ*dP6vZBpgXgJ8E zNmJ)cz7y9Pm$8(Qt?=5gZY|oz#^%rqWB<9J+%X2l(Qj!~8?iaerNY|qMOZ%H$5U&6 zBpsgTFG@}uMIpEUO4QJt@Qqqq6$_hdutrzO8(6cNNb;ZDnOLSF9OCO{ueqHaSV5sp z)=qF+yPIhGLNeyEnRRP)R_uxOtt0eQEMHNuF^W}YaleVxc-PawE+cq53h9sg^s?E% z{_e0^_=^$|jUSsIn}s$=9~nc^v1jM?m`$X8@}B27LROSG*5oIYIR^O$#$nc8_8Hkw zCcQ=)T>CtE_h3_*`6@=A1?8{D3HOjo?+d_2FBm4TU4+TdePCv^nDqv$62JuYPw7wZ z&a7IWEnWe`6t7zi7|0TRqaSb$P`-`JE(9;!YuiW!$IO~cyDQxNP#ksY(|vF6D2T`d z`Sw)JtIba7%7?7r8-ljJdb+F?j8PTi$*9a->*$pcv5fmB!=bx6^T}_uro|;c+fQ6a z*4JX2FN_r38Hnk(Y>EKScHgfBPob0Lr}YJf*yrP^aL4@Oz=H9Lv0=8WrF#WNUeff~ zB<4(u$*MXp?5vc7d7eS<Y@3F~R>}z#8?T0zroyI33!DS;1T$im`@Amr;p(IKk?6OF zYFc-C*EhYWRMBW~8%WpOP`~$BLv*snSfhzFJi0qa`}hAoS_m6IWg0fd+<9UI^LsK@ z`}`W6o8;-D59F^+q;}dUJki*ieHqh#$aDnlB;W3so7ffsZby3ytdJQhE17|2&EAIf zxA;_Yqn?TU%uAOKOP$c=!xv&jdKvQBJEr5AZ@*8s?h<38@SI};2&8cF#&A!UjFI^z z5E3~uIun-lK{V}c$(xT(Z$r!A&u(3IsrEzAa{`YR5KW>*lx&|t#78>XX!ULKe*M+@ zDQ|a7fX5DhPi&n=vAX*<{N=uY>gjT0OGer9O3jl6c@o}{vwf;3sSm-d@HYCe)D?}d z*0)I!dvKdiEXQ7)J&6pVS+bi#74YDl9oOCTTOGQ!<RjEwF#!`RnI%@P#Xalx;WbU0 z@Mi*2@<xuwOU@=#D;jrQ%GpOjn0iOOUe!E=X}8_gwjrl}AHoZz;EjStb==tmnpb{v zw~jwL{|!4*LaoS~pdULhvyA(2pzsfRSo`g0Au{|Fe%#h(_vh7rTRauWG9V0)#?0|1 z0I|b<pt3dlyqJbUiTpl5%SQ*pHIbEL5Ab>_frPv8?t475UL7k(!aCY;`jX%qJL*S^ zcw_gd)~ciH^7LIx>*p%Lf4G>D>i5&q92x%3<s%93IDiw^mbvH!dC>y88QZ8B;e>s$ zrx1<!qL1~!@iF(;-n^0(z1v&ipDZO>akhCynO6CrX37c9emx8TO3uL%a$;wsQZfVj zDGGEu%CLx{NU7CWE~A6Ys6y0NdVfUqM$d#gm=r4aRD{=QPtwx}{KDNK@V4Udbt(zM z>}*05R#|Z!wv`gie0QEf@%w(ZVPj(z-~L*l6IO6u*QkK~vPWRt^J!;Lo28TdrMZe2 zA;jA@BKCepTK~J1XDU7LoQV=ob%m)J)0F%Pb%;ak%I(`s9Er1u3v#<+nLfrqsE|?$ z!QDf1;teQP)M?r7WFQ>+*dcH|F+gJ@buoj;^7oiTued;EL^qc$sb*w-X*cqQuipoB zT8gX^m|Sa(9+)oAta5a-XU&REdzm+jxwkbnD~gQaT32pKuIHC;9OTg4k(h3YbMmgu zm({m%RuPb_BHC54n7JNTzQWFA4;)9NOAxdzQIX`hu$1V^D<doT65Rh2Rui6@4pJEn z-DCxIyi_U7U{73xk%pS^Q5-k*t0P!V3CB0KjQw)f6yk-$97WTv7k&@}(Mu<U7eW_4 zX$V}qbqwWgUnF@<<8HQC*c}-}uWD#oNdAO&jgewQzCB|-p4`xd`LroGDKGyu#8@yH zylO9Nc;E_TB`e%#Tf4qwj@p|!uud)xlvrais1)Y;$Tboy6it<u0*Sz)A4Jm1@m^*~ zcCjp8Zh)qm>Xv^%16XvwpF^Cs*c=n0q#rq`eLp26@cAV;K1i5>wuD<a>x%eo-%I!y zPEv-QN7<qW-r&x^7C^m%%Z3(!*<o56VVc&O1X#Q6*13U3S*D+^9XUg4ZnojiFMK;1 zXf5hVEzPc6L9;x5{NpJ|!)rZ9LT*Q7(JTBR5ma46rGn{)j;-Zm>N81A(NX@`Mqmz4 zAIUCMGFlUH89_vf>f>s?o5N4WyL(9(OZ~Cm=g2dK6?%$-^Mf~1D1Y*gj5Z6a^%x*& zLO{{;eV(C6@mb8B^go*9Jy+xnV`A~n`4Y4Q*;RG8=`?vii1z(wa=<FO7W^3PS=UZb z^A-p`FbtMEX6DV(c=W|GD#Pc)M9U$|pbi;Ji$sOw7-S`zq~J1feAF+kX>={Ms*gTf zP?la(LL9HqK;r?!QR|llW+gd1{G6F)ki0q8?j0CJ&6`_}wxMUdxon96<&nm~V&8o# z4Pu!~!s<$oo~Oga^(iMi`cvJb2fmdr%9cbb2{fVo3EOKD1oyaD7<3CI98L<fj$Ay! z5x!%oo!Q^6msarN8D(t{i_KZV5IpR@?Z0@z;r^Rcb3xF1j{PA_`ED88fw5~gud2on zcF|{hs?Uh<%ULj+iY&uk&<5Tj-)tBpXSpwK81%*6833<YYTIJOR>TeE@kv3J5~n}C z%#2EL@pP()xLaP&)8n=(cZ=ajU?W~Nhl<T1ZbKsI6vTmM`mqvlF8tFYp6HYrR(du} z+-&Z1V=Z_MINWl*gI70v4#~#=%-dZG1MU#F1yw*>eNwd-O_O6$yZb2}5G&&&QEAGi ztI=f(@Ir7ZE^39?P#B$|GdB)5#P#3D8yGh$ET=MBAb739&#l|_uTPEvh+`V6g^}UQ z$>5^kCx?%39PD=8Icm<F`YU>XlkeMQ*0=lW1lJTO(tP@UIfqbY0q?-?+v6wK@85Y8 zSkzu*=(hZV;=T7Gtd5(^vra_{O&Lvux^g1{C8NNYa+fV%T5(8-Q|*k=lF?0`Q_3Jd zqTogAp%nVo;s^W)$Ns3$y|c*QY5-g@%ZL`XrVHY+(Bhf1KD`Fahqs>Vy>`GgKejqe z-NsZb4c&>yu00LM==vB4q6!mA^?&E75RUq{LFrx6`ji3n{GQXwwzZy-mGjD<qOgIb zRq4lCD%cg?4XTQSZ1z~^E9RU*p!s}C#@Na3ndy*4hd*u#?`Y!?o#!*{*Hpi(X?}xu z+IF;^(5nu#<+C_Vox;oqp`DMC)5Bnr1G#7WJo~99jm&%@hC7;^W3BS#k>uqjg9c`K z3T-pm6tTT}R?>C@;hBLoRdV*kH!CswFR%1Ja&QrF_o=Yq{AowNl|gax9Sr(nJJ;wD zu=ph1&7mZJ@x+<_or(WseW<^fD7({*_EYY~Gmu~3L<(fb{!w`i-kYrJqfiY+h4#$+ zf%XBoRIN=JCas5arz_o*bH^3vw?-Q-gSuv<`a^>QDSY~3b7SKQA@j?mirHKzd`NL9 zvex;-qZo*qXsMrDgaQ$DM7_V>K6XSxtKdA~KG1qgrio9Ue;`k5eihyHCMjgwwQ7#b zhYVoJy6ZEX-FJS6BZSBiIpLXF@**~uLZ;+O2748Z*!DjK_(Hk#OPX-aI8@c39q<FG z1wZlI`0jck`oSF~wMUpLKIsK>=~$HHY=Mkp4f}=4saf#Q8@aB<Te~k_t2~BsC#zHZ zjqk@RH0gdb`5yD;tp+ksNti^uYnbL)<U3pH03#;jEFC)98VV=z^NSQGKxRq5Wj5I2 zG~6R!0i_h3SR(rHub<vodYZ6B6T!W^_6dhn=6xBxmmk+zi<atvgT}_1Vq^W>b#u8C z-OHWH)5c*4?@`tH)Q=_N{YD8HF|av^g{egzspzMof|9hWeIM87kH(I5){ix!cf2VL z6GaZp4l8Io6dGh6k54XkN63!BMv469^X*9?4^u7Zy;9H-<B(kowwwRk?4qb*Wk8wV zkPCBiJ51dLMAfL2)nx~<A%wevx&EtOR&g|5>`1Z8NI{KC9yMH3mk8V5t6sG#yZ~D@ zga%idY`|U1J-bw|L<{E<VNUh5JiLN}G{4&upi3sYfXVXM!4xH_8ar2(U@M5Qt#BH7 zZ{xh}(t&MHp8u>h|2ne}TtBzAN<$$=x}I6=PPe_!`8>#|G!~JMT{PKtRtRaV9fOU# zX&N>#Z!yOcd=*@uqdo0zjTDKxcHu-%Mw`$71Q)ZvOO38>LTuOGd*jsZ$KlmOs#vZx zkBf(ZL+^Dk53^x8`M%*kqfa{${Q`%_JCru}81hTmn`mo4o1;+tXWCEe=<3?(1MVL% zw2@+)y6y})*pMqn%B-!meYTIs?61F`e{E_yaq^^#B>B{Y^3Cr(*}4kg{>zYq0pGkh z8s7juqxJ8=RW;xW>Tj1_(Q3he@qF#v+2dU`l=(tsyVQ+cc9fPI&#rxWW=~QFci75L zG2rUkthO=g{$S@nwcK{ZpM&H1nQ|x<V9hZN{Sndb>-k@9^xa>ca4OvU!Xd00U_k0j zJAdaP=Zvyq7dp7J!`F+on=$+)W9P<Ra+LF1{VB+Q_FbO2TPzs_dfkWdFwCs-n^Ci{ zwyw+g82j?2Nl;KwK4av}|5e_5M>V~z>%!5cO9jP3lkQSc2m;atLQ$H4bV3PDL24+{ zJJ=8@fkg==^j-tfJ1nJ#5+D$yE1`#C2rbn6L-#&=fBTGkzH`sL-?(?&FM~hG7)gG! zyzhMHeBSqYX6l{cWGP?#kx@xe(VfS1v%P^Ey}5I}fxjtV{^RR)?3ursfz$B>-~6`t z_-IOQwlaVd25EpUO3|qt{C?_XU#Wqk{Aw8SM>Aqhb7m-I?phz_bYJ`P^*@3(Ieff$ z6-U|s9hjZZkmCjKpFi+kx}^1huLLVceL9xo56!>cYC0L@{*ONM>geltAM~MIDr|*w z!C5S_#rs#KVMZj-#t<TTK<?CdMU91A63jnb)%#quvC<WMpF7#~_JXr$WR3S^AHgXV z*n9$Wv;@EjPT%(-ZvQWawa&khHZTz!N&FfjROoceZAZA`ibX|XJw;yInmYAgVW|G9 z;Aa3<k-iIzE(ib(t~7c3N9-slD#9#Mlw7!8bNQVGVA$G3V0HX)=O<YFlxDke{cf<0 z-mlVHplSbkOz&)QfetTuWc3gLK#e7=R=303O%#4U{u=Q3ive#!7nj8E2K&6GScnA+ zS@#<Nn%95jJnxv!eKcv(2E6gP@Ds~o?DC38ee9ASEH5v=M2G}#H~aNDQ=#84P@R54 z^%uHI?FLc$7R5@Hp8G6ce2X}$k><aX2uS#t^8%SFsqXMPt5p$;Pu9=YA7FQH04=Dk z{J03L)h)8^u3N=_gW=D917P^|j8i-hn+k<Ok-ie5qIzj3hl|~DXF=Cwts0MJPL9l= zdV2NLC;<Th=0~s6C8s^|ekcPS`!@6oVfO*2W;p=J9^L?0u2;@jy}+%$A|6u6>u2+O z=7)enT=)Gg+g*;3IsiR(i!}TsOq~dnYT^MHcmCC*$@3cI?(82wemF8hLV)&dK>*|e z^tm*TAPxgiy+{G-c(r$i7mv1Z8kVfqp3wSSAflxE_I}9`HwFq?PLaU<-HjRJC41)j zcc-W`_Sa1MNTg=D-wobR&pm6Ub7t5TxMAs>qRttd-?kq!x8`|`)2B%N8c5=@7tIMU z^DmC6Qs`s>08$!z)=Gm>;%9R%|7^~Nc89Ci9dWvIT(1C=api+%#Qc2LuK{>UWdi$W zR6ZL}W_?Bco2U?AL!z?(TQTCl!?#jPU(VAA0o~>V5PoK&;ZK{R`Dt^W1+(JJe|L=h zU=ex=De5#j`CnsUjq}o=fAa4~WB7tMg9HmR^C82NGa%VrF9wO$*4E!lFLDB|G(i4k zY^f28KO*NeIV>0+CjDOV&GuN}bk<fzOF05B12gg`6C9f+Q;L;7C>CEpl~vm=(VUKV z{O><oH5x-=MFUBMPGNe9KAGPu2D~Zr@uvYsR)aDZqcItzNw(6^`J+<u^jXF}r~P?r zmJS%sXv&;s?RF?gfI3wzNQ3sg25=@v<R-v7zU6vNLjTp*<XrMEB*^%X3$l-o4vhZh zm4LU2O`Ti$*_@`IhV_H`B}55(3qYyW3^iRJ2JTH23;~`dfeml6y1WzKoia}`{n>c- zUvB&naAO6)jfY1^adEeSP9-q;_T6OVI(<B!)K9bGA0V;50j&JOpO?-9%hWkA4K3iP zOHx03!r@%&1en}&H5re9Ex<7%WZTS7ztt4>E4jk3>+sJ;_n%7z{qIdA{I5-kaM7{y zXyX$Em<iTTPuz2=q60-ri+>zQyLRza=(+Gft4B8<(6d!AWGD@@g7tRfmQmt4%Ly9# zE0Yh<UG5l5^lzYx{_dR^sBx`I&w@|KIg12Aon_Z}Tj+~2jO4YODruWur2;P<*6f6x z3v4oWs~dYR$7_ZOYF2BRp8=(BEW45#RRy+gOf%l<04$#HlqTrWVrsc4?In7rMMH}v zRd5X-Uf;^nRxLw^zVH4g?Hu1wq^EIc`GRnvmSy)n-hSSi+%up*GIEW*!>m@~3IJvN zfqV3;r0`3>>jzz5q^?}fc+i(BsIV-<t7q9%Rot**0_HW&fwr48eh0QR%%@>ZmP5gX zYuLe8bzTJTMLiGpZ)aAR(OT5~tPuuNTj7j|(8?CqHp|+awYTi6eT);*FIciq@f8PB zjw5VevwKT>@u6sXwqo9uXdyAx0@n(tPd{pe-o3*k)i8IfKqq#8;2t!<t`7*)DW|wU z$e*b@<IniKCa0Qc;YYBNlnccq4^ErUQFkPUAah)n@2f7paWB@+G1aH(hBc9=Ita(3 zo=8{yV&4+@gmzy)ENeDvT^S^spAQ}y+9=UhW@O)V7a!#!4w|=!rQcxx5l&Ew6`FX# zTbJ+FN|#gm;Li%`Gyp_|oW@XD4j^jFi?cFPe>B1ziis`w_~s?!YZv#IEIArb*f66_ zedHsIiT=~jkGioURG^EG-RCi;l?DpZVy>(qwy?woxr5`EQV*VA-Z-|6-_+0M{tU|t zAyPPOD`Lj80T*LF^{<O(?*>27{bJ%8YlCPARUHwO(#_f&4MRKI;(HD8H}{h-+P*&h zafcE%VpVQ0>eJnq^TheX5zN!jm{!82H(mJ#oYM_3N{h*?t6X=^s)PESA%LASEqgkA zpI^^u=GR+&X@5KaCI!6f;8R>;>`O*FbHZJ|yn;81<Y3r^6fID}KMj|n<yt9!z9^pC zA0HdGkr-9D6|7f~o?VVSTY|NIAbc=YptbzLQYB^ZkrSrajn@~mT;tPJRQxoFy>D~> z+3tQ3wXRF;Zwhw$1)-wFn)pdwHK*|laF&IDMF(DF(L1(x9+qatMitDXM+?mG2+Ij` zwX*6L>@n}~J~|90&(`at62}h>EmFZ%z%Ki(QeE}o(BjOFBz~ov;~p4;+Pd#DbXKYk z+>I)q+kn<#TRU<z3<bV0msgvH12-~gr&U@Rgfg0hTci>%=pn$WRFx|kTMJ$@kDd+o zZaqMM)ttjk+Fcmd&EMmWbm@vsRD6KdBb{*s*u6d|6M}bE^$u2tyzPV=7=#;`LqMQ2 zaxPrD+bl)h%lO*laZ0tun^oC@YJ)6h8AA&tBf^wkL3CO!5#W(m*~`624u(kY+7u)Q zB~?Cft#6KQR_u?#AL^w<lM9~Zl<;X5ASYeeTnZtADyE2UDQ<RZ`*doQ1#>ipe`o)h z>(SdQJ+Txdi?IKTRE!Zfzn-H}?y`<{m6v;J=x!|Y%<OGfU=&LYVhg!z_;s9-Rez;{ zR;fXM<<dN=YA@oK4Q9x`UsRS?SKWSZ1|9AkqSD}$gI{nQDUspTGJhy6=To9Pwrn=I z)oVGfyJR1+i!d&H#lYjVZJwi^SYkN=$#NyyO{N55b@4Im^Vfj2pa<{}qM6oy3?%3Z zXz8&>_+tD;0d~;qzksA^`3JbYpH8cbHi<ZI8kD_RzQzcjc^0OhEHW;|N6)VewJJG5 zNu?V1+vi|=97%Ms@eVwdd&+5f!K~b{gd0w3Hw!~6z4YqKB?oX;Ib}^_GbR}~(&p5s zB*%PSy?CSlBp<3o9b1);Jfiul83bDG*`5wavwFU0WB263#>FdYc+46F*7QJ*TbUGk zC6s0*+@&?k5QfW`F6V)MdKE<F2Y*uYsV=3z#l%G#S<;)QqQFlBdY$^b#VerT8UffD z3rEZS0Tz4`ib(S*`+(4Q-%>%w8$T$?HRj0cS~B1?!kIwH7@wmh@5pG>9fcyZ!<cDz z&q_+k&OF}_N4YwC1m50(pOc`8DMat%%vdCeka8z<w30zs`0d?$Y3kB!A>>LtLFi;_ z7S^Y_m2a&Zm)AR@z#fv$EKZGIGN%G7t8RKOPvv4=oh+)fAw^)y)W-Ton1zQr!TQ)E zM5NQD6Sn5+)Or)2w*-SX`4j#6zrG7X5EEK|Yo1Cx3)-(ID5c;NIKAJR^9@R6yuZLq zQ2PE3e_3_2-oLMm)4E!}X=+{alLFBna|LeZxRO4haR^oJK@IQ%^`7Ffd$J`SYc=Mx zMn+XTrW>^e`*YxVZ>f<hAlc{VoFHadx>&2}Lz;pPSX0o=t038QV9T#%7A{KVN4O!} ziHfVvuPk`&R{TtI*MOihJiI46xmqy1a|p?*#kXa6!`b!6_qasj-rbph3d=<H+(5ok z`oI8*%K8$&%e`3Q0UpgeHjp{a^HNYSz_Htfk}EM4F<52Q*b3)5ZHTmxonL62Q}1B3 z@;VcuS}IqqR2Wb!)}_$EhlL$JMAKSKbyq1ahz@P-q0Xy3eTHkKha)$vTYjtH2Z1Du zBsXIA9-0q=;KNC(3e-napGfTG{t{o-T|2C~0(bT1A!V{mHSOGlP8mJS*W3nc1j%z< z#xN#sc6YpVkyz`5cHwxYy-JS{&mgH7lCtsBfbcHTj=-~py#TrhnP?}gemotK=fH?q zMmeJfjGwyL`Wjj|WS}-{h9{+REedB94GB}<CDNNX@wMFo`W3lVeh~0+vmbqlSpjE3 zx5`c8kEdVu;*%dWBh@8Mz+<rI2r-ogez{9(LGl{<cex17!tgJ1+NRDrMr~T5+kOK! zSgIT>taufz4)xj!p%Zrqp(5+UDNWoe_GL3~#u)+sxT~!tKvGdR$wabv+E}{ueB@vm z9rNi6fo)<#@b8#X&5?&1<YJOy$*gvoRUojLolyt}N`MA{?SpV|$35fKv`%}K5^Z;I z;2kW=B7rIW_(LIB_a&l!if$r<Mr=apg&U7iLD9gm1HRU%JU5^_khz<VUS_0WtntfH z;j73tyCDbV^|E^<atg2tE)${qk%L=Ke7HjEP+i!FVp02<2}aQC6a|{CB^~V6v@=_c z;#!uvNC_1(j?NaBm8f!JOC~&Eiyc^z<BxKtZ6s{GArxi+I;*m;uVa0wPFEdSU|Tvn zTe|zyZ*{DoywC+H?WD#MP1{keEtJRq>giFjmf3w1_M_hu?{VeU@!ys)HxCzqK=W4& z2nO~mD)ZU<8c(2c^fow5yGql%gqrgx7Z_PI4f}fc^}$)tr_!%l(nsxV`LR8}Lsf|p z$fDX;$haKraIC=S@&|4qB&yD!Xj4^p(3DY*ch9M|)8W0-p-4oj67LHT=<FX}1j-G1 z*wKwDX*y>3Hp6M)c%umHkPxVJI=C(vZ`3kgveqXCn-!rj^Kjp|zMSXd;iRf*f2&@u zd>B^B+YY9LpvRF7+DyvmQBS*Kt-r7FA{Ak<8DQj06x!Q)2AQ2=^VZY@J!4o^3^#l$ ztkQrRFF&U%=7=xXTdvq!N>wVK@27H?U<9BA4vQAAX_2Z$^mPEqg#)vk?WG`U>BCA8 zaYu<bc1?Wh&RUz2Z;z}}WTWvPWWMropI!ZR_(zg0jZ@%6l~ZA%AIg!8o}$h9ntd7F zJC$?iz|$7K4nsHctA!8_U*<(Ha1oWxGs3CQ`<a12)xuyc*G3}==;sd*WNO8|C*kmk z$k=V6JpCtJ9@^!C>;?r@bwT508zyV8(J|@Zh@#x8yoyLD)Aey3RuWJ;;UyZjXfj{8 zVvj3p^8!W`0GpnM4*uXlYqf*dbM*>mCPfUL-xTuE(JpGqb!#@7Ik@9+{$=}_qqNZW z@q<M?ab+}iLj?ch&uK+7RsA-wG`9j%j~-9pfi&A#i-x2NLsyaW>*y3PTphIi4G1Xq zcl9dP)hjx9E#a}-Hc?>zB;oCO@aXYwF)3ZMgtvuj^XX;r*eLS-wS9*OJ?Vsu*z9xd z;<th8RqdfuYhW3B#C~CkZh^>(C@<drF9y(SP=yY}$&BPXjrb&1s54~<uWbED0uGzZ z-G8urWRdA%CNpqw(AJ)fF_h~jF6XO_YYz!N)?PHuN}TB(3cXLQYpnN&etcT0i;Z^l zF2MjTjc_l~^=6?ez7!)zz5djG9bVACx>sK-3uo`sUU?G9{8+8z6&IiP2P=aZ-t&&Y zB<wIClQZUHiZ6u`Deb5L-#AhF_@bc-&v{po!UsG<x%0~?K~9|{8UymXCn+n9J7SMx z2>w|w5QQ|R#T%0Xd}vB!W?b21g3eY^pJ`R>Y|QM4k3~6vo}~ECdaKd|duI6*x-ng! zQi7JHl9#CKEdJ5IdaLq75lpn3f1?P)#WEB!^*hoYyr&h1kF6cnT}6v)W<zDLdJ2YR zo2fwl+Npn-F0ea+CN!l<UKK(;xqveu11AJo#CD=PB29eeYoy@rWwB0^t314eg@W$S zW3>AsN-N^HC0h(~CgPSqBNf0h%Z3J}y30aIRbwbO*S2nfVP>&&FU=&lH$%VU6Xp>v z&;*Ulk<sq?x)adrTQ^F*iFk1P?lK~)9flM!!a3Ed19H>0toJ6<muHNNp)EU(sJ_0a zyGpLSvk_seOD3XRe7sJL89Mq|%vcsfX}lk9JkJBzAJ@UBnai+5Nd`DMkgjNG|6_g3 zJQ$W(Q2eE1S$(Y9U?w2{oB0DzC*ot?fN3%pC;XW0&4UYPK;O6@5xj3*jwFSY2FVOX z1~~XNL*bZ4KGQOrvQU`c7|rQn?waP^;MH4Jz?ECT-F}ufn~&havYC!XnVlQkUecZO zx*iL3Cf4*mPJ8?2^WJwAd-LAMLmuT3HMZoL&#&Lt^QEsf=~sgtZnG3)8~cwElakrb zw*x_y91|OsCJtzya%|Z>RL{Xaz^`_v25ke>n&qi2HB4yTCT97ZIWTr$34HC`k5AS? zRsd11nBO#yI}*5E<r84U!3lIy$E$WZ2te4cJE~T;j(@>p`KT<b(IPXod-#eKYs32Q zQ$L<r!%3B#rphy9M2jIRz{Fy;$#K)3H=v{FO@d-49476*+Wr02VltRI-XwgUx7xCe z<lcjPVs;MLn7s`1n%16E6RP}+hkHSLaC5BwbEBTfP6nm<HGPF-_-VxbD~h!|=ux{y zg<{gPuu+3jywPp?vD<Hk4YgEYB%XPzd(~!Lu6OM|FBtPCd~H&Xyxo~=yXNF7%wKk7 zZfU<_x7KBp{+_3p$uUaB>y~~{Pqbu0b_jHwmf~|55tlZ4<iDIva;^MPq9<LHt5=2R z0#2(50+!oxY_N%_1Uu)uxwe+a(rf)@nb{G|-Acoi!W*bhsr|rpO~2E;-wT8l%-2+v zWv!P#Y1Z?Jd*RWTvsvviI}r&eRXMN;1xK@Yj}R&v4s8H|Z#Ju7ujwZ*Ga00t<Q+^I zG%%WuJFU}4`_;jJ>n4@5b12D2`<}KlY5Gc!_znMwZ)_#sv(ST%AtF>SDL&rTC3-XQ zxoytu3N>1nR2uKd^^DTn&8++JCNiX_dYDI7O>#j_mMw`w_e~D1B7PnPs$9&DV?3my zav!yHCvb62#%GxvRD%~oyj7Co@*iPu<lRvZQT*;RO<Ok(2nD0n5kf4jq|%9lAtoA5 z8B*q^{!6X`$|Fg-_=Mtc+cM_oeP=-D3i2Q2+!S!1F_MgJam-oBx#!b4lDk)Fj;8W# zDe|WjA)}UTq!iudUp6uvF4D|H5^~=!+(%j<p9iRiNx!LI4)yAr*emy#d!Pj~HzxO` zepOavHHs=S;ZL#*l5d}xBc+&(1sn)Bc70qnR1|;d8xfax-|Ub^k?F3ue~yc1bMa*F z-a6YI01mq|OsM0Ciecb7X@7zik#3<hpk^wT17ts|aJCEguRr=3ks1Cg70EwwJC0w- z;AzpQf}+RK;6=OtD@$(w*CJ*A$5bd^Dng^1o+Gl7_>61Q|55Y051{*KP7vsu_VP?i z%fBl{|MwIBGlSCyB&z=QV#!$01W}|D|GA{yT{cbl9sotZT0X_eE(S~?7Hv30g8%*% z(7+%6|AtThCyhe$&q8tC;?>|G5m(Gj1e3*OA#LQbW_9S*5d*jvSC2<&Ow|YgIc}+> z*-c_s1>uSu_}WrKu!L1#?QjG=cMl|Q;nSz54yjdH=T&O(O<n;eV3<vRk6wYu{g=bB zJR2FhYMz0M+%P@+fJJ7geV>kUUcY`0l^{!`k=2VF5&pKDEr@5k&KTX#TJTg3wZJ6a zU~W`mY7RX%Hh%~!!vi||&THD$M=I5~K+973jh=TZQ2>XO@J!UBmzXcC(1YVu9(al) zO$;2>gjWhS#%iaoJx~S7UcHsN%i7T9I*Kpe!7AgL-saWcOhM0>K1}umvUQJLxPbce z!^CI)mU?mcMy<T}4RLUapvBV&ohcT0tO$_@W^7<v9Q5a>HBRvfu5<blHC?ud;AUnd zB(Z=#NPCF34udr-@;39h8MKsxs<SCAtE7#PsLHu(N+RS$pMc{$%OJuDEr@7Rho^4H zX6VGp2jyja3uj*|E9Wq~<}~J+T3W@kb|_F0clN!PBMX!hhCHTM!y0Er*Lzt)%sW2$ z%`ZOCoXsbI1Gr3d9J_~ee_Kmg)3($Au2ZrLpa%_jUc)+^<})rnz8n0uZ;%&OQ7&H( zM|-S27+$d?3vWnU7*xZ8T4pjvJe3u$O652HhTqRsrgod_x<23QVIVXR_x?h}%}yRA z?mNXyO_s3C8_?pWKu6v+E$&6pyxGo9Bb*v=C~^u2KCkj=;r#~{Xyf<j=tVnIyXYIV zwDe-buwmu-c5fGECC}qs!R+6R=C%%fFI}1kA0u&lSNcW|-7E^b#vISuZ;BNMs-lAD zVMh}t`#39tr(310VOTk|f1CQ+qc$@8=>1trowg-EW;i!g9zA_r?on)15<Z3ycpxCI zrWz1kWYRDSR^WsIm^xDQM3SYjj1~0cGO4libU8T9#J&wDuB9kC%b^a|=-Ng5>z6_w z<s9|+wHN8Km}Ol5Gu;!%+Ls96@!Z>HJPY|c6T5BjelaUUp(iFOE4^Wb7j0z;;e6ou zihN_0l;Z4Vq1YGCoDQ3WDpMCEzYQ}G406;|w6&uXK@<6F{U!db`L?QCbA@*cPVuTM zgZK7co616J-bo;jJE_pzt~S%CPNXOw2DNeLo`TG=^M~2Y$f6Z4Lod+9du^_gw}6H0 z31fH0E%!rRo*)G2l1j9kT*RpHDsX%gMHH#TP7Me4#3o{lO|nB#+Li~EFNlr=&lgc( zI9Rt*0%vFB8EVyUmUBDMfy*=2x20TH*K&ucLM5>xw|3VRQ~V4)4jX)9^`&^n_h`Gv zqN<&El!Cx=_v6c*)I9?u8XUd_@x<=t?%uDm!F?T%8{;eOsau{su+Fv{`#sNi8}Gvo zR^<y&C+~V!RID9za7%NWVrB}oWiDiGBG$l6GD;ya#Y}y$4cD&uUR)?eNThu-ken=o z${UKoKheR7iyN0TDj<l7BMT{N#N;B~ZUEM<=|>*igh1^N9gFAtBs_|zBtG?Z?8NI- zbtHeSu5~_PT;Rs;rRp%yvj~M%c@<K`*LD$_i<2dY3)ypl&op&)-&$p}Okl?{Z(s?} zH&+h|kQvLy)zkcPJuP`2k<Lx3{nmQICsM*3k*@enkCAT50RpTK;3?cHT4ITp9>$la z@h=<WeA&%oHuO*=n6@^4)9H$A%{8prAIw;I^7!V<y4Xs2Cfsdw;;{l}>2L&s2Ps%$ zE0t!jF5RKW>L)mIMBxp%z&6K6{a|Z}=ks1)?LqsaNsJTeWx*zUU*q1vCa)w2^l{O# zjjYOqO)_k`A2^dWLLA;_MxsWOg#dz+QyvoT4wCHx@RpaXqu7U3@!HecS+Y7yy6ws{ zPi)@Cv5w`j$q0m{ql<j4V}@E13q+hA#a8p_QzH3w55IQ`lN*XEaj;I+z9JZT%1ugN zNKO^Mz~eb(j6i?QZdd$l7h_FNQ7;-S(y#&_jf$X1-(d5LB_oryt>cwlT$dNCXEang zM+zXFT#u~jmtwW@d)#ZTKs221*T5CV^(*Z(c!IpI9t2hyn3)~NnYUDEQz+uF#G3^v zMY!$lKyu@<qhJ3Aw;Yt%K^Cr3RLm@r|D%4p24aI8c*H&?+CBqOB4KdTHU7I;d+ic= z(#%ugI8XT$t%uJ>Vz=nQA<0?6h$D>uUBmIRCj~PP7rdwD3WgSfn@iUsynQD3Qpvd* z(8e*pBtxXMMZMrjmUh{U*H{0`uFbT07&A+z78VPZKK~)NSp&~kNm^pO!Vwe|&$mx- z3|yU)3%iaZ=ouk=;H|-g#QgI_gg2!-RA|%|>CC!AcT-_YcuKtQkUB*Btzm`Yd7l@N z8X0~znGC2jf+QqraE~b_j*G;+i!vGL5J)|62l7-}bEAF{N*}cHH=e%DFVXd-Mr=g; z<VE-9G8Ugppfb}`)|Yf<lQ6x63m<hjfDRs~QN5<QmFfxo(MGPX*=4>y`B77d)<{}y z+Au2y;a@fTO(yXmr8G#aYIDwo1(PPwoTtUW)nfjELE5pS${2kVf6jK4*>b9)aQ@wm zj#8VEMqLL9acB<$+k5mPdZo9#wHSbF?4F%@eeDTk13c^P3H+@cwj`<?l@d_(y^q%G zRAb#rNVWXa_&#}h(5<CES*b*`hALhBQRXJ<4nC*Ap;;bx#Sn6P=^ni}k1j5Qr3N{^ zIR_SJqt@L!VC8){fl*T#j79{(B<qJX)-JTB-iC6{b9q7mT$mOq?EA*0Jiei*%gGxp z86=+<I-Qh}i~!Cd$xG9b=ZS$C^MstCnW|I!Lc$NQ;Z8e$#bCT|DJZX3zY(hRA;9`r zMC9k4Vwu}Js_8eP*!wFY^v%l!_2!p43!-}}tRlx2w(Rmg$Y|!X5gEBSyCHgtFLrC- z^g8pqUDXbLnF}{N&2j8$M?!$idB|vTl4Kn7&>INat6Z1Vd;t9fQ&u8ZE{LTn)f?wn z#Pcjh2@@VM&5)<%6`0c2d;5n_f`r+HWn*-6De30D86ca7Ez#9BOpQA(FSs^6XfZ`{ zP>lE}7UAt@@dC0Ert+{xoMdMrC7^Z5+3JOairKS=3!EKH77qD5BmJyG&Vq6wdVvaB zI4j#!6729$oxtBJmn}zHSoFFg;DVeh_!5;_MP%UmP6eM*Cwqn?fV*LHMEbwOG(Tlg zzSczslMa;mjIxSXcamC)d@b_I5v2(pMg-A5i<pGf;}1SBH1v~#Sklj|BcPae+^p>L zYuE-ZuUYTD?v6lLg2jghvj$6><g!vd)^Je^^UPVL%A+k8>g^nt^@kSe&c|s9i^u9A zA<8OsPE{@(*oQQ-siv>?r|ASE7KO<-d*O#o*?juVh90lzmzBK#z~AKoxt%99hJ9Q_ zx06L?gyG~$!DlA+#v|@#wkDYeHKJJm2wWwkMYAZz7!0d3G_gmpg05vhr8%GRRZ<T= zDxH<S_Z)RYktrxmPMv<SbZB-fvHpAgGG=^0g+oM*hK{FHUgfZw6i}KpQKsTt6SAt2 zIi!7K4r=eLA0-y*Zrz~o^Yl<J6dTaS^IeoMFKhD6NrIJc*YWIHs66jXogC%rJ)c0a z!1a2gZUF!ykL_Ry(p;{!D`sY!FP$rBd!)xS^Fv%ufE#tf&-QH2OMohzyW=2rhB#P| z<J{j@jqbZQAO6HgDR#ro-pc-1=3sR+pQ)Z4VW1Gl(%Fu(R1p<jMZPR>wrfb#t#b-l zP}>yOw!jIWNLO4te%rXtUFBKaP~PWQ$UT;+)<JA<3;9Fvu~0ZxTRAm!!VlA5?7yAP z;N;3RAH}Ic+UIpM4YRN}?m`|80a%Vj^TPC!E5tlnWE8?K-Vvmp6i6om=5`5`>)YFG zM=MPWx2Ax2D5bM}rUT-1LU`u$*B-W!9WTE1GNqUj){_4CZDJ}*_0H|b<V{bT8QN}X zQs+n7QFT$3ON!a>K{^fX9*c)|>K5USV)xFxmIAEg4S;@V@4N7t;W2+AhcFu%uF=i7 z%^K1BxLR{JOZXzoIZx0h+Gx4Yn5gvF3wlMb7OyuoI0aL=>cLIz-d-Q#fi_A&6UFDz z43owiYo0T=Qhg9<DuV*fDxVaa+HGs0xGy*mIj|Ex&bL~9r(d9rnLL0!Ow-CI_J3$< zgX6CvoT?pw%W?+3=C0?WIl(e`(p!zgANdUDhtI4Z6%Uaj`+zI5F;hx<&K(MGO23vJ zN80<&=Jjbc7RmZ1ZTwX`_kd5&Pemyy)|JW9QK(h$v4EA3g(<NAY*7s&4mF}I!rb>D z^zN0$6NvaZxcMR7&>eb4Cx$O!y5AjtFL~EluQPG^J+rtO_e0Yr+WQOEoX^ajvxaPn zN`|5*>vat^;=tcOvr0)fOkAO=gmiQkR?a}wwDNSnyJ40^VhcALy+2@}V|qf;_fy6V zZ5n-s!(D`EpBvH;-U4?jjl*>&_z5-sUx8^dXd|mrrFyM@6d)w%67x3Rbu@1sLawil zDVba?EGp2gyKbgZy<m%5u~a$fw?IQeAY)j8kGj1g3!bip$zMSr#K*-Hd5BZroKr8Z z@c_mo=m-@{({f9KRL?u{x@?`>eRsQGE;SNy76eqiXI#6s{E#ZV+ZJ{#pW(4)T0|GD zyI*lu+=!=kq(<voXlV2aW~jAK;W(vu8BZ(UT59jm%9ysd-2AlNdLFL7u3|YUDxYWC zjVn}0L`iK+s41J}Wa1@W>|7)G3biC^4b)6eZ>?wyBTb4wNJ~GiVCAE4v}Lfr(wXdI zYt*>#jJi!o?zqYeywYe`DZ1rq=2)5aYp7pmO>8G8KGJ$qe$SV2x>ObGu@OepmpszT zD&^_HzY7b^ZUD*)F8HNN5HlG%YsYI}37Z8rJy~8}=KWw<XZ6Pvcl^PV&K8V*s!(%E z#zR#xFTWN6ixQoBL)D(WxTKQ-q}6LXDDimrZokIc>$2(Bu#&gEW+|_f2#ENh&^R?8 z#g5*ksQYgwbUbUH77gVs)wATntFIdgGCB<H6h7^Hnsle)LQ8~QBgKQ{bq2(DJJs_X zsS)ek=|Na>uUKFL-KquR<h|`2Osa^21^f2u^FgI&p>Yr!)9J;kV_|D~mYQnB5RMxz zoIf(MFRmCBs)iZBAS5d{reth8om?vgrIde5PO98X-9BS$Bvy`=^Q|sY5`7#p<I%Ov zlh@E>JIcaeP)n<ZIsaZu)6Fy8rx`Cb251%&)txGQ!82b=ZV;-f?DnbG1w%<C_72=6 z%6zh`UEdL0cjf6|>($&YEZGlD?45~m9(*BPK!4Ju$0qKQJ@VpncE^ZOVXSM?0SmYl zyKB^J|3*7}CY;AQERCjf!ud3Pe6!TkugrCZ<8@#gIf8oDi=9bkZ0dWZYcOt)aolJ) zyOQ1eMy=X?)ykFB_Ho^)d}Pa1j?9`E0mFBiP4%wv#&bBDtr?Pw?_!)S3BsVSH%N_y zkBd#|{d*CBkcz<~*WmYNl?$M+=ejeV-}wF~ftrsyup!xDauV%4x~;Bf&m%A;;Pdvt zHm)hqmNFMMwor;=zQQb0wW-EG4At`rjP%@$J?>kEkwT$&5#idC*)J^*@9Jwv+pu!! zD}W@%J-O~p`@e)bS2{=Jh5lwDQ+^4zfcZXNGV~iC@>s(f6)VJa;EteJd@Tjbs6`{C z#TA5bqg`Woo#$DzB0XN*d{V1<nr3;SE*1T%p|icwuaqL)t*gfzkS+{~Au-;llaET@ zrz{LM`CTVFE5FsPu?HQi&`Z@DBpF|VZEg|PwuIaY=VNR;!BcATZXdNSiqZ1VcaUC< z=5K1>U$i4b&~{Q~uEPbq3;K;t&2a~m0P?y~CdKLm3LbDR-J?u6$x*8N)?sQuH4-ze z52!xaYuzlrot&Yp;ydtFT{)F%`to0FB|u=ADu58I;~VWm1n;$jF<GT=7&_|`Tgv4S zG_fdnCr=zu%d{Hqwa!6m)K9k4A}rQ|jh1yh1ED=U{it_2>#lShxH4ua==FfC3s-)d zou1DJtv2p~$N<>4Dd()51<g02$4Yf>o%+bH<A_y~Vg`@0%W<J>j_IX0D)dL49T79V z>+V`E2{X<b1vFmttUS=eLk9SyAfNgrC=%n_+(zhVDNq;TNIIYO-Be7u9(-JVnUhJO zuyF>-Y_y{Z9riM<x6{sCkib;s*ktW6cr+{5y`SifcotSg5tBY}?=M(u@hF(JBfQN< zZ*U;<4Xx5dC`6?Gg3c1e{%}F`W@$l(Q-9;P<)yOqUQx-}#_g5Jv2G$eVe=OJ!ws0P zPw#$%W{9)%VzXDi&a%!`I}%wNQ-8nexi&E}MVxg3_qy4i|K<iicxkWQ_~l!xL`Jp= zQM(GKB-#s`l~(V(Q}6M7)|i;h-JFFGDp+9;o3^bsW@Gwh@<{YGRemdIk&<W;7zw9M z5UVw2>PCwQoJn{6xH$bpBvlFg-AvD}JKM#QziTMh^Dx;4*X=@oR4#C7<`0MV22puf zq{e76Iizk8gN$$JmV&wBcRfsd%H>Q9Q$-n=JC{BiqC0g#pU%(LiOo`SJ}&YaFTWDI zLWF#q($_RGjH2qra~yIV^QX;>*J;l?a6JqNQxzuOdn*b0dI`V=Xx%4V<_`xh9%1=$ zy)3yr?%%1<W1p0hM2m;Yw-Wa;#g^dEZq4;_x~xCPQ3ie9uR?CnyyV%28)Bwmx2=_< z${7QU!unX2@G$>0t*5G{nO<2BQ$?H-H$zig7#MAkBq?^GP$k5aBJ4ITa_6J-vua*^ z4Wm_2e%tC?i-C8KP}LR__xC7a&hxX)yZPd<&DL{B8DwE1elga*PfxGId>3(05~ly8 zP;S1XuHUs+`-yc@EK6Dj^6I_(t?mbwd{~{b23k9dmgcw;;Ak;^5@Sy{KXId=?Yy|+ zn_6A?I(E4HiUL@mTI;Y^FlVOX31`+&EUpqk!$DNHuehQm*S9a)K7Kf((6OkyR}gd; zxiMyLtScTXibfy5HAi{{OS+pM#$o2it4|}lN=@HLN?NI4wL`$wjf{fU;jh^Zh#8|g zJ%2GBcyUXfTE}i5I7AC0ybEUg<~$fBZDgq7%^JB;;vxng-S-B7pRU?k=WJU5TUk-$ zw2M37VQx8-me)1CpF-LA*!g3Q%F^GpKKG?n(J+LOxKNj7bMD&BHdl>5Sh1x~2i~et zVArhMK8lOk_*hv4PXLJ#zfO@zE4%%<G1%d=oEt);cAZTQ?h$K$wuW=JOcz5e0vjdQ zN`{KOrB5QnuAT>rytSB9tEg<_l6k~ol52^jYuL>IN+q-y63~s<yt>E=-2B1aGC5>R zy@AI=3+>#o;gxJo7={1M^1G|2x)77Jq{ahMBei1Rz?A1r{r5#eNQw}dxm;d>rPr%T zN5C`DHY<)5nuHW-S!SKL9Xou0=2jhuPU|Inz6$!p6fO6-lk1fwQ2Lls2TPPTjiR4G zHu)g+(9@aPoHqTmlyUt}+RA7t<jaucT3HJJqD{9NAN>Oiz@OUo;js$wn3J-%=kyv+ zL?plTyGl5)y6kMHpjSVn&zQfke}8)uRfl?x#@+SUfi)$x7>C@iSt@&05p%-9jit{E zq8#WZFy&87BiP{UU51%$D`$B{rLApuj6#n_*)V1c(&Z-hX)}*Da6^;r`kghjQ;Yeo z;k&?L(Vs%5_Z8NV5tPh0US=3kGb;G92A{Cy>oaHgNcP}boG2?ioU*_OSCg_%N_1Ci zTq`Yl>^ObxvsydrX(#N}=7b66#OAwoE(|Pb6{hMl2QYVa2eC9N*WQuvwrbV-vYM$i zM~(VJnQG#0Ah(_U^cqrR{YYh3s5r1yMFH`25JuiuUMO5>$!pT`<eAy?t*;Vd6j>l~ zea$<hS1vs8Cy=`K6$)-!SNo@~Bu`Y4UQ;F#GOAn5i>;>Cjz5g=`R0w{TsbKVEqB;k zbvRwFJR63PXJTdCR*gYRmuKtp{)||Zqpi9dShI8Fffg*0E?GNqOujD^@e-{?iu$f% zTBaA|>&bHxMb}=%BxEjPJ*JX#StKH=OePWQEB%LYNhWJrU}sWrN}*sHO@y$8V@6z_ zN<Y%DL;3+8H7lWJQl-(5YLL#ffG8}dO1odX2)6c(uu7iEgz7zWNf3~5dUNCK4|y@r zIhonI`vBxX@Ya76yZPQ(?dW-CAr&Lb=%0W}>M+;Tk$p3}BWJ6q*n#Ezv-s*_(OuvI zT^XmggLv|UJui@eM}4t_!}$#eOS#mHeEAs}kGHqCC0rN6GqbYLoR8;h-V9kkh_N`5 zMmODgM3w$Ib8~bV)jXMz><>rwW}JV`u47_?e*Ert?%TJYeE<HPikiA2&h0XZ)d65^ z05Cp8>>Ab=cni=1);j<VlY&d6lJtc?+3G^8yc7$|H;t;xjE+OclvJW~J9H#gs79;X zQOA7`=Bt+W%8c7voK=){L{jCbK^M=>dPkm;K=_#c_Rm1|KU7I5ZqffxC8>#IdL;j1 z1A=yM;lh8`_<g1PBXb>1e1TVMWYsLA)6)2bz>;~PtPw1XjZN&;tAyjm7byXh&BrNh zZi)LTF3UgCLIJH*{iS8Gl96bU9(P}1B?gdBG{x=J$?;KAn#u|$M!C(^7hu20Zrn+w z#WtMM1e*A-0evQpmJ-Xtt>({mJ#hP7t!mpRKDh-}+NZz?<`XiC_vBiyHpFxp=ZDL) zrz|tX>v*5cSUHU86c1%}RNhfn77`LZIeGaJ=&cl1uWtYtxKqxE4?QW_i~Smqnp=Vj z^Nf<~amB3N86c`cKv)L!N9gJ8*%t$T@ff-@QkBvG0^8qSOV@r$Yy5mG`hOE4;eRrt z_KWfi49kBY<@Nv7aGh!q{r7d*ZW$cTAJO_8{BUNIbXMHH^1f^O#&!1G>#e^p@0Yc@ z_jSF%Pev4!l;G1JzoWZ;J>$oZS3yBRhN==D02>6vz@`CrI|G_}4B24u5d!${e<zDk z-J(hZU7Pecq4-1DWY9^3mh>q7D_SiV!C!ac$lF(ZPO@}P8HVkbIf{*%g@<56(vARf z(R3Yqa7xTcFZ#IXujF*<z<(1VdYGVbH}O2S(x?=dz<)XC+gML80pJ*t{Sp@2Au@n? z8Bp#4AoyoMCO}(^J}zd2wLAcUE?Ys}$su>CQvAcbrF+TdsT=?UQVz0_1n`Pp_X7z* zwKnoVtJzK9x`Cf`Ly3KMa$~~c1<>oAYuE$6)0SibEny@<Do;fR{(A1=*sY`NALDJh z<VOb@xZXx?K6S<=u37IH1;|z2L*##nxB1I|{>#SUGc;x~PMLJA4?Olk9P1HY9qf{; z0v2D43>y&*jQJfz<DBpe@gl7{O{zKkP3bu^*g-YCha+Y(KZj^KVj7SAFddNLOvwt2 zZBb)N%jUhxGU4T=&(p(F^_63kZ^|iP=g0_Z3hyIp)elbAFzVW=0n7M}pENL;S@CZ{ zr{rp>`PDOMGwu8hhI%)M)qD2i6)uaL8Qp+*2{w=v-CVsAS-8b5w!i(EkN2qGbzjl6 z%C3~DNQpshv_dtHc)udJa0h$A4ZG{Qoi){2jO2G=dw|pMtb{hMX3{EcFI5EVTB`xP z#i74Hb>Qqz1%RjXJf+Kv_it~%Qxe*4pf5nBiBH_r-rnnYaHOFa3!dS_xjI$pyq7e} z?F`t9NM(8S1S^fqaSyYy>#qz!`4Hs$>Up3<(SOsLpm-9c*sBjmX<C2;%T|ubxL+3v z5c7HetI(j$HJ$UPcE`0*rN!_QN#X5%9%gIdFwNE85R_B6!-*=CP^=ewvaaTE_(1#W z3A`|urozf6^k#<mZsch38^sqjzeF71beb~%5OH|O<DSVtQ-ZTNIgZ9iZoUNgg^y1; z(?7YZygxMtg4_O0x&gR8^`Y|B6RV8lKLzlp@7Nb>Q-=2uEP0Zhh82f(`+8DM0w%8a zy%=3CgeR8-Zy=(JR@ILDc9r3dpnp<`*k64CzXt+kg#62M<fmMcj>$bk`|em<H^qp! zl8FpnUz+^cjWu1Ulgn(vPFftRMb`W$D|fwBeQUcEtyfcrTh*`i%p#M8bz~B+XAEWP zS4FK5L9ef!vWjl)UB|wO`zaQob6e4-<Eqg14%QJ*5K}DJ#0FQ|OcmApA1hA9))}(Q z*yjmhK9JYdIqxeaX*}}gnW{C-YMB?Zhdk%qey#@(Out`PYL@ZdH&4r-4Rme)Uo1iQ zeWv0}egk6;acnZkhkwKI=*6=9lw_k~q||XTLz7!Ctkm5JT2t(@UTYQ0er$*PHgFl< zY<9;de}Dh45M8rV_~K^;5+K_MIUVzF+RWcHClR=+A7yS*{!Nvf8&{A<VX#+iHk;48 z-D_dZ*oW=Yf<|Paqz@q|`#z^DBcnhu%B@qP(Wy#e(oby$jYlL@+;oNME}fpCke~cz z=xW!+<q99q5x0{LyA?N!6uIE-l>>Zn$!1~mac+cBMFX~Bf8Qw0umbq4^S`1jiS<X= zPZxV(5Pl;wr>Vskie=luMgIs9+x}AGv%G&j6cFv8O_MN@x#^*<s!wm1*Q3$;d0V8A z3ygEczf3|Em2%)M-uQ)fW$o`<NTR&~&ivWGw{uV7l*`S2J*y$`k(R4&R!)y7kz;X@ zST>0sx49gN?j4kwK0%4%6>X;uljtp*T1(#$s(#Ek4jZ%KJX_MTHl&Z=rcO{IcA6=< z)exingO&=zn#GjZj)<X+W`SuR9U`i*^a4z0+a1uGJSDpS6vq51swb<#Bw#`epg^UL z+=FNNiw3Fr*+wineta1WK=~XX_FMTE?fNgmr>eti4Q2XMi)0=!=Y5SCft`ez)Gv}o zE0;Lqh5uE~z|jB5moTw@t3tJRg(wC;w#45Bto-f2S^4|_P~X6RYW(eJ)uXi@Jl#I3 zKRNCHA;YI{V31IiZ2{QbwMHN)umFAe83EP+3UMXBpx_WQ5%Pan&Dulx_rc46>YqqZ za~p(zfdY^lrT#T$oz_Y{48JM*bR*CWgL-0Sp2|o*1Pa>Of5x;6AU8Tv<HqgOV20C` z@8+pSVJR}O3#aTK{@)L&r}_Y(R*^fw+*W(f=^OV;BK*9fpV;sVok|Jxl3)DOBc1@O z#A((E5OobM`5)b@m{-5B*8|}4rz`fKEB-}{0$;cDCqQk!b_mSXZ>Mv`2J}2bj37SF u$i!reaAyZLwf`AqQ_z1-q}Ac+iOdPp$wK}WVZqaQuB@OgU-IPn+y4gCYNmex From aab708eb27b0a3809685d7d3e1057ac0f9ea8b54 Mon Sep 17 00:00:00 2001 From: parmar-abhinav <abhinav.parmar@infosys.com> Date: Sun, 21 Jul 2024 10:14:29 +0530 Subject: [PATCH 183/212] Added test cases for gitlab:issue:edit examples Signed-off-by: parmar-abhinav <abhinav.parmar@infosys.com> --- .changeset/plenty-tools-exist.md | 5 + .../actions/gitlabIssueEdit.examples.test.ts | 817 ++++++++++++++++++ .../src/actions/gitlabIssueEdit.examples.ts | 177 +++- 3 files changed, 997 insertions(+), 2 deletions(-) create mode 100644 .changeset/plenty-tools-exist.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts diff --git a/.changeset/plenty-tools-exist.md b/.changeset/plenty-tools-exist.md new file mode 100644 index 0000000000..0ce22a74f2 --- /dev/null +++ b/.changeset/plenty-tools-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:issue:edit examples diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts new file mode 100644 index 0000000000..7e50eac66c --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts @@ -0,0 +1,817 @@ +/* + * Copyright 2024 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. + */ + +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { editGitlabIssueAction } from './gitlabIssueEdit'; +import { examples } from './gitlabIssueEdit.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + Issues: { + edit: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:issue:edit', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(1999, 6, 14, 12, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'myIntegrationsToken', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = editGitlabIssueAction({ integrations }); + + it(`Should ${examples[0].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[1].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: false, + discussionLocked: true, + epicId: undefined, + labels: 'phase1:label1,phase2:label2', + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: false, + discussionLocked: true, + epicId: undefined, + labels: 'phase1:label1,phase2:label2', + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[3].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: 'Computer banks to rule the world', + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: 'close', + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[4].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: 'Computer banks to rule the world', + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: 'reopen', + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[5].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 20], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[6].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[7].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: input.confidential, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[8].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: input.discussionLocked, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[9].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + it(`Should ${examples[10].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + it(`Should ${examples[11].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: input.issueType, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts index 861c0736f0..8686a66250 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts @@ -50,7 +50,7 @@ export const examples: TemplateExample[] = [ title: 'Test Issue', assignees: [18], description: 'This is the edited description of the issue', - updatedAt: '2024-05-10 18:00:00.000', + updatedAt: '2024-05-10T18:00:00.000Z', dueDate: '2024-09-28', }, }, @@ -72,7 +72,7 @@ export const examples: TemplateExample[] = [ assignees: [18, 15], description: 'This is the description of the issue', confidential: false, - updatedAt: '2024-05-10 18:00:00.000', + updatedAt: '2024-05-10T18:00:00.000Z', dueDate: '2024-09-28', discussionLocked: true, epicId: 1, @@ -82,4 +82,177 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Edit a gitlab issue to change its state to close', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + stateEvent: 'close', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to change its state to reopened', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + stateEvent: 'reopen', + }, + }, + ], + }), + }, + { + description: + 'Edit a gitlab issue to assign it to multiple users and set milestone', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test issue with milestone', + assignees: [18, 20], + description: 'This issue has milestone set', + milestoneId: 5, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to add weight and update labels', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with weight and labels', + description: 'This issue has weight and new labels', + weight: 3, + labels: 'bug,urgent', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to make it confidential', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Issue', + description: 'This issue is confidential', + confidential: true, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to lock the discussion', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Locked Discussion Issue', + description: 'This discussion on this issue is locked', + discussionLocked: true, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to remove labels and update milestone', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with labels removed and milestone updated', + description: 'This issue has labels removed and milestone updated', + removeLabels: 'phase1:label1', + milestoneId: 6, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to remove some labels and new ones', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with labels updated', + description: 'This issue has labels removed and new ones added', + removeLabels: 'bug,urgent', + labels: 'enhancement:documentation', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to change issue type and add labels', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with type and labels', + description: 'This issue has been changes and new labels added', + labels: 'task,high-priority', + issueType: 'task', + }, + }, + ], + }), + }, ]; From 1ba4c2f6da8412660a236fa3588c4fa129cdf92e Mon Sep 17 00:00:00 2001 From: parmar-abhinav <abhinav.parmar@infosys.com> Date: Sun, 21 Jul 2024 10:59:49 +0530 Subject: [PATCH 184/212] Added test cases for gitlab:pipeline:trigger examples Signed-off-by: parmar-abhinav <abhinav.parmar@infosys.com> --- .changeset/strange-papayas-beg.md | 5 + .../gitlabPipelineTrigger.examples.test.ts | 320 ++++++++++++++++++ .../actions/gitlabPipelineTrigger.examples.ts | 63 ++++ 3 files changed, 388 insertions(+) create mode 100644 .changeset/strange-papayas-beg.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts diff --git a/.changeset/strange-papayas-beg.md b/.changeset/strange-papayas-beg.md new file mode 100644 index 0000000000..6ce33fb029 --- /dev/null +++ b/.changeset/strange-papayas-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:pipeline:trigger examples diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts new file mode 100644 index 0000000000..53cbfb0b87 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts @@ -0,0 +1,320 @@ +/* + * Copyright 2024 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. + */ + +import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createTriggerGitlabPipelineAction } from './gitlabPipelineTrigger'; +import { examples } from './gitlabPipelineTrigger.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + PipelineTriggerTokens: { + create: jest.fn(), + trigger: jest.fn(), + remove: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:pipeline:trigger', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(1988, 5, 3, 12, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'glpat-abcdef', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createTriggerGitlabPipelineAction({ integrations }); + + it(`should ${examples[0].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + + it(`should ${examples[1].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + it(`should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + it(`should ${examples[3].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts index d0840030df..f213fcd83f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts @@ -39,4 +39,67 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Trigger a GitLab Project Pipeline with No Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: {}, + }, + }, + ], + }), + }, + { + description: 'Trigger a GitLab Project Pipeline with Single Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: { var_one: 'one' }, + }, + }, + ], + }), + }, + { + description: 'Trigger a GitLab Project Pipeline with Multiple Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: { var_one: 'one', var_two: 'two', var_three: 'three' }, + }, + }, + ], + }), + }, ]; From e53074fb0ec07d93025beefa783f52de2ce21b4c Mon Sep 17 00:00:00 2001 From: Stephen Glass <stephen@stephen.glass> Date: Sun, 21 Jul 2024 01:44:13 -0400 Subject: [PATCH 185/212] Update default backend plugin to use RootConfigService Signed-off-by: Stephen Glass <stephen@stephen.glass> --- .changeset/heavy-numbers-love.md | 5 +++++ .../cli/templates/default-backend-plugin/package.json.hbs | 1 - .../templates/default-backend-plugin/src/service/router.ts | 5 ++--- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 .changeset/heavy-numbers-love.md diff --git a/.changeset/heavy-numbers-love.md b/.changeset/heavy-numbers-love.md new file mode 100644 index 0000000000..c18d144636 --- /dev/null +++ b/.changeset/heavy-numbers-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated default backend plugin to use `RootConfigService` instead of `Config`. This also removes the dependency on `@backstage/config` as it's no longer used. diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 3b808fc5e5..8dd31cbc38 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -31,7 +31,6 @@ "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", "@backstage/backend-defaults": "{{versionQuery '@backstage/backend-defaults'}}", "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}", - "@backstage/config": "{{versionQuery '@backstage/config'}}", "express": "{{versionQuery 'express' '4.17.1'}}", "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", "node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}" diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.ts b/packages/cli/templates/default-backend-plugin/src/service/router.ts index 312d7e252e..ced654aa82 100644 --- a/packages/cli/templates/default-backend-plugin/src/service/router.ts +++ b/packages/cli/templates/default-backend-plugin/src/service/router.ts @@ -1,12 +1,11 @@ import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; -import { LoggerService } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; +import { LoggerService, RootConfigService } from '@backstage/backend-plugin-api'; import express from 'express'; import Router from 'express-promise-router'; export interface RouterOptions { logger: LoggerService; - config: Config; + config: RootConfigService; } export async function createRouter( From ef742dc56e889f254983cd1a0575c0880a104aa0 Mon Sep 17 00:00:00 2001 From: parmar-abhinav <abhinav.parmar@infosys.com> Date: Sun, 21 Jul 2024 12:29:54 +0530 Subject: [PATCH 186/212] Added test cases for gitlab:projectAccessToken:create example Signed-off-by: parmar-abhinav <abhinav.parmar@infosys.com> --- .changeset/six-rats-kick.md | 5 + ...bProjectAccessTokenCreate.examples.test.ts | 256 ++++++++++++++++++ ...gitlabProjectAccessTokenCreate.examples.ts | 158 +++++++++++ .../gitlabProjectAccessTokenCreate.test.ts | 221 +++++++++++++++ 4 files changed, 640 insertions(+) create mode 100644 .changeset/six-rats-kick.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts diff --git a/.changeset/six-rats-kick.md b/.changeset/six-rats-kick.md new file mode 100644 index 0000000000..23958a2502 --- /dev/null +++ b/.changeset/six-rats-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:projectAccessToken:create example diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts index 811926cc82..0762212809 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts @@ -51,6 +51,10 @@ describe('gitlab:projectAccessToken:create examples', () => { host: 'hosted.gitlab.com', apiBaseUrl: 'https://api.hosted.gitlab.com', }, + { + host: 'gitlab.example.com', + apiBaseUrl: 'https://api.gitlab.example.com', + }, ], }, }); @@ -192,4 +196,256 @@ describe('gitlab:projectAccessToken:create examples', () => { expect(mockContext.output).toHaveBeenCalledWith('access_token', 'TOKEN'); }); + + it(`should ${examples[5].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[5].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '456', + 'tokenname', + ['read_repository'], + { + accessLevel: 30, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[6].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[6].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '456', + 'full-access-token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2024-12-31', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[7].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[7].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[8].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[8].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository', 'read_api'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[9].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[9].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 10, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[10].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[10].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[11].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[11].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 50, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[12].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[12].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'no-expiry-token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[13].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[13].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts index 8d4f335a46..89f8ae6269 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts @@ -102,4 +102,162 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitLab project access token with an access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '456', + accessLevel: 30, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with multiple options', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '456', + accessLevel: 40, + name: 'full-access-token', + expiresAt: '2024-12-31', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with a token for authorization', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + token: 'personal-access-token', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with read-only scopes', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + scopes: ['read_repository', 'read_api'], + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with guest access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 10, + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with maintainer access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 40, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with owner access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 50, + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with a specified name and no expiration date', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + name: 'no-expiry-token', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token for a specific gitlab instance', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.example.com?repo=repo&owner=owner', + projectId: '101112', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts new file mode 100644 index 0000000000..34ce8878f2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts @@ -0,0 +1,221 @@ +/* + * Copyright 2024 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. + */ +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createGitlabProjectAccessTokenAction } from './gitlabProjectAccessTokenCreate'; // Adjust the import based on your project structure + +import { DateTime } from 'luxon'; + +jest.mock('node-fetch'); + +const mockGitlabClient = { + ProjectAccessTokens: { + create: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:projectAccessToken:create examples', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'gitlab-token', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createGitlabProjectAccessTokenAction({ integrations }); + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + }, + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should create a GitLab project access token with minimal options.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '987', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '987', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with custom scopes.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '987', + scopes: ['read_registry', 'write_repository'], + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '987', + 'tokenname', + ['read_registry', 'write_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a specified name.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '2110', + name: 'token', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '2110', + 'token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a numeric project ID.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 23, + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + 23, + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a specified expired Date.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '123', + expiresAt: '1999-07-14', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '123', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '1999-07-14', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); +}); From fba7537ad1fd83d9578d71ea546b36b3cf1be9f3 Mon Sep 17 00:00:00 2001 From: Jordan Slott <jordan.slott@twosigma.com> Date: Sun, 21 Jul 2024 15:31:35 -0400 Subject: [PATCH 187/212] Fixes #25709 Memoize filtered entity graph nodes Signed-off-by: Jordan Slott <jordan.slott@twosigma.com> --- .changeset/light-pianos-exercise.md | 5 +++++ .../EntityRelationsGraph/useEntityRelationGraph.ts | 12 ++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .changeset/light-pianos-exercise.md diff --git a/.changeset/light-pianos-exercise.md b/.changeset/light-pianos-exercise.md new file mode 100644 index 0000000000..4bf161eb06 --- /dev/null +++ b/.changeset/light-pianos-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Memoize entity graph nodes when applying an `entityFilter` to prevent repeated redraws diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index af2a491ada..07e3085f1f 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useEntityStore } from './useEntityStore'; import { pickBy } from 'lodash'; @@ -104,9 +104,13 @@ export function useEntityRelationGraph({ requestEntities, ]); - const filteredEntities = entityFilter - ? pickBy(entities, (value, _key) => entityFilter(value)) - : entities; + const filteredEntities = useMemo( + () => + entityFilter + ? pickBy(entities, (value, _key) => entityFilter(value)) + : entities, + [entities, entityFilter], + ); return { entities: filteredEntities, From 4e79d199ccb03c1b57dd4b889b83c058b13fd399 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Wed, 17 Jul 2024 19:59:42 +0200 Subject: [PATCH 188/212] backend-defaults,test-utils: use, export, and test health service Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/gentle-dryers-smile.md | 5 ++ .changeset/soft-gorillas-refuse.md | 5 ++ .../api-report-rootHttpRouter.md | 8 ++ .../rootHttpRouter/createHealthRouter.ts | 6 +- .../src/entrypoints/rootHttpRouter/index.ts | 1 + .../rootHttpRouterServiceFactory.test.ts | 74 +++++++++++++++++++ .../src/next/wiring/TestBackend.test.ts | 9 +++ .../src/next/wiring/TestBackend.ts | 7 +- 8 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 .changeset/gentle-dryers-smile.md create mode 100644 .changeset/soft-gorillas-refuse.md create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts diff --git a/.changeset/gentle-dryers-smile.md b/.changeset/gentle-dryers-smile.md new file mode 100644 index 0000000000..b061461da1 --- /dev/null +++ b/.changeset/gentle-dryers-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +The `createHealthRouter` utility that allows you to create a health check router is now exported via `@backstage/backend-defaults/rootHttpRouter`. diff --git a/.changeset/soft-gorillas-refuse.md b/.changeset/soft-gorillas-refuse.md new file mode 100644 index 0000000000..323c4d20d2 --- /dev/null +++ b/.changeset/soft-gorillas-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +The default services for `startTestBackend` and `ServiceFactoryTester` now includes the Root Health Service. diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index 329398a7b8..36f3aa3ab6 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// <reference types="express" /> /// <reference types="node" /> import { Config } from '@backstage/config'; @@ -17,10 +18,17 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; import { RequestListener } from 'http'; import { RootConfigService } from '@backstage/backend-plugin-api'; +import { RootHealthService } from '@backstage/backend-plugin-api'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; +import { Router } from 'express'; import type { Server } from 'node:http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +// @public (undocumented) +export function createHealthRouter(options: { + health: RootHealthService; +}): Router; + // @public export function createHttpServer( listener: RequestListener, diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts index 007d1dd9cd..3772ab0fab 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts @@ -1,5 +1,3 @@ -import { RootHealthService } from '@backstage/backend-plugin-api'; - /* * Copyright 2024 The Backstage Authors * @@ -16,9 +14,13 @@ import { RootHealthService } from '@backstage/backend-plugin-api'; * limitations under the License. */ +import { RootHealthService } from '@backstage/backend-plugin-api'; import Router from 'express-promise-router'; import { Request, Response } from 'express'; +/** + * @public + */ export function createHealthRouter(options: { health: RootHealthService }) { const router = Router(); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts index 9c2d96dab2..e9aee8a0e0 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts @@ -18,6 +18,7 @@ export { DefaultRootHttpRouter, type DefaultRootHttpRouterOptions, } from './DefaultRootHttpRouter'; +export { createHealthRouter } from './createHealthRouter'; export * from './http'; export { rootHttpRouterServiceFactory, diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts new file mode 100644 index 0000000000..dea0159dff --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2022 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. + */ + +import { + ServiceFactoryTester, + mockServices, +} from '@backstage/backend-test-utils'; +import { Express } from 'express'; +import request from 'supertest'; +import { rootHttpRouterServiceFactory } from './rootHttpRouterServiceFactory'; +import { coreServices } from '@backstage/backend-plugin-api'; + +describe('rootHttpRouterServiceFactory', () => { + it('should make the health endpoints available', async () => { + let app: Express | undefined = undefined; + + const tester = ServiceFactoryTester.from( + rootHttpRouterServiceFactory({ + configure(options) { + options.applyDefaults(); + app = options.app; + }, + }), + { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + app: { baseUrl: 'http://localhost' }, + backend: { + baseUrl: 'http://localhost', + listen: { host: '', port: 0 }, + }, + }, + }), + ], + }, + ); + + // Trigger creation of the http service, accessing the app instance through the configure callback + await tester.getSubject(); + + await request(app!) + .get('/.backstage/health/v1/liveness') + .expect(200, { status: 'ok' }); + + await request(app!).get('/.backstage/health/v1/readiness').expect(503, { + message: 'Backend has not started yet', + status: 'error', + }); + + const lifecycle = await tester.getService(coreServices.rootLifecycle); + + await (lifecycle as any).startup(); // Trigger startup by calling the private startup method + + await request(app!).get('/.backstage/health/v1/readiness').expect(200, { + status: 'ok', + }); + + expect('test').toBe('test'); + }); +}); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index ecba34290d..a01d4bebf5 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -242,6 +242,15 @@ describe('TestBackend', () => { expect(res.body).toEqual({ message: 'pong' }); }); + it('should expose health check endpoints', async () => { + const { server } = await startTestBackend({ features: [] }); + + const res = await request(server).get('/.backstage/health/v1/liveness'); + + expect(res.status).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + }); + it('should provide extension point implementations', async () => { expect.assertions(3); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 16ee6b78d2..3d662a8c12 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -37,6 +37,7 @@ import express from 'express'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter'; /** @public */ export interface TestBackendOptions<TExtensionPoints extends any[]> { @@ -73,6 +74,7 @@ export const defaultServiceFactories = [ mockServices.lifecycle.factory(), mockServices.logger.factory(), mockServices.permissions.factory(), + mockServices.rootHealth.factory(), mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), mockServices.scheduler.factory(), @@ -251,15 +253,18 @@ export async function startTestBackend<TExtensionPoints extends any[]>( config: coreServices.rootConfig, lifecycle: coreServices.rootLifecycle, rootLogger: coreServices.rootLogger, + health: coreServices.rootHealth, }, - async factory({ config, lifecycle, rootLogger }) { + async factory({ config, lifecycle, rootLogger, health }) { const router = DefaultRootHttpRouter.create(); const logger = rootLogger.child({ service: 'rootHttpRouter' }); const app = express(); const middleware = MiddlewareFactory.create({ config, logger }); + const healthRouter = createHealthRouter({ health }); + app.use(healthRouter); app.use(router.handler()); app.use(middleware.notFound()); app.use(middleware.error()); From 31bfc4417f7a4d04d5899ce1d638505c30f9f627 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Sat, 20 Jul 2024 12:47:09 +0200 Subject: [PATCH 189/212] frontend-plugin-api: update extension data ref declaration to allow embedding of ID in type Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/dull-ghosts-double.md | 7 ++ .changeset/tiny-oranges-pretend.md | 19 ++++++ .../architecture/03-extensions.md | 4 +- .../architecture/08-naming-patterns.md | 20 +++--- .../src/tree/instantiateAppNodeTree.test.ts | 8 ++- packages/frontend-plugin-api/api-report.md | 52 +++++++++++--- .../src/extensions/createApiExtension.ts | 5 +- .../createAppRootWrapperExtension.tsx | 7 +- .../extensions/createComponentExtension.tsx | 2 +- .../src/extensions/createNavItemExtension.tsx | 2 +- .../src/extensions/createNavLogoExtension.tsx | 2 +- .../src/extensions/createRouterExtension.tsx | 7 +- .../extensions/createSignInPageExtension.tsx | 2 +- .../src/extensions/createThemeExtension.ts | 5 +- .../extensions/createTranslationExtension.ts | 2 +- .../src/wiring/coreExtensionData.ts | 8 ++- .../src/wiring/createExtension.test.ts | 2 +- .../src/wiring/createExtension.ts | 2 +- .../src/wiring/createExtensionDataRef.test.ts | 9 +++ .../src/wiring/createExtensionDataRef.ts | 68 +++++++++++++------ .../src/wiring/createPlugin.test.ts | 4 +- plugins/catalog-react/api-report-alpha.md | 13 +++- plugins/catalog-react/src/alpha.tsx | 18 ++--- plugins/home/api-report-alpha.md | 6 +- plugins/home/src/alpha.tsx | 4 +- plugins/search-react/api-report-alpha.md | 1 + plugins/search-react/src/alpha.tsx | 2 +- 27 files changed, 204 insertions(+), 77 deletions(-) create mode 100644 .changeset/dull-ghosts-double.md create mode 100644 .changeset/tiny-oranges-pretend.md diff --git a/.changeset/dull-ghosts-double.md b/.changeset/dull-ghosts-double.md new file mode 100644 index 0000000000..0bc5a09791 --- /dev/null +++ b/.changeset/dull-ghosts-double.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-home': patch +--- + +Updated alpha definitions of extension data references. diff --git a/.changeset/tiny-oranges-pretend.md b/.changeset/tiny-oranges-pretend.md new file mode 100644 index 0000000000..87227fb962 --- /dev/null +++ b/.changeset/tiny-oranges-pretend.md @@ -0,0 +1,19 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Extension data references can now be defined in a way that encapsulates the ID string in the type, in addition to the data type itself. The old way of creating extension data references is deprecated and will be removed in a future release. + +For example, the following code: + +```ts +export const myExtension = createExtensionDataRef<MyType>('my-plugin.my-data'); +``` + +Should be updated to the following: + +```ts +export const myExtension = createExtensionDataRef<MyType>().with({ + id: 'my-plugin.my-data', +}); +``` diff --git a/docs/frontend-system/architecture/03-extensions.md b/docs/frontend-system/architecture/03-extensions.md index cdc73a17ec..2f3daba6f8 100644 --- a/docs/frontend-system/architecture/03-extensions.md +++ b/docs/frontend-system/architecture/03-extensions.md @@ -91,7 +91,9 @@ To create a new extension data reference to represent a type of shared extension ```ts export const reactElementExtensionDataRef = - createExtensionDataRef<React.JSX.Element>('my-plugin.reactElement'); + createExtensionDataRef<React.JSX.Element>().with({ + id: 'my-plugin.reactElement', + }); ``` The `ExtensionDataRef` can then be used to describe an output property of the extension. This will enforce typing on the return value of the extension factory: diff --git a/docs/frontend-system/architecture/08-naming-patterns.md b/docs/frontend-system/architecture/08-naming-patterns.md index a17525f3ce..f031b80d84 100644 --- a/docs/frontend-system/architecture/08-naming-patterns.md +++ b/docs/frontend-system/architecture/08-naming-patterns.md @@ -98,9 +98,9 @@ export interface SearchResultItemExtensionData { } export const searchResultItemExtensionDataRef = - createExtensionDataRef<SearchResultItemExtensionData>( - 'search.search-result-item', - ); + createExtensionDataRef<SearchResultItemExtensionData>().with({ + id: 'search.search-result-item', + }); ``` #### Grouped Extension Data @@ -109,8 +109,12 @@ This way of defining extension data is similar to the standalone way, but it use ```ts export const coreExtensionData = { - reactElement: createExtensionDataRef<ReactElement>('core.react-element'), - routePath: createExtensionDataRef<string>('core.route-path'), + reactElement: createExtensionDataRef<ReactElement>().with({ + id: 'core.react-element', + }), + routePath: createExtensionDataRef<string>().with({ + id: 'core.route-path', + }), }; ``` @@ -125,9 +129,9 @@ export function createGraphiQLEndpointExtension(options) { // Use a TypeScript namespace to merge the extension data references with the extension creator export namespace createGraphiQLEndpointExtension { - export const endpointDataRef = createExtensionDataRef</* ... */>( - 'graphiql.graphiql-endpoint.endpoint', - ); + export const endpointDataRef = createExtensionDataRef</* ... */>().with({ + id: 'graphiql.graphiql-endpoint.endpoint', + }); } ``` diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 3330b9add8..c5c66e8319 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -32,9 +32,11 @@ import { resolveAppTree } from './resolveAppTree'; import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; import { withLogCollector } from '@backstage/test-utils'; -const testDataRef = createExtensionDataRef<string>('test'); -const otherDataRef = createExtensionDataRef<number>('other'); -const inputMirrorDataRef = createExtensionDataRef<unknown>('mirror'); +const testDataRef = createExtensionDataRef<string>().with({ id: 'test' }); +const otherDataRef = createExtensionDataRef<number>().with({ id: 'other' }); +const inputMirrorDataRef = createExtensionDataRef<unknown>().with({ + id: 'mirror', +}); const simpleExtension = resolveExtensionDefinition( createExtension({ diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 29c165aaaa..65445b5c4d 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -151,6 +151,7 @@ export { AnyApiRef }; export type AnyExtensionDataMap = { [name in string]: ExtensionDataRef< unknown, + string, { optional?: true; } @@ -317,13 +318,15 @@ export { configApiRef }; // @public (undocumented) export interface ConfigurableExtensionDataRef< + TId extends string, TData, TConfig extends { optional?: true; } = {}, -> extends ExtensionDataRef<TData, TConfig> { +> extends ExtensionDataRef<TData, TId, TConfig> { // (undocumented) optional(): ConfigurableExtensionDataRef< + TId, TData, TData & { optional: true; @@ -347,9 +350,17 @@ export type CoreErrorBoundaryFallbackProps = { // @public (undocumented) export const coreExtensionData: { - reactElement: ConfigurableExtensionDataRef<JSX_2.Element, {}>; - routePath: ConfigurableExtensionDataRef<string, {}>; - routeRef: ConfigurableExtensionDataRef<RouteRef<AnyRouteRefParams>, {}>; + reactElement: ConfigurableExtensionDataRef< + 'core.reactElement', + JSX_2.Element, + {} + >; + routePath: ConfigurableExtensionDataRef<'core.routing.path', string, {}>; + routeRef: ConfigurableExtensionDataRef< + 'core.routing.ref', + RouteRef<AnyRouteRefParams>, + {} + >; }; // @public (undocumented) @@ -385,7 +396,11 @@ export function createApiExtension< // @public (undocumented) export namespace createApiExtension { const // (undocumented) - factoryDataRef: ConfigurableExtensionDataRef<AnyApiFactory, {}>; + factoryDataRef: ConfigurableExtensionDataRef< + 'core.api.factory', + AnyApiFactory, + {} + >; } export { createApiFactory }; @@ -440,6 +455,7 @@ export function createAppRootWrapperExtension< export namespace createAppRootWrapperExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'app.root.wrapper', React_2.ComponentType<{ children?: React_2.ReactNode; }>, @@ -477,6 +493,7 @@ export function createComponentExtension< export namespace createComponentExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'core.component.component', { ref: ComponentRef; impl: ComponentType; @@ -544,10 +561,17 @@ export interface CreateExtensionBlueprintOptions< output: TOutput; } -// @public (undocumented) +// @public @deprecated (undocumented) export function createExtensionDataRef<TData>( id: string, -): ConfigurableExtensionDataRef<TData>; +): ConfigurableExtensionDataRef<string, TData>; + +// @public (undocumented) +export function createExtensionDataRef<TData>(): { + with<TId extends string>(options: { + id: TId; + }): ConfigurableExtensionDataRef<TId, TData>; +}; // @public (undocumented) export function createExtensionInput< @@ -646,6 +670,7 @@ export function createNavItemExtension(options: { export namespace createNavItemExtension { const // (undocumented) targetDataRef: ConfigurableExtensionDataRef< + 'core.nav-item.target', { title: string; icon: IconComponent_2; @@ -667,6 +692,7 @@ export function createNavLogoExtension(options: { export namespace createNavLogoExtension { const // (undocumented) logoElementsDataRef: ConfigurableExtensionDataRef< + 'core.nav-logo.logo-elements', { logoIcon?: JSX.Element | undefined; logoFull?: JSX.Element | undefined; @@ -760,6 +786,7 @@ export function createRouterExtension< export namespace createRouterExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'app.router.wrapper', React_2.ComponentType<{ children?: React_2.ReactNode; }>, @@ -796,6 +823,7 @@ export function createSignInPageExtension< export namespace createSignInPageExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'core.sign-in-page.component', React_2.ComponentType<SignInPageProps>, {} >; @@ -818,7 +846,11 @@ export function createThemeExtension( // @public (undocumented) export namespace createThemeExtension { const // (undocumented) - themeDataRef: ConfigurableExtensionDataRef<AppTheme, {}>; + themeDataRef: ConfigurableExtensionDataRef< + 'core.theme.theme', + AppTheme, + {} + >; } // @public (undocumented) @@ -831,6 +863,7 @@ export function createTranslationExtension(options: { export namespace createTranslationExtension { const // (undocumented) translationDataRef: ConfigurableExtensionDataRef< + 'core.translation.translation', | TranslationResource<string> | TranslationMessages< string, @@ -934,11 +967,12 @@ export interface ExtensionBoundaryProps { // @public (undocumented) export type ExtensionDataRef< TData, + TId extends string = string, TConfig extends { optional?: true; } = {}, > = { - id: string; + id: TId; T: TData; config: TConfig; $$type: '@backstage/ExtensionDataRef'; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index 1680332960..93346f1ec9 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -72,6 +72,7 @@ export function createApiExtension< /** @public */ export namespace createApiExtension { - export const factoryDataRef = - createExtensionDataRef<AnyApiFactory>('core.api.factory'); + export const factoryDataRef = createExtensionDataRef<AnyApiFactory>().with({ + id: 'core.api.factory', + }); } diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx index bbc51806af..ce2eb89f25 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -77,8 +77,7 @@ export function createAppRootWrapperExtension< /** @public */ export namespace createAppRootWrapperExtension { - export const componentDataRef = - createExtensionDataRef<ComponentType<PropsWithChildren<{}>>>( - 'app.root.wrapper', - ); + export const componentDataRef = createExtensionDataRef< + ComponentType<PropsWithChildren<{}>> + >().with({ id: 'app.root.wrapper' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index a1231d8353..a40ee3b069 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -92,5 +92,5 @@ export namespace createComponentExtension { export const componentDataRef = createExtensionDataRef<{ ref: ComponentRef; impl: ComponentType; - }>('core.component.component'); + }>().with({ id: 'core.component.component' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx index 4b92d4dace..2a2c47835a 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -61,5 +61,5 @@ export namespace createNavItemExtension { title: string; icon: IconComponent; routeRef: RouteRef<undefined>; - }>('core.nav-item.target'); + }>().with({ id: 'core.nav-item.target' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx index 3d66b26d6b..1f2419bd02 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx @@ -51,5 +51,5 @@ export namespace createNavLogoExtension { export const logoElementsDataRef = createExtensionDataRef<{ logoIcon?: JSX.Element; logoFull?: JSX.Element; - }>('core.nav-logo.logo-elements'); + }>().with({ id: 'core.nav-logo.logo-elements' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx index 32d61f7e60..12f946e8a7 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -77,8 +77,7 @@ export function createRouterExtension< /** @public */ export namespace createRouterExtension { - export const componentDataRef = - createExtensionDataRef<ComponentType<PropsWithChildren<{}>>>( - 'app.router.wrapper', - ); + export const componentDataRef = createExtensionDataRef< + ComponentType<PropsWithChildren<{}>> + >().with({ id: 'app.router.wrapper' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx index b525cc3a7b..312d30c78c 100644 --- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx @@ -79,5 +79,5 @@ export function createSignInPageExtension< export namespace createSignInPageExtension { export const componentDataRef = createExtensionDataRef< ComponentType<SignInPageProps> - >('core.sign-in-page.component'); + >().with({ id: 'core.sign-in-page.component' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts index cc6dbdabe9..1740f05a5a 100644 --- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -33,6 +33,7 @@ export function createThemeExtension(theme: AppTheme) { /** @public */ export namespace createThemeExtension { - export const themeDataRef = - createExtensionDataRef<AppTheme>('core.theme.theme'); + export const themeDataRef = createExtensionDataRef<AppTheme>().with({ + id: 'core.theme.theme', + }); } diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts index 8497a64b88..9445e32e24 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts @@ -38,5 +38,5 @@ export function createTranslationExtension(options: { export namespace createTranslationExtension { export const translationDataRef = createExtensionDataRef< TranslationResource | TranslationMessages - >('core.translation.translation'); + >().with({ id: 'core.translation.translation' }); } diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index a6ff41f6b0..cb48cb8df3 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -20,7 +20,9 @@ import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ export const coreExtensionData = { - reactElement: createExtensionDataRef<JSX.Element>('core.reactElement'), - routePath: createExtensionDataRef<string>('core.routing.path'), - routeRef: createExtensionDataRef<RouteRef>('core.routing.ref'), + reactElement: createExtensionDataRef<JSX.Element>().with({ + id: 'core.reactElement', + }), + routePath: createExtensionDataRef<string>().with({ id: 'core.routing.path' }), + routeRef: createExtensionDataRef<RouteRef>().with({ id: 'core.routing.ref' }), }; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 42ab21db80..4323737540 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -18,7 +18,7 @@ import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; -const stringData = createExtensionDataRef<string>('string'); +const stringData = createExtensionDataRef<string>().with({ id: 'string' }); function unused(..._any: any[]) {} diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 28dc6a4e29..e87f3a638e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -22,7 +22,7 @@ import { ExtensionInput } from './createExtensionInput'; /** @public */ export type AnyExtensionDataMap = { - [name in string]: ExtensionDataRef<unknown, { optional?: true }>; + [name in string]: ExtensionDataRef<unknown, string, { optional?: true }>; }; /** @public */ diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts index f7a0dc82d0..f979a7def0 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts @@ -18,6 +18,15 @@ import { createExtensionDataRef } from './createExtensionDataRef'; describe('createExtensionDataRef', () => { it('can be created and read', () => { + const ref = createExtensionDataRef().with({ id: 'foo' }); + expect(ref.id).toBe('foo'); + expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}'); + const refOptional = ref.optional(); + expect(refOptional.id).toBe('foo'); + expect(String(refOptional)).toBe('ExtensionDataRef{id=foo,optional=true}'); + }); + + it('can be created and read in the deprecated way', () => { const ref = createExtensionDataRef('foo'); expect(ref.id).toBe('foo'); expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}'); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts index a7f4e4374e..a1c7b462f2 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -17,9 +17,10 @@ /** @public */ export type ExtensionDataRef< TData, + TId extends string = string, TConfig extends { optional?: true } = {}, > = { - id: string; + id: TId; T: TData; config: TConfig; $$type: '@backstage/ExtensionDataRef'; @@ -27,30 +28,59 @@ export type ExtensionDataRef< /** @public */ export interface ConfigurableExtensionDataRef< + TId extends string, TData, TConfig extends { optional?: true } = {}, -> extends ExtensionDataRef<TData, TConfig> { - optional(): ConfigurableExtensionDataRef<TData, TData & { optional: true }>; +> extends ExtensionDataRef<TData, TId, TConfig> { + optional(): ConfigurableExtensionDataRef< + TId, + TData, + TData & { optional: true } + >; } -// TODO: change to options object with ID. -/** @public */ +/** + * @public + * @deprecated Use the following form instead: `createExtensionDataRef<Type>().with({ id: 'core.foo' })` + */ export function createExtensionDataRef<TData>( id: string, -): ConfigurableExtensionDataRef<TData> { +): ConfigurableExtensionDataRef<string, TData>; +/** @public */ +export function createExtensionDataRef<TData>(): { + with<TId extends string>(options: { + id: TId; + }): ConfigurableExtensionDataRef<TId, TData>; +}; +export function createExtensionDataRef<TData>(id?: string): + | ConfigurableExtensionDataRef<string, TData> + | { + with<TId extends string>(options: { + id: TId; + }): ConfigurableExtensionDataRef<TId, TData>; + } { + const createRef = <TId extends string>(refId: TId) => + ({ + id: refId, + $$type: '@backstage/ExtensionDataRef', + config: {}, + optional() { + return { + ...this, + config: { ...this.config, optional: true }, + }; + }, + toString() { + const optional = Boolean(this.config.optional); + return `ExtensionDataRef{id=${refId},optional=${optional}}`; + }, + } as ConfigurableExtensionDataRef<TId, TData, { optional?: true }>); + if (id) { + return createRef(id); + } return { - id, - $$type: '@backstage/ExtensionDataRef', - config: {}, - optional() { - return { - ...this, - config: { ...this.config, optional: true }, - }; + with<TId extends string>(options: { id: TId }) { + return createRef(options.id); }, - toString() { - const optional = Boolean(this.config.optional); - return `ExtensionDataRef{id=${id},optional=${optional}}`; - }, - } as ConfigurableExtensionDataRef<TData, { optional?: true }>; + }; } diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index dab8a9778a..bb9bdc2107 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -27,7 +27,9 @@ import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import { createExtensionInput } from './createExtensionInput'; import { BackstagePlugin } from './types'; -const nameExtensionDataRef = createExtensionDataRef<string>('name'); +const nameExtensionDataRef = createExtensionDataRef<string>().with({ + id: 'name', +}); const Extension1 = createExtension({ name: '1', diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index b82c7ae3e2..d2bfb29458 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -17,12 +17,21 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) export const catalogExtensionData: { - entityContentTitle: ConfigurableExtensionDataRef<string, {}>; + entityContentTitle: ConfigurableExtensionDataRef< + 'catalog.entity-content-title', + string, + {} + >; entityFilterFunction: ConfigurableExtensionDataRef< + 'catalog.entity-filter-function', (entity: Entity) => boolean, {} >; - entityFilterExpression: ConfigurableExtensionDataRef<string, {}>; + entityFilterExpression: ConfigurableExtensionDataRef< + 'catalog.entity-filter-expression', + string, + {} + >; }; // @alpha (undocumented) diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index 85712b358a..e81b8e5ebf 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -36,15 +36,15 @@ export * from './translation'; /** @alpha */ export const catalogExtensionData = { - entityContentTitle: createExtensionDataRef<string>( - 'catalog.entity-content-title', - ), - entityFilterFunction: createExtensionDataRef<(entity: Entity) => boolean>( - 'catalog.entity-filter-function', - ), - entityFilterExpression: createExtensionDataRef<string>( - 'catalog.entity-filter-expression', - ), + entityContentTitle: createExtensionDataRef<string>().with({ + id: 'catalog.entity-content-title', + }), + entityFilterFunction: createExtensionDataRef< + (entity: Entity) => boolean + >().with({ id: 'catalog.entity-filter-function' }), + entityFilterExpression: createExtensionDataRef<string>().with({ + id: 'catalog.entity-filter-expression', + }), }; // TODO: Figure out how to merge with provided config schema diff --git a/plugins/home/api-report-alpha.md b/plugins/home/api-report-alpha.md index 35ce0bb86c..1dd3da96be 100644 --- a/plugins/home/api-report-alpha.md +++ b/plugins/home/api-report-alpha.md @@ -11,7 +11,11 @@ const _default: BackstagePlugin<{}, {}>; export default _default; // @alpha (undocumented) -export const titleExtensionDataRef: ConfigurableExtensionDataRef<string, {}>; +export const titleExtensionDataRef: ConfigurableExtensionDataRef< + 'title', + string, + {} +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index e9ff26c9e1..b13c2a2e0e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -31,7 +31,9 @@ const rootRouteRef = createRouteRef(); /** * @alpha */ -export const titleExtensionDataRef = createExtensionDataRef<string>('title'); +export const titleExtensionDataRef = createExtensionDataRef<string>().with({ + id: 'title', +}); const homePage = createPageExtension({ defaultPath: '/home', diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index 36f5f049c5..aea85d0f81 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -31,6 +31,7 @@ export function createSearchResultListItemExtension< export namespace createSearchResultListItemExtension { const // (undocumented) itemDataRef: ConfigurableExtensionDataRef< + 'search.search-result-list-item.item', { predicate?: SearchResultItemExtensionPredicate | undefined; component: SearchResultItemExtensionComponent; diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index db198fa99d..a22eaf4244 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -137,5 +137,5 @@ export namespace createSearchResultListItemExtension { export const itemDataRef = createExtensionDataRef<{ predicate?: SearchResultItemExtensionPredicate; component: SearchResultItemExtensionComponent; - }>('search.search-result-list-item.item'); + }>().with({ id: 'search.search-result-list-item.item' }); } From 0bfe7efa2284b31592c9966278f6d5ad5850c27a Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Thu, 18 Jul 2024 14:54:24 +0200 Subject: [PATCH 190/212] chore: wip Signed-off-by: blam <ben@blam.sh> Signed-off-by: blam <ben@blam.sh> --- .../frontend-app-api/src/wiring/createApp.tsx | 1 + .../src/extensions/IconBundleBlueprint.ts | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index cf6ea6dab4..ae7283f301 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -171,6 +171,7 @@ export interface CreateAppFeatureLoader { /** @public */ export function createApp(options?: { + /** @deprecated - TODO */ icons?: { [key in string]: IconComponent }; features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi }>; diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts new file mode 100644 index 0000000000..7008dba5a1 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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. + */ +import { IconComponent } from '../icons'; +import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; + +const iconsDataRef = + createExtensionDataRef<{ [key in string]: IconComponent }>('core.icons'); + +/** @public */ +export const IconBundleBlueprint = createExtensionBlueprint({ + kind: 'icon-bundle', + namespace: 'app', + attachTo: { id: 'app', input: 'icons' }, + output: { + icons: iconsDataRef, + }, + factory: (_, params: { icons: { [key in string]: IconComponent } }) => params, +}); From 39f36a944251d056362702622d82eacd6ff54bb6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Mon, 22 Jul 2024 14:06:27 +0200 Subject: [PATCH 191/212] auth-backend-module-microsoft-provider: update scope handling to define required scopes in backend Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .changeset/new-scissors-try.md | 5 ++ docs/auth/microsoft/provider.md | 4 +- .../src/authenticator.ts | 20 ++++-- .../src/module.test.ts | 65 ++++++++++++++++++- 4 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 .changeset/new-scissors-try.md diff --git a/.changeset/new-scissors-try.md b/.changeset/new-scissors-try.md new file mode 100644 index 0000000000..a9e9a45ce7 --- /dev/null +++ b/.changeset/new-scissors-try.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +--- + +Updated the Microsoft authenticator to accurately define required scopes, but to also omit the required and additional scopes when requesting resource scopes. diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index f799eeb627..60104b99b7 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -67,8 +67,6 @@ auth: clientSecret: ${AZURE_CLIENT_SECRET} tenantId: ${AZURE_TENANT_ID} domainHint: ${AZURE_TENANT_ID} - additionalScopes: - - Mail.Send signIn: resolvers: # typically you would pick one of these @@ -86,7 +84,7 @@ The Microsoft provider is a structure with three mandatory configuration keys: Leave blank if your app registration is multi tenant. When specified, this reduces login friction for users with accounts in multiple tenants by automatically filtering away accounts from other tenants. For more details, see [Home Realm Discovery](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/home-realm-discovery-policy) -- `additionalScopes` (optional): List of scopes for the App Registration. The default and mandatory value is ['user.read']. +- `additionalScopes` (optional): List of scopes for the App Registration, to be requested in addition to the required ones. ### Resolvers diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts index 1b6eb87fae..fe7dc8f7e1 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts @@ -21,21 +21,30 @@ import { PassportProfile, } from '@backstage/plugin-auth-node'; import { ExtendedMicrosoftStrategy } from './strategy'; -import { union } from 'lodash'; /** @public */ export const microsoftAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['email', 'openid', 'offline_access', 'user.read'], + transform({ requested, granted, required, additional }) { + // Resources scopes are of the form `<resource>/<scope>`, and are handled + // separately from the normal scopes in the client. When request a + // resource scope we should only include forward the request scope along + // with offline_access. + const hasResourceScope = Array.from(requested).some(s => s.includes('/')); + if (hasResourceScope) { + return [...requested, 'offline_access']; + } + return [...requested, ...granted, ...required, ...additional]; + }, + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); const tenantId = config.getString('tenantId'); const domainHint = config.getOptionalString('domainHint'); - const scope = union( - ['user.read'], - config.getOptionalStringArray('additionalScopes'), - ); const helper = PassportOAuthAuthenticatorHelper.from( new ExtendedMicrosoftStrategy( @@ -44,7 +53,6 @@ export const microsoftAuthenticator = createOAuthAuthenticator({ clientSecret: clientSecret, callbackURL: callbackUrl, tenant: tenantId, - scope: scope, }, ( accessToken: string, diff --git a/plugins/auth-backend-module-microsoft-provider/src/module.test.ts b/plugins/auth-backend-module-microsoft-provider/src/module.test.ts index b50956b9eb..a072b99bde 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/module.test.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/module.test.ts @@ -67,7 +67,7 @@ describe('authModuleMicrosoftProvider', () => { expect(startUrl.pathname).toBe('/my-tenant-id/oauth2/v2.0/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', - scope: 'User.Read.All', + scope: 'email openid offline_access user.read User.Read.All', client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, state: expect.any(String), @@ -97,6 +97,7 @@ describe('authModuleMicrosoftProvider', () => { clientSecret: 'another-client-secret', tenantId: 'another-tenant-id', domainHint: 'somedomain', + additionalScopes: ['some-extra-scope'], }, }, }, @@ -125,7 +126,7 @@ describe('authModuleMicrosoftProvider', () => { expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', - scope: 'user.read', + scope: 'email openid offline_access user.read some-extra-scope', client_id: 'another-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, state: expect.any(String), @@ -137,4 +138,64 @@ describe('authModuleMicrosoftProvider', () => { nonce: decodeURIComponent(nonceCookie.value), }); }); + + it('should not include required scopes for resources', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleMicrosoftProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + microsoft: { + development: { + clientId: 'another-client-id', + clientSecret: 'another-client-secret', + tenantId: 'another-tenant-id', + additionalScopes: ['some-extra-scope'], + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/microsoft/start?env=development&scope=some-resource/some-scope', + ); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('microsoft-nonce', { + domain: 'localhost', + path: '/api/auth/microsoft/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://login.microsoftonline.com'); + expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + scope: 'some-resource/some-scope offline_access', + client_id: 'another-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); }); From 09d1370799f92e7043ae0d1d5ebbd0798ab917d3 Mon Sep 17 00:00:00 2001 From: Calvin Lee <calvin.lee@harness.io> Date: Mon, 22 Jul 2024 22:29:47 +0900 Subject: [PATCH 192/212] Update .changeset/witty-bears-behave.md Co-authored-by: Ben Lambert <ben@blam.sh> Signed-off-by: Calvin Lee <calvin.lee@harness.io> --- .changeset/witty-bears-behave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/witty-bears-behave.md b/.changeset/witty-bears-behave.md index 7514759189..174467b9eb 100644 --- a/.changeset/witty-bears-behave.md +++ b/.changeset/witty-bears-behave.md @@ -2,4 +2,4 @@ '@backstage/integration': patch --- -Updated functions for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls +Updated functions for `getHarnessEditContentsUrl`, `getHarnessFileContentsUrl`, `getHarnessArchiveUrl`, `getHarnessLatestCommitUrl` and `parseHarnessUrl` to handle account and org level urls From dc37210a290aa28ed4e12d4d22a3f4696c693af1 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Mon, 22 Jul 2024 15:38:22 +0200 Subject: [PATCH 193/212] feat: add support for collecting icons using the new extension blueprint Signed-off-by: blam <ben@blam.sh> --- packages/frontend-app-api/src/extensions/App.tsx | 4 ++++ packages/frontend-app-api/src/wiring/createApp.tsx | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/extensions/App.tsx b/packages/frontend-app-api/src/extensions/App.tsx index 82e7600178..7794aceb79 100644 --- a/packages/frontend-app-api/src/extensions/App.tsx +++ b/packages/frontend-app-api/src/extensions/App.tsx @@ -24,6 +24,7 @@ import { createExtensionInput, createThemeExtension, createTranslationExtension, + IconBundleBlueprint, } from '@backstage/frontend-plugin-api'; export const App = createExtension({ @@ -42,6 +43,9 @@ export const App = createExtension({ translations: createExtensionInput({ translation: createTranslationExtension.translationDataRef, }), + icons: createExtensionInput({ + icon: IconBundleBlueprint.dataRefs.icons, + }), root: createExtensionInput( { element: coreExtensionData.reactElement, diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index ae7283f301..4d929b206f 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -25,6 +25,7 @@ import { createThemeExtension, createTranslationExtension, FrontendFeature, + IconBundleBlueprint, iconsApiRef, RouteResolutionApi, routeResolutionApiRef, @@ -247,6 +248,7 @@ export function createApp(options?: { * @public */ export function createSpecializedApp(options?: { + /** @deprecated - TODO */ icons?: { [key in string]: IconComponent }; features?: FrontendFeature[]; config?: ConfigApi; @@ -374,6 +376,11 @@ function createApiHolder( (x): x is typeof createTranslationExtension.translationDataRef.T => !!x, ) ?? []; + const extensionIcons = tree.root.edges.attachments + .get('icons') + ?.map(e => e.instance?.getData(IconBundleBlueprint.dataRefs.icons)) + .reduce((acc, bundle) => ({ ...acc, ...bundle }), {}); + for (const factory of pluginApis) { factoryRegistry.register('default', factory); } @@ -414,7 +421,8 @@ function createApiHolder( factoryRegistry.register('static', { api: iconsApiRef, deps: {}, - factory: () => new DefaultIconsApi({ ...defaultIcons, ...icons }), + factory: () => + new DefaultIconsApi({ ...defaultIcons, ...extensionIcons, ...icons }), }); factoryRegistry.register('static', { From 4a18c942be1d49700540005f9d6a1566d0268f98 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Mon, 22 Jul 2024 15:39:33 +0200 Subject: [PATCH 194/212] feat: support exporting of dataRefs through blueprints Signed-off-by: blam <ben@blam.sh> --- .../src/extensions/IconBundleBlueprint.ts | 10 ++++--- .../src/extensions/index.ts | 1 + .../src/wiring/createExtensionBlueprint.ts | 27 ++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts index 7008dba5a1..2ad1404102 100644 --- a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts @@ -16,8 +16,9 @@ import { IconComponent } from '../icons'; import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; -const iconsDataRef = - createExtensionDataRef<{ [key in string]: IconComponent }>('core.icons'); +const iconsDataRef = createExtensionDataRef<{ + [key in string]: IconComponent; +}>().with({ id: 'core.icons' }); /** @public */ export const IconBundleBlueprint = createExtensionBlueprint({ @@ -27,5 +28,8 @@ export const IconBundleBlueprint = createExtensionBlueprint({ output: { icons: iconsDataRef, }, - factory: (_, params: { icons: { [key in string]: IconComponent } }) => params, + factory: (params: { icons: { [key in string]: IconComponent } }) => params, + dataRefs: { + icons: iconsDataRef, + }, }); diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 562cb728cb..267201db81 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -25,3 +25,4 @@ export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; export { createComponentExtension } from './createComponentExtension'; export { createTranslationExtension } from './createTranslationExtension'; +export { IconBundleBlueprint } from './IconBundleBlueprint'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 6c79017a0a..b504db5f03 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -34,6 +34,7 @@ export interface CreateExtensionBlueprintOptions< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { kind: string; namespace?: string; @@ -50,6 +51,8 @@ export interface CreateExtensionBlueprintOptions< inputs: Expand<ResolvedExtensionInputs<TInputs>>; }, ): Expand<ExtensionDataValues<TOutput>>; + + dataRefs?: TDataRefs; } /** @@ -60,7 +63,10 @@ export interface ExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { + dataRefs: TDataRefs; + make(args: { namespace?: string; name?: string; @@ -97,15 +103,21 @@ class ExtensionBlueprintImpl< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { constructor( private readonly options: CreateExtensionBlueprintOptions< TParams, TInputs, TOutput, - TConfig + TConfig, + TDataRefs >, - ) {} + ) { + this.dataRefs = options.dataRefs!; + } + + dataRefs: TDataRefs; public make(args: { namespace?: string; @@ -184,8 +196,15 @@ export function createExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap = never, >( - options: CreateExtensionBlueprintOptions<TParams, TInputs, TOutput, TConfig>, -): ExtensionBlueprint<TParams, TInputs, TOutput, TConfig> { + options: CreateExtensionBlueprintOptions< + TParams, + TInputs, + TOutput, + TConfig, + TDataRefs + >, +): ExtensionBlueprint<TParams, TInputs, TOutput, TConfig, TDataRefs> { return new ExtensionBlueprintImpl(options); } From 7777b5fb40b96fcefe6a418c0d3d657b3ae6b6a1 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Mon, 22 Jul 2024 15:45:01 +0200 Subject: [PATCH 195/212] chore: added changeset Signed-off-by: blam <ben@blam.sh> --- .changeset/forty-ties-agree.md | 18 ++++++++++++++++++ .changeset/forty-ties-disagree.md | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 .changeset/forty-ties-agree.md create mode 100644 .changeset/forty-ties-disagree.md diff --git a/.changeset/forty-ties-agree.md b/.changeset/forty-ties-agree.md new file mode 100644 index 0000000000..caa45841a2 --- /dev/null +++ b/.changeset/forty-ties-agree.md @@ -0,0 +1,18 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added a new `IconBundleBlueprint` that lets you create icon bundle extensions that can be installed in an App in order to override or add new app icons. + +```tsx +import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; + +const exampleIconBundle = IconBundleBlueprint.make({ + name: 'example-bundle', + params: { + icons: { + user: MyOwnUserIcon, + }, + }, +}); +``` diff --git a/.changeset/forty-ties-disagree.md b/.changeset/forty-ties-disagree.md new file mode 100644 index 0000000000..214097b35d --- /dev/null +++ b/.changeset/forty-ties-disagree.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Support icon overriding with the new `IconBundleBlueprint` API. From be5cb5ea365f09044cc18788e7b5d9eaa6b3689e Mon Sep 17 00:00:00 2001 From: Devin Lauderdale <devinlauderdale2@gmail.com> Date: Mon, 22 Jul 2024 08:58:25 -0500 Subject: [PATCH 196/212] docs: remove unmaintained dora-metrics plugin Signed-off-by: Devin Lauderdale <devinlauderdale2@gmail.com> --- microsite/data/plugins/dora-metrics.yaml | 10 ---------- microsite/static/img/okay.png | Bin 46181 -> 0 bytes 2 files changed, 10 deletions(-) delete mode 100644 microsite/data/plugins/dora-metrics.yaml delete mode 100644 microsite/static/img/okay.png diff --git a/microsite/data/plugins/dora-metrics.yaml b/microsite/data/plugins/dora-metrics.yaml deleted file mode 100644 index 3f59902f46..0000000000 --- a/microsite/data/plugins/dora-metrics.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: DORA Metrics -author: Okay -authorUrl: https://www.okayhq.com -category: Metrics -description: Embed dashboards (like DORA metrics) in your team or service pages from any dev tools (including Github, Gitlab, Jira, Argo, CircleCI, Buildkite, Pagerduty, Rollbar, Sentry, etc). -documentation: https://github.com/OkayHQ/backstage-plugin -iconUrl: /img/okay.png -npmPackageName: '@okayhq/backstage-plugin' -addedDate: '2022-06-16' diff --git a/microsite/static/img/okay.png b/microsite/static/img/okay.png deleted file mode 100644 index 357219d9d2c51e1d9a16f3f7a0c4fc476936d685..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46181 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 From cf9da6dafbe6c2fd038d9b8176a2f28816b76e10 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Mon, 22 Jul 2024 16:10:05 +0200 Subject: [PATCH 197/212] chore: added test for dataRef exporting Signed-off-by: blam <ben@blam.sh> --- .../wiring/createExtensionBlueprint.test.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index c7b077bdbd..87b3cec152 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { coreExtensionData } from './coreExtensionData'; import { createExtensionBlueprint } from './createExtensionBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createExtensionDataRef } from './createExtensionDataRef'; describe('createExtensionBlueprint', () => { it('should allow creation of extension blueprints', () => { @@ -102,4 +103,28 @@ describe('createExtensionBlueprint', () => { const { container } = createExtensionTester(extension).render(); expect(container.querySelector('h2')).toHaveTextContent('Hello, world!'); }); + + it('should allow exporting the dataRefs from the extension blueprint', () => { + const dataRef = createExtensionDataRef<string>().with({ id: 'test.data' }); + + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + dataRefs: { + data: dataRef, + }, + factory(params: { text: string }) { + return { + element: <h1>{params.text}</h1>, + }; + }, + }); + + expect(TestExtensionBlueprint.dataRefs).toEqual({ + data: dataRef, + }); + }); }); From 493feaca4d4af19c4174a531d268f4d6e63c1a9e Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:10:18 +0100 Subject: [PATCH 198/212] cli: allow running with no configuration files Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .changeset/rich-mugs-dress.md | 7 ++++++ packages/cli/src/lib/bundler/config.ts | 6 ++++- packages/cli/src/lib/config.ts | 8 ++++--- packages/config-loader/api-report.md | 1 + .../src/sources/ConfigSources.test.ts | 13 +++++++++- .../src/sources/ConfigSources.ts | 24 +++++++++++++------ 6 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 .changeset/rich-mugs-dress.md diff --git a/.changeset/rich-mugs-dress.md b/.changeset/rich-mugs-dress.md new file mode 100644 index 0000000000..5008e33a70 --- /dev/null +++ b/.changeset/rich-mugs-dress.md @@ -0,0 +1,7 @@ +--- +'@backstage/config-loader': patch +--- + +Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and +`ConfigSources.defaultForTargets`, which results in omission of a ConfigSource +for the default app-config.yaml configuration file if it's not present. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 1f08f74a85..042f74d1f3 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -48,8 +48,12 @@ const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; export function resolveBaseUrl(config: Config): URL { const baseUrl = config.getOptionalString('app.baseUrl'); + try { - return new URL(baseUrl ?? '/', 'http://localhost:3000'); + return new URL( + baseUrl ?? '/', + `http://localhost:${process.env.PORT ?? '3000'}`, + ); } catch (error) { throw new Error(`Invalid app.baseUrl, ${error}`); } diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 0c657b640e..0be20b0185 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -96,11 +96,13 @@ export async function loadCliConfig(options: Options) { }, }); + const configurationLoadedMessage = appConfigs.length + ? `Loaded config from ${appConfigs.map(c => c.context).join(', ')}` + : `No configuration files found, running without config`; + // printing to stderr to not clobber stdout in case the cli command // outputs structured data (e.g. as config:schema does) - process.stderr.write( - `Loaded config from ${appConfigs.map(c => c.context).join(', ')}\n`, - ); + process.stderr.write(`${configurationLoadedMessage}\n`); try { const frontendAppConfigs = schema.process(appConfigs, { diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index bc8f217bbb..835e53351d 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -21,6 +21,7 @@ export type AsyncConfigSourceGenerator = AsyncGenerator< // @public export interface BaseConfigSourcesOptions { + allowMissingDefaultConfig?: boolean; // (undocumented) remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>; // (undocumented) diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index 4b14572666..e19ac1d51f 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -69,13 +69,18 @@ describe('ConfigSources', () => { }); it('should create default sources for targets', () => { + const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockImplementation(path => { + return path === `${root}app-config.yaml`; + }); + expect( mergeSources( ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), ), ).toEqual([{ name: 'FileConfigSource', path: `${root}app-config.yaml` }]); - const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockReturnValue(true); + fsSpy.mockReturnValue(true); + expect( mergeSources( ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), @@ -159,6 +164,10 @@ describe('ConfigSources', () => { }); it('should create a default source', () => { + const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockImplementation(path => { + return path === `${root}app-config.yaml`; + }); + expect( mergeSources( ConfigSources.default({ @@ -184,6 +193,8 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: resolvePath('b.yaml') }, { name: 'EnvConfigSource', env: { HOME: '/' } }, ]); + + fsSpy.mockRestore(); }); it('should merge sources', () => { diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index ecff39da2d..be8892f9a9 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -74,6 +74,11 @@ export interface BaseConfigSourcesOptions { watch?: boolean; rootDir?: string; remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>; + /** + * Allow the default app-config.yaml to be missing, in which case the source + * will not be created. + */ + allowMissingDefaultConfig?: boolean; /** * A custom substitution function that overrides the default one. @@ -177,14 +182,19 @@ export class ConfigSources { if (argSources.length === 0) { const defaultPath = resolvePath(rootDir, 'app-config.yaml'); const localPath = resolvePath(rootDir, 'app-config.local.yaml'); + const alwaysIncludeDefaultConfigSource = + !options.allowMissingDefaultConfig; + + if (alwaysIncludeDefaultConfigSource || fs.pathExistsSync(defaultPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: defaultPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } - argSources.push( - FileConfigSource.create({ - watch: options.watch, - path: defaultPath, - substitutionFunc: options.substitutionFunc, - }), - ); if (fs.pathExistsSync(localPath)) { argSources.push( FileConfigSource.create({ From 0eedec304dbe8e4f8b1e982a1e29b081cbed3ddd Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:18:07 +0100 Subject: [PATCH 199/212] cli: add changeset for module federation local development changes Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .changeset/strong-otters-compete.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-otters-compete.md diff --git a/.changeset/strong-otters-compete.md b/.changeset/strong-otters-compete.md new file mode 100644 index 0000000000..fdcb982b88 --- /dev/null +++ b/.changeset/strong-otters-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add support for dynamic plugins via the EXPERIMENTAL_MODULE_FEDERATION environment variable when running `yarn start`. From 9d661bb3807a534cf023dc84d816cee5d3c81472 Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:28:13 +0100 Subject: [PATCH 200/212] cli: only consider PORT environment variable for module federation remotes Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- packages/cli/src/lib/bundler/config.ts | 43 +++++++++++++++++++------- packages/cli/src/lib/bundler/server.ts | 7 +++-- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 042f74d1f3..42cdae24e3 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { BackendBundlingOptions, BundlingOptions } from './types'; +import { + BackendBundlingOptions, + BundlingOptions, + ModuleFederationOptions, +} from './types'; import { posix as posixPath, resolve as resolvePath, dirname } from 'path'; import chalk from 'chalk'; import webpack, { ProvidePlugin } from 'webpack'; @@ -46,24 +50,32 @@ import { hasReactDomClient } from './hasReactDomClient'; const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; -export function resolveBaseUrl(config: Config): URL { +export function resolveBaseUrl( + config: Config, + moduleFederation?: ModuleFederationOptions, +): URL { const baseUrl = config.getOptionalString('app.baseUrl'); + const defaultBaseUrl = + moduleFederation?.mode === 'remote' + ? `http://localhost:${process.env.PORT ?? '3000'}` + : 'http://localhost:3000'; + try { - return new URL( - baseUrl ?? '/', - `http://localhost:${process.env.PORT ?? '3000'}`, - ); + return new URL(baseUrl ?? '/', defaultBaseUrl); } catch (error) { throw new Error(`Invalid app.baseUrl, ${error}`); } } -export function resolveEndpoint(config: Config): { +export function resolveEndpoint( + config: Config, + moduleFederation?: ModuleFederationOptions, +): { host: string; port: number; } { - const url = resolveBaseUrl(config); + const url = resolveBaseUrl(config, moduleFederation); return { host: config.getOptionalString('app.listen.host') ?? url.hostname, @@ -114,7 +126,13 @@ export async function createConfig( paths: BundlingPaths, options: BundlingOptions, ): Promise<webpack.Configuration> { - const { checksEnabled, isDev, frontendConfig, publicSubPath = '' } = options; + const { + checksEnabled, + isDev, + frontendConfig, + moduleFederation, + publicSubPath = '', + } = options; const { plugins, loaders } = transforms(options); // Any package that is part of the monorepo but outside the monorepo root dir need @@ -122,14 +140,17 @@ export async function createConfig( const { packages } = await getPackages(cliPaths.targetDir); const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir)); - const validBaseUrl = resolveBaseUrl(frontendConfig); + const validBaseUrl = resolveBaseUrl(frontendConfig, moduleFederation); let publicPath = validBaseUrl.pathname.replace(/\/$/, ''); if (publicSubPath) { publicPath = `${publicPath}${publicSubPath}`.replace('//', '/'); } if (isDev) { - const { host, port } = resolveEndpoint(options.frontendConfig); + const { host, port } = resolveEndpoint( + options.frontendConfig, + options.moduleFederation, + ); plugins.push( new ReactRefreshPlugin({ diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 0421f4d019..6b89847fb6 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -130,8 +130,11 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be } const { frontendConfig, fullConfig } = cliConfig; - const url = resolveBaseUrl(frontendConfig); - const { host, port } = resolveEndpoint(frontendConfig); + const url = resolveBaseUrl(frontendConfig, options.moduleFederation); + const { host, port } = resolveEndpoint( + frontendConfig, + options.moduleFederation, + ); const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ config: fullConfig, From 239dffc970b4b040ffdf9aee24f6937c4994f5ba Mon Sep 17 00:00:00 2001 From: MT Lewis <mtlewis@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:06:42 +0100 Subject: [PATCH 201/212] cli: remove usage of deprecated functionality from @backstage/config-loader Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com> --- .changeset/ninety-icons-smile.md | 5 +++ packages/cli/src/lib/config.ts | 76 +++++++++++++++++++------------- 2 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 .changeset/ninety-icons-smile.md diff --git a/.changeset/ninety-icons-smile.md b/.changeset/ninety-icons-smile.md new file mode 100644 index 0000000000..f137879706 --- /dev/null +++ b/.changeset/ninety-icons-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Remove usage of deprecated functionality from @backstage/config-loader diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 0be20b0185..49cce1f1ed 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -14,14 +14,9 @@ * limitations under the License. */ -import { - ConfigTarget, - loadConfig, - loadConfigSchema, -} from '@backstage/config-loader'; +import { ConfigSources, loadConfigSchema } from '@backstage/config-loader'; import { AppConfig, ConfigReader } from '@backstage/config'; import { paths } from './paths'; -import { isValidUrl } from './urls'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from '@backstage/cli-node'; @@ -37,13 +32,6 @@ type Options = { }; export async function loadCliConfig(options: Options) { - const configTargets: ConfigTarget[] = []; - options.args.forEach(arg => { - if (!isValidUrl(arg)) { - configTargets.push({ path: paths.resolveTarget(arg) }); - } - }); - // Consider all packages in the monorepo when loading in config const { packages } = await getPackages(paths.targetDir); @@ -75,25 +63,53 @@ export async function loadCliConfig(options: Options) { noUndeclaredProperties: options.strict, }); - const { appConfigs } = await loadConfig({ - experimentalEnvFunc: options.mockEnv + const source = ConfigSources.default({ + allowMissingDefaultConfig: true, + substitutionFunc: options.mockEnv ? async name => process.env[name] || 'x' : undefined, - configRoot: paths.targetRoot, - configTargets: configTargets, - watch: options.watch && { - onChange(newAppConfigs) { - const newFrontendAppConfigs = schema.process(newAppConfigs, { - visibility: options.fullVisibility - ? ['frontend', 'backend', 'secret'] - : ['frontend'], - withFilteredKeys: options.withFilteredKeys, - withDeprecatedKeys: options.withDeprecatedKeys, - ignoreSchemaErrors: !options.strict, - }); - options.watch?.(newFrontendAppConfigs); - }, - }, + watch: Boolean(options.watch), + rootDir: paths.targetRoot, + argv: options.args, + }); + + const appConfigs = await new Promise<AppConfig[]>((resolve, reject) => { + async function loadConfigReaderLoop() { + let loaded = false; + + try { + const abortController = new AbortController(); + for await (const { configs } of source.readConfigData({ + signal: abortController.signal, + })) { + if (loaded) { + const newFrontendAppConfigs = schema.process(configs, { + visibility: options.fullVisibility + ? ['frontend', 'backend', 'secret'] + : ['frontend'], + withFilteredKeys: options.withFilteredKeys, + withDeprecatedKeys: options.withDeprecatedKeys, + ignoreSchemaErrors: !options.strict, + }); + options.watch?.(newFrontendAppConfigs); + } else { + resolve(configs); + loaded = true; + + if (!options.watch) { + abortController.abort(); + } + } + } + } catch (error) { + if (loaded) { + console.error(`Failed to reload configuration, ${error}`); + } else { + reject(error); + } + } + } + loadConfigReaderLoop(); }); const configurationLoadedMessage = appConfigs.length From 348087a09636d79e3a2894325b16afef79275ec9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Mon, 22 Jul 2024 16:22:29 +0200 Subject: [PATCH 202/212] auth-backend-module-microsoft-provider: cleanup deps Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- plugins/auth-backend-module-microsoft-provider/package.json | 2 -- yarn.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 67a898cf80..b67fb8c447 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -38,9 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "lodash": "^4.17.21", "node-fetch": "^2.6.7", - "passport": "^0.7.0", "passport-microsoft": "^1.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index a8ed83a61a..ff5eac7b1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4861,10 +4861,8 @@ __metadata: "@types/passport-microsoft": ^1.0.0 express: ^4.18.2 jose: ^5.0.0 - lodash: ^4.17.21 msw: ^1.0.0 node-fetch: ^2.6.7 - passport: ^0.7.0 passport-microsoft: ^1.0.0 supertest: ^6.3.3 languageName: unknown From b1f3c44e42a8f880698e2b4e5c59be133c000738 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Mon, 22 Jul 2024 16:31:09 +0200 Subject: [PATCH 203/212] chore: api-reports Signed-off-by: blam <ben@blam.sh> --- packages/frontend-plugin-api/api-report.md | 46 +++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 65445b5c4d..3c082a7694 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -522,9 +522,16 @@ export function createExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap = never, >( - options: CreateExtensionBlueprintOptions<TParams, TInputs, TOutput, TConfig>, -): ExtensionBlueprint<TParams, TInputs, TOutput, TConfig>; + options: CreateExtensionBlueprintOptions< + TParams, + TInputs, + TOutput, + TConfig, + TDataRefs + >, +): ExtensionBlueprint<TParams, TInputs, TOutput, TConfig, TDataRefs>; // @public (undocumented) export interface CreateExtensionBlueprintOptions< @@ -532,6 +539,7 @@ export interface CreateExtensionBlueprintOptions< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { // (undocumented) attachTo: { @@ -541,6 +549,8 @@ export interface CreateExtensionBlueprintOptions< // (undocumented) configSchema?: PortableSchema<TConfig>; // (undocumented) + dataRefs?: TDataRefs; + // (undocumented) disabled?: boolean; // (undocumented) factory( @@ -917,7 +927,10 @@ export interface ExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { + // (undocumented) + dataRefs: TDataRefs; // (undocumented) make(args: { namespace?: string; @@ -1085,6 +1098,35 @@ export { gitlabAuthApiRef }; export { googleAuthApiRef }; +// @public (undocumented) +export const IconBundleBlueprint: ExtensionBlueprint< + { + icons: { + [x: string]: IconComponent; + }; + }, + AnyExtensionInputMap, + { + icons: ConfigurableExtensionDataRef< + 'core.icons', + { + [x: string]: IconComponent; + }, + {} + >; + }, + unknown, + { + icons: ConfigurableExtensionDataRef< + 'core.icons', + { + [x: string]: IconComponent; + }, + {} + >; + } +>; + // @public export type IconComponent = ComponentType< | { From 98ea3e2ca9329f6b131e3d3f038ba5101b039beb Mon Sep 17 00:00:00 2001 From: Adam Letizia <43392371+koalaty-code@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:40:18 -0500 Subject: [PATCH 204/212] chore(changeset): new change description Co-authored-by: Ben Lambert <ben@blam.sh> Signed-off-by: Adam Letizia <43392371+koalaty-code@users.noreply.github.com> --- .changeset/tough-lies-repair.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tough-lies-repair.md b/.changeset/tough-lies-repair.md index 25b1884bbc..b2f848e292 100644 --- a/.changeset/tough-lies-repair.md +++ b/.changeset/tough-lies-repair.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': patch --- -RepoUrlPicker: refetch user credentials when a new host is selected +Fix issue with `RepoUrlPicker` not refreshing the credentials for a different host From d0e95a70369cb81bf2c74850a5c3b80250388110 Mon Sep 17 00:00:00 2001 From: Rich Barton-Cooper <58304039+richcooper95@users.noreply.github.com> Date: Fri, 3 May 2024 16:01:05 +0100 Subject: [PATCH 205/212] feat: expose uiSchema and formContext in FormProps This commit exposes two extra fields in the FormProps: `uiSchema` and `formContext`. Signed-off-by: Rich Barton-Cooper <rich@syntasso.io> Co-authored-by: Chunyi Lyu <chunyineko@gmail.com> --- .changeset/flat-plums-grow.md | 6 ++++++ plugins/scaffolder-react/api-report.md | 2 +- plugins/scaffolder-react/src/components/types.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/flat-plums-grow.md diff --git a/.changeset/flat-plums-grow.md b/.changeset/flat-plums-grow.md new file mode 100644 index 0000000000..b62f3e65a9 --- /dev/null +++ b/.changeset/flat-plums-grow.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Add ability to customise form fields in the UI by exposing `uiSchema` and `formContext` in +`FormProps` diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index a14490b80c..a19a656d17 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -128,7 +128,7 @@ export interface FieldExtensionUiSchema<TFieldReturnValue, TUiOptions> // @public export type FormProps = Pick< FormProps_2, - 'transformErrors' | 'noHtml5Validate' + 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' >; // @public diff --git a/plugins/scaffolder-react/src/components/types.ts b/plugins/scaffolder-react/src/components/types.ts index 597998bac4..d5d317015d 100644 --- a/plugins/scaffolder-react/src/components/types.ts +++ b/plugins/scaffolder-react/src/components/types.ts @@ -32,7 +32,7 @@ export type TemplateGroupFilter = { */ export type FormProps = Pick< SchemaFormProps, - 'transformErrors' | 'noHtml5Validate' + 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' >; /** From 8cdd846abc49841100cdaf5273d4793a821b475e Mon Sep 17 00:00:00 2001 From: Rich Barton-Cooper <rich@syntasso.io> Date: Mon, 22 Jul 2024 15:55:23 +0100 Subject: [PATCH 206/212] fix: merge uiSchema and formContext in Stepper component This commit ensures that the uiSchema and formContext are merged in the Stepper component form, with the non-props values taking precedence over the props values. The uiSchemas are deep-merged using `lodash.merge` to ensure that nested values are correctly merged. Signed-off-by: Rich Barton-Cooper <rich@syntasso.io> --- .../next/components/Stepper/Stepper.test.tsx | 60 +++++++++++++++++++ .../src/next/components/Stepper/Stepper.tsx | 15 ++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index df58d971fb..78aef3428f 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -549,6 +549,66 @@ describe('Stepper', () => { }); }); + it('should allow overrides to the uiSchema and formContext correctly', async () => { + const manifest: TemplateParameterSchema = { + title: 'Custom Fields', + steps: [ + { + title: 'Test', + schema: { + properties: { + name: { + type: 'string', + 'ui:placeholder': 'Enter your name', + }, + age: { + type: 'number', + 'ui:placeholder': 'Enter your age', + }, + }, + }, + }, + ], + }; + + const uiSchema = { + name: { + 'ui:readonly': true, + 'ui:placeholder': 'Should be overwritten', + }, + }; + + const formContext = { + readOnlyAsDisabled: true, + }; + + const { getByRole } = await renderInTestApp( + <SecretsContextProvider> + <Stepper + manifest={manifest} + onCreate={jest.fn()} + extensions={[]} + formProps={{ uiSchema, formContext }} + initialState={{ name: 'Some Name', age: 40 }} + /> + </SecretsContextProvider>, + ); + + expect(getByRole('textbox', { name: 'name' })).toHaveValue('Some Name'); + expect(getByRole('textbox', { name: 'name' })).toBeDisabled(); + expect(getByRole('textbox', { name: 'name' })).toHaveAttribute( + 'placeholder', + 'Enter your name', + ); + + expect(getByRole('spinbutton', { name: 'age' })).toHaveValue(40); + expect(getByRole('spinbutton', { name: 'age' })).toBeEnabled(); + expect(getByRole('spinbutton', { name: 'age' })).toHaveAttribute( + 'placeholder', + 'Enter your age', + ); + }); + describe('Scaffolder Layouts', () => { it('should render the step in the scaffolder layout', async () => { const ScaffolderLayout: LayoutTemplate = ({ properties }) => ( diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 788c7cd715..3a8858144b 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -51,6 +51,7 @@ import { ErrorListTemplate } from './ErrorListTemplate'; import { makeStyles } from '@material-ui/core/styles'; import { PasswordWidget } from '../PasswordWidget/PasswordWidget'; import ajvErrors from 'ajv-errors'; +import { merge } from 'lodash'; const validator = customizeValidator(); ajvErrors(validator.ajv); @@ -193,6 +194,14 @@ export const Stepper = (stepperProps: StepperProps) => { setFormState(current => ({ ...current, ...formData })); }; + const { + formContext: propFormContext, + uiSchema: propUiSchema, + ...restFormProps + } = props.formProps ?? {}; + + const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema); + return ( <> {isValidating && <LinearProgress variant="indeterminate" />} @@ -229,9 +238,9 @@ export const Stepper = (stepperProps: StepperProps) => { validator={validator} extraErrors={errors as unknown as ErrorSchema} formData={formState} - formContext={{ formData: formState }} + formContext={{ ...propFormContext, formData: formState }} schema={currentStep.schema} - uiSchema={currentStep.uiSchema} + uiSchema={mergedUiSchema} onSubmit={handleNext} fields={fields} showErrorList="top" @@ -241,7 +250,7 @@ export const Stepper = (stepperProps: StepperProps) => { experimental_defaultFormStateBehavior={{ allOf: 'populateDefaults', }} - {...(props.formProps ?? {})} + {...restFormProps} > <div className={styles.footer}> <Button From 3ca333b683d542cee92998256f0a4607f45be452 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne <thomas.cardonne@adevinta.com> Date: Mon, 22 Jul 2024 22:19:06 +0200 Subject: [PATCH 207/212] test(scaffolder-backend-module-gitlab): fix tests for gitlabProjectAccessTokenCreate examples Fixes tests created in #25713 Signed-off-by: Thomas Cardonne <thomas.cardonne@adevinta.com> --- ...tlabProjectAccessTokenCreate.examples.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts index 0762212809..bdf14ce1c8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts @@ -215,7 +215,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository'], { accessLevel: 30, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); @@ -271,7 +271,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository'], { accessLevel: 40, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); @@ -299,7 +299,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository', 'read_api'], { accessLevel: 40, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); @@ -327,7 +327,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository'], { accessLevel: 10, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); @@ -355,7 +355,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository'], { accessLevel: 40, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); @@ -383,7 +383,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository'], { accessLevel: 50, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); @@ -411,7 +411,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository'], { accessLevel: 40, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); @@ -439,7 +439,7 @@ describe('gitlab:projectAccessToken:create examples', () => { ['read_repository'], { accessLevel: 40, - expiresAt: '2025-07-21', + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, }, ); From 97abfa22b9e98ad3c8b950628e19b4240243cb52 Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Tue, 23 Jul 2024 08:01:34 +0200 Subject: [PATCH 208/212] chore: added some docs and cleanup the todos Signed-off-by: blam <ben@blam.sh> --- .../building-apps/03-built-in-extensions.md | 1 + .../building-apps/08-migrating.md | 25 +++++++++++++++++++ .../building-plugins/03-extension-types.md | 4 +++ .../frontend-app-api/src/wiring/createApp.tsx | 4 +-- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/frontend-system/building-apps/03-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md index 4f8417f74f..a5e6476eb0 100644 --- a/docs/frontend-system/building-apps/03-built-in-extensions.md +++ b/docs/frontend-system/building-apps/03-built-in-extensions.md @@ -45,6 +45,7 @@ This extension is the first extension attached to the extension tree. It is resp | themes | The app themes list. | [createThemeExtension.themeDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension.themedataref) | false | See [default themes](#default-theme-extensions). | [createThemeExtension](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension) | | components | The app components list. | [createComponentExtension.componentDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createcomponentextension.componentdataref) | false | See [default components](#default-components-extensions). | [createComponentExtension](https://backstage.io/docs/reference/frontend-plugin-api.createcomponentextension) | | translations | The app translations list. | [createTranslationExtension.translationDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension.translationdataref) | false | - | [createTranslationExtension](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension) | +| icons | The app icons list. | [IconBundleBlueprint.dataRefs.icons](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint.dataRefs.icons) | true | - | [IconBundleBlueprint](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint) | #### Default theme extensions diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 0abdd2aac3..716e0da772 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -314,6 +314,31 @@ const app = createApp({ }); ``` +### `icons` + +Icons are now installed as extensions, using the `IconBundleBlueprint` to make new instances which can be added to the app. + +```ts +import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; + +const exampleIconBundle = IconBundleBlueprint.make({ + name: 'example-bundle', + params: { + icons: { + user: MyOwnUserIcon, + }, + }, +}); + +const app = createApp({ + features: [ + createExtensionOverrides({ + extensions: [exampleIconBundle], + }), + ], +}); +``` + ### `bindRoutes` Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on [binding routes](../architecture/07-routes.md#binding-external-route-references) for more information. diff --git a/docs/frontend-system/building-plugins/03-extension-types.md b/docs/frontend-system/building-plugins/03-extension-types.md index 2bea073ed6..1f3baaa710 100644 --- a/docs/frontend-system/building-plugins/03-extension-types.md +++ b/docs/frontend-system/building-plugins/03-extension-types.md @@ -38,6 +38,10 @@ Sign-in page extension have a single purpose - to implement a custom sign-in pag Theme extensions provide custom themes for the app. They are always attached to the app extension and you can have any number of themes extensions installed in an app at once, letting the user choose which theme to use. +### Icons - [Reference](../../reference/frontend-plugin-api.iconbundleblueprint.md) + +Icon bundle extensions provide the ability to replace or provide new icons to the app. You can use the above blueprint to make new extension instances which can be installed into the app. + ### Translation - [Reference](../../reference/frontend-plugin-api.createtranslationextension.md) Translation extension provide custom translation messages for the app. They can be used both to override the default english messages to custom ones, as well as provide translations for additional languages. diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 4d929b206f..8d39cb65aa 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -172,7 +172,7 @@ export interface CreateAppFeatureLoader { /** @public */ export function createApp(options?: { - /** @deprecated - TODO */ + /** @deprecated - Please use {@link @backstage/frontend-app-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ icons?: { [key in string]: IconComponent }; features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi }>; @@ -248,7 +248,7 @@ export function createApp(options?: { * @public */ export function createSpecializedApp(options?: { - /** @deprecated - TODO */ + /** @deprecated - Please use {@link @backstage/frontend-app-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ icons?: { [key in string]: IconComponent }; features?: FrontendFeature[]; config?: ConfigApi; From 2b4358b04429c02b6108bc93b285006ebd1a1be3 Mon Sep 17 00:00:00 2001 From: Ben Lambert <blam@spotify.com> Date: Tue, 23 Jul 2024 08:07:57 +0200 Subject: [PATCH 209/212] Update flat-plums-grow.md Signed-off-by: Ben Lambert <blam@spotify.com> --- .changeset/flat-plums-grow.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/flat-plums-grow.md b/.changeset/flat-plums-grow.md index b62f3e65a9..7057216786 100644 --- a/.changeset/flat-plums-grow.md +++ b/.changeset/flat-plums-grow.md @@ -2,5 +2,4 @@ '@backstage/plugin-scaffolder-react': patch --- -Add ability to customise form fields in the UI by exposing `uiSchema` and `formContext` in -`FormProps` +Add ability to customise form fields in the UI by exposing `uiSchema` and `formContext` in `FormProps` From 663568ebb6946f09a16458feca290059cb2ae79a Mon Sep 17 00:00:00 2001 From: blam <ben@blam.sh> Date: Tue, 23 Jul 2024 08:28:46 +0200 Subject: [PATCH 210/212] chore: fixing todo package Signed-off-by: blam <ben@blam.sh> --- packages/frontend-app-api/src/wiring/createApp.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 8d39cb65aa..d82d0ca782 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -172,7 +172,7 @@ export interface CreateAppFeatureLoader { /** @public */ export function createApp(options?: { - /** @deprecated - Please use {@link @backstage/frontend-app-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ + /** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ icons?: { [key in string]: IconComponent }; features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi }>; @@ -248,7 +248,7 @@ export function createApp(options?: { * @public */ export function createSpecializedApp(options?: { - /** @deprecated - Please use {@link @backstage/frontend-app-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ + /** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ icons?: { [key in string]: IconComponent }; features?: FrontendFeature[]; config?: ConfigApi; From fbc7819133216ada61e2782a91b96a5d7954a0b2 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren <heikki.hellgren@op.fi> Date: Tue, 23 Jul 2024 10:17:41 +0300 Subject: [PATCH 211/212] fix: use es2022 in cli bundler and in storybook fixes #25737 Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi> --- .changeset/cuddly-zebras-crash.md | 5 +++++ packages/cli/src/lib/bundler/optimization.ts | 4 ++-- packages/cli/src/lib/bundler/transforms.ts | 6 +++--- storybook/.storybook/main.js | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/cuddly-zebras-crash.md diff --git a/.changeset/cuddly-zebras-crash.md b/.changeset/cuddly-zebras-crash.md new file mode 100644 index 0000000000..473ac4053f --- /dev/null +++ b/.changeset/cuddly-zebras-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Use ES2022 in CLI bundler diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index 891e0793fe..cf240b4791 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -28,13 +28,13 @@ export const optimization = ( minimize: !isDev, minimizer: [ new EsbuildPlugin({ - target: 'es2019', + target: 'ES2022', format: 'iife', exclude: 'remoteEntry.js', }), // Avoid iife wrapping of module federation remote entry as it breaks the variable assignment new EsbuildPlugin({ - target: 'es2019', + target: 'ES2022', format: undefined, include: 'remoteEntry.js', }), diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 0900b8e48e..1be54f7a73 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -57,7 +57,7 @@ export const transforms = (options: TransformOptions): Transforms => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'ES2022', externalHelpers: !isBackend, parser: { syntax: 'typescript', @@ -85,7 +85,7 @@ export const transforms = (options: TransformOptions): Transforms => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'ES2022', externalHelpers: !isBackend, parser: { syntax: 'ecmascript', @@ -118,7 +118,7 @@ export const transforms = (options: TransformOptions): Transforms => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'es2022', externalHelpers: !isBackend, parser: { syntax: 'ecmascript', diff --git a/storybook/.storybook/main.js b/storybook/.storybook/main.js index cb5f364a32..799c6c7c85 100644 --- a/storybook/.storybook/main.js +++ b/storybook/.storybook/main.js @@ -69,7 +69,7 @@ module.exports = Object.assign(({ args }) => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'ES2022', parser: { syntax: 'typescript', tsx: true, @@ -89,7 +89,7 @@ module.exports = Object.assign(({ args }) => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'ES2022', parser: { syntax: 'ecmascript', jsx: true, From c79eac60653eefe154ea0d68ccd0a9a72785bbc4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg <poldsberg@gmail.com> Date: Tue, 23 Jul 2024 10:04:38 +0200 Subject: [PATCH 212/212] scaffolder: fix deprecation comment formatting Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> --- .../src/deprecated.ts | 12 +++-- .../src/scaffolder/actions/deprecated.ts | 54 ++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts b/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts index a17c88c3a4..a9daa771f0 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts @@ -19,25 +19,29 @@ import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bi export { createPublishBitbucketAction } from './actions/bitbucket'; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ export const createPublishBitbucketCloudAction = bitbucketCloud.createPublishBitbucketCloudAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ export const createBitbucketPipelinesRunAction = bitbucketCloud.createBitbucketPipelinesRunAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerAction = bitbucketServer.createPublishBitbucketServerAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerPullRequestAction = bitbucketServer.createPublishBitbucketServerPullRequestAction; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts index fd5fdd49e8..5d037b44b6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts @@ -22,56 +22,66 @@ import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bi import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubActionsDispatchAction = github.createGithubActionsDispatchAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubDeployKeyAction = github.createGithubDeployKeyAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubEnvironmentAction = github.createGithubEnvironmentAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubIssuesLabelAction = github.createGithubIssuesLabelAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export type CreateGithubPullRequestActionOptions = github.CreateGithubPullRequestActionOptions; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubRepoCreateAction = github.createGithubRepoCreateAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubRepoPushAction = github.createGithubRepoPushAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubWebhookAction = github.createGithubWebhookAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createPublishGithubAction = github.createPublishGithubAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createPublishGithubPullRequestAction = github.createPublishGithubPullRequestAction; @@ -83,46 +93,54 @@ export const createPublishBitbucketAction = bitbucket.createPublishBitbucketAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ export const createPublishBitbucketCloudAction = bitbucketCloud.createPublishBitbucketCloudAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerAction = bitbucketServer.createPublishBitbucketServerAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerPullRequestAction = bitbucketServer.createPublishBitbucketServerPullRequestAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-azure instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-azure` instead */ export const createPublishAzureAction = azure.createPublishAzureAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gerrit instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ export const createPublishGerritAction = gerrit.createPublishGerritAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gerrit instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ export const createPublishGerritReviewAction = gerrit.createPublishGerritReviewAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gitlab instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ export const createPublishGitlabAction = gitlab.createPublishGitlabAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gitlab instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ export const createPublishGitlabMergeRequestAction = gitlab.createPublishGitlabMergeRequestAction;