diff --git a/.changeset/real-keys-juggle.md b/.changeset/real-keys-juggle.md new file mode 100644 index 0000000000..cb323853de --- /dev/null +++ b/.changeset/real-keys-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': minor +--- + +Fixed issue where specifying a branch for discovery did not work diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index 493f0dda51..ce06fd6221 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -214,6 +214,66 @@ describe('azure', () => { ).resolves.toEqual(response.results); }); + it('searches in specific branch if parameter is set', async () => { + const response: CodeSearchResponse = { + count: 1, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + project: { + name: '*', + }, + repository: { + name: 'backstage', + }, + }, + ], + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: + 'path:/catalog-info.yaml repo:backstage proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], + $skip: 0, + $top: 1000, + filters: { + Branch: ['development'], + }, + }); + return res(ctx.json(response)); + }, + ), + ); + + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + + await expect( + codeSearch( + credentialsProvider, + azureConfig, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + 'development', + ), + ).resolves.toEqual(response.results); + }); + it('can search using onpremise api', async () => { const response: CodeSearchResponse = { count: 1, diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts index c8ff8dd739..3bdff90110 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.ts @@ -37,6 +37,16 @@ export interface CodeSearchResultItem { branch?: string; } +interface CodeSearchRequest { + searchText: string; + $orderBy: Array<{ field: string; sortOrder: string }>; + $skip: number; + $top: number; + filters?: { + Branch: string[]; + }; +} + const isCloud = (host: string) => host === 'dev.azure.com'; const PAGE_SIZE = 1000; @@ -48,6 +58,7 @@ export async function codeSearch( project: string, repo: string, path: string, + branch: string, ): Promise { const searchBaseUrl = isCloud(azureConfig.host) ? 'https://almsearch.dev.azure.com' @@ -62,23 +73,29 @@ export async function codeSearch( url: `https://${azureConfig.host}/${org}`, }); + const searchRequestBody: CodeSearchRequest = { + searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], + $skip: items.length, + $top: PAGE_SIZE, + }; + + if (branch) { + searchRequestBody.filters = { Branch: [branch] }; + } + const response = await fetch(searchUrl, { headers: { ...credentials?.headers, 'Content-Type': 'application/json', }, method: 'POST', - body: JSON.stringify({ - searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, - $orderBy: [ - { - field: 'path', - sortOrder: 'ASC', - }, - ], - $skip: items.length, - $top: PAGE_SIZE, - }), + body: JSON.stringify(searchRequestBody), }); if (response.status !== 200) { diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index 36dd920621..0817465d83 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -35,6 +35,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { project: 'my-proj', repo: '', catalogPath: '/catalog-info.yaml', + branch: '', }); expect( @@ -47,6 +48,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { project: 'engineering', repo: 'backstage', catalogPath: '/catalog.yaml', + branch: '', }); expect( @@ -59,6 +61,20 @@ describe('AzureDevOpsDiscoveryProcessor', () => { project: 'engineering', repo: 'backstage', catalogPath: '/src/*/catalog.yaml', + branch: '', + }); + + expect( + parseUrl( + 'https://azuredevops.mycompany.com/spotify/engineering/_git/backstage?path=/src/*/catalog.yaml&version=GBdevelopment', + ), + ).toEqual({ + baseUrl: 'https://azuredevops.mycompany.com', + org: 'spotify', + project: 'engineering', + repo: 'backstage', + catalogPath: '/src/*/catalog.yaml', + branch: 'development', }); }); @@ -164,6 +180,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', '', '/catalog-info.yaml', + '', ); expect(emitter).toHaveBeenCalledTimes(2); expect(emitter).toHaveBeenCalledWith({ @@ -214,6 +231,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', 'backstage', '/catalog-info.yaml', + '', ); expect(emitter).toHaveBeenCalledTimes(1); expect(emitter).toHaveBeenCalledWith({ @@ -227,6 +245,49 @@ describe('AzureDevOpsDiscoveryProcessor', () => { }); }); + it('output locations with branch if specified in target', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml&version=GBdevelopment', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + project: { + name: '*', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + 'development', + ); + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml&version=GBdevelopment', + presence: 'optional', + }, + }); + }); + it('output single locations with different file name from code search', async () => { const location: LocationSpec = { type: 'azure-discovery', @@ -256,6 +317,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', '', '/src/*/catalog.yaml', + '', ); expect(emitter).toHaveBeenCalledTimes(1); expect(emitter).toHaveBeenCalledWith({ @@ -286,6 +348,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { 'engineering', 'backstage', '/catalog-info.yaml', + '', ); expect(emitter).not.toHaveBeenCalled(); }); diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index d9638e6cc7..ddb3c5f59f 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -92,7 +92,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); } - const { baseUrl, org, project, repo, catalogPath } = parseUrl( + const { baseUrl, org, project, repo, catalogPath, branch } = parseUrl( location.target, ); this.logger.info( @@ -106,6 +106,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { project, repo, catalogPath, + branch, ); this.logger.debug( @@ -113,10 +114,16 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); for (const file of files) { + let target = `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`; + + if (branch) { + target += `&version=GB${branch}`; + } + emit( processingResult.location({ type: 'url', - target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, + target, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting // an error if it couldn't. @@ -138,11 +145,18 @@ export function parseUrl(urlString: string): { project: string; repo: string; catalogPath: string; + branch: string; } { const url = new URL(urlString); const path = url.pathname.slice(1).split('/'); const catalogPath = url.searchParams.get('path') || '/catalog-info.yaml'; + let branch = url.searchParams.get('version') || ''; + + if (branch.startsWith('GB')) { + // DevOps prefixes branch names with 'GB' in URLs + branch = branch.slice(2); + } if (path.length === 2 && path[0].length && path[1].length) { return { @@ -151,6 +165,7 @@ export function parseUrl(urlString: string): { project: decodeURIComponent(path[1]), repo: '', catalogPath, + branch, }; } else if ( path.length === 4 && @@ -165,6 +180,7 @@ export function parseUrl(urlString: string): { project: decodeURIComponent(path[1]), repo: decodeURIComponent(path[3]), catalogPath, + branch, }; } diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts index f56369a0d1..44e618ffcd 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -158,6 +158,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { this.config.project, this.config.repository, this.config.path, + this.config.branch || '', ); logger.info(`Discovered ${files.length} catalog files`);