From 7b3ed9b928b5997aa5bb0a5e6976ee7a2a85280a Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sun, 27 Aug 2023 18:36:03 -0400 Subject: [PATCH 01/41] Add Bitbucket Server Event Integration Signed-off-by: David Lilienfeld --- .changeset/nine-falcons-repeat.md | 6 + .../integrations/bitbucketServer/discovery.md | 96 +++- .../package.json | 8 + .../report.api.md | 75 ++- .../src/index.ts | 9 +- .../src/lib/BitbucketServerClient.test.ts | 87 ++- .../src/lib/BitbucketServerClient.ts | 44 +- .../src/lib/index.ts | 5 +- .../src/lib/types.ts | 51 +- ...oduleBitbucketServerEntityProvider.test.ts | 32 +- ...alogModuleBitbucketServerEntityProvider.ts | 24 +- .../BitbucketServerEntityProvider.test.ts | 495 +++++++++++++++++- .../BitbucketServerEntityProvider.ts | 413 ++++++++++++++- .../BitbucketServerEntityProviderConfig.ts | 2 - .../BitbucketServerLocationParser.ts | 8 +- .../.eslintrc.js | 1 + .../README.md | 48 ++ .../api-report-alpha.md | 14 + .../api-report.md | 18 + .../catalog-info.yaml | 9 + .../package.json | 55 ++ .../src/alpha.ts | 18 + .../src/index.ts | 24 + .../router/BitbucketServerEventRouter.test.ts | 53 ++ .../src/router/BitbucketServerEventRouter.ts | 45 ++ .../eventsModuleBitbucketServerEventRouter.ts | 44 ++ .../eventsModuleBitbucketServerRouter.test.ts | 44 ++ .../src/setupTests.ts | 17 + yarn.lock | 15 + 29 files changed, 1715 insertions(+), 45 deletions(-) create mode 100644 .changeset/nine-falcons-repeat.md create mode 100644 plugins/events-backend-module-bitbucket-server/.eslintrc.js create mode 100644 plugins/events-backend-module-bitbucket-server/README.md create mode 100644 plugins/events-backend-module-bitbucket-server/api-report-alpha.md create mode 100644 plugins/events-backend-module-bitbucket-server/api-report.md create mode 100644 plugins/events-backend-module-bitbucket-server/catalog-info.yaml create mode 100644 plugins/events-backend-module-bitbucket-server/package.json create mode 100644 plugins/events-backend-module-bitbucket-server/src/alpha.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/index.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/setupTests.ts diff --git a/.changeset/nine-falcons-repeat.md b/.changeset/nine-falcons-repeat.md new file mode 100644 index 0000000000..a67e925343 --- /dev/null +++ b/.changeset/nine-falcons-repeat.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-server': minor +'@backstage/plugin-events-backend-module-bitbucket-server': minor +--- + +Added the ability for the plugin to receive events coming from Bitbucket Server push webhooks. It then performs a delta mutation on the catalog. diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index 9ba6c7a96d..a66db83806 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -26,7 +26,47 @@ dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to you yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server ``` -And update your backend by adding the following line: +### Installation with New Backend System + +```ts +// optional if you want HTTP endpojnts to receive external events +// backend.add(import('@backstage/plugin-events-backend/alpha')); +// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events +// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha')); +backend.add( + import('@backstage/plugin-events-backend-module-bitbucket-server/alpha'), +); +backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-server/alpha'), +); +``` + +You need to decide how you want to receive events from external sources like + +- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) +- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) + +Further documentation: + +- +- +- + +### Installation with Legacy Backend System + +#### Installation without Events Support + +You will have to add the entity provider in the catalog initialization code of your +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 +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server +``` + +And then add the entity provider to your catalog builder: ```ts title="packages/backend/src/index.ts" backend.add(import('@backstage/plugin-catalog-backend')); @@ -37,6 +77,60 @@ backend.add( /* highlight-add-end */ ``` +#### Installation with Events Support + +Please follow the installation instructions at + +- +- + +Additionally, you need to decide how you want to receive events from external sources like + +- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) + - Bitbucket Server events webhook url should be set to `{backstageBaseUrl}/api/events/http/bitbucketServer` +- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) + +Set up your provider + +```ts title="packages/backend/src/plugins/catalog.ts" +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +/* highlight-add-start */ +import { BitbucketServerEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-server'; +/* highlight-add-end */ + +import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); + /* highlight-add-start */ + const bitbucketServerProvider = BitbucketServerEntityProvider.fromConfig( + env.config, + { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + logger: env.logger, + scheduler: env.scheduler, + events: env.events, + tokenManager: env.tokenManager, + }, + ); + env.eventBroker.subscribe(bitbucketServerProvider); + builder.addEntityProvider(bitbucketServerProvider); + /* highlight-add-end */ + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; +} +``` + +**Attention:** +`catalogApi` and `tokenManager` are required at this variant +compared to the one without events support. + ## Configuration To use the entity provider, you'll need a [Bitbucket Server integration set up](locations.md). diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 6113f4b41b..a7c403b534 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -51,17 +51,25 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "p-throttle": "^4.1.1", + "@backstage/plugin-events-node": "workspace:^", + "@types/node-fetch": "^2.5.12", + "node-fetch": "^2.7.0", "uuid": "^11.0.0" }, "devDependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", "luxon": "^3.0.0", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index 0da9b78139..c72733806b 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -5,14 +5,18 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { EventsService } from '@backstage/plugin-events-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; +import { TaskRunner } from '@backstage/backend-tasks'; +import { TokenManager } from '@backstage/backend-common'; // @public export class BitbucketServerClient { @@ -22,6 +26,11 @@ export class BitbucketServerClient { config: BitbucketServerIntegrationConfig; }): BitbucketServerClient; // (undocumented) + getDefaultBranch(options: { + projectKey: string; + repo: string; + }): Promise; + // (undocumented) getFile(options: { projectKey: string; repo: string; @@ -47,6 +56,16 @@ export class BitbucketServerClient { }; } +// @public (undocumented) +export type BitbucketServerDefaultBranch = { + id: string; + displayId: string; + type: string; + latestCommit: string; + latestChangeset: string; + isDefault: boolean; +}; + // @public export class BitbucketServerEntityProvider implements EntityProvider { connect(connection: EntityProviderConnection): Promise; @@ -55,9 +74,12 @@ export class BitbucketServerEntityProvider implements EntityProvider { config: Config, options: { logger: LoggerService; + events?: EventsService; parser?: BitbucketServerLocationParser; - schedule?: SchedulerServiceTaskRunner; - scheduler?: SchedulerService; + schedule?: TaskRunner; + scheduler?: PluginTaskScheduler; + catalogApi?: CatalogApi; + tokenManager?: TokenManager; }, ): BitbucketServerEntityProvider[]; getProviderName(): string; @@ -65,6 +87,52 @@ export class BitbucketServerEntityProvider implements EntityProvider { refresh(logger: LoggerService): Promise; } +// @public (undocumented) +export namespace BitbucketServerEvents { + // (undocumented) + export type Actor = { + name?: string; + id: number; + }; + // (undocumented) + export type Change = { + ref: { + id: string; + displayId: string; + type: string; + }; + }; + // (undocumented) + export interface Event { + // (undocumented) + eventKey: string; + } + // (undocumented) + export interface RefsChangedEvent extends Event { + // (undocumented) + actor: Actor; + // (undocumented) + changes: Change[]; + // (undocumented) + commits: undefined; + // (undocumented) + date: string; + // (undocumented) + repository: Repository; + // (undocumented) + ToCommit: undefined; + } + // (undocumented) + export type Repository = { + slug: string; + id: number; + name: string; + project: BitbucketServerProject; + }; + { + } +} + // @public (undocumented) export type BitbucketServerListOptions = { [key: string]: number | undefined; @@ -108,6 +176,7 @@ export type BitbucketServerRepository = { }[] >; archived: boolean; + defaultBranch: string; }; // @public (undocumented) diff --git a/plugins/catalog-backend-module-bitbucket-server/src/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/index.ts index f05c9ca0e9..96e3a1a0a7 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/index.ts @@ -21,12 +21,7 @@ */ export { default } from './module'; -export { BitbucketServerClient } from './lib'; -export type { - BitbucketServerProject, - BitbucketServerRepository, - BitbucketServerPagedResponse, - BitbucketServerListOptions, -} from './lib'; +export * from './lib'; + export { BitbucketServerEntityProvider } from './providers'; export type { BitbucketServerLocationParser } from './providers'; 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 6abcede9de..fbb4054390 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 @@ -23,7 +23,12 @@ import { BitbucketServerPagedResponse, paginated, } from './BitbucketServerClient'; -import { BitbucketServerProject, BitbucketServerRepository } from './types'; +import { + BitbucketServerDefaultBranch, + BitbucketServerProject, + BitbucketServerRepository, +} from './types'; +import { NotFoundError } from '@backstage/errors'; const server = setupServer(); @@ -122,6 +127,7 @@ describe('BitbucketServerClient', () => { ], }, archived: false, + defaultBranch: 'master', }, ], }; @@ -200,6 +206,7 @@ describe('BitbucketServerClient', () => { ], }, archived: false, + defaultBranch: 'master', }; return res(ctx.json(response)); @@ -217,4 +224,82 @@ describe('BitbucketServerClient', () => { 'https://bitbucket.mycompany.com/projects/test-project', ); }); + + it('getRepository no repo', async () => { + server.use( + rest.get( + `${config.apiBaseUrl}/projects/test-project/repos/wrong-repo`, + (_, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + const error = async () => { + await client.getRepository({ + projectKey: 'test-project', + repo: 'wrong-repo', + }); + }; + + await expect(error).rejects.toThrow( + "Repository 'wrong-repo' in project 'test-project' does not exist.", + ); + await expect(error).rejects.toThrow(NotFoundError); + }); + + it('getDefaultBranch success', async () => { + server.use( + rest.get( + `${config.apiBaseUrl}/projects/test-project/repos/test-repo/default-branch`, + (req, res, ctx) => { + if ( + req.headers.get('authorization') !== + 'Basic dGVzdC11c2VyOnRlc3QtcHc=' + ) { + return res(ctx.status(400)); + } + const response: BitbucketServerDefaultBranch = { + id: 'refs/heads/master', + displayId: 'master', + type: 'BRANCH', + latestCommit: '0f2f3ae484054696568bf4560ba4da280f7df82a', + latestChangeset: '0f2f3ae484054696568bf4560ba4da280f7df82a', + isDefault: true, + }; + + return res(ctx.json(response)); + }, + ), + ); + + const repo = await client.getDefaultBranch({ + projectKey: 'test-project', + repo: 'test-repo', + }); + expect(repo.displayId).toEqual('master'); + }); + + it('getDefaultBranch endpoint', async () => { + server.use( + rest.get( + `${config.apiBaseUrl}/projects/test-project/repos/wrong-repo/default-branch`, + (_, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + const error = async () => { + await client.getDefaultBranch({ + projectKey: 'test-project', + repo: 'wrong-repo', + }); + }; + + await expect(error).rejects.toThrow( + "Your Bitbucket Server version no longer supports the default branch endpoint or 'wrong-repo' in 'test-project' does not exist.", + ); + await expect(error).rejects.toThrow(NotFoundError); + }); }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts index 3dd61d64e7..ba5f75268c 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts @@ -18,7 +18,6 @@ import { BitbucketServerIntegrationConfig, getBitbucketServerRequestOptions, } from '@backstage/integration'; -import { BitbucketServerProject, BitbucketServerRepository } from './types'; import pThrottle from 'p-throttle'; // 1 per second @@ -32,6 +31,15 @@ const throttledFetch = throttle( return await fetch(url, options); }, ); +import { + BitbucketServerDefaultBranch, + BitbucketServerRepository, +} from './index'; + +import { BitbucketServerProject } from './types'; +import { NotFoundError } from '@backstage/errors'; + +import { ResponseError } from '@backstage/errors'; /** * A client for interacting with a Bitbucket Server instance @@ -92,7 +100,35 @@ export class BitbucketServerClient { request, getBitbucketServerRequestOptions(this.config), ); - return response.json(); + if (response.ok) { + return response.json(); + } + if (response.status === 404) { + throw new NotFoundError( + `Repository '${options.repo}' in project '${options.projectKey}' does not exist.`, + ); + } + throw await ResponseError.fromResponse(response); + } + + async getDefaultBranch(options: { + projectKey: string; + repo: string; + }): Promise { + const request = `${this.config.apiBaseUrl}/projects/${options.projectKey}/repos/${options.repo}/default-branch`; + const response = await fetch( + request, + getBitbucketServerRequestOptions(this.config), + ); + if (response.ok) { + return response.json(); + } + if (response.status === 404) { + throw new NotFoundError( + `Your Bitbucket Server version no longer supports the default branch endpoint or '${options.repo}' in '${options.projectKey}' does not exist.`, + ); + } + throw await ResponseError.fromResponse(response); } resolvePath(options: { projectKey: string; repo: string; path: string }): { @@ -163,7 +199,9 @@ export type BitbucketServerPagedResponse = { values: T[]; nextPageStart: number; }; - +/** + * @public + */ export async function* paginated( request: ( options: BitbucketServerListOptions, diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts index 8b72c9335b..14fb01629e 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts @@ -19,7 +19,4 @@ export type { BitbucketServerPagedResponse, BitbucketServerListOptions, } from './BitbucketServerClient'; -export type { - BitbucketServerRepository, - BitbucketServerProject, -} from './types'; +export * from './types'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts index 381eecb493..e014528791 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts @@ -15,7 +15,7 @@ */ /** @public */ -export type BitbucketServerRepository = { +type BitbucketServerRepository = { project: { key: string; }; @@ -28,9 +28,56 @@ export type BitbucketServerRepository = { }[] >; archived: boolean; + defaultBranch: string; }; /** @public */ -export type BitbucketServerProject = { +type BitbucketServerProject = { key: string; }; + +/** @public */ +type BitbucketServerDefaultBranch = { + id: string; + displayId: string; + type: string; + latestCommit: string; + latestChangeset: string; + isDefault: boolean; +}; + +/** @public */ +namespace BitbucketServerEvents { + interface Event { + eventKey: string; + } + + export interface RefsChangedEvent extends Event { + date: string; + actor: Actor; + repository: Repository; + changes: Change[]; + commits: undefined; + ToCommit: undefined; + } + type Actor = { + name?: string; + id: number; + }; + type Change = { + ref: { id: string; displayId: string; type: string }; + }; + type Repository = { + slug: string; + id: number; + name: string; + project: BitbucketServerProject; + }; +} + +export type { + BitbucketServerDefaultBranch, + BitbucketServerProject, + BitbucketServerEvents, + BitbucketServerRepository, +}; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index fbd4fa7a36..27420e9300 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -16,20 +16,35 @@ import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { Duration } from 'luxon'; import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider'; -import { BitbucketServerEntityProvider } from '../providers'; +import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider'; describe('catalogModuleBitbucketServerEntityProvider', () => { it('should register provider at the catalog extension point', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); let addedProviders: Array | undefined; let usedSchedule: SchedulerServiceTaskScheduleDefinition | undefined; - const extensionPoint = { + const catalogExtensionPointImpl = { addEntityProvider: (providers: any) => { addedProviders = providers; }, }; + const connection = jest.fn() as unknown as EntityProviderConnection; const runner = jest.fn(); const scheduler = mockServices.scheduler.mock({ createScheduledTaskRunner(schedule) { @@ -60,7 +75,9 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { }; await startTestBackend({ - extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + extensionPoints: [ + [catalogProcessingExtensionPoint, catalogExtensionPointImpl], + ], features: [ catalogModuleBitbucketServerEntityProvider, mockServices.rootConfig.factory({ data: config }), @@ -72,9 +89,14 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { expect(usedSchedule?.frequency).toEqual({ months: 1 }); expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); - expect(addedProviders?.pop()?.getProviderName()).toEqual( + expect(runner).not.toHaveBeenCalled(); + const provider = addedProviders!.pop()!; + expect(provider.getProviderName()).toEqual( 'bitbucketServer-provider:default', ); - expect(runner).not.toHaveBeenCalled(); + await provider.connect(connection); + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('bitbucketServer-provider:default'); + expect(runner).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts index 2c78d219d9..615bbb3477 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts @@ -18,8 +18,12 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { BitbucketServerEntityProvider } from '../providers'; +import { + catalogProcessingExtensionPoint, + catalogServiceRef, +} from '@backstage/plugin-catalog-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider'; /** * @public @@ -31,14 +35,28 @@ export const catalogModuleBitbucketServerEntityProvider = createBackendModule({ env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, + catalogApi: catalogServiceRef, config: coreServices.rootConfig, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, + tokenManager: coreServices.tokenManager, }, - async init({ catalog, config, logger, scheduler }) { + async init({ + catalog, + catalogApi, + config, + events, + logger, + scheduler, + tokenManager, + }) { const providers = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi, + events, logger, scheduler, + tokenManager, }); catalog.addEntityProvider(providers); 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 05e0ea6a01..dd2735177b 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 @@ -14,6 +14,7 @@ * limitations under the License. */ +import { TokenManager } from '@backstage/backend-common'; import { SchedulerService, SchedulerServiceTaskRunner, @@ -24,11 +25,22 @@ import { registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { + DeferredEntity, + EntityProviderConnection, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { BitbucketServerEntityProvider } from './BitbucketServerEntityProvider'; +import { + BitbucketServerEntityProvider, + toDeferredEntities, +} from './BitbucketServerEntityProvider'; import { BitbucketServerPagedResponse } from '../lib'; +import { Entity, LocationEntity } from '@backstage/catalog-model'; +import { BitbucketServerEvents } from '../lib/index'; +import { CatalogApi } from '@backstage/catalog-client'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; class PersistingTaskRunner implements SchedulerServiceTaskRunner { private tasks: SchedulerServiceTaskInvocationDefinition[] = []; @@ -57,9 +69,15 @@ function pagedResponse(values: any): BitbucketServerPagedResponse { const logger = mockServices.logger.mock(); +const events = DefaultEventsService.create({ logger }); + const server = setupServer(); -function setupStubs(projects: Project[], baseUrl: string) { +function setupStubs( + projects: Project[], + baseUrl: string, + defaultBranch: string, +) { // Stub projects server.use( rest.get(`${baseUrl}/rest/api/1.0/projects`, (_, res, ctx) => { @@ -93,6 +111,7 @@ function setupStubs(projects: Project[], baseUrl: string) { ], }, archived: repo.archived ?? false, + defaultBranch: defaultBranch, }); } return res(ctx.json(pagedResponse(response))); @@ -102,6 +121,107 @@ function setupStubs(projects: Project[], baseUrl: string) { } } +const host = 'bitbucket.mycompany.com'; +const targetPath = `/catalog-info.yaml`; +const test1RepoUrl = `https://${host}/projects/TEST/repos/test1/browse`; + +function setupRepositoryReqHandler(defaultBranch: string) { + server.use( + rest.get( + `https://${host}/rest/api/1.0/projects/TEST/repos/test1`, + (_, res, ctx) => { + const response = { + slug: 'test1', + id: 1, + name: 'test1', + project: { + key: 'TEST', + id: 1, + name: 'TEST', + links: { + self: [ + { + href: `https://${host}/projects/TEST`, + }, + ], + }, + }, + links: { + self: [ + { + href: `${test1RepoUrl}`, + }, + ], + }, + defaultBranch: defaultBranch, + }; + return res(ctx.json(response)); + }, + ), + ); +} + +const tokenManager = { + getToken: async () => { + return { token: 'fake-token' }; + }, +} as any as TokenManager; +const repoPushEvent: BitbucketServerEvents.RefsChangedEvent = { + eventKey: 'repo:refs_changed', + date: '2017-09-19T09:45:32+1000', + actor: { + name: 'admin', + id: 1, + }, + repository: { + slug: 'test1', + id: 84, + name: 'test1', + project: { + key: 'TEST', + }, + }, + changes: [ + { + ref: { + id: 'refs/heads/master', + displayId: 'master', + type: 'BRANCH', + }, + }, + ], + commits: undefined, + ToCommit: undefined, +}; +const repoPushEventParams = { + topic: 'bitbucketServer.repo:refs_changed', + eventPayload: repoPushEvent, + metadata: { 'x-event-key': 'repo:refs_changed' }, +}; + +const createLocationEntity = ( + repoUrl: string, + path: string, + defaultBranch: string, +): LocationEntity => { + const target = `${repoUrl}${path}`; + + const entity = locationSpecToLocationEntity({ + location: { + type: 'url', + target: target, + presence: 'optional', + }, + }); + entity.metadata.annotations = { + ...entity.metadata.annotations, + [`${host}/repo-url`]: target, + ['bitbucket.org/default-branch']: defaultBranch, + }; + + return entity; +}; + describe('BitbucketServerEntityProvider', () => { registerMswTestHooks(server); afterEach(() => { @@ -157,6 +277,7 @@ describe('BitbucketServerEntityProvider', () => { }); const providers = BitbucketServerEntityProvider.fromConfig(config, { logger, + events, schedule, }); @@ -191,6 +312,7 @@ describe('BitbucketServerEntityProvider', () => { }); const providers = BitbucketServerEntityProvider.fromConfig(config, { logger, + events, schedule, }); @@ -204,7 +326,6 @@ describe('BitbucketServerEntityProvider', () => { }); it('apply full update on scheduled execution with filters', async () => { - const host = 'bitbucket.mycompany.com'; const config = new ConfigReader({ integrations: { bitbucketServer: [ @@ -253,6 +374,7 @@ describe('BitbucketServerEntityProvider', () => { { key: 'other-project', repos: [{ name: 'other-repo' }] }, ], `https://${host}`, + 'master', ); await provider.connect(entityProviderConnection); @@ -270,6 +392,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:${url}`, 'backstage.io/managed-by-origin-location': `url:${url}`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', }, @@ -291,7 +414,6 @@ describe('BitbucketServerEntityProvider', () => { }); it('apply full update on scheduled execution without filters', async () => { - const host = 'bitbucket.mycompany.com'; const config = new ConfigReader({ integrations: { bitbucketServer: [ @@ -329,6 +451,7 @@ describe('BitbucketServerEntityProvider', () => { { key: 'other-project', repos: [{ name: 'other-repo' }] }, ], `https://${host}`, + 'master', ); await provider.connect(entityProviderConnection); @@ -345,6 +468,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', }, @@ -364,6 +488,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f', }, @@ -441,7 +566,6 @@ describe('BitbucketServerEntityProvider', () => { }); it('apply full update with schedule in config', async () => { - const host = 'bitbucket.mycompany.com'; const config = new ConfigReader({ integrations: { bitbucketServer: [ @@ -488,6 +612,7 @@ describe('BitbucketServerEntityProvider', () => { { key: 'other-project', repos: [{ name: 'other-repo' }] }, ], `https://${host}`, + 'master', ); await provider.connect(entityProviderConnection); @@ -504,6 +629,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', }, @@ -523,6 +649,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f', }, @@ -542,4 +669,360 @@ describe('BitbucketServerEntityProvider', () => { entities: expectedEntities, }); }); + + it('Multiple location entities to deferred entities', async () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + catalog: { + providers: { + bitbucketServer: { + host: host, + }, + }, + }, + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + }); + const providers = BitbucketServerEntityProvider.fromConfig(config, { + logger, + events, + schedule, + }); + + expect(providers).toHaveLength(1); + expect(providers[0].getProviderName()).toEqual( + 'bitbucketServer-provider:default', + ); + + const locationEntities = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + [`${host}/repo-url`]: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', + }, + name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', + }, + spec: { + presence: 'optional', + target: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + type: 'url', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + [`${host}/repo-url`]: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', + }, + name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f', + }, + spec: { + presence: 'optional', + target: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + type: 'url', + }, + }, + ]; + + const deferredEntities = toDeferredEntities( + locationEntities, + providers[0].getProviderName(), + ); + + expect(deferredEntities).toEqual([ + { + locationKey: providers[0].getProviderName(), + entity: locationEntities[0], + }, + { + locationKey: providers[0].getProviderName(), + entity: locationEntities[1], + }, + ]); + }); + + it('refresh onRepoPush', async () => { + const schedule = new PersistingTaskRunner(); + const keptModule = createLocationEntity( + test1RepoUrl, + `/kept-module:${targetPath}`, + 'master', + ); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + setupRepositoryReqHandler('master'); + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/kept-module:/catalog-info.yaml`, + defaultBranch: 'master', + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + request: { filter: Record }, + options: { token: string }, + ): Promise<{ items: Entity[] }> => { + if ( + options.token !== 'fake-token' || + request.filter.kind !== 'Location' || + request.filter[`metadata.annotations.${host}/repo-url`] !== + `${test1RepoUrl}/kept-module:${targetPath}` + ) { + return { items: [] }; + } + return { + items: [keptModule], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [`url:${test1RepoUrl}/kept-module:${targetPath}`], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('no refresh onRepoPush due to different default branch', async () => { + const schedule = new PersistingTaskRunner(); + const keptModule = createLocationEntity( + test1RepoUrl, + `/kept-module:${targetPath}`, + 'main', + ); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + setupRepositoryReqHandler('main'); + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/kept-module:/catalog-info.yaml`, + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + request: { filter: Record }, + options: { token: string }, + ): Promise<{ items: Entity[] }> => { + if ( + options.token !== 'fake-token' || + request.filter.kind !== 'Location' || + request.filter[`metadata.annotations.${host}/repo-url`] !== + `${test1RepoUrl}/kept-module:${targetPath}` + ) { + return { items: [] }; + } + return { + items: [keptModule], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('add onRepoPush', async () => { + const schedule = new PersistingTaskRunner(); + setupRepositoryReqHandler('master'); + const addedModule = createLocationEntity( + test1RepoUrl, + `/added-module:${targetPath}`, + 'master', + ); + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/added-module:/catalog-info.yaml`, + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + _request: { filter: Record }, + _options: { token: string }, + ): Promise<{ items: Entity[] }> => { + return { + items: [], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + const addedEntities = [ + { + entity: addedModule, + locationKey: 'bitbucketServer-provider:mainProvider', + }, + ]; + const removedEntities: DeferredEntity[] = []; + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: addedEntities, + removed: removedEntities, + }); + }); + + it('fail add onRepoPush from wrong default branch', async () => { + const schedule = new PersistingTaskRunner(); + setupRepositoryReqHandler('main'); + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/added-module:/catalog-info.yaml`, + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + _request: { filter: Record }, + _options: { token: string }, + ): Promise<{ items: Entity[] }> => { + return { + items: [], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index 46f492d64c..4afc02b8b5 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; +import { Entity, LocationEntity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { @@ -24,6 +25,7 @@ import { import { EntityProvider, EntityProviderConnection, + DeferredEntity, } from '@backstage/plugin-catalog-node'; import * as uuid from 'uuid'; import { BitbucketServerClient, paginated } from '../lib'; @@ -40,6 +42,12 @@ import { SchedulerService, SchedulerServiceTaskRunner, } from '@backstage/backend-plugin-api'; +import { BitbucketServerEvents } from '../lib'; +import { EventsService } from '@backstage/plugin-events-node'; +import { CatalogApi } from '@backstage/catalog-client'; +import { TokenManager } from '@backstage/backend-common'; + +const TOPIC_REPO_REFS_CHANGED = 'bitbucketServer.repo:refs_changed'; /** * Discovers catalog files located in Bitbucket Server. @@ -56,14 +64,23 @@ export class BitbucketServerEntityProvider implements EntityProvider { private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; private connection?: EntityProviderConnection; + private readonly catalogApi?: CatalogApi; + private readonly events?: EventsService; + private readonly tokenManager?: TokenManager; + private eventConfigErrorThrown = false; + private readonly targetAnnotation: string; + private readonly defaultBranchAnnotation: string; static fromConfig( config: Config, options: { logger: LoggerService; + events?: EventsService; parser?: BitbucketServerLocationParser; - schedule?: SchedulerServiceTaskRunner; - scheduler?: SchedulerService; + schedule?: TaskRunner; + scheduler?: PluginTaskScheduler; + catalogApi?: CatalogApi; + tokenManager?: TokenManager; }, ): BitbucketServerEntityProvider[] { const integrations = ScmIntegrations.fromConfig(config); @@ -98,6 +115,9 @@ export class BitbucketServerEntityProvider implements EntityProvider { options.logger, taskRunner, options.parser, + options.catalogApi, + options.events, + options.tokenManager, ); }); } @@ -108,6 +128,9 @@ export class BitbucketServerEntityProvider implements EntityProvider { logger: LoggerService, taskRunner: SchedulerServiceTaskRunner, parser?: BitbucketServerLocationParser, + catalogApi?: CatalogApi, + events?: EventsService, + tokenManager?: TokenManager, ) { this.integration = integration; this.config = config; @@ -116,6 +139,11 @@ export class BitbucketServerEntityProvider implements EntityProvider { target: this.getProviderName(), }); this.scheduleFn = this.createScheduleFn(taskRunner); + this.catalogApi = catalogApi; + this.tokenManager = tokenManager; + this.targetAnnotation = `${this.config.host.split(':')[0]}/repo-url`; + this.defaultBranchAnnotation = 'bitbucket.org/default-branch'; + this.events = events; } private createScheduleFn( @@ -154,6 +182,22 @@ export class BitbucketServerEntityProvider implements EntityProvider { async connect(connection: EntityProviderConnection): Promise { this.connection = connection; await this.scheduleFn(); + + if (this.events) { + await this.events.subscribe({ + id: this.getProviderName(), + topics: [TOPIC_REPO_REFS_CHANGED], + onEvent: async params => { + if (params.topic !== TOPIC_REPO_REFS_CHANGED) { + return; + } + + await this.onRepoPush( + params.eventPayload as BitbucketServerEvents.RefsChangedEvent, + ); + }, + }); + } } async refresh(logger: LoggerService) { @@ -218,10 +262,373 @@ export class BitbucketServerEntityProvider implements EntityProvider { presence: 'optional', }, })) { + if (entity.metadata.annotations === undefined) { + entity.metadata.annotations = {}; + } + if (repository.defaultBranch === undefined) { + const defaultBranchResponse = await client.getDefaultBranch({ + repo: repository.slug, + projectKey: project.key, + }); + entity.metadata.annotations[this.defaultBranchAnnotation] = + defaultBranchResponse.displayId; + } else { + entity.metadata.annotations[this.defaultBranchAnnotation] = + repository.defaultBranch; + } result.push(entity); } } } return result; } + + /** + * Checks if the webhook was triggered on a commit to the head branch of a repository + * @param event Bitbucket Server webhook repo:refs_changed event + */ + private isDefaultBranchPush( + defaultBranch: String, + event: BitbucketServerEvents.RefsChangedEvent, + ): boolean { + return event.changes.some(c => defaultBranch === c.ref.displayId); + } + + /** + * Checks if the provider is able to handle events + * @returns Boolean + */ + private canHandleEvents(): boolean { + if ( + this.catalogApi !== undefined && + this.catalogApi !== null && + this.tokenManager !== undefined && + this.tokenManager !== null + ) { + return true; + } + + if (!this.eventConfigErrorThrown) { + this.eventConfigErrorThrown = true; + throw new Error( + `${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.`, + ); + } + + return false; + } + + /** + * Creates a Bitbucket Server location entity for the repository that is referenced in the push event passed in. + * + * @param event A Bitbucket Server push event with repository information. + * + * @returns An array of location entities for the repository. + * + * @example + * + * const RefsChangedEvent = { + * "eventKey": "repo:refs_changed", + * "date": "2022-01-01T00:00:00Z", + * "actor": { + * "name": "johndoe", + * "emailAddress": "johndoe@example.com", + * "id": 123, + * "displayName": "John Doe", + * "active": true, + * "slug": "johndoe", + * "type": "NORMAL" + * }, + * "repository": { + * "slug": "my-repo", + * "id": 123, + * "name": "My Repository", + * "project": { + * "key": "my-project", + * "id": 456, + * "name": "My Project", + * "description": "My project description", + * "public": true, + * "type": "NORMAL" + * } + * }, + * "changes": [ + * { + * "ref": { + * "id": "refs/heads/master", + * "displayId": "master", + * "type": "BRANCH" + * }, + * "refId": "refs/heads/master", + * "fromHash": "0123456789abcdef0123456789abcdef0123456", + * "toHash": "fedcba9876543210fedcba9876543210fedcba9", + * "type": "UPDATE" + * } + * ] + * }; + * + * const locationEntities = await getLocationEntity(RefsChangedEvent); + * + * // locationEntities: + * // [ + * // { + * // kind: 'Location', + * // metadata: { + * // name: 'my-repo', + * // namespace: 'my-project', + * // annotations: { + * // 'backstage.io/managed-by-location': 'url/catalog-info-path', + * // 'backstage.io/managed-by-origin-location': 'url/catalog-info-path', + * // 'host/repo-url': 'url', + * // }, + * // }, + * // spec: { + * // type: 'bitbucket', + * // target: 'url/catalog-info-path', + * // presence: 'optional', + * // }, + * // }, + * // ] + */ + private async getLocationEntity( + event: BitbucketServerEvents.RefsChangedEvent, + ): Promise { + const client = BitbucketServerClient.fromConfig({ + config: this.integration.config, + }); + const result: Entity[] = []; + try { + const repository = await client.getRepository({ + projectKey: event.repository.project.key, + repo: event.repository.slug, + }); + + for await (const entity of this.parser({ + client, + logger: this.logger, + location: { + type: 'url', + target: `${repository.links.self[0].href}${this.config.catalogPath}`, + presence: 'optional', + }, + })) { + entity.metadata.annotations![ + this.targetAnnotation + ] = `${repository.links.self[0].href}${this.config.catalogPath}`; + + if (entity.metadata.annotations === undefined) { + entity.metadata.annotations = {}; + } + + if (repository.defaultBranch === undefined) { + const defaultBranchResponse = await client.getDefaultBranch({ + repo: repository.slug, + projectKey: event.repository.project.key, + }); + entity.metadata.annotations[this.defaultBranchAnnotation] = + defaultBranchResponse.displayId; + } else { + entity.metadata.annotations[this.defaultBranchAnnotation] = + repository.defaultBranch; + } + result.push(entity); + } + } catch (error: any) { + if (error.name === 'NotFoundError') { + this.logger.error(error.message); + } + } + + return result; + } + + /** + * Finds if there are existing location entities for the repository that was pushed. If there are, it simply refreshes those entities, + * if not, it discovers any entity that was added and removed in the list of entities + * @param event - A Bitbucket Server webhook event for repo:refs_change + */ + private async onRepoPush( + event: BitbucketServerEvents.RefsChangedEvent, + ): Promise { + if (!this.canHandleEvents()) { + this.logger.error( + 'Bitbucket Server catalog entity provider is not set up to handle events. Missing tokenManager or catalogApi.', + ); + return; + } + + if (!this.connection) { + throw new Error('Not initialized'); + } + + const repoSlug = event.repository.slug; + const catalogRepoUrl: string = `https://${this.config.host}/projects/${event.repository.project.key}/repos/${repoSlug}/browse${this.config.catalogPath}`; + this.logger.info(`handle repo:push event for ${catalogRepoUrl}`); + const targets = await this.getLocationEntity(event); + if (targets.length === 0) { + this.logger.error('Failed to create location entity.'); + return; + } + const { token } = await this.tokenManager!.getToken(); + const existing = await this.findExistingLocations(catalogRepoUrl, token); + const stillExisting: LocationEntity[] = []; + const removed: DeferredEntity[] = []; + existing.forEach(item => { + if ( + targets.find( + value => + value.metadata.annotations![this.targetAnnotation] === + item.spec.target, + ) + ) { + stillExisting.push(item); + } else { + removed.push({ + locationKey: this.getProviderName(), + entity: item, + }); + } + }); + + const added = await this.getAddedEntities(targets, existing); + + if ( + stillExisting.length > 0 && + stillExisting[0].metadata.annotations![this.defaultBranchAnnotation] !== + undefined && + !this.isDefaultBranchPush( + stillExisting[0].metadata.annotations![this.defaultBranchAnnotation], + event, + ) + ) { + return; + } else if ( + added.length > 0 && + added[0].entity.metadata.annotations![this.defaultBranchAnnotation] !== + undefined && + !this.isDefaultBranchPush( + added[0].entity.metadata.annotations![this.defaultBranchAnnotation], + event, + ) + ) { + return; + } else if ( + removed.length > 0 && + removed[0].entity.metadata.annotations![this.defaultBranchAnnotation] !== + undefined && + !this.isDefaultBranchPush( + removed[0].entity.metadata.annotations![this.defaultBranchAnnotation], + event, + ) + ) { + return; + } + + const promises: Promise[] = [ + this.connection.refresh({ + keys: stillExisting.map(entity => `url:${entity.spec.target}`), + }), + ]; + + if (added.length > 0 || removed.length > 0) { + promises.push( + this.connection.applyMutation({ + type: 'delta', + added: added, + removed: removed, + }), + ); + } + + await Promise.all(promises); + + return; + } + + /** + * Gets the location entities that are to be newly added to the catalog. + * @param targets Location entities for catalog files in the repository that was pushed + * @param existing The location entities in the repository that was pushed that already exist + * @returns Returns all deferred entities that represent location entities that don't exist in the catalog yet + */ + private async getAddedEntities( + targets: Entity[], + existing: LocationEntity[], + ): Promise { + const added: DeferredEntity[] = toDeferredEntities( + targets.filter( + target => + !existing.find( + item => + item.spec.target === + target.metadata.annotations![this.targetAnnotation], + ), + ), + this.getProviderName(), + ); + return added; + } + + /** + * Finds all location entities in the catalog that already have the annotation `metadata.annotations.${this.config.host}/repo-url` + * that is equivalent to @param repoURL\. + * @param repoURL URL for the reposity that the method finds the existing location entities for + * @param token Token from class token manager + */ + private async findExistingLocations( + catalogRepoUrl: string, + token: string, + ): Promise { + const filter: Record = {}; + filter.kind = 'Location'; + filter[`metadata.annotations.${this.targetAnnotation}`] = catalogRepoUrl; + + return this.catalogApi!.getEntities({ filter }, { token }).then( + result => result.items, + ) as Promise; + } + + // private static toLocationSpec(target: string): LocationSpec { + // return { + // type: 'url', + // target: target, + // presence: 'required', + // }; + // } +} + +/** + * Converts an array of entities into an array of deferred entities with the provider's name as the location key. + * + * @param targets An array of entities to convert. + * + * @returns An array of deferred entities with the provider's name as the location key. + * + * @example + * + * const entities = [ + * { kind: 'Component', namespace: 'default', name: 'my-component' }, + * { kind: 'System', namespace: 'default', name: 'my-system' }, + * { kind: 'API', namespace: 'default', name: 'my-api' }, + * ]; + * + * const deferredEntities = toDeferredEntities(entities); + * + * // deferredEntities: + * // [ + * // { locationKey: 'my-provider', entity: { kind: 'Component', namespace: 'default', name: 'my-component' } }, + * // { locationKey: 'my-provider', entity: { kind: 'System', namespace: 'default', name: 'my-system' } }, + * // { locationKey: 'my-provider', entity: { kind: 'API', namespace: 'default', name: 'my-api' } }, + * // ] + */ +export function toDeferredEntities( + targets: Entity[], + locationKey: string, +): DeferredEntity[] { + return targets.map(entity => { + return { + locationKey, + entity, + }; + }); } diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts index 018ff23069..3b921b6bab 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts @@ -48,10 +48,8 @@ export function readProviderConfigs( // simple/single config variant return [readProviderConfig(DEFAULT_PROVIDER_ID, providersConfig)]; } - return providersConfig.keys().map(id => { const providerConfig = providersConfig.getConfig(id); - return readProviderConfig(id, providerConfig); }); } diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts index 6aea23387e..1c48d5e7bd 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import { - LocationSpec, - locationSpecToLocationEntity, -} from '@backstage/plugin-catalog-node'; +import { locationSpecToLocationEntity } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; -import { BitbucketServerClient } from '../lib'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { BitbucketServerClient } from '../lib'; /** * A custom callback that reacts to finding a location by yielding entities. diff --git a/plugins/events-backend-module-bitbucket-server/.eslintrc.js b/plugins/events-backend-module-bitbucket-server/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-bitbucket-server/README.md b/plugins/events-backend-module-bitbucket-server/README.md new file mode 100644 index 0000000000..2c8fdff092 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/README.md @@ -0,0 +1,48 @@ +# events-backend-module-bitbucket-server + +Welcome to the `events-backend-module-bitbucket-server` backend plugin! + +This package is a module for the `events-backend` backend plugin +and extends the event system with an `BitbucketServerEventRouter`. + +The event router will subscribe to the topic `bitbucketServer` +and route the events to more concrete topics based on the value +of the provided `x-event-key` metadata field. + +Examples: + +| x-event-key | topic | +| ------------------- | ----------------------------------- | +| `repo:refs_changed` | `bitbucketServer.repo:refs_changed` | +| `repo:modified` | `bitbucketServer.repo:modified` | + +Please find all possible webhook event types at the +[official documentation](https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html). + +## Installation + +Install the [`events-backend` plugin](../events-backend/README.md). + +Install this module: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-bitbucket-server +``` + +```ts +// packages/backend/src/index.ts +backend.add( + import('@backstage/plugin-events-backend-module-bitbucket-server/alpha'), +); +``` + +### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new BitbucketCloudEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); +``` diff --git a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md new file mode 100644 index 0000000000..62fb3e2a6b --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md @@ -0,0 +1,14 @@ +## API Report File for "@backstage/plugin-events-backend-module-bitbucket-server" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const eventsModuleBitbucketServerEventRouter: () => BackendFeature; +export default eventsModuleBitbucketServerEventRouter; +export { eventsModuleBitbucketServerEventRouter }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-bitbucket-server/api-report.md b/plugins/events-backend-module-bitbucket-server/api-report.md new file mode 100644 index 0000000000..18292a0d11 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/api-report.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/plugin-events-backend-module-bitbucket-server" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; +import { SubTopicEventRouter } from '@backstage/plugin-events-node'; + +// @public +export class BitbucketServerEventRouter extends SubTopicEventRouter { + constructor(options: { events: EventsService }); + // (undocumented) + protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; +} +``` diff --git a/plugins/events-backend-module-bitbucket-server/catalog-info.yaml b/plugins/events-backend-module-bitbucket-server/catalog-info.yaml new file mode 100644 index 0000000000..d2a04ca2a8 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-events-backend-module-bitbucket-server + title: '@backstage/plugin-events-backend-module-bitbucket-server' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json new file mode 100644 index 0000000000..025f559b44 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-events-backend-module-bitbucket-server", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/events-backend-module-bitbucket-server" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-events-node": "workspace:^" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/events-backend-module-bitbucket-server/src/alpha.ts b/plugins/events-backend-module-bitbucket-server/src/alpha.ts new file mode 100644 index 0000000000..c741a37f24 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { eventsModuleBitbucketServerEventRouter } from './service/eventsModuleBitbucketServerEventRouter'; +export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-server/src/index.ts b/plugins/events-backend-module-bitbucket-server/src/index.ts new file mode 100644 index 0000000000..78c97808a4 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 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. + */ + +/** + * The module "bitbucket-server" for the Backstage backend plugin "events-backend" + * adding an event router for Bitbucket Server. + * + * @packageDocumentation + */ + +export { BitbucketServerEventRouter } from './router/BitbucketServerEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts new file mode 100644 index 0000000000..6fdb4020f3 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { BitbucketServerEventRouter } from './BitbucketServerEventRouter'; + +describe('BitbucketServerEventRouter', () => { + const events = new TestEventsService(); + const eventRouter = new BitbucketServerEventRouter({ events }); + const topic = 'bitbucketServer'; + const eventPayload = { test: 'payload' }; + const metadata = { 'x-event-key': 'test:type' }; + + beforeEach(() => { + events.reset(); + }); + + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-event-key', () => { + eventRouter.onEvent({ topic, eventPayload }); + + expect(events.published).toEqual([]); + }); + + it('with x-event-key', () => { + eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('bitbucketServer.test:type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts new file mode 100644 index 0000000000..45031cc5f3 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts @@ -0,0 +1,45 @@ +/* + * 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 { + EventParams, + EventsService, + SubTopicEventRouter, +} from '@backstage/plugin-events-node'; + +/** + * Subscribes to the generic `bitbucketServer` topic + * and publishes the events under the more concrete sub-topic + * depending on the `x-event-key` provided. + * + * @public + */ +export class BitbucketServerEventRouter extends SubTopicEventRouter { + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'bitbucketServer', + }); + } + + protected getSubscriberId(): string { + return 'BitbucketServerEventRouter'; + } + + protected determineSubTopic(params: EventParams): string | undefined { + return params.metadata?.['x-event-key'] as string | undefined; + } +} diff --git a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts new file mode 100644 index 0000000000..60fc67eb4f --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts @@ -0,0 +1,44 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { BitbucketServerEventRouter } from '../router/BitbucketServerEventRouter'; + +/** + * Module for the events-backend plugin, adding an event router for Bitbucket Server. + * + * Registers the `BitbucketServerEventRouter`. + * + * @alpha + */ +export const eventsModuleBitbucketServerEventRouter = createBackendModule({ + pluginId: 'events', + moduleId: 'bitbucket-server-event-router', + register(env) { + env.registerInit({ + deps: { + events: eventsServiceRef, + }, + async init({ events }) { + const eventRouter = new BitbucketServerEventRouter({ + events, + }); + await eventRouter.subscribe(); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts new file mode 100644 index 0000000000..56e158b8ab --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { createServiceFactory } from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { eventsModuleBitbucketServerEventRouter } from './eventsModuleBitbucketServerEventRouter'; + +describe('eventsModuleBitbucketServerEventRouter', () => { + it('should be correctly wired and set up', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); + + await startTestBackend({ + features: [ + eventsServiceFactory(), + eventsModuleBitbucketServerEventRouter(), + ], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter'); + }); +}); diff --git a/plugins/events-backend-module-bitbucket-server/src/setupTests.ts b/plugins/events-backend-module-bitbucket-server/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ + +export {}; diff --git a/yarn.lock b/yarn.lock index b337860101..42754c71b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5739,11 +5739,13 @@ __metadata: dependencies: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 @@ -6557,6 +6559,19 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + supertest: ^6.1.3 + languageName: unknown + linkType: soft + "@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit": version: 0.0.0-use.local resolution: "@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit" From 02e6dd48e9a37e2b77bacd76c5499f6c543513b8 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Wed, 19 Jun 2024 00:47:06 -0400 Subject: [PATCH 02/41] Fixed catalog backend tests and move backend-common to dependencies Signed-off-by: David Lilienfeld --- .../package.json | 2 +- ...oduleBitbucketServerEntityProvider.test.ts | 47 +++++++++---------- .../BitbucketServerEntityProvider.ts | 4 +- .../api-report-alpha.md | 4 +- .../package.json | 44 ++++++++--------- yarn.lock | 26 +++++++++- 6 files changed, 76 insertions(+), 51 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index a7c403b534..d31a3fc87d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -50,6 +50,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/catalog-client": "workspace:^", @@ -66,7 +67,6 @@ "uuid": "^11.0.0" }, "devDependencies": { - "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index 27420e9300..e389f430d7 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -22,7 +22,6 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsServiceRef } from '@backstage/plugin-events-node'; -import { Duration } from 'luxon'; import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider'; import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider'; @@ -53,35 +52,35 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { }, }); - const config = { - catalog: { - providers: { - bitbucketServer: { - host: 'bitbucket.mycompany.com', - schedule: { - frequency: 'P1M', - timeout: 'PT3M', - }, - }, - }, - }, - integrations: { - bitbucketServer: [ - { - host: 'bitbucket.mycompany.com', - }, - ], - }, - }; - await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, catalogExtensionPointImpl], ], features: [ + eventsServiceFactory(), catalogModuleBitbucketServerEntityProvider, - mockServices.rootConfig.factory({ data: config }), - mockServices.logger.factory(), + mockServices.rootConfig.factory({ + data: { + catalog: { + providers: { + bitbucketServer: { + host: 'bitbucket.mycompany.com', + schedule: { + frequency: 'P1M', + timeout: 'PT3M', + }, + }, + }, + }, + integrations: { + bitbucketServer: [ + { + host: 'bitbucket.mycompany.com', + }, + ], + }, + }, + }), scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index 4afc02b8b5..bf4c895121 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -77,8 +77,8 @@ export class BitbucketServerEntityProvider implements EntityProvider { logger: LoggerService; events?: EventsService; parser?: BitbucketServerLocationParser; - schedule?: TaskRunner; - scheduler?: PluginTaskScheduler; + schedule?: SchedulerServiceTaskRunner; + scheduler?: SchedulerService; catalogApi?: CatalogApi; tokenManager?: TokenManager; }, diff --git a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md index 62fb3e2a6b..2eb8fc6075 100644 --- a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md +++ b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md @@ -3,10 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; +import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; // @alpha -const eventsModuleBitbucketServerEventRouter: () => BackendFeature; +const eventsModuleBitbucketServerEventRouter: BackendFeatureCompat; export default eventsModuleBitbucketServerEventRouter; export { eventsModuleBitbucketServerEventRouter }; diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json index 025f559b44..4f38cb1f05 100644 --- a/plugins/events-backend-module-bitbucket-server/package.json +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -1,17 +1,28 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-server", "version": "0.1.0", - "main": "src/index.ts", - "types": "src/index.ts", - "license": "Apache-2.0", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "events", + "pluginPackage": "@backstage/plugin-events-backend" + }, "publishConfig": { "access": "public" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/events-backend-module-bitbucket-server" + }, + "license": "Apache-2.0", "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, + "main": "src/index.ts", + "types": "src/index.ts", "typesVersions": { "*": { "alpha": [ @@ -22,22 +33,17 @@ ] } }, - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "plugins/events-backend-module-bitbucket-server" - }, - "backstage": { - "role": "backend-plugin-module" - }, + "files": [ + "dist" + ], "scripts": { - "start": "backstage-cli package start", "build": "backstage-cli package build", - "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", "prepack": "backstage-cli package prepack", - "postpack": "backstage-cli package postpack" + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", @@ -46,10 +52,6 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/plugin-events-backend-test-utils": "workspace:^", - "supertest": "^6.1.3" - }, - "files": [ - "dist" - ] + "@backstage/plugin-events-backend-test-utils": "workspace:^" + } } diff --git a/yarn.lock b/yarn.lock index 42754c71b9..dd86d50acc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3794,6 +3794,29 @@ __metadata: languageName: unknown linkType: soft +"@backstage/backend-tasks@workspace:^, @backstage/backend-tasks@workspace:packages/backend-tasks": + version: 0.0.0-use.local + resolution: "@backstage/backend-tasks@workspace:packages/backend-tasks" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/types": "workspace:^" + "@opentelemetry/api": ^1.3.0 + "@types/luxon": ^3.0.0 + cron: ^3.0.0 + knex: ^3.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + uuid: ^9.0.0 + wait-for-expect: ^3.0.2 + zod: ^3.22.4 + languageName: unknown + linkType: soft + "@backstage/backend-test-utils@workspace:^, @backstage/backend-test-utils@workspace:packages/backend-test-utils": version: 0.0.0-use.local resolution: "@backstage/backend-test-utils@workspace:packages/backend-test-utils" @@ -5737,7 +5760,9 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-bitbucket-server@workspace:plugins/catalog-backend-module-bitbucket-server" dependencies: + "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" @@ -6568,7 +6593,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - supertest: ^6.1.3 languageName: unknown linkType: soft From 9463dfeda0d248de8837c5b17e251f911e6bf913 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Tue, 20 Aug 2024 23:55:13 -0400 Subject: [PATCH 03/41] Fix unused imports and api report Signed-off-by: David Lilienfeld --- .../catalog-backend-module-bitbucket-server/report.api.md | 5 ++--- .../catalogModuleBitbucketServerEntityProvider.test.ts | 1 - .../src/providers/BitbucketServerEntityProvider.ts | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index c72733806b..040626a83d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -15,7 +15,6 @@ import { LocationSpec } from '@backstage/plugin-catalog-common'; import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; -import { TaskRunner } from '@backstage/backend-tasks'; import { TokenManager } from '@backstage/backend-common'; // @public @@ -76,8 +75,8 @@ export class BitbucketServerEntityProvider implements EntityProvider { logger: LoggerService; events?: EventsService; parser?: BitbucketServerLocationParser; - schedule?: TaskRunner; - scheduler?: PluginTaskScheduler; + schedule?: SchedulerServiceTaskRunner; + scheduler?: SchedulerService; catalogApi?: CatalogApi; tokenManager?: TokenManager; }, diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index e389f430d7..4ae6ff3aae 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -17,7 +17,6 @@ import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { createServiceFactory } from '@backstage/backend-plugin-api'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index bf4c895121..aa7c3d5863 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { Entity, LocationEntity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; From 71c0954ab417cd91cd1ab5e811410c7ddc28c75a Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Mon, 26 Aug 2024 14:04:46 -0400 Subject: [PATCH 04/41] Remove tokenmanager from module Signed-off-by: David Lilienfeld --- ...atalogModuleBitbucketServerEntityProvider.test.ts | 2 +- .../catalogModuleBitbucketServerEntityProvider.ts | 12 +----------- .../api-report-alpha.md | 4 ++-- .../eventsModuleBitbucketServerRouter.test.ts | 5 +---- 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index 4ae6ff3aae..f9345170ed 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -56,7 +56,7 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { [catalogProcessingExtensionPoint, catalogExtensionPointImpl], ], features: [ - eventsServiceFactory(), + eventsServiceFactory, catalogModuleBitbucketServerEntityProvider, mockServices.rootConfig.factory({ data: { diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts index 615bbb3477..a829428296 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts @@ -40,23 +40,13 @@ export const catalogModuleBitbucketServerEntityProvider = createBackendModule({ events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, - tokenManager: coreServices.tokenManager, }, - async init({ - catalog, - catalogApi, - config, - events, - logger, - scheduler, - tokenManager, - }) { + async init({ catalog, catalogApi, config, events, logger, scheduler }) { const providers = BitbucketServerEntityProvider.fromConfig(config, { catalogApi, events, logger, scheduler, - tokenManager, }); catalog.addEntityProvider(providers); diff --git a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md index 2eb8fc6075..3c4ce45a1f 100644 --- a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md +++ b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md @@ -3,10 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; +import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -const eventsModuleBitbucketServerEventRouter: BackendFeatureCompat; +const eventsModuleBitbucketServerEventRouter: BackendFeature; export default eventsModuleBitbucketServerEventRouter; export { eventsModuleBitbucketServerEventRouter }; diff --git a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts index 56e158b8ab..a66c000089 100644 --- a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts @@ -32,10 +32,7 @@ describe('eventsModuleBitbucketServerEventRouter', () => { }); await startTestBackend({ - features: [ - eventsServiceFactory(), - eventsModuleBitbucketServerEventRouter(), - ], + features: [eventsServiceFactory, eventsModuleBitbucketServerEventRouter], }); expect(events.subscribed).toHaveLength(1); From cf3bb62037595f5e7dfc5270953083f24a96dbc5 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Thu, 29 Aug 2024 10:56:12 -0400 Subject: [PATCH 05/41] Remove deprecated package and fix yarn file Signed-off-by: David Lilienfeld --- .../package.json | 1 - yarn.lock | 24 ------------------- 2 files changed, 25 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index d31a3fc87d..922d6aea43 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -52,7 +52,6 @@ "dependencies": { "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", - "@backstage/backend-tasks": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", diff --git a/yarn.lock b/yarn.lock index dd86d50acc..b3a662a179 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3794,29 +3794,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/backend-tasks@workspace:^, @backstage/backend-tasks@workspace:packages/backend-tasks": - version: 0.0.0-use.local - resolution: "@backstage/backend-tasks@workspace:packages/backend-tasks" - dependencies: - "@backstage/backend-common": "workspace:^" - "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/types": "workspace:^" - "@opentelemetry/api": ^1.3.0 - "@types/luxon": ^3.0.0 - cron: ^3.0.0 - knex: ^3.0.0 - lodash: ^4.17.21 - luxon: ^3.0.0 - uuid: ^9.0.0 - wait-for-expect: ^3.0.2 - zod: ^3.22.4 - languageName: unknown - linkType: soft - "@backstage/backend-test-utils@workspace:^, @backstage/backend-test-utils@workspace:packages/backend-test-utils": version: 0.0.0-use.local resolution: "@backstage/backend-test-utils@workspace:packages/backend-test-utils" @@ -5762,7 +5739,6 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" From cb417ca1113b5097ab83a8cbca40ca07d0400298 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sat, 9 Nov 2024 22:25:34 -0500 Subject: [PATCH 06/41] Fixed all catalog update tests Signed-off-by: David Lilienfeld --- .../package.json | 1 - .../report.api.md | 12 ++++++-- .../src/lib/types.ts | 4 --- ...alogModuleBitbucketServerEntityProvider.ts | 12 +++++++- .../BitbucketServerEntityProvider.test.ts | 30 ++++++++++++------- .../BitbucketServerEntityProvider.ts | 23 +++++++------- ...pi-report-alpha.md => report-alpha.api.md} | 2 +- .../{api-report.md => report.api.md} | 11 +++++++ .../src/index.ts | 2 ++ .../eventsModuleBitbucketServerEventRouter.ts | 2 +- yarn.lock | 1 - 11 files changed, 69 insertions(+), 31 deletions(-) rename plugins/events-backend-module-bitbucket-server/{api-report-alpha.md => report-alpha.api.md} (97%) rename plugins/events-backend-module-bitbucket-server/{api-report.md => report.api.md} (55%) diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 922d6aea43..0d5ba5611e 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -50,7 +50,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index 040626a83d..94a0fed689 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -4,6 +4,7 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { AuthService } from '@backstage/backend-plugin-api'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; @@ -15,7 +16,6 @@ import { LocationSpec } from '@backstage/plugin-catalog-common'; import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; -import { TokenManager } from '@backstage/backend-common'; // @public export class BitbucketServerClient { @@ -55,6 +55,8 @@ export class BitbucketServerClient { }; } +// Warning: (ae-missing-release-tag) "BitbucketServerDefaultBranch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type BitbucketServerDefaultBranch = { id: string; @@ -78,7 +80,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { schedule?: SchedulerServiceTaskRunner; scheduler?: SchedulerService; catalogApi?: CatalogApi; - tokenManager?: TokenManager; + auth?: AuthService; }, ): BitbucketServerEntityProvider[]; getProviderName(): string; @@ -86,6 +88,8 @@ export class BitbucketServerEntityProvider implements EntityProvider { refresh(logger: LoggerService): Promise; } +// Warning: (ae-missing-release-tag) "BitbucketServerEvents" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export namespace BitbucketServerEvents { // (undocumented) @@ -156,11 +160,15 @@ export type BitbucketServerPagedResponse = { nextPageStart: number; }; +// Warning: (ae-missing-release-tag) "BitbucketServerProject" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type BitbucketServerProject = { key: string; }; +// Warning: (ae-missing-release-tag) "BitbucketServerRepository" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type BitbucketServerRepository = { project: { diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts index e014528791..51e014fb97 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -/** @public */ type BitbucketServerRepository = { project: { key: string; @@ -31,12 +30,10 @@ type BitbucketServerRepository = { defaultBranch: string; }; -/** @public */ type BitbucketServerProject = { key: string; }; -/** @public */ type BitbucketServerDefaultBranch = { id: string; displayId: string; @@ -46,7 +43,6 @@ type BitbucketServerDefaultBranch = { isDefault: boolean; }; -/** @public */ namespace BitbucketServerEvents { interface Event { eventKey: string; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts index a829428296..2d68b6ef49 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts @@ -40,13 +40,23 @@ export const catalogModuleBitbucketServerEntityProvider = createBackendModule({ events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, + auth: coreServices.auth, }, - async init({ catalog, catalogApi, config, events, logger, scheduler }) { + async init({ + catalog, + catalogApi, + config, + events, + logger, + scheduler, + auth, + }) { const providers = BitbucketServerEntityProvider.fromConfig(config, { catalogApi, events, logger, scheduler, + auth, }); catalog.addEntityProvider(providers); 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 dd2735177b..81699550bd 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 @@ -14,11 +14,11 @@ * limitations under the License. */ -import { TokenManager } from '@backstage/backend-common'; import { SchedulerService, SchedulerServiceTaskRunner, SchedulerServiceTaskInvocationDefinition, + AuthService, } from '@backstage/backend-plugin-api'; import { mockServices, @@ -69,6 +69,8 @@ function pagedResponse(values: any): BitbucketServerPagedResponse { const logger = mockServices.logger.mock(); +const authService = mockServices.auth.mock(); + const events = DefaultEventsService.create({ logger }); const server = setupServer(); @@ -161,11 +163,11 @@ function setupRepositoryReqHandler(defaultBranch: string) { ); } -const tokenManager = { - getToken: async () => { - return { token: 'fake-token' }; - }, -} as any as TokenManager; +// const authService = { +// getPluginRequestToken: async ({onBehalfOf, targetPluginId}) => { +// return { token: 'fake-token' }; +// }, +// } as any as AuthService; const repoPushEvent: BitbucketServerEvents.RefsChangedEvent = { eventKey: 'repo:refs_changed', date: '2017-09-19T09:45:32+1000', @@ -769,6 +771,10 @@ describe('BitbucketServerEntityProvider', () => { setupRepositoryReqHandler('master'); + authService.getPluginRequestToken.mockResolvedValue({ + token: 'fake-token', + }); + const config = new ConfigReader({ integrations: { bitbucketServer: [ @@ -809,12 +815,13 @@ describe('BitbucketServerEntityProvider', () => { }; }, }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { catalogApi: catalogApi as any as CatalogApi, logger, schedule, events, - tokenManager, + auth: authService as any as AuthService, })[0]; await provider.connect(entityProviderConnection); @@ -885,7 +892,7 @@ describe('BitbucketServerEntityProvider', () => { logger, schedule, events, - tokenManager, + auth: authService as any as AuthService, })[0]; await provider.connect(entityProviderConnection); @@ -898,6 +905,9 @@ describe('BitbucketServerEntityProvider', () => { it('add onRepoPush', async () => { const schedule = new PersistingTaskRunner(); setupRepositoryReqHandler('master'); + authService.getPluginRequestToken.mockResolvedValue({ + token: 'fake-token', + }); const addedModule = createLocationEntity( test1RepoUrl, `/added-module:${targetPath}`, @@ -945,7 +955,7 @@ describe('BitbucketServerEntityProvider', () => { logger, schedule, events, - tokenManager, + auth: authService as any as AuthService, })[0]; await provider.connect(entityProviderConnection); @@ -1015,7 +1025,7 @@ describe('BitbucketServerEntityProvider', () => { logger, schedule, events, - tokenManager, + auth: authService as any as AuthService, })[0]; await provider.connect(entityProviderConnection); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index aa7c3d5863..85878468cc 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -37,6 +37,7 @@ import { defaultBitbucketServerLocationParser, } from './BitbucketServerLocationParser'; import { + AuthService, LoggerService, SchedulerService, SchedulerServiceTaskRunner, @@ -44,7 +45,6 @@ import { import { BitbucketServerEvents } from '../lib'; import { EventsService } from '@backstage/plugin-events-node'; import { CatalogApi } from '@backstage/catalog-client'; -import { TokenManager } from '@backstage/backend-common'; const TOPIC_REPO_REFS_CHANGED = 'bitbucketServer.repo:refs_changed'; @@ -65,7 +65,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { private connection?: EntityProviderConnection; private readonly catalogApi?: CatalogApi; private readonly events?: EventsService; - private readonly tokenManager?: TokenManager; + private readonly auth?: AuthService; private eventConfigErrorThrown = false; private readonly targetAnnotation: string; private readonly defaultBranchAnnotation: string; @@ -79,7 +79,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { schedule?: SchedulerServiceTaskRunner; scheduler?: SchedulerService; catalogApi?: CatalogApi; - tokenManager?: TokenManager; + auth?: AuthService; }, ): BitbucketServerEntityProvider[] { const integrations = ScmIntegrations.fromConfig(config); @@ -116,7 +116,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { options.parser, options.catalogApi, options.events, - options.tokenManager, + options.auth, ); }); } @@ -129,7 +129,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { parser?: BitbucketServerLocationParser, catalogApi?: CatalogApi, events?: EventsService, - tokenManager?: TokenManager, + auth?: AuthService, ) { this.integration = integration; this.config = config; @@ -139,7 +139,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { }); this.scheduleFn = this.createScheduleFn(taskRunner); this.catalogApi = catalogApi; - this.tokenManager = tokenManager; + this.auth = auth; this.targetAnnotation = `${this.config.host.split(':')[0]}/repo-url`; this.defaultBranchAnnotation = 'bitbucket.org/default-branch'; this.events = events; @@ -301,8 +301,8 @@ export class BitbucketServerEntityProvider implements EntityProvider { if ( this.catalogApi !== undefined && this.catalogApi !== null && - this.tokenManager !== undefined && - this.tokenManager !== null + this.auth !== undefined && + this.auth !== null ) { return true; } @@ -451,7 +451,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { ): Promise { if (!this.canHandleEvents()) { this.logger.error( - 'Bitbucket Server catalog entity provider is not set up to handle events. Missing tokenManager or catalogApi.', + 'Bitbucket Server catalog entity provider is not set up to handle events. Missing authService or catalogApi.', ); return; } @@ -468,7 +468,10 @@ export class BitbucketServerEntityProvider implements EntityProvider { this.logger.error('Failed to create location entity.'); return; } - const { token } = await this.tokenManager!.getToken(); + const { token } = await this.auth!.getPluginRequestToken({ + onBehalfOf: await this.auth!.getOwnServiceCredentials(), + targetPluginId: 'catalog', // e.g. 'catalog' + }); const existing = await this.findExistingLocations(catalogRepoUrl, token); const stillExisting: LocationEntity[] = []; const removed: DeferredEntity[] = []; diff --git a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md b/plugins/events-backend-module-bitbucket-server/report-alpha.api.md similarity index 97% rename from plugins/events-backend-module-bitbucket-server/api-report-alpha.md rename to plugins/events-backend-module-bitbucket-server/report-alpha.api.md index 3c4ce45a1f..61559711f3 100644 --- a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md +++ b/plugins/events-backend-module-bitbucket-server/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -// @alpha +// @public const eventsModuleBitbucketServerEventRouter: BackendFeature; export default eventsModuleBitbucketServerEventRouter; export { eventsModuleBitbucketServerEventRouter }; diff --git a/plugins/events-backend-module-bitbucket-server/api-report.md b/plugins/events-backend-module-bitbucket-server/report.api.md similarity index 55% rename from plugins/events-backend-module-bitbucket-server/api-report.md rename to plugins/events-backend-module-bitbucket-server/report.api.md index 18292a0d11..aaf7b22293 100644 --- a/plugins/events-backend-module-bitbucket-server/api-report.md +++ b/plugins/events-backend-module-bitbucket-server/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { EventParams } from '@backstage/plugin-events-node'; import { EventsService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -15,4 +16,14 @@ export class BitbucketServerEventRouter extends SubTopicEventRouter { // (undocumented) protected getSubscriberId(): string; } + +// @public +const eventsModuleBitbucketServerEventRouter: BackendFeature; +export default eventsModuleBitbucketServerEventRouter; +export { eventsModuleBitbucketServerEventRouter }; + +// Warnings were encountered during analysis: +// +// src/router/BitbucketServerEventRouter.d.ts:13:5 - (ae-undocumented) Missing documentation for "getSubscriberId". +// src/router/BitbucketServerEventRouter.d.ts:14:5 - (ae-undocumented) Missing documentation for "determineSubTopic". ``` diff --git a/plugins/events-backend-module-bitbucket-server/src/index.ts b/plugins/events-backend-module-bitbucket-server/src/index.ts index 78c97808a4..afa3e204d2 100644 --- a/plugins/events-backend-module-bitbucket-server/src/index.ts +++ b/plugins/events-backend-module-bitbucket-server/src/index.ts @@ -22,3 +22,5 @@ */ export { BitbucketServerEventRouter } from './router/BitbucketServerEventRouter'; +export { eventsModuleBitbucketServerEventRouter } from './service/eventsModuleBitbucketServerEventRouter'; +export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts index 60fc67eb4f..2a8a26358b 100644 --- a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts @@ -23,7 +23,7 @@ import { BitbucketServerEventRouter } from '../router/BitbucketServerEventRouter * * Registers the `BitbucketServerEventRouter`. * - * @alpha + * @public */ export const eventsModuleBitbucketServerEventRouter = createBackendModule({ pluginId: 'events', diff --git a/yarn.lock b/yarn.lock index b3a662a179..baddc683e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5737,7 +5737,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-bitbucket-server@workspace:plugins/catalog-backend-module-bitbucket-server" dependencies: - "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" From 6d489aaac5eb6dfb783c49020c2aa46c4ae050b9 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sun, 10 Nov 2024 20:27:30 -0500 Subject: [PATCH 07/41] Remove documentation temporarily Signed-off-by: David Lilienfeld --- .../report.api.md | 17 +-- .../src/lib/types.ts | 4 + .../BitbucketServerEntityProvider.ts | 125 ------------------ 3 files changed, 13 insertions(+), 133 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index 94a0fed689..abf7c913c8 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -5,6 +5,7 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; import { AuthService } from '@backstage/backend-plugin-api'; +import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; @@ -55,8 +56,6 @@ export class BitbucketServerClient { }; } -// Warning: (ae-missing-release-tag) "BitbucketServerDefaultBranch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BitbucketServerDefaultBranch = { id: string; @@ -88,8 +87,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { refresh(logger: LoggerService): Promise; } -// Warning: (ae-missing-release-tag) "BitbucketServerEvents" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export namespace BitbucketServerEvents { // (undocumented) @@ -160,15 +157,11 @@ export type BitbucketServerPagedResponse = { nextPageStart: number; }; -// Warning: (ae-missing-release-tag) "BitbucketServerProject" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BitbucketServerProject = { key: string; }; -// Warning: (ae-missing-release-tag) "BitbucketServerRepository" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BitbucketServerRepository = { project: { @@ -189,4 +182,12 @@ export type BitbucketServerRepository = { // @public (undocumented) const catalogModuleBitbucketServerEntityProvider: BackendFeature; export default catalogModuleBitbucketServerEntityProvider; + +// @public (undocumented) +export function paginated( + request: ( + options: BitbucketServerListOptions, + ) => Promise>, + options?: BitbucketServerListOptions, +): AsyncGenerator; ``` diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts index 51e014fb97..e014528791 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ type BitbucketServerRepository = { project: { key: string; @@ -30,10 +31,12 @@ type BitbucketServerRepository = { defaultBranch: string; }; +/** @public */ type BitbucketServerProject = { key: string; }; +/** @public */ type BitbucketServerDefaultBranch = { id: string; displayId: string; @@ -43,6 +46,7 @@ type BitbucketServerDefaultBranch = { isDefault: boolean; }; +/** @public */ namespace BitbucketServerEvents { interface Event { eventKey: string; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index 85878468cc..c6df6a19b4 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -282,10 +282,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { return result; } - /** - * Checks if the webhook was triggered on a commit to the head branch of a repository - * @param event Bitbucket Server webhook repo:refs_changed event - */ private isDefaultBranchPush( defaultBranch: String, event: BitbucketServerEvents.RefsChangedEvent, @@ -317,78 +313,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { return false; } - /** - * Creates a Bitbucket Server location entity for the repository that is referenced in the push event passed in. - * - * @param event A Bitbucket Server push event with repository information. - * - * @returns An array of location entities for the repository. - * - * @example - * - * const RefsChangedEvent = { - * "eventKey": "repo:refs_changed", - * "date": "2022-01-01T00:00:00Z", - * "actor": { - * "name": "johndoe", - * "emailAddress": "johndoe@example.com", - * "id": 123, - * "displayName": "John Doe", - * "active": true, - * "slug": "johndoe", - * "type": "NORMAL" - * }, - * "repository": { - * "slug": "my-repo", - * "id": 123, - * "name": "My Repository", - * "project": { - * "key": "my-project", - * "id": 456, - * "name": "My Project", - * "description": "My project description", - * "public": true, - * "type": "NORMAL" - * } - * }, - * "changes": [ - * { - * "ref": { - * "id": "refs/heads/master", - * "displayId": "master", - * "type": "BRANCH" - * }, - * "refId": "refs/heads/master", - * "fromHash": "0123456789abcdef0123456789abcdef0123456", - * "toHash": "fedcba9876543210fedcba9876543210fedcba9", - * "type": "UPDATE" - * } - * ] - * }; - * - * const locationEntities = await getLocationEntity(RefsChangedEvent); - * - * // locationEntities: - * // [ - * // { - * // kind: 'Location', - * // metadata: { - * // name: 'my-repo', - * // namespace: 'my-project', - * // annotations: { - * // 'backstage.io/managed-by-location': 'url/catalog-info-path', - * // 'backstage.io/managed-by-origin-location': 'url/catalog-info-path', - * // 'host/repo-url': 'url', - * // }, - * // }, - * // spec: { - * // type: 'bitbucket', - * // target: 'url/catalog-info-path', - * // presence: 'optional', - * // }, - * // }, - * // ] - */ private async getLocationEntity( event: BitbucketServerEvents.RefsChangedEvent, ): Promise { @@ -441,11 +365,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { return result; } - /** - * Finds if there are existing location entities for the repository that was pushed. If there are, it simply refreshes those entities, - * if not, it discovers any entity that was added and removed in the list of entities - * @param event - A Bitbucket Server webhook event for repo:refs_change - */ private async onRepoPush( event: BitbucketServerEvents.RefsChangedEvent, ): Promise { @@ -547,12 +466,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { return; } - /** - * Gets the location entities that are to be newly added to the catalog. - * @param targets Location entities for catalog files in the repository that was pushed - * @param existing The location entities in the repository that was pushed that already exist - * @returns Returns all deferred entities that represent location entities that don't exist in the catalog yet - */ private async getAddedEntities( targets: Entity[], existing: LocationEntity[], @@ -571,12 +484,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { return added; } - /** - * Finds all location entities in the catalog that already have the annotation `metadata.annotations.${this.config.host}/repo-url` - * that is equivalent to @param repoURL\. - * @param repoURL URL for the reposity that the method finds the existing location entities for - * @param token Token from class token manager - */ private async findExistingLocations( catalogRepoUrl: string, token: string, @@ -589,40 +496,8 @@ export class BitbucketServerEntityProvider implements EntityProvider { result => result.items, ) as Promise; } - - // private static toLocationSpec(target: string): LocationSpec { - // return { - // type: 'url', - // target: target, - // presence: 'required', - // }; - // } } -/** - * Converts an array of entities into an array of deferred entities with the provider's name as the location key. - * - * @param targets An array of entities to convert. - * - * @returns An array of deferred entities with the provider's name as the location key. - * - * @example - * - * const entities = [ - * { kind: 'Component', namespace: 'default', name: 'my-component' }, - * { kind: 'System', namespace: 'default', name: 'my-system' }, - * { kind: 'API', namespace: 'default', name: 'my-api' }, - * ]; - * - * const deferredEntities = toDeferredEntities(entities); - * - * // deferredEntities: - * // [ - * // { locationKey: 'my-provider', entity: { kind: 'Component', namespace: 'default', name: 'my-component' } }, - * // { locationKey: 'my-provider', entity: { kind: 'System', namespace: 'default', name: 'my-system' } }, - * // { locationKey: 'my-provider', entity: { kind: 'API', namespace: 'default', name: 'my-api' } }, - * // ] - */ export function toDeferredEntities( targets: Entity[], locationKey: string, From a8a42c387d70362f07c72df45109822aea5d3c6f Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sun, 24 Nov 2024 20:12:20 -0500 Subject: [PATCH 08/41] Fix api report Signed-off-by: David Lilienfeld --- plugins/catalog-backend-module-bitbucket-server/report.api.md | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index abf7c913c8..add82b0a90 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; From 0f12f17fb21f93664678a2fb026acaa3a83a94ab Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sun, 24 Nov 2024 20:31:44 -0500 Subject: [PATCH 09/41] Fix events backend BBS report Signed-off-by: David Lilienfeld --- plugins/events-backend-module-bitbucket-server/report.api.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/events-backend-module-bitbucket-server/report.api.md b/plugins/events-backend-module-bitbucket-server/report.api.md index aaf7b22293..e0b55a5f57 100644 --- a/plugins/events-backend-module-bitbucket-server/report.api.md +++ b/plugins/events-backend-module-bitbucket-server/report.api.md @@ -21,9 +21,4 @@ export class BitbucketServerEventRouter extends SubTopicEventRouter { const eventsModuleBitbucketServerEventRouter: BackendFeature; export default eventsModuleBitbucketServerEventRouter; export { eventsModuleBitbucketServerEventRouter }; - -// Warnings were encountered during analysis: -// -// src/router/BitbucketServerEventRouter.d.ts:13:5 - (ae-undocumented) Missing documentation for "getSubscriberId". -// src/router/BitbucketServerEventRouter.d.ts:14:5 - (ae-undocumented) Missing documentation for "determineSubTopic". ``` From 1fa7494869b15531ac4bbbb985c1ffe17820b2a6 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Fri, 6 Dec 2024 23:43:50 -0500 Subject: [PATCH 10/41] Fix yarn lock Signed-off-by: David Lilienfeld --- yarn.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index baddc683e1..554c2410f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5747,9 +5747,13 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 msw: ^1.0.0 p-throttle: ^4.1.1 + node-fetch: ^2.7.0 uuid: ^11.0.0 languageName: unknown linkType: soft @@ -20106,7 +20110,7 @@ __metadata: languageName: node linkType: hard -"@types/node-fetch@npm:^2.6.4, @types/node-fetch@npm:^2.6.9": +"@types/node-fetch@npm:^2.5.12, @types/node-fetch@npm:^2.6.4, @types/node-fetch@npm:^2.6.9": version: 2.6.12 resolution: "@types/node-fetch@npm:2.6.12" dependencies: From 26fe586505050d511e8fcef2ecf80b8bb0b76991 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Fri, 6 Dec 2024 23:58:48 -0500 Subject: [PATCH 11/41] Fix package deps catalog plugin Signed-off-by: David Lilienfeld --- plugins/catalog-backend-module-bitbucket-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 0d5ba5611e..0f2861ca56 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -60,7 +60,6 @@ "@backstage/plugin-catalog-node": "workspace:^", "p-throttle": "^4.1.1", "@backstage/plugin-events-node": "workspace:^", - "@types/node-fetch": "^2.5.12", "node-fetch": "^2.7.0", "uuid": "^11.0.0" }, @@ -68,6 +67,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", + "@types/node-fetch": "^2.5.12", "luxon": "^3.0.0", "msw": "^1.0.0" }, From 351f3aedb9828e16b1d779e46af969684b98193e Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Fri, 13 Dec 2024 17:39:11 -0500 Subject: [PATCH 12/41] Fix package.json and yarn.lock Signed-off-by: David Lilienfeld --- plugins/catalog-backend-module-bitbucket-server/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 0f2861ca56..45edfa150f 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -58,9 +58,9 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", - "p-throttle": "^4.1.1", "@backstage/plugin-events-node": "workspace:^", "node-fetch": "^2.7.0", + "p-throttle": "^4.1.1", "uuid": "^11.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 554c2410f7..4605124ba9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5752,8 +5752,8 @@ __metadata: "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 msw: ^1.0.0 - p-throttle: ^4.1.1 node-fetch: ^2.7.0 + p-throttle: ^4.1.1 uuid: ^11.0.0 languageName: unknown linkType: soft From 00d989fd7a943d7e221585d2dc83b18dd5cb2aa4 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Fri, 13 Dec 2024 17:43:19 -0500 Subject: [PATCH 13/41] Moved throttle below imports in BitbucketServerClient Signed-off-by: David Lilienfeld --- .../src/lib/BitbucketServerClient.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts index ba5f75268c..a29b0ae4ff 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts @@ -19,6 +19,13 @@ import { getBitbucketServerRequestOptions, } from '@backstage/integration'; import pThrottle from 'p-throttle'; +import { + BitbucketServerDefaultBranch, + BitbucketServerRepository, +} from './index'; +import { BitbucketServerProject } from './types'; +import { NotFoundError } from '@backstage/errors'; +import { ResponseError } from '@backstage/errors'; // 1 per second const throttle = pThrottle({ @@ -31,15 +38,6 @@ const throttledFetch = throttle( return await fetch(url, options); }, ); -import { - BitbucketServerDefaultBranch, - BitbucketServerRepository, -} from './index'; - -import { BitbucketServerProject } from './types'; -import { NotFoundError } from '@backstage/errors'; - -import { ResponseError } from '@backstage/errors'; /** * A client for interacting with a Bitbucket Server instance From bb305509fdf3f213e5a512ecc5e147bdd12f7e91 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sun, 2 Mar 2025 22:52:59 -0500 Subject: [PATCH 14/41] Fix all suggestions | working tests Signed-off-by: David Lilienfeld --- .../integrations/bitbucketServer/discovery.md | 10 ++- .../package.json | 2 - .../report-alpha.api.md | 6 -- .../report.api.md | 4 +- .../src/alpha.ts | 6 +- .../src/lib/BitbucketServerClient.ts | 1 + ...alogModuleBitbucketServerEntityProvider.ts | 6 +- .../BitbucketServerEntityProvider.test.ts | 71 +++++++------------ .../BitbucketServerEntityProvider.ts | 31 ++++---- .../README.md | 14 +--- .../package.json | 2 +- .../report-alpha.api.md | 7 -- .../report.api.md | 12 ---- .../src/alpha.ts | 3 +- .../src/index.ts | 1 - yarn.lock | 4 +- 16 files changed, 52 insertions(+), 128 deletions(-) diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index a66db83806..150e92f11b 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -30,14 +30,12 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbuck ```ts // optional if you want HTTP endpojnts to receive external events -// backend.add(import('@backstage/plugin-events-backend/alpha')); +// backend.add(import('@backstage/plugin-events-backend')); // optional if you want to use AWS SQS instead of HTTP endpoints to receive external events -// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha')); +// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs')); +backend.add(import('@backstage/plugin-events-backend-module-bitbucket-server')); backend.add( - import('@backstage/plugin-events-backend-module-bitbucket-server/alpha'), -); -backend.add( - import('@backstage/plugin-catalog-backend-module-bitbucket-server/alpha'), + import('@backstage/plugin-catalog-backend-module-bitbucket-server'), ); ``` diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 45edfa150f..41c4fabd69 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -59,7 +59,6 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "node-fetch": "^2.7.0", "p-throttle": "^4.1.1", "uuid": "^11.0.0" }, @@ -67,7 +66,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", - "@types/node-fetch": "^2.5.12", "luxon": "^3.0.0", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md b/plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md index 06212c3c60..7082701333 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md @@ -3,11 +3,5 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; - -// @alpha (undocumented) -const _feature: BackendFeature; -export default _feature; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index add82b0a90..cdbe714be3 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -6,7 +6,7 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; -import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogService } from '@backstage/plugin-catalog-node'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; @@ -77,7 +77,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { parser?: BitbucketServerLocationParser; schedule?: SchedulerServiceTaskRunner; scheduler?: SchedulerService; - catalogApi?: CatalogApi; + catalogApi?: CatalogService; auth?: AuthService; }, ): BitbucketServerEntityProvider[]; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts b/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts index cba672ce49..706d03bd5d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -import { default as feature } from './module'; - -/** @alpha */ -const _feature = feature; -export default _feature; +import {} from './module'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts index a29b0ae4ff..6deb47ca6a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts @@ -197,6 +197,7 @@ export type BitbucketServerPagedResponse = { values: T[]; nextPageStart: number; }; + /** * @public */ diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts index 2d68b6ef49..d66ef4e2a2 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts @@ -18,10 +18,8 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { - catalogProcessingExtensionPoint, - catalogServiceRef, -} from '@backstage/plugin-catalog-node/alpha'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { eventsServiceRef } from '@backstage/plugin-events-node'; import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider'; 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 81699550bd..ba4e3e0411 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 @@ -18,7 +18,7 @@ import { SchedulerService, SchedulerServiceTaskRunner, SchedulerServiceTaskInvocationDefinition, - AuthService, + BackstageCredentials, } from '@backstage/backend-plugin-api'; import { mockServices, @@ -26,6 +26,7 @@ import { } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { + CatalogService, DeferredEntity, EntityProviderConnection, locationSpecToLocationEntity, @@ -39,8 +40,8 @@ import { import { BitbucketServerPagedResponse } from '../lib'; import { Entity, LocationEntity } from '@backstage/catalog-model'; import { BitbucketServerEvents } from '../lib/index'; -import { CatalogApi } from '@backstage/catalog-client'; import { DefaultEventsService } from '@backstage/plugin-events-node'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; class PersistingTaskRunner implements SchedulerServiceTaskRunner { private tasks: SchedulerServiceTaskInvocationDefinition[] = []; @@ -163,11 +164,6 @@ function setupRepositoryReqHandler(defaultBranch: string) { ); } -// const authService = { -// getPluginRequestToken: async ({onBehalfOf, targetPluginId}) => { -// return { token: 'fake-token' }; -// }, -// } as any as AuthService; const repoPushEvent: BitbucketServerEvents.RefsChangedEvent = { eventKey: 'repo:refs_changed', date: '2017-09-19T09:45:32+1000', @@ -771,9 +767,7 @@ describe('BitbucketServerEntityProvider', () => { setupRepositoryReqHandler('master'); - authService.getPluginRequestToken.mockResolvedValue({ - token: 'fake-token', - }); + // authService.getOwnServiceCredentials(); const config = new ConfigReader({ integrations: { @@ -797,13 +791,14 @@ describe('BitbucketServerEntityProvider', () => { }, }); - const catalogApi = { + const catalogApi = catalogServiceMock.mock({ getEntities: async ( request: { filter: Record }, - options: { token: string }, + credentials: { credentials: BackstageCredentials }, ): Promise<{ items: Entity[] }> => { if ( - options.token !== 'fake-token' || + credentials.credentials !== + (await authService.getOwnServiceCredentials()) || request.filter.kind !== 'Location' || request.filter[`metadata.annotations.${host}/repo-url`] !== `${test1RepoUrl}/kept-module:${targetPath}` @@ -814,14 +809,14 @@ describe('BitbucketServerEntityProvider', () => { items: [keptModule], }; }, - }; + }); const provider = BitbucketServerEntityProvider.fromConfig(config, { - catalogApi: catalogApi as any as CatalogApi, + catalogApi: catalogApi, logger, schedule, events, - auth: authService as any as AuthService, + auth: authService, })[0]; await provider.connect(entityProviderConnection); @@ -869,13 +864,14 @@ describe('BitbucketServerEntityProvider', () => { }, }); - const catalogApi = { + const catalogApi = catalogServiceMock.mock({ getEntities: async ( request: { filter: Record }, - options: { token: string }, + credentials: { credentials: BackstageCredentials }, ): Promise<{ items: Entity[] }> => { if ( - options.token !== 'fake-token' || + credentials.credentials !== + (await authService.getOwnServiceCredentials()) || request.filter.kind !== 'Location' || request.filter[`metadata.annotations.${host}/repo-url`] !== `${test1RepoUrl}/kept-module:${targetPath}` @@ -886,13 +882,14 @@ describe('BitbucketServerEntityProvider', () => { items: [keptModule], }; }, - }; + }); + const provider = BitbucketServerEntityProvider.fromConfig(config, { - catalogApi: catalogApi as any as CatalogApi, + catalogApi: catalogApi, logger, schedule, events, - auth: authService as any as AuthService, + auth: authService, })[0]; await provider.connect(entityProviderConnection); @@ -940,22 +937,13 @@ describe('BitbucketServerEntityProvider', () => { }, }); - const catalogApi = { - getEntities: async ( - _request: { filter: Record }, - _options: { token: string }, - ): Promise<{ items: Entity[] }> => { - return { - items: [], - }; - }, - }; + const catalogApi = catalogServiceMock({ entities: [] }); const provider = BitbucketServerEntityProvider.fromConfig(config, { - catalogApi: catalogApi as any as CatalogApi, + catalogApi: catalogApi, logger, schedule, events, - auth: authService as any as AuthService, + auth: authService, })[0]; await provider.connect(entityProviderConnection); @@ -1010,22 +998,13 @@ describe('BitbucketServerEntityProvider', () => { }, }); - const catalogApi = { - getEntities: async ( - _request: { filter: Record }, - _options: { token: string }, - ): Promise<{ items: Entity[] }> => { - return { - items: [], - }; - }, - }; + const catalogApi = catalogServiceMock({ entities: [] }); const provider = BitbucketServerEntityProvider.fromConfig(config, { - catalogApi: catalogApi as any as CatalogApi, + catalogApi: catalogApi as any as CatalogService, logger, schedule, events, - auth: authService as any as AuthService, + auth: authService, })[0]; await provider.connect(entityProviderConnection); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index c6df6a19b4..b462f8756a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -25,6 +25,7 @@ import { EntityProvider, EntityProviderConnection, DeferredEntity, + CatalogService, } from '@backstage/plugin-catalog-node'; import * as uuid from 'uuid'; import { BitbucketServerClient, paginated } from '../lib'; @@ -38,13 +39,13 @@ import { } from './BitbucketServerLocationParser'; import { AuthService, + BackstageCredentials, LoggerService, SchedulerService, SchedulerServiceTaskRunner, } from '@backstage/backend-plugin-api'; import { BitbucketServerEvents } from '../lib'; import { EventsService } from '@backstage/plugin-events-node'; -import { CatalogApi } from '@backstage/catalog-client'; const TOPIC_REPO_REFS_CHANGED = 'bitbucketServer.repo:refs_changed'; @@ -63,7 +64,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; private connection?: EntityProviderConnection; - private readonly catalogApi?: CatalogApi; + private readonly catalogApi?: CatalogService; private readonly events?: EventsService; private readonly auth?: AuthService; private eventConfigErrorThrown = false; @@ -78,7 +79,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { parser?: BitbucketServerLocationParser; schedule?: SchedulerServiceTaskRunner; scheduler?: SchedulerService; - catalogApi?: CatalogApi; + catalogApi?: CatalogService; auth?: AuthService; }, ): BitbucketServerEntityProvider[] { @@ -127,7 +128,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { logger: LoggerService, taskRunner: SchedulerServiceTaskRunner, parser?: BitbucketServerLocationParser, - catalogApi?: CatalogApi, + catalogApi?: CatalogService, events?: EventsService, auth?: AuthService, ) { @@ -294,19 +295,14 @@ export class BitbucketServerEntityProvider implements EntityProvider { * @returns Boolean */ private canHandleEvents(): boolean { - if ( - this.catalogApi !== undefined && - this.catalogApi !== null && - this.auth !== undefined && - this.auth !== null - ) { + if (this.catalogApi && this.auth) { return true; } if (!this.eventConfigErrorThrown) { this.eventConfigErrorThrown = true; throw new Error( - `${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.`, + `${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or AuthService.`, ); } @@ -387,11 +383,10 @@ export class BitbucketServerEntityProvider implements EntityProvider { this.logger.error('Failed to create location entity.'); return; } - const { token } = await this.auth!.getPluginRequestToken({ - onBehalfOf: await this.auth!.getOwnServiceCredentials(), - targetPluginId: 'catalog', // e.g. 'catalog' - }); - const existing = await this.findExistingLocations(catalogRepoUrl, token); + const existing = await this.findExistingLocations( + catalogRepoUrl, + await this.auth!.getOwnServiceCredentials(), + ); const stillExisting: LocationEntity[] = []; const removed: DeferredEntity[] = []; existing.forEach(item => { @@ -486,13 +481,13 @@ export class BitbucketServerEntityProvider implements EntityProvider { private async findExistingLocations( catalogRepoUrl: string, - token: string, + credentials: BackstageCredentials, ): Promise { const filter: Record = {}; filter.kind = 'Location'; filter[`metadata.annotations.${this.targetAnnotation}`] = catalogRepoUrl; - return this.catalogApi!.getEntities({ filter }, { token }).then( + return this.catalogApi!.getEntities({ filter }, { credentials }).then( result => result.items, ) as Promise; } diff --git a/plugins/events-backend-module-bitbucket-server/README.md b/plugins/events-backend-module-bitbucket-server/README.md index 2c8fdff092..4950015010 100644 --- a/plugins/events-backend-module-bitbucket-server/README.md +++ b/plugins/events-backend-module-bitbucket-server/README.md @@ -32,17 +32,5 @@ yarn add --cwd packages/backend @backstage/plugin-events-backend-module-bitbucke ```ts // packages/backend/src/index.ts -backend.add( - import('@backstage/plugin-events-backend-module-bitbucket-server/alpha'), -); -``` - -### Legacy Backend System - -```ts -// packages/backend/src/plugins/events.ts -const eventRouter = new BitbucketCloudEventRouter({ - events: env.events, -}); -await eventRouter.subscribe(); +backend.add(import('@backstage/plugin-events-backend-module-bitbucket-server')); ``` diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json index 4f38cb1f05..4cfe424618 100644 --- a/plugins/events-backend-module-bitbucket-server/package.json +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-server", - "version": "0.1.0", + "version": "0.0.0", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-server/report-alpha.api.md b/plugins/events-backend-module-bitbucket-server/report-alpha.api.md index 61559711f3..47b9068604 100644 --- a/plugins/events-backend-module-bitbucket-server/report-alpha.api.md +++ b/plugins/events-backend-module-bitbucket-server/report-alpha.api.md @@ -3,12 +3,5 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; - -// @public -const eventsModuleBitbucketServerEventRouter: BackendFeature; -export default eventsModuleBitbucketServerEventRouter; -export { eventsModuleBitbucketServerEventRouter }; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/events-backend-module-bitbucket-server/report.api.md b/plugins/events-backend-module-bitbucket-server/report.api.md index e0b55a5f57..916f7fa2ab 100644 --- a/plugins/events-backend-module-bitbucket-server/report.api.md +++ b/plugins/events-backend-module-bitbucket-server/report.api.md @@ -4,18 +4,6 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { EventParams } from '@backstage/plugin-events-node'; -import { EventsService } from '@backstage/plugin-events-node'; -import { SubTopicEventRouter } from '@backstage/plugin-events-node'; - -// @public -export class BitbucketServerEventRouter extends SubTopicEventRouter { - constructor(options: { events: EventsService }); - // (undocumented) - protected determineSubTopic(params: EventParams): string | undefined; - // (undocumented) - protected getSubscriberId(): string; -} // @public const eventsModuleBitbucketServerEventRouter: BackendFeature; diff --git a/plugins/events-backend-module-bitbucket-server/src/alpha.ts b/plugins/events-backend-module-bitbucket-server/src/alpha.ts index c741a37f24..588ee62daa 100644 --- a/plugins/events-backend-module-bitbucket-server/src/alpha.ts +++ b/plugins/events-backend-module-bitbucket-server/src/alpha.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { eventsModuleBitbucketServerEventRouter } from './service/eventsModuleBitbucketServerEventRouter'; -export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter'; +import {} from './service/eventsModuleBitbucketServerEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-server/src/index.ts b/plugins/events-backend-module-bitbucket-server/src/index.ts index afa3e204d2..6cf9aeb5e2 100644 --- a/plugins/events-backend-module-bitbucket-server/src/index.ts +++ b/plugins/events-backend-module-bitbucket-server/src/index.ts @@ -21,6 +21,5 @@ * @packageDocumentation */ -export { BitbucketServerEventRouter } from './router/BitbucketServerEventRouter'; export { eventsModuleBitbucketServerEventRouter } from './service/eventsModuleBitbucketServerEventRouter'; export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter'; diff --git a/yarn.lock b/yarn.lock index 4605124ba9..783947ce26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5749,10 +5749,8 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.7.0 p-throttle: ^4.1.1 uuid: ^11.0.0 languageName: unknown @@ -20110,7 +20108,7 @@ __metadata: languageName: node linkType: hard -"@types/node-fetch@npm:^2.5.12, @types/node-fetch@npm:^2.6.4, @types/node-fetch@npm:^2.6.9": +"@types/node-fetch@npm:^2.6.4, @types/node-fetch@npm:^2.6.9": version: 2.6.12 resolution: "@types/node-fetch@npm:2.6.12" dependencies: From 9785da55cf95508c503e7af259008340ab3e7a29 Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Wed, 5 Mar 2025 12:50:48 -0500 Subject: [PATCH 15/41] Fix api reports Signed-off-by: David Lilienfeld --- .../package.json | 4 -- .../report-alpha.api.md | 7 --- .../report.api.md | 54 ------------------- .../src/alpha.ts | 17 ------ .../src/index.ts | 11 +++- .../src/lib/BitbucketServerClient.ts | 3 -- .../src/lib/index.ts | 8 ++- .../src/lib/types.ts | 1 - .../package.json | 4 -- .../report-alpha.api.md | 7 --- .../report.api.md | 1 - .../src/alpha.ts | 17 ------ .../src/index.ts | 2 - 13 files changed, 17 insertions(+), 119 deletions(-) delete mode 100644 plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md delete mode 100644 plugins/catalog-backend-module-bitbucket-server/src/alpha.ts delete mode 100644 plugins/events-backend-module-bitbucket-server/report-alpha.api.md delete mode 100644 plugins/events-backend-module-bitbucket-server/src/alpha.ts diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 41c4fabd69..1f9b8baa27 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -21,16 +21,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md b/plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md deleted file mode 100644 index 7082701333..0000000000 --- a/plugins/catalog-backend-module-bitbucket-server/report-alpha.api.md +++ /dev/null @@ -1,7 +0,0 @@ -## API Report File for "@backstage/plugin-catalog-backend-module-bitbucket-server" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index cdbe714be3..7cb13e651f 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -86,52 +86,6 @@ export class BitbucketServerEntityProvider implements EntityProvider { refresh(logger: LoggerService): Promise; } -// @public (undocumented) -export namespace BitbucketServerEvents { - // (undocumented) - export type Actor = { - name?: string; - id: number; - }; - // (undocumented) - export type Change = { - ref: { - id: string; - displayId: string; - type: string; - }; - }; - // (undocumented) - export interface Event { - // (undocumented) - eventKey: string; - } - // (undocumented) - export interface RefsChangedEvent extends Event { - // (undocumented) - actor: Actor; - // (undocumented) - changes: Change[]; - // (undocumented) - commits: undefined; - // (undocumented) - date: string; - // (undocumented) - repository: Repository; - // (undocumented) - ToCommit: undefined; - } - // (undocumented) - export type Repository = { - slug: string; - id: number; - name: string; - project: BitbucketServerProject; - }; - { - } -} - // @public (undocumented) export type BitbucketServerListOptions = { [key: string]: number | undefined; @@ -181,12 +135,4 @@ export type BitbucketServerRepository = { // @public (undocumented) const catalogModuleBitbucketServerEntityProvider: BackendFeature; export default catalogModuleBitbucketServerEntityProvider; - -// @public (undocumented) -export function paginated( - request: ( - options: BitbucketServerListOptions, - ) => Promise>, - options?: BitbucketServerListOptions, -): AsyncGenerator; ``` diff --git a/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts b/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts deleted file mode 100644 index 706d03bd5d..0000000000 --- a/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 {} from './module'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/index.ts index 96e3a1a0a7..722be03d7a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/index.ts @@ -21,7 +21,16 @@ */ export { default } from './module'; -export * from './lib'; + +export { BitbucketServerClient } from './lib'; + +export type { + BitbucketServerProject, + BitbucketServerRepository, + BitbucketServerPagedResponse, + BitbucketServerListOptions, + BitbucketServerDefaultBranch, +} from './lib'; export { BitbucketServerEntityProvider } from './providers'; export type { BitbucketServerLocationParser } from './providers'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts index 6deb47ca6a..70e791c992 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts @@ -198,9 +198,6 @@ export type BitbucketServerPagedResponse = { nextPageStart: number; }; -/** - * @public - */ export async function* paginated( request: ( options: BitbucketServerListOptions, diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts index 14fb01629e..4533cb1305 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts @@ -19,4 +19,10 @@ export type { BitbucketServerPagedResponse, BitbucketServerListOptions, } from './BitbucketServerClient'; -export * from './types'; +export type { + BitbucketServerDefaultBranch, + BitbucketServerRepository, + BitbucketServerEvents, + BitbucketServerProject, +} from './types'; +// export * from './types'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts index e014528791..efdf3d7cf8 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts @@ -46,7 +46,6 @@ type BitbucketServerDefaultBranch = { isDefault: boolean; }; -/** @public */ namespace BitbucketServerEvents { interface Event { eventKey: string; diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json index 4cfe424618..29945bd16f 100644 --- a/plugins/events-backend-module-bitbucket-server/package.json +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -18,16 +18,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/plugins/events-backend-module-bitbucket-server/report-alpha.api.md b/plugins/events-backend-module-bitbucket-server/report-alpha.api.md deleted file mode 100644 index 47b9068604..0000000000 --- a/plugins/events-backend-module-bitbucket-server/report-alpha.api.md +++ /dev/null @@ -1,7 +0,0 @@ -## API Report File for "@backstage/plugin-events-backend-module-bitbucket-server" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/events-backend-module-bitbucket-server/report.api.md b/plugins/events-backend-module-bitbucket-server/report.api.md index 916f7fa2ab..913556cfe9 100644 --- a/plugins/events-backend-module-bitbucket-server/report.api.md +++ b/plugins/events-backend-module-bitbucket-server/report.api.md @@ -8,5 +8,4 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @public const eventsModuleBitbucketServerEventRouter: BackendFeature; export default eventsModuleBitbucketServerEventRouter; -export { eventsModuleBitbucketServerEventRouter }; ``` diff --git a/plugins/events-backend-module-bitbucket-server/src/alpha.ts b/plugins/events-backend-module-bitbucket-server/src/alpha.ts deleted file mode 100644 index 588ee62daa..0000000000 --- a/plugins/events-backend-module-bitbucket-server/src/alpha.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 {} from './service/eventsModuleBitbucketServerEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-server/src/index.ts b/plugins/events-backend-module-bitbucket-server/src/index.ts index 6cf9aeb5e2..8a765ecfdf 100644 --- a/plugins/events-backend-module-bitbucket-server/src/index.ts +++ b/plugins/events-backend-module-bitbucket-server/src/index.ts @@ -20,6 +20,4 @@ * * @packageDocumentation */ - -export { eventsModuleBitbucketServerEventRouter } from './service/eventsModuleBitbucketServerEventRouter'; export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter'; From 79c541d889c53ef1830cd6dcbcd3490ce1f10aca Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sun, 9 Mar 2025 00:02:39 -0500 Subject: [PATCH 16/41] Remove legacy backend docs Signed-off-by: David Lilienfeld --- .../integrations/bitbucketServer/discovery.md | 79 ------------------- 1 file changed, 79 deletions(-) diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index 150e92f11b..7947913182 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -50,85 +50,6 @@ Further documentation: - - -### Installation with Legacy Backend System - -#### Installation without Events Support - -You will have to add the entity provider in the catalog initialization code of your -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 -yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server -``` - -And then add the entity provider to your catalog builder: - -```ts title="packages/backend/src/index.ts" -backend.add(import('@backstage/plugin-catalog-backend')); -/* highlight-add-start */ -backend.add( - import('@backstage/plugin-catalog-backend-module-bitbucket-server'), -); -/* highlight-add-end */ -``` - -#### Installation with Events Support - -Please follow the installation instructions at - -- -- - -Additionally, you need to decide how you want to receive events from external sources like - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - - Bitbucket Server events webhook url should be set to `{backstageBaseUrl}/api/events/http/bitbucketServer` -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) - -Set up your provider - -```ts title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-start */ -import { BitbucketServerEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-server'; -/* highlight-add-end */ - -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - builder.addProcessor(new ScaffolderEntitiesProcessor()); - /* highlight-add-start */ - const bitbucketServerProvider = BitbucketServerEntityProvider.fromConfig( - env.config, - { - catalogApi: new CatalogClient({ discoveryApi: env.discovery }), - logger: env.logger, - scheduler: env.scheduler, - events: env.events, - tokenManager: env.tokenManager, - }, - ); - env.eventBroker.subscribe(bitbucketServerProvider); - builder.addEntityProvider(bitbucketServerProvider); - /* highlight-add-end */ - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; -} -``` - -**Attention:** -`catalogApi` and `tokenManager` are required at this variant -compared to the one without events support. - ## Configuration To use the entity provider, you'll need a [Bitbucket Server integration set up](locations.md). From 9a6080e6771e2a97837480683da877d6f07f4037 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Tue, 18 Mar 2025 12:16:14 +0200 Subject: [PATCH 17/41] feat(notifications): support notification send throttling Signed-off-by: Hellgren Heikki --- .changeset/rotten-bobcats-notice.md | 5 + plugins/notifications-backend/config.d.ts | 32 ++++ plugins/notifications-backend/package.json | 6 +- .../src/service/router.ts | 138 +++++++++++------- yarn.lock | 2 + 5 files changed, 127 insertions(+), 56 deletions(-) create mode 100644 .changeset/rotten-bobcats-notice.md create mode 100644 plugins/notifications-backend/config.d.ts diff --git a/.changeset/rotten-bobcats-notice.md b/.changeset/rotten-bobcats-notice.md new file mode 100644 index 0000000000..0f6d1c74f9 --- /dev/null +++ b/.changeset/rotten-bobcats-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +Allow throttling notification sending not to block the system with huge amount of receiving users diff --git a/plugins/notifications-backend/config.d.ts b/plugins/notifications-backend/config.d.ts new file mode 100644 index 0000000000..5dd795f8d3 --- /dev/null +++ b/plugins/notifications-backend/config.d.ts @@ -0,0 +1,32 @@ +/* + * 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 { HumanDuration } from '@backstage/types'; + +export interface Config { + /** + * Configuration options for notifications-backend + */ + notifications?: { + /** + * Concurrency limit for notification sending, defaults to 10 + */ + concurrencyLimit?: number; + /** + * Throttle duration between notification sending, defaults to 50ms + */ + throttleInterval?: HumanDuration | string; + }; +} diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index c96539caf8..6e9de108db 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -26,6 +26,7 @@ "types": "src/index.ts", "files": [ "dist", + "config.d.ts", "migrations/**/*.{js,d.ts}" ], "scripts": { @@ -49,9 +50,11 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-notifications-node": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", + "@backstage/types": "workspace:^", "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^3.0.0", + "p-throttle": "^4.1.1", "uuid": "^11.0.0", "winston": "^3.2.1", "yn": "^4.0.0" @@ -68,5 +71,6 @@ "@types/supertest": "^2.0.8", "msw": "^1.0.0", "supertest": "^7.0.0" - } + }, + "configSchema": "config.d.ts" } diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 368974b6ca..7f0279b437 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -48,7 +48,9 @@ import { } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; import { getUsersForEntityRef } from './getUsersForEntityRef'; -import { Config } from '@backstage/config'; +import { Config, readDurationFromConfig } from '@backstage/config'; +import { durationToMilliseconds } from '@backstage/types'; +import pThrottle from 'p-throttle'; /** @internal */ export interface RouterOptions { @@ -82,6 +84,19 @@ export async function createRouter( const WEB_NOTIFICATION_CHANNEL = 'Web'; const store = await DatabaseNotificationsStore.create({ database }); const frontendBaseUrl = config.getString('app.baseUrl'); + const concurrencyLimit = + config.getOptionalNumber('notifications.concurrencyLimit') ?? 10; + const throttleInterval = config.has('notifications.throttleInterval') + ? durationToMilliseconds( + readDurationFromConfig(config, { + key: 'notifications.throttleInterval', + }), + ) + : 50; + const throttle = pThrottle({ + limit: concurrencyLimit, + interval: throttleInterval, + }); const getUser = async (req: Request) => { const credentials = await httpAuth.credentials(req, { allow: ['user'] }); @@ -470,71 +485,84 @@ export async function createRouter( }, channel: 'notifications', }); - postProcessNotification(ret, opts); } + postProcessNotification(ret, opts); return notification; }; + const sendUserNotification = async ( + baseNotification: Omit, + user: string, + opts: NotificationSendOptions, + origin: string, + scope?: string, + ): Promise => { + const userNotification = { + ...baseNotification, + id: uuid(), + user, + }; + const notification = await preProcessNotification(userNotification, opts); + + const enabled = await isNotificationsEnabled({ + user, + channel: WEB_NOTIFICATION_CHANNEL, + origin: userNotification.origin, + }); + + let ret = notification; + + if (!enabled) { + postProcessNotification(ret, opts); + return undefined; + } + + let existingNotification; + if (scope) { + existingNotification = await store.getExistingScopeNotification({ + user, + scope, + origin, + }); + } + + if (existingNotification) { + const restored = await store.restoreExistingNotification({ + id: existingNotification.id, + notification, + }); + ret = restored ?? notification; + } else { + await store.saveNotification(notification); + } + + if (signals) { + await signals.publish({ + recipients: { type: 'user', entityRef: [user] }, + message: { + action: 'new_notification', + notification_id: ret.id, + }, + channel: 'notifications', + }); + } + postProcessNotification(ret, opts); + return ret; + }; + const sendUserNotifications = async ( baseNotification: Omit, users: string[], opts: NotificationSendOptions, origin: string, - ) => { - const notifications = []; + ): Promise => { const { scope } = opts.payload; const uniqueUsers = [...new Set(users)]; - for (const user of uniqueUsers) { - const userNotification = { - ...baseNotification, - id: uuid(), - user, - }; - const notification = await preProcessNotification(userNotification, opts); - - const enabled = await isNotificationsEnabled({ - user, - channel: WEB_NOTIFICATION_CHANNEL, - origin: userNotification.origin, - }); - - let ret = notification; - if (enabled) { - let existingNotification; - if (scope) { - existingNotification = await store.getExistingScopeNotification({ - user, - scope, - origin, - }); - } - - if (existingNotification) { - const restored = await store.restoreExistingNotification({ - id: existingNotification.id, - notification, - }); - ret = restored ?? notification; - } else { - await store.saveNotification(notification); - } - - notifications.push(ret); - - if (signals) { - await signals.publish({ - recipients: { type: 'user', entityRef: [user] }, - message: { - action: 'new_notification', - notification_id: ret.id, - }, - channel: 'notifications', - }); - } - } - postProcessNotification(ret, opts); - } - return notifications; + const throttled = throttle((user: string) => + sendUserNotification(baseNotification, user, opts, origin, scope), + ); + const sent = await Promise.all(uniqueUsers.map(user => throttled(user))); + return sent.filter(n => n !== undefined); }; const createNotificationHandler = async ( diff --git a/yarn.lock b/yarn.lock index 0ea358c567..1f38a972bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7053,12 +7053,14 @@ __metadata: "@backstage/plugin-notifications-node": "workspace:^" "@backstage/plugin-signals-backend": "workspace:^" "@backstage/plugin-signals-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 knex: ^3.0.0 msw: ^1.0.0 + p-throttle: ^4.1.1 supertest: ^7.0.0 uuid: ^11.0.0 winston: ^3.2.1 From 4b622bf795e853763da97adab37807b6b70fa1cb Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Sat, 22 Mar 2025 07:17:52 +0530 Subject: [PATCH 18/41] feat(catalog-import): adding translation Signed-off-by: its-mitesh-kumar --- .../DefaultImportPage/DefaultImportPage.tsx | 5 +++- .../ImportInfoCard/ImportInfoCard.test.tsx | 17 +++++++---- .../ImportInfoCard/ImportInfoCard.tsx | 5 +++- plugins/catalog-import/src/translation.ts | 28 +++++++++++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 plugins/catalog-import/src/translation.ts diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index fa89ac2caf..cdff61fe73 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -28,6 +28,8 @@ import { useTheme } from '@material-ui/core/styles'; import React from 'react'; import { ImportInfoCard } from '../ImportInfoCard'; import { ImportStepper } from '../ImportStepper'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogImportTranslationRef } from '../../translation'; /** * The default catalog import page. @@ -35,6 +37,7 @@ import { ImportStepper } from '../ImportStepper'; * @public */ export const DefaultImportPage = () => { + const { t } = useTranslationRef(catalogImportTranslationRef); const theme = useTheme(); const configApi = useApi(configApiRef); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); @@ -54,7 +57,7 @@ export const DefaultImportPage = () => { return ( -
+
{supportTitle} diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx index c33de77ffd..caa6be223d 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx @@ -25,6 +25,8 @@ import { screen } from '@testing-library/react'; import React from 'react'; import { CatalogImportApi, catalogImportApiRef } from '../../api'; import { ImportInfoCard } from './ImportInfoCard'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogImportTranslationRef } from '../../translation'; describe('', () => { let apis: TestApiRegistry; @@ -50,6 +52,14 @@ describe('', () => { }); it('renders without exploding', async () => { + let translatedText = ''; + + const TestComponent = () => { + const { t } = useTranslationRef(catalogImportTranslationRef); + translatedText = t('importInfoCard.title'); + return ; + }; + await renderInTestApp( ', () => { [catalogImportApiRef, catalogImportApi], ]} > - + , ); - - expect( - screen.getByText('Register an existing component'), - ).toBeInTheDocument(); + expect(screen.getByText(translatedText)).toBeInTheDocument(); }); it('renders section on GitHub discovery if supported', async () => { diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx index be8a457572..c0790006e5 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx @@ -21,6 +21,8 @@ import Typography from '@material-ui/core/Typography'; import React from 'react'; import { catalogImportApiRef } from '../../api'; import { useCatalogFilename } from '../../hooks'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogImportTranslationRef } from '../../translation'; /** * Props for {@link ImportInfoCard}. @@ -43,6 +45,7 @@ export const ImportInfoCard = (props: ImportInfoCardProps) => { exampleRepositoryUrl = 'https://github.com/backstage/backstage', } = props; + const { t } = useTranslationRef(catalogImportTranslationRef); const configApi = useApi(configApiRef); const appTitle = configApi.getOptionalString('app.title') || 'Backstage'; const catalogImportApi = useApi(catalogImportApiRef); @@ -53,7 +56,7 @@ export const ImportInfoCard = (props: ImportInfoCardProps) => { return ( Date: Sat, 22 Mar 2025 07:22:03 +0530 Subject: [PATCH 19/41] adding changeset file Signed-off-by: its-mitesh-kumar --- .changeset/moody-eagles-smile.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/moody-eagles-smile.md diff --git a/.changeset/moody-eagles-smile.md b/.changeset/moody-eagles-smile.md new file mode 100644 index 0000000000..a2aab46cd8 --- /dev/null +++ b/.changeset/moody-eagles-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': minor +--- + +adding translation for `Register an existing component` text From f7cb5383e926b1d6f59fce9afafc8bb926339a55 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 25 Mar 2025 20:57:09 +0100 Subject: [PATCH 20/41] canon: prop extraction fixes Signed-off-by: Patrik Oldsberg --- .changeset/famous-eggs-dance.md | 5 +++ packages/canon/src/utils/extractProps.ts | 54 ++++++++++++++++-------- 2 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 .changeset/famous-eggs-dance.md diff --git a/.changeset/famous-eggs-dance.md b/.changeset/famous-eggs-dance.md new file mode 100644 index 0000000000..8293746402 --- /dev/null +++ b/.changeset/famous-eggs-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Internal refactor and fixes to the prop extraction logic for layout components. diff --git a/packages/canon/src/utils/extractProps.ts b/packages/canon/src/utils/extractProps.ts index 8631f4e228..1668117909 100644 --- a/packages/canon/src/utils/extractProps.ts +++ b/packages/canon/src/utils/extractProps.ts @@ -14,6 +14,16 @@ * limitations under the License. */ +type BasePropDef = { + type: string; + values?: readonly unknown[]; + default?: unknown; + required?: boolean; + className?: string; + responsive?: true; + customProperties?: string[]; +}; + export function extractProps( props: { className?: string; @@ -22,45 +32,53 @@ export function extractProps( as?: keyof JSX.IntrinsicElements; [key: string]: any; }, - propDefs: { [key: string]: any }, + propDefs: { [name in string]: BasePropDef }, ) { let className: string[] = (props.className || '').split(' '); let style: React.CSSProperties = { ...props.style }; - const hasProp = (key: string) => props.hasOwnProperty(key); for (const key in propDefs) { + const propDef = propDefs[key]; + // Check if the prop is present or has a default value - if (!hasProp(key) && !propDefs[key].hasOwnProperty('default')) { + if (!Object.hasOwn(props, key) && !propDef.hasOwnProperty('default')) { continue; // Skip processing if neither is present } - const value = hasProp(key) ? props[key] : propDefs[key].default; - const propDefsValues = propDefs[key].values; - const propDefsCustomProperties = propDefs[key].customProperties; - const propDefsClassName = propDefs[key].className; - const isResponsive = propDefs[key].responsive; + const value = Object.hasOwn(props, key) + ? (props[key] as unknown) + : propDefs[key].default; + const propDefsValues = propDef.values; + const propDefsCustomProperties = propDef.customProperties; + const propDefsClassName = propDef.className; + const isResponsive = propDef.responsive; - const handleValue = (val: string, prefix: string = '') => { + const handleValue = (val: unknown, prefix: string = '') => { // Skip adding class name if the key is "as" if (key === 'as') return; - if (propDefsValues.includes(val)) { + if (propDefsValues?.includes(val)) { className.push(`${prefix}${propDefsClassName}-${val}`); } else { - const customPropertyKey = - isResponsive && prefix - ? `${propDefsCustomProperties}-${prefix.slice(0, -1)}` - : propDefsCustomProperties; - (style as any)[customPropertyKey] = val; + if (propDefsCustomProperties) { + for (const customProperty of propDefsCustomProperties) { + const customPropertyKey = + isResponsive && prefix + ? `${customProperty}-${prefix.slice(0, -1)}` + : customProperty; + style[customPropertyKey as keyof typeof style] = val as any; + } + } className.push(`${prefix}${propDefsClassName}`); } }; - if (isResponsive && typeof value === 'object') { + if (isResponsive && typeof value === 'object' && value !== null) { + const breakpointValues = value as { [key: string]: unknown }; // Handle responsive object values - for (const breakpoint in value) { + for (const breakpoint in breakpointValues) { const prefix = breakpoint === 'initial' ? '' : `${breakpoint}:`; - handleValue(value[breakpoint], prefix); + handleValue(breakpointValues[breakpoint], prefix); } } else { handleValue(value); From 5d7bad4920956ce0e8d51efa8efb59a18da20d36 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 25 Mar 2025 22:04:26 +0100 Subject: [PATCH 21/41] core-components: fix AlertDisplay messaging Signed-off-by: Patrik Oldsberg --- .changeset/funny-lizards-clean.md | 5 +++++ packages/core-components/src/translation.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/funny-lizards-clean.md diff --git a/.changeset/funny-lizards-clean.md b/.changeset/funny-lizards-clean.md new file mode 100644 index 0000000000..7c85e28188 --- /dev/null +++ b/.changeset/funny-lizards-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fixed the messaging in the `AlertDisplay` where it was claiming that there were older messages available rather than newer. diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index 13bd62bab8..40b6e94798 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -106,8 +106,8 @@ export const coreComponentsTranslationRef = createTranslationRef({ }, }, alertDisplay: { - message_one: '({{ count }} older message)', - message_other: '({{ count }} older messages)', + message_one: '({{ count }} newer message)', + message_other: '({{ count }} newer messages)', }, autoLogout: { stillTherePrompt: { From 552170ddad461854c5de9731720ac28b1cd98c3f Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Wed, 26 Mar 2025 09:26:16 -0700 Subject: [PATCH 22/41] Add Slack Notification Processor Signed-off-by: Adam Kunicki --- .changeset/light-cameras-fold.md | 5 + docs/notifications/processors.md | 30 +- .../.eslintrc.js | 1 + .../README.md | 56 +++ .../catalog-info.yaml | 10 + .../config.d.ts | 28 ++ .../package.json | 61 +++ .../report.api.md | 20 + .../src/index.ts | 28 ++ .../lib/SlackNotificationProcessor.test.ts | 285 +++++++++++++ .../src/lib/SlackNotificationProcessor.ts | 285 +++++++++++++ .../src/lib/constants.ts | 33 ++ .../src/lib/index.ts | 18 + .../src/lib/types.ts | 20 + .../src/lib/util.ts | 92 ++++ .../src/module.ts | 58 +++ yarn.lock | 392 ++++++++++++++++-- 17 files changed, 1384 insertions(+), 38 deletions(-) create mode 100644 .changeset/light-cameras-fold.md create mode 100644 plugins/notifications-backend-module-slack/.eslintrc.js create mode 100644 plugins/notifications-backend-module-slack/README.md create mode 100644 plugins/notifications-backend-module-slack/catalog-info.yaml create mode 100644 plugins/notifications-backend-module-slack/config.d.ts create mode 100644 plugins/notifications-backend-module-slack/package.json create mode 100644 plugins/notifications-backend-module-slack/report.api.md create mode 100644 plugins/notifications-backend-module-slack/src/index.ts create mode 100644 plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts create mode 100644 plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts create mode 100644 plugins/notifications-backend-module-slack/src/lib/constants.ts create mode 100644 plugins/notifications-backend-module-slack/src/lib/index.ts create mode 100644 plugins/notifications-backend-module-slack/src/lib/types.ts create mode 100644 plugins/notifications-backend-module-slack/src/lib/util.ts create mode 100644 plugins/notifications-backend-module-slack/src/module.ts diff --git a/.changeset/light-cameras-fold.md b/.changeset/light-cameras-fold.md new file mode 100644 index 0000000000..8ec49c5d02 --- /dev/null +++ b/.changeset/light-cameras-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-slack': minor +--- + +Added a new Slack NotificationProcessor for use with the notifications plugin diff --git a/docs/notifications/processors.md b/docs/notifications/processors.md index abee7c97c3..2d7de61ba0 100644 --- a/docs/notifications/processors.md +++ b/docs/notifications/processors.md @@ -104,4 +104,32 @@ Apart from STMP, the email processor also supports the following transmissions: - sendmail - stream (only for debugging purposes) -See more information at https://github.com/backstage/backstage/blob/master/plugins/notifications-backend-module-email/README.md +See more information at + +### Slack Processor + +Slack processor is used to send notifications to users and channels in Slack. To install the Slack processor, add the `@backstage/plugin-notifications-backend-module-slack` package to your backend. + +```bash +yarn workspace backend add @backstage/plugin-notifications-backend-module-slack +``` + +Add the Slack processor to your backend: + +```ts +import { createBackend } from '@backstage/plugin-notifications-backend'; +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-notifications-backend-module-slack')); +``` + +To configure the Slack processor, you need to add the following configuration to your `app-config.yaml`: + +```yaml +notifications: + processors: + slack: + - token: xoxb-XXXXXXXXX +``` + +See more information including how to configure your Slack App at diff --git a/plugins/notifications-backend-module-slack/.eslintrc.js b/plugins/notifications-backend-module-slack/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/notifications-backend-module-slack/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/notifications-backend-module-slack/README.md b/plugins/notifications-backend-module-slack/README.md new file mode 100644 index 0000000000..e64bac868c --- /dev/null +++ b/plugins/notifications-backend-module-slack/README.md @@ -0,0 +1,56 @@ +# @backstage/plugin-notifications-backend-module-slack + +The Slack backend module for the notifications plugin. + +## Getting Started + +### Module Installation + +Add the module to your backend: + +```ts +// packages/backend/src/index.ts +backend.add(import('@backstage/plugin-notifications-backend-module-slack')); +``` + +### Slack Configuration + +To use this you'll need to create a Slack App or use an existing one. It should have at least the following scopes: +`chat:write`, `users:read`, `im:write` (for direct message support). + +Additionally you may include scopes `chat:write.public` in order to send messages to public channels your app is not +a member of. + +These scopes are under OAuth & Permissions. You will also want to save the Bot User OAuth Token. This will be needed +in the following step to configure `app-config.yaml`. + +### Configure Backstage + +You'll now need to configure the Slack module in your `app-config.yaml`. + +```yaml +notifications: + processors: + slack: + - token: xoxb-XXXXXXXXX +``` + +Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send +messages to more than one Slack workspace. Org-Wide App installation is not currently supported. + +### Entity Requirements + +Entities must be annotated with one of the following annotations: + +- `slack.com/user-id` for direct messages +- `slack.com/channel-name` for public channel messages +- `slack.com/channel-id` for public or private channel messages + +Slack prefers use of ID over name and `slack.com/channel-id` is the recommended annotation. + +### Observability + +The processor includes the following counter metrics if you are exporting metrics using OpenTelemetry: + +- `notifications.processors.slack.sent.count` - The number of messages sent +- `notifications.processors.slack.error.count` - The number of messages that failed to send diff --git a/plugins/notifications-backend-module-slack/catalog-info.yaml b/plugins/notifications-backend-module-slack/catalog-info.yaml new file mode 100644 index 0000000000..327ae8239f --- /dev/null +++ b/plugins/notifications-backend-module-slack/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-notifications-backend-module-slack + title: '@backstage/plugin-notifications-backend-module-slack' + description: The slack backend module for the notifications plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/notifications-backend-module-slack/config.d.ts b/plugins/notifications-backend-module-slack/config.d.ts new file mode 100644 index 0000000000..f620f0e206 --- /dev/null +++ b/plugins/notifications-backend-module-slack/config.d.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2025 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. + */ +export interface Config { + notifications?: { + processors?: { + slack?: Array<{ + /** + * Slack Bot Token. Usually starts with `xoxb-`. + * @visibility secret + */ + token?: string; + }>; + }; + }; +} diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json new file mode 100644 index 0000000000..2bc7a8eb00 --- /dev/null +++ b/plugins/notifications-backend-module-slack/package.json @@ -0,0 +1,61 @@ +{ + "name": "@backstage/plugin-notifications-backend-module-slack", + "version": "0.0.0", + "description": "The slack backend module for the notifications plugin.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "notifications", + "pluginPackage": "@backstage/plugin-notifications-backend" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/notifications-backend-module-slack" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "config.d.ts" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-notifications-common": "workspace:^", + "@backstage/plugin-notifications-node": "workspace:^", + "@backstage/types": "workspace:^", + "@opentelemetry/api": "^1.9.0", + "@slack/bolt": "^3.21.4", + "@slack/types": "^2.14.0", + "@slack/web-api": "^7.5.0", + "dataloader": "2.2.2", + "p-throttle": "4.1.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@faker-js/faker": "^8.4.1", + "msw": "^2.2.14" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/notifications-backend-module-slack/report.api.md b/plugins/notifications-backend-module-slack/report.api.md new file mode 100644 index 0000000000..dfd2460e91 --- /dev/null +++ b/plugins/notifications-backend-module-slack/report.api.md @@ -0,0 +1,20 @@ +## API Report File for "@backstage/plugin-notifications-backend-module-slack" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @public +export const ANNOTATION_SLACK_CHANNEL_ID = 'slack.com/channel-id'; + +// @public +export const ANNOTATION_SLACK_CHANNEL_NAME = 'slack.com/channel-name'; + +// @public +export const ANNOTATION_SLACK_USER_ID = 'slack.com/user-id'; + +// @public +const notificationsModuleSlack: BackendFeature; +export default notificationsModuleSlack; +``` diff --git a/plugins/notifications-backend-module-slack/src/index.ts b/plugins/notifications-backend-module-slack/src/index.ts new file mode 100644 index 0000000000..14cde37d13 --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2025 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. + */ + +/** + * The slack backend module for the notifications plugin. + * + * @packageDocumentation + */ + +export { + ANNOTATION_SLACK_CHANNEL_ID, + ANNOTATION_SLACK_CHANNEL_NAME, + ANNOTATION_SLACK_USER_ID, +} from './lib'; +export { notificationsModuleSlack as default } from './module'; diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts new file mode 100644 index 0000000000..5396a71736 --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -0,0 +1,285 @@ +/* + * Copyright 2025 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 { mockServices } from '@backstage/backend-test-utils'; +import { SlackNotificationProcessor } from './SlackNotificationProcessor'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { WebClient } from '@slack/web-api'; +import { Entity } from '@backstage/catalog-model'; + +jest.mock('@slack/web-api', () => { + const mockSlack = { + chat: { + postMessage: jest.fn(() => ({ + ok: true, + ts: '1234567890.123456', + channel: 'C12345678', + })), + }, + conversations: { + list: jest.fn(() => ({ + ok: true, + channels: [{ id: 'C12345678', name: 'test' }], + })), + }, + users: { + list: jest.fn(() => ({ + ok: true, + members: [ + { + id: 'U12345678', + name: 'test', + profile: { email: 'test@example.com' }, + real_name: 'Test User', + is_bot: false, + is_app_user: false, + deleted: false, + }, + ], + })), + }, + }; + return { WebClient: jest.fn(() => mockSlack) }; +}); + +const DEFAULT_ENTITIES_RESPONSE = { + items: [ + { + kind: 'User', + metadata: { + name: 'mock', + namespace: 'default', + annotations: { + 'slack.com/user-id': 'U12345678', + }, + }, + spec: { + type: 'service', + owner: 'group:default/mock', + }, + } as unknown as Entity, + { + kind: 'Group', + metadata: { + name: 'mock', + namespace: 'default', + annotations: { + 'slack.com/channel-id': 'C12345678', + }, + }, + } as unknown as Entity, + ], +}; + +describe('SlackNotificationProcessor', () => { + const logger = mockServices.logger.mock(); + const auth = mockServices.auth(); + const discovery = mockServices.discovery(); + const config = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + }, + ], + }, + }, + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should send a notification to a group', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + discovery, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ + recipients: { type: 'entity', entityRef: 'group:default/mock' }, + payload: { title: 'notification' }, + }); + + expect(slack.chat.postMessage).toHaveBeenCalledWith({ + channel: 'C12345678', + text: 'notification', + attachments: [ + { + color: '#00A699', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'No description provided', + }, + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'View More', + }, + url: '', + action_id: 'button-action', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: 'Severity: normal', + emoji: true, + }, + { + type: 'plain_text', + text: 'Topic: N/A', + emoji: true, + }, + ], + }, + ], + fallback: 'notification', + }, + ], + }); + }); + + it('should send a notification to a user', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + discovery, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: 'user:default/mock', + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, + }, + { + recipients: { type: 'entity', entityRef: 'user:default/mock' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledWith({ + channel: 'U12345678', + text: 'notification', + attachments: [ + { + color: '#00A699', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'No description provided', + }, + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'View More', + }, + url: '', + action_id: 'button-action', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: 'Severity: normal', + emoji: true, + }, + { + type: 'plain_text', + text: 'Topic: N/A', + emoji: true, + }, + ], + }, + ], + fallback: 'notification', + }, + ], + }); + }); + it('should not send broadcast messages', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + discovery, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + processor.processOptions({ + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }); + processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts new file mode 100644 index 0000000000..225f2f20ae --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -0,0 +1,285 @@ +/* + * Copyright 2025 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 { + AuthService, + DiscoveryService, + LoggerService, +} from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity, parseEntityRef } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { NotFoundError } from '@backstage/errors'; +import { Notification } from '@backstage/plugin-notifications-common'; +import { + NotificationProcessor, + NotificationSendOptions, +} from '@backstage/plugin-notifications-node'; +import { durationToMilliseconds } from '@backstage/types'; +import { Counter, metrics } from '@opentelemetry/api'; +import { ChatPostMessageArguments, WebClient } from '@slack/web-api'; +import DataLoader from 'dataloader'; +import pThrottle from 'p-throttle'; +import { + ANNOTATION_SLACK_CHANNEL_ID, + ANNOTATION_SLACK_CHANNEL_NAME, + ANNOTATION_SLACK_USER_ID, +} from './constants'; +import { toChatPostMessageArgs } from './util'; + +export class SlackNotificationProcessor implements NotificationProcessor { + private readonly logger: LoggerService; + private readonly catalog: CatalogApi; + private readonly auth: AuthService; + private readonly slack: WebClient; + private readonly sendNotifications; + private readonly messagesSent: Counter; + private readonly messagesFailed: Counter; + + static fromConfig( + config: Config, + options: { + auth: AuthService; + discovery: DiscoveryService; + logger: LoggerService; + catalog: CatalogApi; + slack?: WebClient; + }, + ): SlackNotificationProcessor[] { + const slackConfig = + config.getOptionalConfigArray('notifications.processors.slack') ?? []; + return slackConfig.map(c => { + const token = c.getString('token'); + const slack = options.slack ?? new WebClient(token); + return new SlackNotificationProcessor({ + slack, + ...options, + }); + }); + } + + private constructor(options: { + slack: WebClient; + auth: AuthService; + discovery: DiscoveryService; + logger: LoggerService; + catalog: CatalogApi; + }) { + const { auth, catalog, logger, slack } = options; + this.logger = logger; + this.catalog = catalog; + this.auth = auth; + this.slack = slack; + + const meter = metrics.getMeter('default'); + this.messagesSent = meter.createCounter( + 'notifications.processors.slack.sent.count', + { + description: 'Number of messages sent to Slack successfully', + }, + ); + this.messagesFailed = meter.createCounter( + 'notifications.processors.slack.error.count', + { + description: 'Number of messages that failed to send to Slack', + }, + ); + + const throttle = pThrottle({ + limit: 10, + interval: durationToMilliseconds({ minutes: 1 }), + }); + const throttled = throttle((opts: ChatPostMessageArguments) => + this.sendNotification(opts), + ); + this.sendNotifications = async (opts: ChatPostMessageArguments[]) => { + const results = await Promise.allSettled( + opts.map(message => throttled(message)), + ); + + let successCount = 0; + let failureCount = 0; + + results.forEach(result => { + if (result.status === 'fulfilled') { + successCount++; + } else { + this.logger.error( + `Failed to send Slack channel notification: ${result.reason.message}`, + ); + failureCount++; + } + }); + + this.messagesSent.add(successCount); + this.messagesFailed.add(failureCount); + }; + } + + getName(): string { + return 'SlackNotificationProcessor'; + } + + async processOptions( + options: NotificationSendOptions, + ): Promise { + if (options.recipients.type !== 'entity') { + return options; + } + + const entityRefs = [options.recipients.entityRef].flat(); + + const outbound: ChatPostMessageArguments[] = []; + await Promise.all( + entityRefs.map(async entityRef => { + const compoundEntityRef = parseEntityRef(entityRef); + // skip users as they are sent direct messages, but allow all other entity kinds + // to have a channel id annotation. + if (compoundEntityRef.kind === 'user') { + return; + } + + let channel; + try { + channel = await this.getChannelId(entityRef); + } catch (error) { + this.logger.error( + `Failed to get Slack channel for entity: ${ + (error as Error).message + }`, + ); + return; + } + + if (!channel) { + this.logger.debug(`No Slack channel found for entity: ${entityRef}`); + return; + } + + this.logger.debug( + `Sending notification with payload: ${JSON.stringify( + options.payload, + )}`, + ); + + const payload = toChatPostMessageArgs({ + channel, + payload: options.payload, + }); + + this.logger.debug( + `Sending Slack channel notification: ${JSON.stringify(payload)}`, + ); + outbound.push(payload); + }), + ); + + console.log('dispatching message'); + await this.sendNotifications(outbound); + + return options; + } + + async postProcess( + notification: Notification, + options: NotificationSendOptions, + ): Promise { + if (options.recipients.type === 'broadcast' || !notification.user) { + return; + } + + const entityRefs = [options.recipients.entityRef].flat(); + if (entityRefs.some(e => parseEntityRef(e).kind === 'group')) { + // We've already dispatched a slack channel message, so let's not send a DM. + return; + } + + const destination = await this.getSlackUserId(notification.user); + if (!destination) { + this.logger.error(`No email found for user entity: ${notification.user}`); + return; + } + + const payload = toChatPostMessageArgs({ + channel: destination, + payload: options.payload, + }); + + this.logger.debug(`Sending DM notification: ${JSON.stringify(payload)}`); + + // batch it up + await this.sendNotifications([payload]); + } + + async getEntities( + entityRefs: readonly string[], + ): Promise<(Entity | undefined)[]> { + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + + const response = await this.catalog.getEntitiesByRefs( + { + entityRefs: entityRefs.slice(), + fields: [ + `metadata.annotations.${ANNOTATION_SLACK_CHANNEL_NAME}`, + `metadata.annotations.${ANNOTATION_SLACK_CHANNEL_ID}`, + `metadata.annotations.${ANNOTATION_SLACK_USER_ID}`, + ], + }, + { + token, + }, + ); + + return response.items; + } + + async getSlackUserId(entityRef: string): Promise { + const entityLoader = new DataLoader( + entityRefs => this.getEntities(entityRefs), + ); + const entity = await entityLoader.load(entityRef); + + return entity?.metadata?.annotations?.[ANNOTATION_SLACK_USER_ID]; + } + + async getChannelId(entityRef: string): Promise { + const entityLoader = new DataLoader( + entityRefs => this.getEntities(entityRefs), + ); + const entity = await entityLoader.load(entityRef); + + if (!entity) { + console.log(`Entity not found: ${entityRef}`); + throw new NotFoundError(`Entity not found: ${entityRef}`); + } + + return ( + entity?.metadata?.annotations?.[ANNOTATION_SLACK_CHANNEL_ID] || + entity?.metadata?.annotations?.[ANNOTATION_SLACK_CHANNEL_NAME] + ); + } + + async sendNotification(args: ChatPostMessageArguments): Promise { + const response = await this.slack.chat.postMessage(args); + + if (!response.ok) { + throw new Error(`Failed to send notification: ${response.error}`); + } + } +} diff --git a/plugins/notifications-backend-module-slack/src/lib/constants.ts b/plugins/notifications-backend-module-slack/src/lib/constants.ts new file mode 100644 index 0000000000..24c1868d55 --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/lib/constants.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2025 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. + */ + +/** + * @public + * The annotation key for the entity's Slack user ID + */ +export const ANNOTATION_SLACK_USER_ID = 'slack.com/user-id'; + +/** + * @public + * The annotation key for the entity's Slack channel name. + */ +export const ANNOTATION_SLACK_CHANNEL_NAME = 'slack.com/channel-name'; + +/** + * @public + * The annotation key for the entity's Slack channel ID. + */ +export const ANNOTATION_SLACK_CHANNEL_ID = 'slack.com/channel-id'; diff --git a/plugins/notifications-backend-module-slack/src/lib/index.ts b/plugins/notifications-backend-module-slack/src/lib/index.ts new file mode 100644 index 0000000000..97835063b3 --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/lib/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 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. + */ + +export { SlackNotificationProcessor } from './SlackNotificationProcessor'; +export * from './constants'; diff --git a/plugins/notifications-backend-module-slack/src/lib/types.ts b/plugins/notifications-backend-module-slack/src/lib/types.ts new file mode 100644 index 0000000000..bd87e9656c --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/lib/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2025 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. + */ + +export interface SlackNotificationOptions { + url: string; + payload: string; +} diff --git a/plugins/notifications-backend-module-slack/src/lib/util.ts b/plugins/notifications-backend-module-slack/src/lib/util.ts new file mode 100644 index 0000000000..e39701d88a --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/lib/util.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2025 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 { + NotificationPayload, + NotificationSeverity, +} from '@backstage/plugin-notifications-common'; +import { ChatPostMessageArguments, KnownBlock } from '@slack/web-api'; + +export function toChatPostMessageArgs(options: { + channel: string; + payload: NotificationPayload; +}): ChatPostMessageArguments { + const { channel, payload } = options; + + const args: ChatPostMessageArguments = { + channel, + text: payload.title, + attachments: [ + { + color: getColor(payload.severity), + blocks: toSlackBlockKit(payload), + fallback: payload.title, + }, + ], + }; + + return args; +} + +export function toSlackBlockKit(payload: NotificationPayload): KnownBlock[] { + const { description, link, severity, topic } = payload; + return [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: description ?? 'No description provided', + }, + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'View More', + }, + url: link ?? '', + action_id: 'button-action', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: `Severity: ${severity ?? 'normal'}`, + emoji: true, + }, + { + type: 'plain_text', + text: `Topic: ${topic ?? 'N/A'}`, + emoji: true, + }, + ], + }, + ]; +} + +function getColor(severity: NotificationSeverity | undefined) { + switch (severity) { + case 'critical': + return '#FF0000'; // Red + case 'high': + return '#FFA500'; // Orange + case 'low': + case 'normal': + default: + return '#00A699'; // Neutral color + } +} diff --git a/plugins/notifications-backend-module-slack/src/module.ts b/plugins/notifications-backend-module-slack/src/module.ts new file mode 100644 index 0000000000..cb096dabba --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/module.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 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 { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { CatalogClient } from '@backstage/catalog-client'; +import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; +import { SlackNotificationProcessor } from './lib/SlackNotificationProcessor'; + +/** + * The Slack notification processor for use with the notifications plugin. + * This allows sending of notifications via Slack DMs or to channels. + * + * @public + */ +export const notificationsModuleSlack = createBackendModule({ + pluginId: 'notifications', + moduleId: 'slack', + register(reg) { + reg.registerInit({ + deps: { + auth: coreServices.auth, + config: coreServices.rootConfig, + discovery: coreServices.discovery, + logger: coreServices.logger, + notifications: notificationsProcessingExtensionPoint, + }, + async init({ auth, config, discovery, logger, notifications }) { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + + notifications.addProcessor( + SlackNotificationProcessor.fromConfig(config, { + auth, + discovery, + logger, + catalog: catalogClient, + }), + ); + }, + }); + }, +}); diff --git a/yarn.lock b/yarn.lock index 648c69d5cc..0881f8546f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6779,6 +6779,33 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-notifications-backend-module-slack@workspace:plugins/notifications-backend-module-slack": + version: 0.0.0-use.local + resolution: "@backstage/plugin-notifications-backend-module-slack@workspace:plugins/notifications-backend-module-slack" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-notifications-common": "workspace:^" + "@backstage/plugin-notifications-node": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/types": "workspace:^" + "@faker-js/faker": ^8.4.1 + "@opentelemetry/api": ^1.9.0 + "@slack/bolt": ^3.21.4 + "@slack/types": ^2.14.0 + "@slack/web-api": ^7.5.0 + dataloader: 2.2.2 + msw: ^2.2.14 + p-throttle: 4.1.1 + languageName: unknown + linkType: soft + "@backstage/plugin-notifications-backend@workspace:^, @backstage/plugin-notifications-backend@workspace:plugins/notifications-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-notifications-backend@workspace:plugins/notifications-backend" @@ -9644,6 +9671,13 @@ __metadata: languageName: node linkType: hard +"@faker-js/faker@npm:^8.4.1": + version: 8.4.1 + resolution: "@faker-js/faker@npm:8.4.1" + checksum: d802d531f8929562715adc279cfec763c9a4bc596ec67b0ce43fd0ae61b285d2b0eec6f1f4aa852452a63721a842fe7e81926dce7bd92acca94b01e2a1f55f5a + languageName: node + linkType: hard + "@fastify/busboy@npm:^2.0.0": version: 2.1.1 resolution: "@fastify/busboy@npm:2.1.1" @@ -16403,6 +16437,121 @@ __metadata: languageName: node linkType: hard +"@slack/bolt@npm:^3.21.4": + version: 3.22.0 + resolution: "@slack/bolt@npm:3.22.0" + dependencies: + "@slack/logger": ^4.0.0 + "@slack/oauth": ^2.6.3 + "@slack/socket-mode": ^1.3.6 + "@slack/types": ^2.13.0 + "@slack/web-api": ^6.13.0 + "@types/express": ^4.16.1 + "@types/promise.allsettled": ^1.0.3 + "@types/tsscmp": ^1.0.0 + axios: ^1.7.4 + express: ^4.21.0 + path-to-regexp: ^8.1.0 + promise.allsettled: ^1.0.2 + raw-body: ^2.3.3 + tsscmp: ^1.0.6 + checksum: edd5c7cf658808effde87c936f19a0cc2b7d49ac97471651f2b1bb3db0074b92dc8ad3c9657577105d93c48df9ba16c382902c0d90082854cbbe86bfc7753827 + languageName: node + linkType: hard + +"@slack/logger@npm:^3.0.0": + version: 3.0.0 + resolution: "@slack/logger@npm:3.0.0" + dependencies: + "@types/node": ">=12.0.0" + checksum: 6512d0e9e4be47ea465705ab9b6e6901f36fa981da0d4a657fde649d452b567b351002049b5ee0a22569b5119bf6c2f61befd5b8022d878addb7a99c91b03389 + languageName: node + linkType: hard + +"@slack/logger@npm:^4.0.0": + version: 4.0.0 + resolution: "@slack/logger@npm:4.0.0" + dependencies: + "@types/node": ">=18.0.0" + checksum: dc79e9d2032c4bf9ce01d96cc72882f003dd376d036f172d4169662cfc2c9b384a80d5546b06021578dd473e7059f064303f0ba851eeb153387f2081a1e3062e + languageName: node + linkType: hard + +"@slack/oauth@npm:^2.6.3": + version: 2.6.3 + resolution: "@slack/oauth@npm:2.6.3" + dependencies: + "@slack/logger": ^3.0.0 + "@slack/web-api": ^6.12.1 + "@types/jsonwebtoken": ^8.3.7 + "@types/node": ">=12" + jsonwebtoken: ^9.0.0 + lodash.isstring: ^4.0.1 + checksum: 6b556da01bd2b026177b4074cd44bdeff00165fb4297ef8f350035ca79ababfff0c0993a297a46ab742bb97469c6c1c8f5790c328ecf6370290fe31014ba3c5e + languageName: node + linkType: hard + +"@slack/socket-mode@npm:^1.3.6": + version: 1.3.6 + resolution: "@slack/socket-mode@npm:1.3.6" + dependencies: + "@slack/logger": ^3.0.0 + "@slack/web-api": ^6.12.1 + "@types/node": ">=12.0.0" + "@types/ws": ^7.4.7 + eventemitter3: ^5 + finity: ^0.5.4 + ws: ^7.5.3 + checksum: a84c15a6d25a21f76258d1ccebeec1d78b0a0dac0b02ffdfcb3596e7acda5459e4b99a42207eab7e57bed7a2a1d85ac173adf5e07aa66949eac9cc9df3b43947 + languageName: node + linkType: hard + +"@slack/types@npm:^2.11.0, @slack/types@npm:^2.13.0, @slack/types@npm:^2.14.0, @slack/types@npm:^2.9.0": + version: 2.14.0 + resolution: "@slack/types@npm:2.14.0" + checksum: fbef74d50d0de8f16125f7178bd2e664a69eeefd827b09c6f78153957278fc4400049685c756076e7dbcabd04c22730ac783bcc5d36fd588c7749d35f02c2afd + languageName: node + linkType: hard + +"@slack/web-api@npm:^6.12.1, @slack/web-api@npm:^6.13.0": + version: 6.13.0 + resolution: "@slack/web-api@npm:6.13.0" + dependencies: + "@slack/logger": ^3.0.0 + "@slack/types": ^2.11.0 + "@types/is-stream": ^1.1.0 + "@types/node": ">=12.0.0" + axios: ^1.7.4 + eventemitter3: ^3.1.0 + form-data: ^2.5.0 + is-electron: 2.2.2 + is-stream: ^1.1.0 + p-queue: ^6.6.1 + p-retry: ^4.0.0 + checksum: 77f0d506bbb011ae43d322e5152e8b1ec2b88aa01256da6b3c9ff8ce106d2284f887cad2d9f044e0fe34dc865d60f2bce1c6bb5c4117150ff71a7ef341f5dfeb + languageName: node + linkType: hard + +"@slack/web-api@npm:^7.5.0": + version: 7.8.0 + resolution: "@slack/web-api@npm:7.8.0" + dependencies: + "@slack/logger": ^4.0.0 + "@slack/types": ^2.9.0 + "@types/node": ">=18.0.0" + "@types/retry": 0.12.0 + axios: ^1.7.8 + eventemitter3: ^5.0.1 + form-data: ^4.0.0 + is-electron: 2.2.2 + is-stream: ^2 + p-queue: ^6 + p-retry: ^4 + retry: ^0.13.1 + checksum: d76fcb6cfe8a8ebdaf71aaee7dfef54f3ce4fb5958f9f841bcba095097349f649c47e5e79adbaabbd4051fe0c8d6d96445adeeab3d67416c161ae20868f637f0 + languageName: node + linkType: hard + "@smithy/abort-controller@npm:^3.1.2, @smithy/abort-controller@npm:^3.1.9": version: 3.1.9 resolution: "@smithy/abort-controller@npm:3.1.9" @@ -19452,7 +19601,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6": +"@types/express@npm:*, @types/express@npm:^4.16.1, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -19614,6 +19763,15 @@ __metadata: languageName: node linkType: hard +"@types/is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "@types/is-stream@npm:1.1.0" + dependencies: + "@types/node": "*" + checksum: 23fcb06cd8adc0124d4c44071bd4b447c41f5e4c2eccb6166789c7fc0992b566e2e8b628a3800ff4472b686d9085adbec203925068bf72e350e085650e83adec + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.1 resolution: "@types/istanbul-lib-coverage@npm:2.0.1" @@ -19732,6 +19890,15 @@ __metadata: languageName: node linkType: hard +"@types/jsonwebtoken@npm:^8.3.7": + version: 8.5.9 + resolution: "@types/jsonwebtoken@npm:8.5.9" + dependencies: + "@types/node": "*" + checksum: 33815ab02d1371b423118316b7706d2f2ec03eeee5e1494be72da50425d2384e5e0a09ea193f7a5ab4b4f6a9c5847147305f50e965f3d927a95bdf8adb471b2a + languageName: node + linkType: hard + "@types/jsonwebtoken@npm:^9.0.0": version: 9.0.0 resolution: "@types/jsonwebtoken@npm:9.0.0" @@ -19940,7 +20107,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^22.0.0": +"@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:^22.0.0": version: 22.13.10 resolution: "@types/node@npm:22.13.10" dependencies: @@ -20167,6 +20334,13 @@ __metadata: languageName: node linkType: hard +"@types/promise.allsettled@npm:^1.0.3": + version: 1.0.6 + resolution: "@types/promise.allsettled@npm:1.0.6" + checksum: 07dca8da25b49c0dc323201095552d86159483dc910dc61c345357c9c196b8498e6be4bf260cc2a9a539a725108df61b53db1d82723ed9886bb7c72fedd65f14 + languageName: node + linkType: hard + "@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.3": version: 15.7.14 resolution: "@types/prop-types@npm:15.7.14" @@ -20371,6 +20545,13 @@ __metadata: languageName: node linkType: hard +"@types/retry@npm:0.12.0": + version: 0.12.0 + resolution: "@types/retry@npm:0.12.0" + checksum: 61a072c7639f6e8126588bf1eb1ce8835f2cb9c2aba795c4491cf6310e013267b0c8488039857c261c387e9728c1b43205099223f160bb6a76b4374f741b5603 + languageName: node + linkType: hard + "@types/retry@npm:0.12.2": version: 0.12.2 resolution: "@types/retry@npm:0.12.2" @@ -20666,6 +20847,13 @@ __metadata: languageName: node linkType: hard +"@types/tsscmp@npm:^1.0.0": + version: 1.0.2 + resolution: "@types/tsscmp@npm:1.0.2" + checksum: c02c0bb9f14f550947fea9fa6f9f3c28e6b2d47a6d049a5450ed466fb0c8a685b6ff37d070d4c43d930a5affc9d828f5e16e35cde1e734de228ffd2df76ac2a8 + languageName: node + linkType: hard + "@types/unist@npm:*, @types/unist@npm:^2.0.0": version: 2.0.6 resolution: "@types/unist@npm:2.0.6" @@ -20742,6 +20930,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^7.4.7": + version: 7.4.7 + resolution: "@types/ws@npm:7.4.7" + dependencies: + "@types/node": "*" + checksum: b4c9b8ad209620c9b21e78314ce4ff07515c0cadab9af101c1651e7bfb992d7fd933bd8b9c99d110738fd6db523ed15f82f29f50b45510288da72e964dedb1a3 + languageName: node + linkType: hard + "@types/xml-encryption@npm:^1.2.4": version: 1.2.4 resolution: "@types/xml-encryption@npm:1.2.4" @@ -22947,6 +23144,21 @@ __metadata: languageName: node linkType: hard +"array.prototype.map@npm:^1.0.5": + version: 1.0.8 + resolution: "array.prototype.map@npm:1.0.8" + dependencies: + call-bind: ^1.0.8 + call-bound: ^1.0.3 + define-properties: ^1.2.1 + es-abstract: ^1.23.6 + es-array-method-boxes-properly: ^1.0.0 + es-object-atoms: ^1.0.0 + is-string: ^1.1.1 + checksum: df321613636ec8461965d72421569ece78f269460535ced5ec88db9aaa4fc58a9f26e597d72e726f105c55fa4b4b6db0d3156489dc13dfbc7a098b4f1d17b5ab + languageName: node + linkType: hard + "array.prototype.tosorted@npm:^1.1.4": version: 1.1.4 resolution: "array.prototype.tosorted@npm:1.1.4" @@ -23301,7 +23513,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.0.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.7.7": +"axios@npm:^1.0.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.7.7, axios@npm:^1.7.8": version: 1.8.4 resolution: "axios@npm:1.8.4" dependencies: @@ -24345,13 +24557,13 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1": - version: 1.0.1 - resolution: "call-bind-apply-helpers@npm:1.0.1" +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: es-errors: ^1.3.0 function-bind: ^1.1.2 - checksum: 3c55343261bb387c58a4762d15ad9d42053659a62681ec5eb50690c6b52a4a666302a01d557133ce6533e8bd04530ee3b209f23dd06c9577a1925556f8fcccdf + checksum: b2863d74fcf2a6948221f65d95b91b4b2d90cfe8927650b506141e669f7d5de65cea191bf788838bc40d13846b7886c5bc5c84ab96c3adbcf88ad69a72fcdc6b languageName: node linkType: hard @@ -26420,6 +26632,13 @@ __metadata: languageName: node linkType: hard +"dataloader@npm:2.2.2": + version: 2.2.2 + resolution: "dataloader@npm:2.2.2" + checksum: 4dabd247089c29f194e94d5434d504f99156c5c214a03463c20f3f17f40398d7e179edee69a27c16e315519ac8739042a810090087ae26449a0e685156a02c65 + languageName: node + linkType: hard + "dataloader@npm:^2.0.0, dataloader@npm:^2.2.2": version: 2.2.3 resolution: "dataloader@npm:2.2.3" @@ -27649,6 +27868,13 @@ __metadata: languageName: node linkType: hard +"es-array-method-boxes-properly@npm:^1.0.0": + version: 1.0.0 + resolution: "es-array-method-boxes-properly@npm:1.0.0" + checksum: 2537fcd1cecf187083890bc6f5236d3a26bf39237433587e5bf63392e88faae929dbba78ff0120681a3f6f81c23fe3816122982c160d63b38c95c830b633b826 + languageName: node + linkType: hard + "es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1": version: 1.0.1 resolution: "es-define-property@npm:1.0.1" @@ -27663,6 +27889,23 @@ __metadata: languageName: node linkType: hard +"es-get-iterator@npm:^1.0.2": + version: 1.1.3 + resolution: "es-get-iterator@npm:1.1.3" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.1.3 + has-symbols: ^1.0.3 + is-arguments: ^1.1.1 + is-map: ^2.0.2 + is-set: ^2.0.2 + is-string: ^1.0.7 + isarray: ^2.0.5 + stop-iteration-iterator: ^1.0.0 + checksum: 8fa118da42667a01a7c7529f8a8cca514feeff243feec1ce0bb73baaa3514560bd09d2b3438873cf8a5aaec5d52da248131de153b28e2638a061b6e4df13267d + languageName: node + linkType: hard + "es-iterator-helpers@npm:^1.2.1": version: 1.2.1 resolution: "es-iterator-helpers@npm:1.2.1" @@ -27694,12 +27937,12 @@ __metadata: languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0": - version: 1.0.0 - resolution: "es-object-atoms@npm:1.0.0" +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" dependencies: es-errors: ^1.3.0 - checksum: 26f0ff78ab93b63394e8403c353842b2272836968de4eafe97656adfb8a7c84b9099bf0fe96ed58f4a4cddc860f6e34c77f91649a58a5daa4a9c40b902744e3c + checksum: 214d3767287b12f36d3d7267ef342bbbe1e89f899cfd67040309fc65032372a8e60201410a99a1645f2f90c1912c8c49c8668066f6bdd954bcd614dda2e3da97 languageName: node linkType: hard @@ -28540,7 +28783,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^5.0.1": +"eventemitter3@npm:^5, eventemitter3@npm:^5.0.1": version: 5.0.1 resolution: "eventemitter3@npm:5.0.1" checksum: 543d6c858ab699303c3c32e0f0f47fc64d360bf73c3daf0ac0b5079710e340d6fe9f15487f94e66c629f5f82cd1a8678d692f3dbb6f6fcd1190e1b97fcad36f8 @@ -28985,7 +29228,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.14.0, express@npm:^4.17.1, express@npm:^4.18.1, express@npm:^4.18.2, express@npm:^4.19.2, express@npm:^4.21.2": +"express@npm:^4.14.0, express@npm:^4.17.1, express@npm:^4.18.1, express@npm:^4.18.2, express@npm:^4.19.2, express@npm:^4.21.0, express@npm:^4.21.2": version: 4.21.2 resolution: "express@npm:4.21.2" dependencies: @@ -29546,6 +29789,13 @@ __metadata: languageName: node linkType: hard +"finity@npm:^0.5.4": + version: 0.5.4 + resolution: "finity@npm:0.5.4" + checksum: eeea74de356ba963231108c3f8e2de44b4114497389121d603f8c3e8316b8d0772ff06b731af08ef5d6ca6b0e3a0fffab452122eca48837a98a2f7e5548b6be2 + languageName: node + linkType: hard + "first-chunk-stream@npm:^2.0.0": version: 2.0.0 resolution: "first-chunk-stream@npm:2.0.0" @@ -30132,21 +30382,21 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": - version: 1.2.6 - resolution: "get-intrinsic@npm:1.2.6" +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" dependencies: - call-bind-apply-helpers: ^1.0.1 - dunder-proto: ^1.0.0 + call-bind-apply-helpers: ^1.0.2 es-define-property: ^1.0.1 es-errors: ^1.3.0 - es-object-atoms: ^1.0.0 + es-object-atoms: ^1.1.1 function-bind: ^1.1.2 + get-proto: ^1.0.1 gopd: ^1.2.0 has-symbols: ^1.1.0 hasown: ^2.0.2 - math-intrinsics: ^1.0.0 - checksum: a7592a0b7f023a2e83c0121fa9449ca83780e370a5feeebe8452119474d148016e43b455049134ae7a683b9b11b93d3f65eac199a0ad452ab740d5f0c299de47 + math-intrinsics: ^1.1.0 + checksum: 301008e4482bb9a9cb49e132b88fee093bff373b4e6def8ba219b1e96b60158a6084f273ef5cafe832e42cd93462f4accb46a618d35fe59a2b507f2388c5b79d languageName: node linkType: hard @@ -30171,6 +30421,16 @@ __metadata: languageName: node linkType: hard +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: ^1.0.1 + es-object-atoms: ^1.0.0 + checksum: 4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b + languageName: node + linkType: hard + "get-stdin@npm:^9.0.0": version: 9.0.0 resolution: "get-stdin@npm:9.0.0" @@ -32025,13 +32285,13 @@ __metadata: languageName: node linkType: hard -"is-arguments@npm:^1.0.4": - version: 1.1.1 - resolution: "is-arguments@npm:1.1.1" +"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": + version: 1.2.0 + resolution: "is-arguments@npm:1.2.0" dependencies: - call-bind: ^1.0.2 - has-tostringtag: ^1.0.0 - checksum: 7f02700ec2171b691ef3e4d0e3e6c0ba408e8434368504bb593d0d7c891c0dbfda6d19d30808b904a6cb1929bca648c061ba438c39f296c2a8ca083229c49f27 + call-bound: ^1.0.2 + has-tostringtag: ^1.0.2 + checksum: aae9307fedfe2e5be14aebd0f48a9eeedf6b8c8f5a0b66257b965146d1e94abdc3f08e3dce3b1d908e1fa23c70039a88810ee1d753905758b9b6eebbab0bafeb languageName: node linkType: hard @@ -32193,6 +32453,13 @@ __metadata: languageName: node linkType: hard +"is-electron@npm:2.2.2": + version: 2.2.2 + resolution: "is-electron@npm:2.2.2" + checksum: de5aa8bd8d72c96675b8d0f93fab4cc21f62be5440f65bc05c61338ca27bd851a64200f31f1bf9facbaa01b3dbfed7997b2186741d84b93b63e0aff1db6a9494 + languageName: node + linkType: hard + "is-elevated@npm:^3.0.0": version: 3.0.0 resolution: "is-elevated@npm:3.0.0" @@ -32323,7 +32590,7 @@ __metadata: languageName: node linkType: hard -"is-map@npm:^2.0.3": +"is-map@npm:^2.0.2, is-map@npm:^2.0.3": version: 2.0.3 resolution: "is-map@npm:2.0.3" checksum: e6ce5f6380f32b141b3153e6ba9074892bbbbd655e92e7ba5ff195239777e767a976dcd4e22f864accaf30e53ebf961ab1995424aef91af68788f0591b7396cc @@ -32529,7 +32796,7 @@ __metadata: languageName: node linkType: hard -"is-set@npm:^2.0.3": +"is-set@npm:^2.0.2, is-set@npm:^2.0.3": version: 2.0.3 resolution: "is-set@npm:2.0.3" checksum: 36e3f8c44bdbe9496c9689762cc4110f6a6a12b767c5d74c0398176aa2678d4467e3bf07595556f2dba897751bde1422480212b97d973c7b08a343100b0c0dfe @@ -32561,7 +32828,7 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": +"is-stream@npm:^2, is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 @@ -32935,6 +33202,23 @@ __metadata: languageName: node linkType: hard +"iterate-iterator@npm:^1.0.1": + version: 1.0.2 + resolution: "iterate-iterator@npm:1.0.2" + checksum: 97b3ed4f2bebe038be57d03277879e406b2c537ceeeab7f82d4167f9a3cff872cc2cc5da3dc9920ff544ca247329d2a4d44121bb8ef8d0807a72176bdbc17c84 + languageName: node + linkType: hard + +"iterate-value@npm:^1.0.2": + version: 1.0.2 + resolution: "iterate-value@npm:1.0.2" + dependencies: + es-get-iterator: ^1.0.2 + iterate-iterator: ^1.0.1 + checksum: 446a4181657df1872e5020713206806757157db6ab375dee05eb4565b66e1244d7a99cd36ce06862261ad4bd059e66ba8192f62b5d1ff41d788c3b61953af6c3 + languageName: node + linkType: hard + "iterator.prototype@npm:^1.1.4": version: 1.1.4 resolution: "iterator.prototype@npm:1.1.4" @@ -35627,7 +35911,7 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0": +"math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" checksum: 0e513b29d120f478c85a70f49da0b8b19bc638975eca466f2eeae0071f3ad00454c621bf66e16dd435896c208e719fc91ad79bbfba4e400fe0b372e7c1c9c9a2 @@ -36973,7 +37257,7 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.0.0, msw@npm:^2.0.8": +"msw@npm:^2.0.0, msw@npm:^2.0.8, msw@npm:^2.2.14": version: 2.7.3 resolution: "msw@npm:2.7.3" dependencies: @@ -38458,7 +38742,7 @@ __metadata: languageName: node linkType: hard -"p-queue@npm:^6.6.2": +"p-queue@npm:^6, p-queue@npm:^6.6.1, p-queue@npm:^6.6.2": version: 6.6.2 resolution: "p-queue@npm:6.6.2" dependencies: @@ -38468,6 +38752,16 @@ __metadata: languageName: node linkType: hard +"p-retry@npm:^4, p-retry@npm:^4.0.0": + version: 4.6.2 + resolution: "p-retry@npm:4.6.2" + dependencies: + "@types/retry": 0.12.0 + retry: ^0.13.1 + checksum: 45c270bfddaffb4a895cea16cb760dcc72bdecb6cb45fef1971fa6ea2e91ddeafddefe01e444ac73e33b1b3d5d29fb0dd18a7effb294262437221ddc03ce0f2e + languageName: node + linkType: hard + "p-retry@npm:^6.2.0": version: 6.2.0 resolution: "p-retry@npm:6.2.0" @@ -38479,7 +38773,7 @@ __metadata: languageName: node linkType: hard -"p-throttle@npm:^4.1.1": +"p-throttle@npm:4.1.1, p-throttle@npm:^4.1.1": version: 4.1.1 resolution: "p-throttle@npm:4.1.1" checksum: fe8709f3c3b1da7c033479375c2c302e80c1a5d86449013afa7cd46d1dc210bc824a7e4a9d088e66d31987d00878c2b5491bb2fe76246d4d2fc9a1636f5f8298 @@ -40193,6 +40487,20 @@ __metadata: languageName: node linkType: hard +"promise.allsettled@npm:^1.0.2": + version: 1.0.7 + resolution: "promise.allsettled@npm:1.0.7" + dependencies: + array.prototype.map: ^1.0.5 + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + get-intrinsic: ^1.2.1 + iterate-value: ^1.0.2 + checksum: 96186392286e5ab9aef1a1a725c061c8cf268b6cf141f151daa3834bb8e1680f3b159af6536ce59cf80d4a6a5ad1d8371d05759980cc6c90d58800ddb0a7c119 + languageName: node + linkType: hard + "promise.series@npm:^0.2.0": version: 0.2.0 resolution: "promise.series@npm:0.2.0" @@ -40613,7 +40921,7 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.5.2, raw-body@npm:^2.4.1": +"raw-body@npm:2.5.2, raw-body@npm:^2.3.3, raw-body@npm:^2.4.1": version: 2.5.2 resolution: "raw-body@npm:2.5.2" dependencies: @@ -43876,6 +44184,16 @@ __metadata: languageName: node linkType: hard +"stop-iteration-iterator@npm:^1.0.0": + version: 1.1.0 + resolution: "stop-iteration-iterator@npm:1.1.0" + dependencies: + es-errors: ^1.3.0 + internal-slot: ^1.1.0 + checksum: be944489d8829fb3bdec1a1cc4a2142c6b6eb317305eeace1ece978d286d6997778afa1ae8cb3bd70e2b274b9aa8c69f93febb1e15b94b1359b11058f9d3c3a1 + languageName: node + linkType: hard + "stoppable@npm:^1.1.0": version: 1.1.0 resolution: "stoppable@npm:1.1.0" @@ -45518,7 +45836,7 @@ __metadata: languageName: node linkType: hard -"tsscmp@npm:1.0.6": +"tsscmp@npm:1.0.6, tsscmp@npm:^1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" checksum: 1512384def36bccc9125cabbd4c3b0e68608d7ee08127ceaa0b84a71797263f1a01c7f82fa69be8a3bd3c1396e2965d2f7b52d581d3a5eeaf3967fbc52e3b3bf @@ -47450,7 +47768,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7, ws@npm:^7.5.5": +"ws@npm:^7, ws@npm:^7.5.3, ws@npm:^7.5.5": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: From 0e9f7fe269772ce46ca5c456e786c32675ec10d7 Mon Sep 17 00:00:00 2001 From: Jackson Chen Date: Wed, 26 Mar 2025 11:33:22 -0400 Subject: [PATCH 23/41] techdocs: update attachTechDocsAddonComponentData with idempotency Signed-off-by: Jackson Chen --- .changeset/common-parrots-wink.md | 5 +++++ plugins/techdocs-react/src/alpha.ts | 23 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 .changeset/common-parrots-wink.md diff --git a/.changeset/common-parrots-wink.md b/.changeset/common-parrots-wink.md new file mode 100644 index 0000000000..7e3578771b --- /dev/null +++ b/.changeset/common-parrots-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-react': patch +--- + +Fix catalog entity docs page not loading due to multiple addons data attachment in the New Frontend System. diff --git a/plugins/techdocs-react/src/alpha.ts b/plugins/techdocs-react/src/alpha.ts index 689ec41977..130479561a 100644 --- a/plugins/techdocs-react/src/alpha.ts +++ b/plugins/techdocs-react/src/alpha.ts @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import React from 'react'; import { TechDocsAddonOptions } from './types'; -import { attachComponentData } from '@backstage/core-plugin-api'; +import { + attachComponentData, + getComponentData, +} from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { getDataKeyByName, TECHDOCS_ADDONS_KEY } from './addons'; import { @@ -53,6 +57,19 @@ export const attachTechDocsAddonComponentData =

( techDocsAddon: ComponentType

, data: TechDocsAddonOptions, ) => { - attachComponentData(techDocsAddon, TECHDOCS_ADDONS_KEY, data); - attachComponentData(techDocsAddon, getDataKeyByName(data.name), true); + const element = React.createElement(techDocsAddon as ComponentType); + + const isDataAttached = getComponentData( + element, + TECHDOCS_ADDONS_KEY, + ); + if (!isDataAttached) { + attachComponentData(techDocsAddon, TECHDOCS_ADDONS_KEY, data); + } + + const dataKey = getDataKeyByName(data.name); + const isDataKeyAttached = getComponentData(element, dataKey); + if (!isDataKeyAttached) { + attachComponentData(techDocsAddon, dataKey, true); + } }; From dccbdd5a6b11246d7dd9c50f138a281615c5a7af Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Wed, 26 Mar 2025 11:42:05 -0700 Subject: [PATCH 24/41] Support broadcast, address some review comments Signed-off-by: Adam Kunicki --- .../config.d.ts | 6 + .../package.json | 6 +- .../src/index.ts | 6 +- .../lib/SlackNotificationProcessor.test.ts | 294 +++++++++++------- .../src/lib/SlackNotificationProcessor.ts | 100 +++--- .../src/lib/constants.ts | 24 +- .../src/lib/util.ts | 12 +- 7 files changed, 270 insertions(+), 178 deletions(-) diff --git a/plugins/notifications-backend-module-slack/config.d.ts b/plugins/notifications-backend-module-slack/config.d.ts index f620f0e206..8cd450940e 100644 --- a/plugins/notifications-backend-module-slack/config.d.ts +++ b/plugins/notifications-backend-module-slack/config.d.ts @@ -22,6 +22,12 @@ export interface Config { * @visibility secret */ token?: string; + /** + * Broadcast notification receivers when receiver is set to config + * These can be Slack User IDs, Slack User Email addresses, Slack Channel + * Names, or Slack Channel IDs. Any valid identifier that chat.postMessage can accept. + */ + broadcastChannels?: string[]; }>; }; }; diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 2bc7a8eb00..7254d3ba6d 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -46,8 +46,8 @@ "@slack/bolt": "^3.21.4", "@slack/types": "^2.14.0", "@slack/web-api": "^7.5.0", - "dataloader": "2.2.2", - "p-throttle": "4.1.1" + "dataloader": "^2.0.0", + "p-throttle": "^4.1.1" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", @@ -55,7 +55,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/test-utils": "workspace:^", "@faker-js/faker": "^8.4.1", - "msw": "^2.2.14" + "msw": "^2.0.0" }, "configSchema": "config.d.ts" } diff --git a/plugins/notifications-backend-module-slack/src/index.ts b/plugins/notifications-backend-module-slack/src/index.ts index 14cde37d13..f8a9e248f7 100644 --- a/plugins/notifications-backend-module-slack/src/index.ts +++ b/plugins/notifications-backend-module-slack/src/index.ts @@ -20,9 +20,5 @@ * @packageDocumentation */ -export { - ANNOTATION_SLACK_CHANNEL_ID, - ANNOTATION_SLACK_CHANNEL_NAME, - ANNOTATION_SLACK_USER_ID, -} from './lib'; +export { ANNOTATION_SLACK_BOT_NOTIFY } from './lib'; export { notificationsModuleSlack as default } from './module'; diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index 5396a71736..4c891ed3ab 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -63,7 +63,7 @@ const DEFAULT_ENTITIES_RESPONSE = { name: 'mock', namespace: 'default', annotations: { - 'slack.com/user-id': 'U12345678', + 'slack.com/bot-notify': 'U12345678', }, }, spec: { @@ -77,7 +77,7 @@ const DEFAULT_ENTITIES_RESPONSE = { name: 'mock', namespace: 'default', annotations: { - 'slack.com/channel-id': 'C12345678', + 'slack.com/bot-notify': 'C12345678', }, }, } as unknown as Entity, @@ -136,17 +136,12 @@ describe('SlackNotificationProcessor', () => { blocks: [ { type: 'section', - text: { - type: 'mrkdwn', - text: 'No description provided', - }, accessory: { type: 'button', text: { type: 'plain_text', text: 'View More', }, - url: '', action_id: 'button-action', }, }, @@ -172,114 +167,205 @@ describe('SlackNotificationProcessor', () => { }); }); - it('should send a notification to a user', async () => { - const slack = new WebClient(); + describe('when a user notification is sent directly', () => { + it('should send a notification to a user', async () => { + const slack = new WebClient(); - const processor = SlackNotificationProcessor.fromConfig(config, { - auth, - discovery, - logger, - catalog: catalogServiceMock({ - entities: DEFAULT_ENTITIES_RESPONSE.items, - }), - slack, - })[0]; + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + discovery, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; - await processor.postProcess( - { - origin: 'plugin', - id: '1234', - user: 'user:default/mock', - created: new Date(), - payload: { - title: 'notification', - link: '/catalog/user/default/jane.doe', - }, - }, - { - recipients: { type: 'entity', entityRef: 'user:default/mock' }, - payload: { title: 'notification' }, - }, - ); - - expect(slack.chat.postMessage).toHaveBeenCalledWith({ - channel: 'U12345678', - text: 'notification', - attachments: [ + await processor.postProcess( { - color: '#00A699', - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: 'No description provided', - }, - accessory: { - type: 'button', - text: { - type: 'plain_text', - text: 'View More', - }, - url: '', - action_id: 'button-action', - }, - }, - { - type: 'context', - elements: [ - { - type: 'plain_text', - text: 'Severity: normal', - emoji: true, - }, - { - type: 'plain_text', - text: 'Topic: N/A', - emoji: true, - }, - ], - }, - ], - fallback: 'notification', + origin: 'plugin', + id: '1234', + user: 'user:default/mock', + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, }, - ], + { + recipients: { type: 'entity', entityRef: 'user:default/mock' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledWith({ + channel: 'U12345678', + text: 'notification', + attachments: [ + { + color: '#00A699', + blocks: [ + { + type: 'section', + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'View More', + }, + action_id: 'button-action', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: 'Severity: normal', + emoji: true, + }, + { + type: 'plain_text', + text: 'Topic: N/A', + emoji: true, + }, + ], + }, + ], + fallback: 'notification', + }, + ], + }); }); }); - it('should not send broadcast messages', async () => { - const slack = new WebClient(); - const processor = SlackNotificationProcessor.fromConfig(config, { - auth, - discovery, - logger, - catalog: catalogServiceMock({ - entities: DEFAULT_ENTITIES_RESPONSE.items, - }), - slack, - })[0]; + describe('when a user notification is expanded from a group', () => { + it('should not send a notification', async () => { + const slack = new WebClient(); - processor.processOptions({ - recipients: { type: 'broadcast' }, - payload: { title: 'notification' }, - }); - processor.postProcess( - { - origin: 'plugin', - id: '1234', - user: null, - created: new Date(), - payload: { - title: 'notification', - link: '/catalog/user/default/jane.doe', + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + discovery, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: 'user:default/mock', + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, }, - }, - { + { + recipients: { type: 'entity', entityRef: 'group:default/group' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).not.toHaveBeenCalled(); + }); + }); + + describe('when broadcast channels are not configured', () => { + it('should not send broadcast messages', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + discovery, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ recipients: { type: 'broadcast' }, payload: { title: 'notification' }, - }, - ); + }); - expect(slack.chat.postMessage).not.toHaveBeenCalled(); + await processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }, + ); + expect(slack.chat.postMessage).not.toHaveBeenCalled(); + }); + }); + + describe('when broadcast channels are configured', () => { + it('should send broadcast messages', async () => { + const slack = new WebClient(); + const broadcastConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastChannels: ['C12345678', 'D12345678'], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(broadcastConfig, { + auth, + discovery, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }); + + await processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }, + ); + expect(slack.chat.postMessage).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index 225f2f20ae..3328c55408 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -33,11 +33,7 @@ import { Counter, metrics } from '@opentelemetry/api'; import { ChatPostMessageArguments, WebClient } from '@slack/web-api'; import DataLoader from 'dataloader'; import pThrottle from 'p-throttle'; -import { - ANNOTATION_SLACK_CHANNEL_ID, - ANNOTATION_SLACK_CHANNEL_NAME, - ANNOTATION_SLACK_USER_ID, -} from './constants'; +import { ANNOTATION_SLACK_BOT_NOTIFY } from './constants'; import { toChatPostMessageArgs } from './util'; export class SlackNotificationProcessor implements NotificationProcessor { @@ -48,6 +44,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly sendNotifications; private readonly messagesSent: Counter; private readonly messagesFailed: Counter; + private readonly broadcastChannels?: string[]; static fromConfig( config: Config, @@ -57,6 +54,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { logger: LoggerService; catalog: CatalogApi; slack?: WebClient; + broadcastChannels?: string[]; }, ): SlackNotificationProcessor[] { const slackConfig = @@ -64,8 +62,10 @@ export class SlackNotificationProcessor implements NotificationProcessor { return slackConfig.map(c => { const token = c.getString('token'); const slack = options.slack ?? new WebClient(token); + const broadcastChannels = c.getOptionalStringArray('broadcastChannels'); return new SlackNotificationProcessor({ slack, + broadcastChannels, ...options, }); }); @@ -77,12 +77,14 @@ export class SlackNotificationProcessor implements NotificationProcessor { discovery: DiscoveryService; logger: LoggerService; catalog: CatalogApi; + broadcastChannels?: string[]; }) { - const { auth, catalog, logger, slack } = options; + const { auth, catalog, logger, slack, broadcastChannels } = options; this.logger = logger; this.catalog = catalog; this.auth = auth; this.slack = slack; + this.broadcastChannels = broadcastChannels; const meter = metrics.getMeter('default'); this.messagesSent = meter.createCounter( @@ -146,15 +148,14 @@ export class SlackNotificationProcessor implements NotificationProcessor { await Promise.all( entityRefs.map(async entityRef => { const compoundEntityRef = parseEntityRef(entityRef); - // skip users as they are sent direct messages, but allow all other entity kinds - // to have a channel id annotation. + // skip users as they are sent direct messages if (compoundEntityRef.kind === 'user') { return; } let channel; try { - channel = await this.getChannelId(entityRef); + channel = await this.getSlackNotificationTarget(entityRef); } catch (error) { this.logger.error( `Failed to get Slack channel for entity: ${ @@ -197,31 +198,50 @@ export class SlackNotificationProcessor implements NotificationProcessor { notification: Notification, options: NotificationSendOptions, ): Promise { - if (options.recipients.type === 'broadcast' || !notification.user) { + const destinations: string[] = []; + + // Handle broadcast case + if (notification.user === null) { + destinations.push(...(this.broadcastChannels ?? [])); + } else if (options.recipients.type === 'entity') { + // Handle user-specific notification + const entityRefs = [options.recipients.entityRef].flat(); + if (entityRefs.some(e => parseEntityRef(e).kind === 'group')) { + // We've already dispatched a slack channel message, so let's not send a DM. + return; + } + + const destination = await this.getSlackNotificationTarget( + notification.user, + ); + + if (!destination) { + this.logger.error( + `No slack.com/bot-notify annotation found for user: ${notification.user}`, + ); + return; + } + + destinations.push(destination); + } + + // If no destinations, nothing to do + if (destinations.length === 0) { return; } - const entityRefs = [options.recipients.entityRef].flat(); - if (entityRefs.some(e => parseEntityRef(e).kind === 'group')) { - // We've already dispatched a slack channel message, so let's not send a DM. - return; - } + // Prepare outbound messages + const outbound = destinations.map(channel => + toChatPostMessageArgs({ channel, payload: options.payload }), + ); - const destination = await this.getSlackUserId(notification.user); - if (!destination) { - this.logger.error(`No email found for user entity: ${notification.user}`); - return; - } - - const payload = toChatPostMessageArgs({ - channel: destination, - payload: options.payload, + // Log debug info + outbound.forEach(payload => { + this.logger.debug(`Sending notification: ${JSON.stringify(payload)}`); }); - this.logger.debug(`Sending DM notification: ${JSON.stringify(payload)}`); - - // batch it up - await this.sendNotifications([payload]); + // Send notifications + await this.sendNotifications(outbound); } async getEntities( @@ -235,11 +255,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { const response = await this.catalog.getEntitiesByRefs( { entityRefs: entityRefs.slice(), - fields: [ - `metadata.annotations.${ANNOTATION_SLACK_CHANNEL_NAME}`, - `metadata.annotations.${ANNOTATION_SLACK_CHANNEL_ID}`, - `metadata.annotations.${ANNOTATION_SLACK_USER_ID}`, - ], + fields: [`metadata.annotations.${ANNOTATION_SLACK_BOT_NOTIFY}`], }, { token, @@ -249,16 +265,9 @@ export class SlackNotificationProcessor implements NotificationProcessor { return response.items; } - async getSlackUserId(entityRef: string): Promise { - const entityLoader = new DataLoader( - entityRefs => this.getEntities(entityRefs), - ); - const entity = await entityLoader.load(entityRef); - - return entity?.metadata?.annotations?.[ANNOTATION_SLACK_USER_ID]; - } - - async getChannelId(entityRef: string): Promise { + async getSlackNotificationTarget( + entityRef: string, + ): Promise { const entityLoader = new DataLoader( entityRefs => this.getEntities(entityRefs), ); @@ -269,10 +278,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { throw new NotFoundError(`Entity not found: ${entityRef}`); } - return ( - entity?.metadata?.annotations?.[ANNOTATION_SLACK_CHANNEL_ID] || - entity?.metadata?.annotations?.[ANNOTATION_SLACK_CHANNEL_NAME] - ); + return entity?.metadata?.annotations?.[ANNOTATION_SLACK_BOT_NOTIFY]; } async sendNotification(args: ChatPostMessageArguments): Promise { diff --git a/plugins/notifications-backend-module-slack/src/lib/constants.ts b/plugins/notifications-backend-module-slack/src/lib/constants.ts index 24c1868d55..cdf052da7e 100644 --- a/plugins/notifications-backend-module-slack/src/lib/constants.ts +++ b/plugins/notifications-backend-module-slack/src/lib/constants.ts @@ -16,18 +16,14 @@ /** * @public - * The annotation key for the entity's Slack user ID + * The annotation key for the entity's Slack ID. This can be + * any valid chat.postMessage destination including: + * - A user ID (U12345678) + * - A channel ID (C12345678) + * - A DM ID (D12345678) + * - A group ID (S12345678) + * + * It can also be a user's email address or a channel name, + * however IDs are preferred. */ -export const ANNOTATION_SLACK_USER_ID = 'slack.com/user-id'; - -/** - * @public - * The annotation key for the entity's Slack channel name. - */ -export const ANNOTATION_SLACK_CHANNEL_NAME = 'slack.com/channel-name'; - -/** - * @public - * The annotation key for the entity's Slack channel ID. - */ -export const ANNOTATION_SLACK_CHANNEL_ID = 'slack.com/channel-id'; +export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify'; diff --git a/plugins/notifications-backend-module-slack/src/lib/util.ts b/plugins/notifications-backend-module-slack/src/lib/util.ts index e39701d88a..5e2adbeec5 100644 --- a/plugins/notifications-backend-module-slack/src/lib/util.ts +++ b/plugins/notifications-backend-module-slack/src/lib/util.ts @@ -46,17 +46,19 @@ export function toSlackBlockKit(payload: NotificationPayload): KnownBlock[] { return [ { type: 'section', - text: { - type: 'mrkdwn', - text: description ?? 'No description provided', - }, + ...(description && { + text: { + type: 'mrkdwn', + text: description ?? 'No description provided', + }, + }), accessory: { type: 'button', text: { type: 'plain_text', text: 'View More', }, - url: link ?? '', + ...(link && { url: link }), action_id: 'button-action', }, }, From cc5f066fa57f892ef7cab5b0cff56d5a9155a648 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Wed, 26 Mar 2025 16:30:52 -0700 Subject: [PATCH 25/41] Update/move docs Signed-off-by: Adam Kunicki --- docs/notifications/processors.md | 43 +++++++++++++-- .../README.md | 53 +------------------ 2 files changed, 41 insertions(+), 55 deletions(-) diff --git a/docs/notifications/processors.md b/docs/notifications/processors.md index 2d7de61ba0..ad3e427364 100644 --- a/docs/notifications/processors.md +++ b/docs/notifications/processors.md @@ -108,7 +108,22 @@ See more information at +Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send +messages to more than one Slack workspace. Org-Wide App installation is not currently supported. + +### Entity Requirements + +Entities must be annotated with the following annotation: + +- `slack.com/bot-notify` + +The value may be any Slack ID supported by [chat.postMessage](https://api.slack.com/methods/chat.postMessage), for example a user (U12345678), channel (C12345678), group, or direct message chat. + +It's also possible to use a user's email address or channel name, however IDs are recommended by Slack. +Private channels/chats must use an ID. + +### Observability + +The processor includes the following counter metrics if you are exporting metrics using OpenTelemetry: + +- `notifications.processors.slack.sent.count` - The number of messages sent +- `notifications.processors.slack.error.count` - The number of messages that failed to send diff --git a/plugins/notifications-backend-module-slack/README.md b/plugins/notifications-backend-module-slack/README.md index e64bac868c..f730ede78a 100644 --- a/plugins/notifications-backend-module-slack/README.md +++ b/plugins/notifications-backend-module-slack/README.md @@ -2,55 +2,4 @@ The Slack backend module for the notifications plugin. -## Getting Started - -### Module Installation - -Add the module to your backend: - -```ts -// packages/backend/src/index.ts -backend.add(import('@backstage/plugin-notifications-backend-module-slack')); -``` - -### Slack Configuration - -To use this you'll need to create a Slack App or use an existing one. It should have at least the following scopes: -`chat:write`, `users:read`, `im:write` (for direct message support). - -Additionally you may include scopes `chat:write.public` in order to send messages to public channels your app is not -a member of. - -These scopes are under OAuth & Permissions. You will also want to save the Bot User OAuth Token. This will be needed -in the following step to configure `app-config.yaml`. - -### Configure Backstage - -You'll now need to configure the Slack module in your `app-config.yaml`. - -```yaml -notifications: - processors: - slack: - - token: xoxb-XXXXXXXXX -``` - -Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send -messages to more than one Slack workspace. Org-Wide App installation is not currently supported. - -### Entity Requirements - -Entities must be annotated with one of the following annotations: - -- `slack.com/user-id` for direct messages -- `slack.com/channel-name` for public channel messages -- `slack.com/channel-id` for public or private channel messages - -Slack prefers use of ID over name and `slack.com/channel-id` is the recommended annotation. - -### Observability - -The processor includes the following counter metrics if you are exporting metrics using OpenTelemetry: - -- `notifications.processors.slack.sent.count` - The number of messages sent -- `notifications.processors.slack.error.count` - The number of messages that failed to send +See [Built-in Processors](https://backstage.io/docs/notifications/processors/#built-in-processors) for detailed documentation From 2eaaf0bda07f13194044292dafc54a3aa0cba280 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Wed, 26 Mar 2025 16:34:29 -0700 Subject: [PATCH 26/41] Update lockfile Signed-off-by: Adam Kunicki --- yarn.lock | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0881f8546f..e15195cb62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6800,9 +6800,9 @@ __metadata: "@slack/bolt": ^3.21.4 "@slack/types": ^2.14.0 "@slack/web-api": ^7.5.0 - dataloader: 2.2.2 - msw: ^2.2.14 - p-throttle: 4.1.1 + dataloader: ^2.0.0 + msw: ^2.0.0 + p-throttle: ^4.1.1 languageName: unknown linkType: soft @@ -26632,13 +26632,6 @@ __metadata: languageName: node linkType: hard -"dataloader@npm:2.2.2": - version: 2.2.2 - resolution: "dataloader@npm:2.2.2" - checksum: 4dabd247089c29f194e94d5434d504f99156c5c214a03463c20f3f17f40398d7e179edee69a27c16e315519ac8739042a810090087ae26449a0e685156a02c65 - languageName: node - linkType: hard - "dataloader@npm:^2.0.0, dataloader@npm:^2.2.2": version: 2.2.3 resolution: "dataloader@npm:2.2.3" @@ -37257,7 +37250,7 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.0.0, msw@npm:^2.0.8, msw@npm:^2.2.14": +"msw@npm:^2.0.0, msw@npm:^2.0.8": version: 2.7.3 resolution: "msw@npm:2.7.3" dependencies: @@ -38773,7 +38766,7 @@ __metadata: languageName: node linkType: hard -"p-throttle@npm:4.1.1, p-throttle@npm:^4.1.1": +"p-throttle@npm:^4.1.1": version: 4.1.1 resolution: "p-throttle@npm:4.1.1" checksum: fe8709f3c3b1da7c033479375c2c302e80c1a5d86449013afa7cd46d1dc210bc824a7e4a9d088e66d31987d00878c2b5491bb2fe76246d4d2fc9a1636f5f8298 From 379934e9b890168d938386e7b82afafc4c64dc04 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Wed, 26 Mar 2025 22:26:34 -0700 Subject: [PATCH 27/41] Update api report Signed-off-by: Adam Kunicki --- plugins/notifications-backend-module-slack/report.api.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/notifications-backend-module-slack/report.api.md b/plugins/notifications-backend-module-slack/report.api.md index dfd2460e91..3c4ddb494d 100644 --- a/plugins/notifications-backend-module-slack/report.api.md +++ b/plugins/notifications-backend-module-slack/report.api.md @@ -6,13 +6,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @public -export const ANNOTATION_SLACK_CHANNEL_ID = 'slack.com/channel-id'; - -// @public -export const ANNOTATION_SLACK_CHANNEL_NAME = 'slack.com/channel-name'; - -// @public -export const ANNOTATION_SLACK_USER_ID = 'slack.com/user-id'; +export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify'; // @public const notificationsModuleSlack: BackendFeature; From ec68dc8b92dc6133f3f2408040ddebcdf70461e3 Mon Sep 17 00:00:00 2001 From: Mitesh Kumar Date: Thu, 27 Mar 2025 12:11:32 +0530 Subject: [PATCH 28/41] Update .changeset/moody-eagles-smile.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Mitesh Kumar --- .changeset/moody-eagles-smile.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/moody-eagles-smile.md b/.changeset/moody-eagles-smile.md index a2aab46cd8..3fd8a748ab 100644 --- a/.changeset/moody-eagles-smile.md +++ b/.changeset/moody-eagles-smile.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-import': minor +'@backstage/plugin-catalog-import': patch --- adding translation for `Register an existing component` text From 4bf3a89a76b8ffc38ecc844349cf5188a77ebf20 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Thu, 27 Mar 2025 12:56:48 +0530 Subject: [PATCH 29/41] exporting translation via alpha Signed-off-by: its-mitesh-kumar --- plugins/catalog-import/report-alpha.api.md | 10 ++++++++++ plugins/catalog-import/src/alpha.tsx | 2 ++ 2 files changed, 12 insertions(+) diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index 91c0e05d30..4cbcc8e7c1 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -10,6 +10,16 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +// @alpha (undocumented) +export const catalogImportTranslationRef: TranslationRef< + 'catalog-import', + { + readonly pageTitle: 'Register an existing component'; + readonly 'importInfoCard.title': 'Register an existing component'; + } +>; // @alpha (undocumented) const _default: FrontendPlugin< diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index cf63c92338..ab6e3f2a68 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -38,6 +38,8 @@ import { CatalogImportClient, catalogImportApiRef } from './api'; import { rootRouteRef } from './plugin'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; +export * from './translation'; + // TODO: It's currently possible to override the import page with a custom one. We need to decide // whether this type of override is typically done with an input or by overriding the entire extension. const catalogImportPage = PageBlueprint.make({ From 003dc1563f7388bf0525b12b13a3a2d006f54f76 Mon Sep 17 00:00:00 2001 From: Johannes Will Date: Wed, 26 Mar 2025 14:14:38 +0100 Subject: [PATCH 30/41] feat: support pathes in gitlab:group:ensureExists (#29399) Signed-off-by: Johannes Will --- .changeset/crazy-hands-film.md | 5 ++ .../gitlabGroupEnsureExists.examples.test.ts | 51 +++++++++++++++++++ .../gitlabGroupEnsureExists.examples.ts | 21 ++++++++ .../actions/gitlabGroupEnsureExists.test.ts | 32 ++++++++++++ .../src/actions/gitlabGroupEnsureExists.ts | 22 ++++++-- 5 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 .changeset/crazy-hands-film.md diff --git a/.changeset/crazy-hands-film.md b/.changeset/crazy-hands-film.md new file mode 100644 index 0000000000..694aa7b8e7 --- /dev/null +++ b/.changeset/crazy-hands-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Updated the path field in the `gitlab:group:ensureExists` action to suport also strings with multiple segments (e.g. group/subgroup) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts index eaeb65ae9c..4d8e0fd0c0 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts @@ -243,4 +243,55 @@ describe('gitlab:group:ensureExists', () => { expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); }); + + it(`Should ${examples[5].description}`, async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'group1', + }, + { + id: 2, + full_path: 'group1/group2', + }, + { + id: 3, + full_path: 'group1/group2/group3', + }, + ]); + mockGitlabClient.Groups.create.mockResolvedValue({ + id: 4, + full_path: 'group1/group2/group3/group4', + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupEnsureExistsAction({ integrations }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[5].example).steps[0].input, + }); + + expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith( + 'Group 4', + 'group4', + { + parentId: 3, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 4); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts index bbe7ad4722..f379bd38ec 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts @@ -102,4 +102,25 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: + 'Create a group nested within another group using path and objects', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabGroup', + name: 'Group', + action: 'gitlab:group:ensureExists', + input: { + repoUrl: 'gitlab.com', + path: [ + 'group1/group2', + { name: 'Group 3', slug: 'group3' }, + { name: 'Group 4', slug: 'group4' }, + ], + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts index f9dc12eb54..3d703eb91f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts @@ -94,6 +94,38 @@ describe('gitlab:group:ensureExists', () => { expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); }); + it('should create a new group from pathstring if it does not exists', async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'bar', + }, + { + id: 2, + full_path: 'foo', + }, + ]); + + mockGitlabClient.Groups.create.mockResolvedValue({ + id: 3, + full_path: 'foo/bar', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: ['foo/bar', 'baz'], + }, + }); + + expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith('bar', 'bar', { + parentId: 2, + }); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); + }); + it('should create a new group from object if it does not exists', async () => { mockGitlabClient.Groups.search.mockResolvedValue([ { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts index 5be5b7c10d..7e5b03ce04 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts @@ -76,11 +76,7 @@ export const createGitlabGroupEnsureExistsAction = (options: { let currentPath: string | null = null; let parentId: number | null = null; - for (const pathElement of path) { - const slug = - typeof pathElement === 'string' ? pathElement : pathElement.slug; - const name = - typeof pathElement === 'string' ? pathElement : pathElement.name; + for (const { name, slug } of pathIterator(path)) { const fullPath: string = currentPath ? `${currentPath}/${slug}` : slug; const result = (await api.Groups.search( fullPath, @@ -119,3 +115,19 @@ export const createGitlabGroupEnsureExistsAction = (options: { }, }); }; + +type PathPart = { name: string; slug: string }; +type PathItem = string | PathPart; + +function* pathIterator(items: PathItem[]): Generator { + for (const item of items) { + if (typeof item === 'string') { + const parts = item.split('/'); + for (const part of parts) { + yield { name: part, slug: part }; + } + } else { + yield item; + } + } +} From 3971a7a755f522675452bd323fb7246672b976c0 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 27 Mar 2025 21:45:06 +0100 Subject: [PATCH 31/41] fix: default catalog table search Signed-off-by: Camila Belo --- .changeset/tasty-moments-do.md | 5 +++++ plugins/catalog/src/components/CatalogTable/CatalogTable.tsx | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/tasty-moments-do.md diff --git a/.changeset/tasty-moments-do.md b/.changeset/tasty-moments-do.md new file mode 100644 index 0000000000..716f65088e --- /dev/null +++ b/.changeset/tasty-moments-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fix the default catalog table search behavior by using the catalog table toolbar for all table variations (including when pagination mode is none). diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 0b2d2e4a4c..f49222849f 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -49,6 +49,7 @@ import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { catalogTranslationRef } from '../../alpha/translation'; import { FavoriteToggleIcon } from '@backstage/core-components'; +import { CatalogTableToolbar } from './CatalogTableToolbar'; /** * Props for {@link CatalogTable}. @@ -251,6 +252,9 @@ export const CatalogTable = (props: CatalogTableProps) => { actions={actions} subtitle={subtitle} emptyContent={emptyContent} + components={{ + Toolbar: CatalogTableToolbar, + }} /> ); }; From b877e46ca8dce119dfb89bad3ca1eddbe12df790 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 28 Mar 2025 19:09:37 +0000 Subject: [PATCH 32/41] kubernetes - Added New Frontend System filter for tab Signed-off-by: Andre Wanlin --- .changeset/dry-cars-build.md | 5 +++++ plugins/kubernetes/package.json | 1 + plugins/kubernetes/src/alpha/entityContents.tsx | 9 ++++++++- yarn.lock | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changeset/dry-cars-build.md diff --git a/.changeset/dry-cars-build.md b/.changeset/dry-cars-build.md new file mode 100644 index 0000000000..60fd3ece49 --- /dev/null +++ b/.changeset/dry-cars-build.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Added New Frontend System filter for the Kubernetes tab to use `isKubernetesAvailable` to control its visibility diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 8d621e0d88..76774fa589 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -63,6 +63,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", diff --git a/plugins/kubernetes/src/alpha/entityContents.tsx b/plugins/kubernetes/src/alpha/entityContents.tsx index 7b6dc78e4b..6720865d3e 100644 --- a/plugins/kubernetes/src/alpha/entityContents.tsx +++ b/plugins/kubernetes/src/alpha/entityContents.tsx @@ -17,6 +17,8 @@ import React from 'react'; import { compatWrapper } from '@backstage/core-compat-api'; import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import { isKubernetesAvailable } from '../Router'; +import { isKind } from '@backstage/plugin-catalog'; export const entityKubernetesContent = EntityContentBlueprint.make({ name: 'kubernetes', @@ -24,7 +26,12 @@ export const entityKubernetesContent = EntityContentBlueprint.make({ defaultPath: '/kubernetes', defaultTitle: 'Kubernetes', defaultGroup: 'deployment', - filter: 'kind:component,resource', + filter: entity => { + if (!isKind('Component')) return false; + if (!isKind('Resource')) return false; + if (!isKubernetesAvailable(entity)) return false; + return true; + }, loader: () => import('./KubernetesContentPage').then(m => compatWrapper(), diff --git a/yarn.lock b/yarn.lock index 648c69d5cc..2ab79a31ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6718,6 +6718,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-react": "workspace:^" From a044efa1873c4675fab288a9d9c328c50fb10af9 Mon Sep 17 00:00:00 2001 From: Luke Albao Date: Fri, 28 Mar 2025 13:41:50 -0700 Subject: [PATCH 33/41] [docs] add branch naming caveat to github discovery integration Signed-off-by: Luke Albao --- docs/integrations/github/discovery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 4da7287383..e2513cdf21 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -152,7 +152,7 @@ If you do so, `default` will be used as provider ID. Wildcards cannot be used if the `validateLocationsExist` option is set to `true`. - **`filters`** _(optional)_: - **`branch`** _(optional)_: - String used to filter results based on the branch name. + String used to filter results based on the branch name. Branch name cannot have any slash (`/`) characters. Defaults to the default Branch of the repository. - **`repository`** _(optional)_: Regular expression used to filter results based on the repository name. From f0c22ebd089334ccfb8209020736fe217cf270f7 Mon Sep 17 00:00:00 2001 From: Luke Albao Date: Fri, 28 Mar 2025 15:58:58 -0700 Subject: [PATCH 34/41] [plugins/catalog-backend-module-github] Throw on invalid branch name Signed-off-by: Luke Albao --- .changeset/clear-pigs-share.md | 5 +++++ .../GithubEntityProviderConfig.test.ts | 20 +++++++++++++++++++ .../providers/GithubEntityProviderConfig.ts | 6 ++++++ 3 files changed, 31 insertions(+) create mode 100644 .changeset/clear-pigs-share.md diff --git a/.changeset/clear-pigs-share.md b/.changeset/clear-pigs-share.md new file mode 100644 index 0000000000..37925b0c10 --- /dev/null +++ b/.changeset/clear-pigs-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': minor +--- + +Explicitly rejects branch names containing a slash character diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts index d00cbc888c..bddb4d27cd 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts @@ -311,4 +311,24 @@ describe('readProviderConfigs', () => { expect(() => readProviderConfigs(config)).toThrow(); }); + + it('throws an error when filters.branch contains a slash', () => { + const config = new ConfigReader({ + catalog: { + providers: { + github: { + invalidBranchUser: { + organization: 'test-org', + catalogPath: '/*/catalog-info.yaml', + filters: { + branch: 'test/a', + }, + }, + }, + }, + }, + }); + + expect(() => readProviderConfigs(config)).toThrow(); + }); }); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts index 2e2592b70a..781239978f 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.ts @@ -104,6 +104,12 @@ function readProviderConfig( ); } + if (branchPattern?.includes('/')) { + throw new Error( + 'Error while processing GitHub provider config. Slash characters (/) are not allowed in filters.branch', + ); + } + const schedule = config.has('schedule') ? readSchedulerServiceTaskScheduleDefinitionFromConfig( config.getConfig('schedule'), From 75a15d1819bf213b3535362062f5ccd851d23d79 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 29 Mar 2025 09:31:35 +0000 Subject: [PATCH 35/41] Refactor based on feedback Signed-off-by: Andre Wanlin --- plugins/kubernetes/package.json | 1 - plugins/kubernetes/src/alpha/entityContents.tsx | 3 --- yarn.lock | 1 - 3 files changed, 5 deletions(-) diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 76774fa589..8d621e0d88 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -63,7 +63,6 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", - "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", diff --git a/plugins/kubernetes/src/alpha/entityContents.tsx b/plugins/kubernetes/src/alpha/entityContents.tsx index 6720865d3e..4be9aa5291 100644 --- a/plugins/kubernetes/src/alpha/entityContents.tsx +++ b/plugins/kubernetes/src/alpha/entityContents.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { compatWrapper } from '@backstage/core-compat-api'; import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { isKubernetesAvailable } from '../Router'; -import { isKind } from '@backstage/plugin-catalog'; export const entityKubernetesContent = EntityContentBlueprint.make({ name: 'kubernetes', @@ -27,8 +26,6 @@ export const entityKubernetesContent = EntityContentBlueprint.make({ defaultTitle: 'Kubernetes', defaultGroup: 'deployment', filter: entity => { - if (!isKind('Component')) return false; - if (!isKind('Resource')) return false; if (!isKubernetesAvailable(entity)) return false; return true; }, diff --git a/yarn.lock b/yarn.lock index 2ab79a31ab..648c69d5cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6718,7 +6718,6 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" - "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-react": "workspace:^" From cd71065a02bed740011daee96a865108a785dff6 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 31 Mar 2025 13:12:54 +0100 Subject: [PATCH 36/41] Adjustment based on feedback Signed-off-by: Andre Wanlin --- plugins/kubernetes/src/alpha/entityContents.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/kubernetes/src/alpha/entityContents.tsx b/plugins/kubernetes/src/alpha/entityContents.tsx index 4be9aa5291..2d27cd66b2 100644 --- a/plugins/kubernetes/src/alpha/entityContents.tsx +++ b/plugins/kubernetes/src/alpha/entityContents.tsx @@ -25,10 +25,7 @@ export const entityKubernetesContent = EntityContentBlueprint.make({ defaultPath: '/kubernetes', defaultTitle: 'Kubernetes', defaultGroup: 'deployment', - filter: entity => { - if (!isKubernetesAvailable(entity)) return false; - return true; - }, + filter: isKubernetesAvailable, loader: () => import('./KubernetesContentPage').then(m => compatWrapper(), From 5b9514f2c42c544dd766c02b4b351dd66a6b915f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 1 Apr 2025 10:16:15 +0100 Subject: [PATCH 37/41] export deprecated type to fix e2e MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/huge-toys-beam.md | 5 +++++ plugins/catalog-import/report.api.md | 13 ++++++++++++- .../StepPrepareCreatePullRequest.tsx | 18 +++++++++++++++++- .../StepPrepareCreatePullRequest/index.ts | 5 ++++- yarn.lock | 6 +++--- 5 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 .changeset/huge-toys-beam.md diff --git a/.changeset/huge-toys-beam.md b/.changeset/huge-toys-beam.md new file mode 100644 index 0000000000..e36cdcc7f0 --- /dev/null +++ b/.changeset/huge-toys-beam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Expose the `UnpackNestedValue` type as it's been removed from `react-hook-form` diff --git a/plugins/catalog-import/report.api.md b/plugins/catalog-import/report.api.md index e31ef8defe..66a39e8b90 100644 --- a/plugins/catalog-import/report.api.md +++ b/plugins/catalog-import/report.api.md @@ -15,13 +15,13 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { FieldErrors } from 'react-hook-form'; import { InfoCardVariants } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; +import { NestedValue } from 'react-hook-form'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmAuthApi } from '@backstage/integration-react'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SubmitHandler } from 'react-hook-form'; import { TextFieldProps } from '@material-ui/core/TextField/TextField'; -import { UnpackNestedValue } from 'react-hook-form'; import { UseFormProps } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form'; @@ -361,6 +361,17 @@ export interface StepPrepareCreatePullRequestProps { ) => React_2.ReactNode; } +// @public @deprecated +export type UnpackNestedValue = T extends NestedValue + ? U + : T extends Date | FileList | File | Blob + ? T + : T extends object + ? { + [K in keyof T]: UnpackNestedValue; + } + : T; + // Warnings were encountered during analysis: // // src/api/CatalogImportApi.d.ts:25:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index 2ca230372b..4ec8b43976 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -27,7 +27,7 @@ import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React, { useCallback, useEffect, useState } from 'react'; -import { UnpackNestedValue, UseFormReturn } from 'react-hook-form'; +import { NestedValue, UseFormReturn } from 'react-hook-form'; import useAsync from 'react-use/esm/useAsync'; import YAML from 'yaml'; import { AnalyzeResult, catalogImportApiRef } from '../../api'; @@ -56,6 +56,22 @@ type FormData = { useCodeowners: boolean; }; +/** + * Helper for unpacking NestedValue into the underlying type. + * + * @public + * @deprecated This is a copy of the type from react-hook-form, and will be removed in a future release + */ +export type UnpackNestedValue = T extends NestedValue + ? U + : T extends Date | FileList | File | Blob + ? T + : T extends object + ? { + [K in keyof T]: UnpackNestedValue; + } + : T; + /** * Props for {@link StepPrepareCreatePullRequest}. * diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts index ce1cd40e19..3340d40f84 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts @@ -23,4 +23,7 @@ export type { PreviewCatalogInfoComponentProps } from './PreviewCatalogInfoCompo export { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; export type { PreviewPullRequestComponentProps } from './PreviewPullRequestComponent'; export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; -export type { StepPrepareCreatePullRequestProps } from './StepPrepareCreatePullRequest'; +export type { + StepPrepareCreatePullRequestProps, + UnpackNestedValue, +} from './StepPrepareCreatePullRequest'; diff --git a/yarn.lock b/yarn.lock index c35ef2b3e2..afe662d7ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40937,11 +40937,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.12.2": - version: 7.53.2 - resolution: "react-hook-form@npm:7.53.2" + version: 7.55.0 + resolution: "react-hook-form@npm:7.55.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 37e2dd0e79cd8d3376a7a2cb72ad7b59f0594be499daa898d2c6bec896fc30c2f86e62e8b41bc9d325f77220bd8d76cb31e917f77f1c92ad5740adb1a4cc69e2 + checksum: ff37fba242c0e94911bf8515bfe1b0670f93bf0ed0c44c923108859244f41da3f0f6dffcf70d570b152ef6b1d56936d943b45fba11c63f21b913fba8f0da862b languageName: node linkType: hard From 55f0aed0f6fb3f491ce0babdbd7aa93cc694b824 Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 1 Apr 2025 11:14:03 +0100 Subject: [PATCH 38/41] chore(plugins): add renovate-hoster plugin Signed-off-by: secustor --- microsite/data/plugins/renovate-hoster.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 microsite/data/plugins/renovate-hoster.yaml diff --git a/microsite/data/plugins/renovate-hoster.yaml b/microsite/data/plugins/renovate-hoster.yaml new file mode 100644 index 0000000000..4cf4f65956 --- /dev/null +++ b/microsite/data/plugins/renovate-hoster.yaml @@ -0,0 +1,9 @@ +--- +title: Renovate Hoster +author: secustor +authorUrl: https://github.com/secustor +category: Quality +description: This plugin enables you to host Renovate CLI yourself inside of Backstage and extract data from it. +documentation: https://github.com/secustor/backstage-plugins/tree/main/plugins/renovate +npmPackageName: '@backstage/backstage-plugin-renovate' +addedDate: '2025-03-31' From 9eec76377d96a10f748dc388c67c44616d879d59 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 1 Apr 2025 11:14:29 +0100 Subject: [PATCH 39/41] chore: fixing changeset Signed-off-by: benjdlambert --- .changeset/crazy-hands-film.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/crazy-hands-film.md b/.changeset/crazy-hands-film.md index 694aa7b8e7..1d5933b317 100644 --- a/.changeset/crazy-hands-film.md +++ b/.changeset/crazy-hands-film.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-gitlab': patch --- -Updated the path field in the `gitlab:group:ensureExists` action to suport also strings with multiple segments (e.g. group/subgroup) +Updated the path field in the `gitlab:group:ensureExists` action to support also strings with multiple segments (e.g. `group/subgroup`) From 2d7e3ba014866594b71d8c6ae8d5be6c94ffbadc Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 1 Apr 2025 11:33:55 +0100 Subject: [PATCH 40/41] chore: fixup of tests and api reports Signed-off-by: benjdlambert --- packages/core-components/report-alpha.api.md | 4 ++-- .../AlertDisplay/AlertDisplay.test.tsx | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index 158f9fe14a..73b4194266 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -55,8 +55,8 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'oauthRequestDialog.rejectAll': 'Reject All'; readonly 'supportButton.title': 'Support'; readonly 'supportButton.close': 'Close'; - readonly 'alertDisplay.message_one': '({{ count }} older message)'; - readonly 'alertDisplay.message_other': '({{ count }} older messages)'; + readonly 'alertDisplay.message_one': '({{ count }} newer message)'; + readonly 'alertDisplay.message_other': '({{ count }} newer messages)'; readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; diff --git a/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx b/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx index b75f773f90..f2f904a3e0 100644 --- a/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx +++ b/packages/core-components/src/components/AlertDisplay/AlertDisplay.test.tsx @@ -92,7 +92,7 @@ describe('', () => { , ); - expect(screen.getByText('(2 older messages)')).toBeInTheDocument(); + expect(screen.getByText('(2 newer messages)')).toBeInTheDocument(); }); }); @@ -189,19 +189,19 @@ describe('', () => { jest.advanceTimersByTime(1000); }); expect(queryByText('transient message one')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); act(() => { jest.advanceTimersByTime(1000); }); expect(queryByText('transient message one')).toBeInTheDocument(); - expect(queryByText('(2 older messages)')).toBeInTheDocument(); + expect(queryByText('(2 newer messages)')).toBeInTheDocument(); // Validate removing messages act(() => { jest.advanceTimersByTime(5000); }); expect(queryByText('transient message two')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); act(() => { jest.advanceTimersByTime(5000); @@ -231,12 +231,12 @@ describe('', () => { // 1s in, message 1 still shown, message 2 added in background act(() => jest.advanceTimersByTime(1000)); expect(queryByText('transient message one')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); // 2s in, message 2 now shown, message 3 added act(() => jest.advanceTimersByTime(1000)); expect(queryByText('transient message two')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); // 3.5s in, message 3 now shown act(() => jest.advanceTimersByTime(1500)); @@ -264,7 +264,7 @@ describe('', () => { // 1s in, message 1 still shown, message 2 added in background act(() => jest.advanceTimersByTime(1000)); expect(queryByText('transient message one')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); // manually remove message 1 fireEvent.click(screen.getByTestId('error-button-close')); @@ -273,7 +273,7 @@ describe('', () => { // 2s in, message 2 now shown, message 3 added act(() => jest.advanceTimersByTime(1000)); expect(queryByText('transient message two')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); // 3s in, message 3 now shown act(() => jest.advanceTimersByTime(1500)); @@ -330,19 +330,19 @@ describe('', () => { jest.advanceTimersByTime(1000); }); expect(queryByText('transient message one')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); act(() => { jest.advanceTimersByTime(1000); }); expect(queryByText('transient message one')).toBeInTheDocument(); - expect(queryByText('(2 older messages)')).toBeInTheDocument(); + expect(queryByText('(2 newer messages)')).toBeInTheDocument(); // Validate removing messages act(() => { jest.advanceTimersByTime(5000); }); expect(queryByText('permanent message')).toBeInTheDocument(); - expect(queryByText('(1 older message)')).toBeInTheDocument(); + expect(queryByText('(1 newer message)')).toBeInTheDocument(); fireEvent.click(screen.getByTestId('error-button-close')); expect(queryByText('transient message three')).toBeInTheDocument(); From 5067db89108edd972f1eca06f14b54f88d9ad0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 1 Apr 2025 11:40:15 +0100 Subject: [PATCH 41/41] Update .changeset/clear-pigs-share.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/clear-pigs-share.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.changeset/clear-pigs-share.md b/.changeset/clear-pigs-share.md index 37925b0c10..a8241760c8 100644 --- a/.changeset/clear-pigs-share.md +++ b/.changeset/clear-pigs-share.md @@ -2,4 +2,8 @@ '@backstage/plugin-catalog-backend-module-github': minor --- -Explicitly rejects branch names containing a slash character +**BREAKING**: Explicitly rejects branch names containing a slash character. + +The module now rejects any configuration that contains slashes in branch names. The reason for this is that the ingestion will run into downstream problems if they were let through. If you had configuration with a slash in the branch name in `filters.branch`, your application may fail to start up. + +If you are affected by this, please move over to using branches that do not have slashes in them.