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:
@@ -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) {
|
||||
|
||||
+63
@@ -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();
|
||||
});
|
||||
|
||||
+18
-2
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user