diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 874df46dc3..b7e9fd031f 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -93,8 +93,9 @@ catalog: filters: branch: 'main' # string repository: '.*' # Regex - topic: 'backstage-include' # optional string - topicIncludesIfMatch: true # optional boolean (default: false) + topic: + name: 'backstage-include' # optional string + type: 'INCLUDES' # optional enum (INCLUDES or EXCLUDES) ``` This provider supports multiple organizations via unique provider IDs. @@ -112,11 +113,13 @@ This provider supports multiple organizations via unique provider IDs. - **repository** _(optional)_: Regular expression used to filter results based on the repository name. - **topic** _(optional)_: - String used to filter results based on repository topics. By default, repositories that match the topic filter will be **ignored**. See below for the inverse. - This is useful for excluding excess locations from being registered such as experiments, practical tests, non-production tooling and so on. - - **topicIncludesIfMatch** _(optional)_: - Default: `false` - If set to `true` and a topic filter is set, **ONLY** repositories matching the topic filter will have a location registered. + - **name** _(optional)_: + String used to filter results based on repository topics. If configured, only repositories that match the topic filter will be registered. See below for the reverse. + This is useful for excluding excess locations from being registered such as experiments, practical tests, non-production tooling and so on. + - **type** _(optional)_: + Default: `INCLUDES` + When set to `INCLUDES`, locations matching the topic filter will be registered and other repositories will be ignored + When set to `EXCLUDES`, locations matching the topic filter will not be registered and other repositories will be registered - **organization**: Name of your organization account/workspace. If you want to add multiple organizations, you need to add one provider config each. diff --git a/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.test.ts index ef3a4dbbdd..3156bc3a3d 100644 --- a/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.test.ts @@ -200,7 +200,9 @@ describe('GitHubEntityProvider', () => { filters: { branch: 'main', repository: 'test-.*', - topic: 'blah', + topic: { + name: 'blah', + }, }, }, }, @@ -268,8 +270,10 @@ describe('GitHubEntityProvider', () => { filters: { branch: 'main', repository: 'test-.*', - topic: 'backstage-include', - topicIncludesIfMatch: true, + topic: { + name: 'backstage-include', + type: 'INCLUDES', + }, }, }, }, diff --git a/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.ts index 9bb0c990df..e77e61cf7e 100644 --- a/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GitHubEntityProvider.ts @@ -36,6 +36,7 @@ import { Logger } from 'winston'; import { readProviderConfigs, GitHubEntityProviderConfig, + TopicTypes, } from './GitHubEntityProviderConfig'; import { getOrganizationRepositories, Repository } from '../lib/github'; @@ -184,17 +185,29 @@ export class GitHubEntityProvider implements EntityProvider { private matchesFilters(repositories: Repository[]) { const repositoryFilter = this.config.filters?.repository; const topicFilter = this.config.filters?.topic; - const topicIncludesIfMatch = this.config.filters?.topicIncludesIfMatch; const matchingRepositories = repositories.filter(r => { - const topics: string[] = r.repositoryTopics.nodes.map( + const repoTopics: string[] = r.repositoryTopics.nodes.map( node => node.topic.name, ); + const satisfiesTopicFilter = ( + topics: string[], + filter: typeof topicFilter, + ) => { + if (!filter || !filter.name) return true; + if (filter.type === TopicTypes.Includes && topics.includes(filter.name)) + return true; + if ( + filter.type === TopicTypes.Excludes && + !topics.includes(filter.name) + ) + return true; + return false; + }; return ( !r.isArchived && (!repositoryFilter || repositoryFilter.test(r.name)) && - (!topicFilter || - (topics.includes(topicFilter) && topicIncludesIfMatch)) && + satisfiesTopicFilter(repoTopics, topicFilter) && r.defaultBranchRef?.name ); }); 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 2648129c6f..6db89f62d8 100644 --- a/plugins/catalog-backend-module-github/src/providers/GitHubEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GitHubEntityProviderConfig.test.ts @@ -71,14 +71,18 @@ describe('readProviderConfigs', () => { providerWithTopicFilter: { organization: 'test-org5', filters: { - topic: 'backstage-exclude', + topic: { + name: 'backstage-exclude', + }, }, }, providerWithTopicFilterAndInclusion: { organization: 'test-org6', filters: { - topic: 'backstage-include', - topicIncludesIfMatch: false, + topic: { + name: 'backstage-include', + type: 'INCLUDES', + }, }, }, }, @@ -95,8 +99,10 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, - topic: undefined, - topicIncludesIfMatch: false, + topic: { + name: undefined, + type: 'EXCLUDES', + }, }, }); expect(providerConfigs[1]).toEqual({ @@ -106,8 +112,10 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, - topic: undefined, - topicIncludesIfMatch: false, + topic: { + name: undefined, + type: 'EXCLUDES', + }, }, }); expect(providerConfigs[2]).toEqual({ @@ -117,8 +125,10 @@ describe('readProviderConfigs', () => { filters: { repository: /^repository.*filter$/, // repo branch: undefined, // branch - topic: undefined, - topicIncludesIfMatch: false, + topic: { + name: undefined, + type: 'EXCLUDES', + }, }, }); expect(providerConfigs[3]).toEqual({ @@ -128,8 +138,10 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: 'branch-name', - topic: undefined, - topicIncludesIfMatch: false, + topic: { + name: undefined, + type: 'EXCLUDES', + }, }, }); expect(providerConfigs[4]).toEqual({ @@ -139,8 +151,10 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, - topic: 'backstage-exclude', - topicIncludesIfMatch: false, + topic: { + name: 'backstage-exclude', + type: 'EXCLUDES', + }, }, }); expect(providerConfigs[5]).toEqual({ @@ -150,8 +164,10 @@ describe('readProviderConfigs', () => { filters: { repository: undefined, branch: undefined, - topic: 'backstage-include', - topicIncludesIfMatch: false, + topic: { + name: 'backstage-include', + type: 'INCLUDES', + }, }, }); }); diff --git a/plugins/catalog-backend-module-github/src/providers/GitHubEntityProviderConfig.ts b/plugins/catalog-backend-module-github/src/providers/GitHubEntityProviderConfig.ts index 104f12c17c..609cdf163d 100644 --- a/plugins/catalog-backend-module-github/src/providers/GitHubEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-github/src/providers/GitHubEntityProviderConfig.ts @@ -16,9 +16,17 @@ import { Config } from '@backstage/config'; +export enum TopicTypes { + Includes = 'INCLUDES', + Excludes = 'EXCLUDES', +} + +const isValidTopicType = (topicType: string) => + (Object.values(TopicTypes) as string[]).includes(topicType); + const DEFAULT_CATALOG_PATH = '/catalog-info.yaml'; const DEFAULT_PROVIDER_ID = 'default'; -const DEFAULT_TOPIC_INCLUSION = false; +const DEFAULT_TOPIC_FILTER_TYPE: TopicTypes = TopicTypes.Excludes; export type GitHubEntityProviderConfig = { id: string; @@ -27,8 +35,10 @@ export type GitHubEntityProviderConfig = { filters?: { repository?: RegExp; branch?: string; - topic?: string; - topicIncludesIfMatch: boolean; + topic?: { + name?: string; + type: TopicTypes; + }; }; }; @@ -61,10 +71,9 @@ function readProviderConfig( config.getOptionalString('catalogPath') ?? DEFAULT_CATALOG_PATH; const repositoryPattern = config.getOptionalString('filters.repository'); const branchPattern = config.getOptionalString('filters.branch'); - const topicPattern = config.getOptionalString('filters.topic'); - const topicIncludesIfMatch = - config.getOptionalBoolean('filters.topicIncludesIfMatch') ?? - DEFAULT_TOPIC_INCLUSION; + const topicFilterConfig = config.getOptionalConfig('filters.topic'); + const topicFilterName = topicFilterConfig?.getOptionalString('name'); + const topicFilterType = topicFilterConfig?.getOptionalString('type'); return { id, @@ -75,8 +84,13 @@ function readProviderConfig( ? compileRegExp(repositoryPattern) : undefined, branch: branchPattern || undefined, - topic: topicPattern || undefined, - topicIncludesIfMatch, + topic: { + name: topicFilterName || undefined, + type: + topicFilterType && isValidTopicType(topicFilterType) + ? (topicFilterType as TopicTypes) + : DEFAULT_TOPIC_FILTER_TYPE, + }, }, }; }