diff --git a/.changeset/angry-boats-hunt.md b/.changeset/angry-boats-hunt.md new file mode 100644 index 0000000000000..32933579cacae --- /dev/null +++ b/.changeset/angry-boats-hunt.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Add `AnnotateScmSlugEntityProcessor` that automatically adds the +`github.com/project-slug` annotation for components coming from GitHub. + +The processor is optional and not automatically registered in the catalog +builder. To add it to your instance, add it to your `CatalogBuilder` using +`addProcessor()`: + +```typescript +const builder = new CatalogBuilder(env); +builder.addProcessor(new AnnotateScmSlugEntityProcessor()); +``` diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts new file mode 100644 index 0000000000000..d31122b6f7f58 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Entity, LocationSpec } from '@backstage/catalog-model'; +import { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; + +describe('AnnotateScmSlugEntityProcessor', () => { + describe('github', () => { + it('adds annotation', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + }; + const location: LocationSpec = { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }; + + const processor = new AnnotateScmSlugEntityProcessor(); + + expect(await processor.preProcessEntity(entity, location)).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'github.com/project-slug': 'backstage/backstage', + }, + }, + }); + }); + + it('does not override existing annotation', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'github.com/project-slug': 'backstage/community', + }, + }, + }; + const location: LocationSpec = { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }; + + const processor = new AnnotateScmSlugEntityProcessor(); + + expect(await processor.preProcessEntity(entity, location)).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'github.com/project-slug': 'backstage/community', + }, + }, + }); + }); + + it('should not add annotation for other providers', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + }; + const location: LocationSpec = { + type: 'url', + target: + 'https://gitlab.com/backstage/backstage/-/blob/master/catalog-info.yaml', + }; + + const processor = new AnnotateScmSlugEntityProcessor(); + + expect(await processor.preProcessEntity(entity, location)).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: {}, + }, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts new file mode 100644 index 0000000000000..d74e9521e06f3 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Entity, LocationSpec } from '@backstage/catalog-model'; +import parseGitUrl from 'git-url-parse'; +import { identity, merge, pickBy } from 'lodash'; +import { CatalogProcessor } from './types'; + +const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; + +export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { + async preProcessEntity( + entity: Entity, + location: LocationSpec, + ): Promise { + if (entity.kind !== 'Component' || location.type !== 'url') { + return entity; + } + + const gitUrl = parseGitUrl(location.target); + let githubProjectSlug = + entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]; + + if (gitUrl.source === 'github.com' && !githubProjectSlug) { + githubProjectSlug = `${gitUrl.owner}/${gitUrl.name}`; + } + + return merge( + { + metadata: { + annotations: pickBy( + { + [GITHUB_ACTIONS_ANNOTATION]: githubProjectSlug, + }, + identity, + ), + }, + }, + entity, + ); + } +} diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index 736c64ea39a40..fe4fe1c1ddb6b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -17,6 +17,7 @@ import * as results from './results'; export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; +export { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor';