diff --git a/.changeset/spicy-carrots-argue.md b/.changeset/spicy-carrots-argue.md new file mode 100644 index 0000000000..b6475b1f7d --- /dev/null +++ b/.changeset/spicy-carrots-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add monorepo support to CodeOwnersProccesor. diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts index 9cb4d414c0..6c2774a6fb 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts @@ -17,11 +17,13 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { CodeOwnersProcessor } from './CodeOwnersProcessor'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; const mockCodeOwnersText = () => ` -* @acme/team-foo @acme/team-bar -/docs @acme/team-bar +* @acme/team-foo @acme/team-bar +/docs @acme/team-docs +/plugins/catalog-* @backstage/maintainers @backstage/catalog-core +**/logs @logs-owner `; describe('CodeOwnersProcessor', () => { @@ -34,8 +36,12 @@ describe('CodeOwnersProcessor', () => { }); describe('preProcessEntity', () => { - const setupTest = ({ kind = 'Component', spec = {} } = {}) => { - const entity = { kind, spec }; + const setupTest = ({ + kind = 'Component', + spec = {}, + metadata = {}, + } = {}) => { + const entity = { kind, spec, metadata }; const config = new ConfigReader({}); const reader = { read: jest.fn(), @@ -101,5 +107,44 @@ describe('CodeOwnersProcessor', () => { spec: { owner: 'team-foo' }, }); }); + it('should match owner based on the targetUrl', async () => { + const { entity, processor } = setupTest(); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation({ basePath: 'docs/' }), + ); + + expect(result).toEqual({ + ...entity, + spec: { owner: 'team-docs' }, + }); + }); + it('should match wildcard pattern', async () => { + const { entity, processor } = setupTest(); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation({ basePath: 'plugins/catalog-foo' }), + ); + + expect(result).toEqual({ + ...entity, + spec: { owner: 'maintainers' }, + }); + }); + it('should match glob pattern', async () => { + const { entity, processor } = setupTest(); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation({ basePath: 'plugins/catalog-foo/logs/1.txt' }), + ); + + expect(result).toEqual({ + ...entity, + spec: { owner: 'User:logs-owner' }, + }); + }); }); }); diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts index 2987e162cc..6a5234c94f 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts @@ -70,7 +70,7 @@ export async function findCodeOwnerByTarget( return undefined; } - const owner = resolveCodeOwner(contents); + const owner = resolveCodeOwner(contents, targetUrl); return owner; } diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts index 4e6c646073..f9cdf79530 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts @@ -18,12 +18,33 @@ import { normalizeCodeOwner, resolveCodeOwner } from './resolve'; const mockCodeOwnersText = () => ` * @acme/team-foo @acme/team-bar -/docs @acme/team-bar +/docs @acme/team-docs `; describe('resolveCodeOwner', () => { it('should parse the codeowners file', () => { - expect(resolveCodeOwner(mockCodeOwnersText())).toBe('team-foo'); + expect( + resolveCodeOwner( + mockCodeOwnersText(), + 'https://github.com/can/be/tree/anything/catalog-info.yaml', + ), + ).toBe('team-foo'); + }); + it('should include the codeowners path into the provided pattern', () => { + expect( + resolveCodeOwner( + mockCodeOwnersText(), + 'https://github.com/acme/repo/tree/main/docs/catalog-info.yaml', + ), + ).toBe('team-docs'); + }); + it('should match only in resource path', () => { + expect( + resolveCodeOwner( + mockCodeOwnersText(), + 'https://github.com/acme/repo/tree/docs/catalog-info.yaml', + ), + ).toBe('team-foo'); }); }); diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts index 00645f0c9d..675f6542aa 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts @@ -15,8 +15,7 @@ */ import * as codeowners from 'codeowners-utils'; -import { CodeOwnersEntry } from 'codeowners-utils'; -import { filter, get, head, pipe, reverse } from 'lodash/fp'; +import parseGitUrl from 'git-url-parse'; const USER_PATTERN = /^@.*/; const GROUP_PATTERN = /^@.*\/.*/; @@ -24,18 +23,14 @@ const EMAIL_PATTERN = /^.*@.*\..*$/; export function resolveCodeOwner( contents: string, - pattern = '*', + catalogInfoFileUrl: string, ): string | undefined { - const owners = codeowners.parse(contents); + const codeOwnerEntries = codeowners.parse(contents); - return pipe( - filter((e: CodeOwnersEntry) => e.pattern === pattern), - reverse, - head, - get('owners'), - head, - normalizeCodeOwner, - )(owners); + const { filepath } = parseGitUrl(catalogInfoFileUrl); + const match = codeowners.matchFile(filepath, codeOwnerEntries); + + return match ? normalizeCodeOwner(match.owners[0]) : undefined; } export function normalizeCodeOwner(owner: string) {