fix(catalog-backend-module-azure): Add branch to Code Search query when provided

Signed-off-by: Ragnar Halldórsson <halldorsson@copopt.com>
This commit is contained in:
Ragnar Halldórsson
2024-02-15 21:38:00 +01:00
parent ecf1a6d91e
commit 534786aa21
6 changed files with 175 additions and 13 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-azure': minor
---
Fixed issue where specifying a branch for discovery did not work
@@ -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,
@@ -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<CodeSearchResultItem[]> {
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) {
@@ -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();
});
@@ -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,
};
}
@@ -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`);