Modifies component registration to use existing file if present
Creates additional check on catalog import form to check for an existing catalog-info.yaml file if repo URL put in. Registers an existing file instead of creating pull request in case catalog file is present. Defaults to the root-most catalog-info.yaml file if multiple defined in the repository.
This commit is contained in:
@@ -30,6 +30,11 @@ export interface CatalogImportApi {
|
||||
fileContent: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
}): Promise<{ link: string; location: string }>;
|
||||
checkForExistingCatalogInfo(options: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
}): Promise<{ exists: boolean; url?: string }>;
|
||||
createRepositoryLocation(options: { location: string }): Promise<void>;
|
||||
generateEntityDefinitions(options: {
|
||||
repo: string;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020 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 { CatalogImportClient } from './CatalogImportClient';
|
||||
|
||||
jest.mock('@octokit/rest', () => ({
|
||||
Octokit: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
repos: {
|
||||
get: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
default_branch: 'main',
|
||||
},
|
||||
}),
|
||||
},
|
||||
search: {
|
||||
code: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
total_count: 2,
|
||||
items: [
|
||||
{ path: 'simple/path/catalog-info.yaml' },
|
||||
{ path: 'co/mple/x/path/catalog-info.yaml' },
|
||||
{ path: 'catalog-info.yaml' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('CatalogImportClient', () => {
|
||||
describe('checkForExistingCatalogInfo', () => {
|
||||
const cic = new CatalogImportClient({
|
||||
discoveryApi: { getBaseUrl: () => Promise.resolve('base') },
|
||||
githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') },
|
||||
configApi: {} as any,
|
||||
});
|
||||
it('should return the closest-to-root catalog-info from multiple responses', async () => {
|
||||
const respo = await cic.checkForExistingCatalogInfo({
|
||||
owner: 'test-user',
|
||||
repo: 'rest-repo',
|
||||
githubIntegrationConfig: { host: 'https://github.com' },
|
||||
});
|
||||
expect(respo.exists).toBe(true);
|
||||
expect(respo.url).toBe('blob/main/catalog-info.yaml');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -91,6 +91,54 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
}
|
||||
}
|
||||
|
||||
async checkForExistingCatalogInfo({
|
||||
owner,
|
||||
repo,
|
||||
githubIntegrationConfig,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
}): Promise<{ exists: boolean; url?: string }> {
|
||||
const token = await this.githubAuthApi.getAccessToken(['repo']);
|
||||
const octo = new Octokit({
|
||||
auth: token,
|
||||
baseUrl: githubIntegrationConfig.apiBaseUrl,
|
||||
});
|
||||
const catalogFileName = 'catalog-info.yaml';
|
||||
const query = `repo:${owner}/${repo}+filename:${catalogFileName}`;
|
||||
|
||||
const searchResult = await octo.search.code({ q: query }).catch(e => {
|
||||
throw new Error(
|
||||
formatHttpErrorMessage(
|
||||
"Couldn't search repository for metadata file.",
|
||||
e,
|
||||
),
|
||||
);
|
||||
});
|
||||
const exists = searchResult.data.total_count > 0;
|
||||
if (exists) {
|
||||
const repoInformation = await octo.repos.get({ owner, repo }).catch(e => {
|
||||
throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e));
|
||||
});
|
||||
const defaultBranch = repoInformation.data.default_branch;
|
||||
|
||||
// Github search sorts returned values with 'best match' using 'multiple factors to boost the most relevant item',
|
||||
// aka magic.
|
||||
// Sorting to use the shortest item, closest to the repository root.
|
||||
const catalogInfoItem = searchResult.data.items
|
||||
.map(it => it.path)
|
||||
.sort((a, b) => a.length - b.length)[0];
|
||||
return Promise.resolve({
|
||||
url: `blob/${defaultBranch}/${catalogInfoItem}`,
|
||||
exists,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
exists,
|
||||
});
|
||||
}
|
||||
|
||||
async submitPrToRepo({
|
||||
owner,
|
||||
repo,
|
||||
|
||||
@@ -59,27 +59,45 @@ export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => {
|
||||
|
||||
const isMounted = useMountedState();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const { generateEntityDefinitions } = useGithubRepos();
|
||||
const {
|
||||
generateEntityDefinitions,
|
||||
checkForExistingCatalogInfo,
|
||||
} = useGithubRepos();
|
||||
|
||||
const onSubmit = async (formData: Record<string, string>) => {
|
||||
const { componentLocation: target } = formData;
|
||||
try {
|
||||
if (!isMounted()) return;
|
||||
const type = !parseGitUri(target).filepathtype ? 'repo' : 'file';
|
||||
async function saveCatalogFileConfig(target: string) {
|
||||
const data = await catalogApi.addLocation({ target });
|
||||
saveConfig({
|
||||
type: 'file',
|
||||
location: data.location.target,
|
||||
config: data.entities,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'repo') {
|
||||
async function trySaveRepositoryConfig(target: string) {
|
||||
const existingCatalog = await checkForExistingCatalogInfo(target);
|
||||
if (existingCatalog.exists) {
|
||||
const targetUrl = target.endsWith('/')
|
||||
? `${target}${existingCatalog.url}`
|
||||
: `${target}/${existingCatalog.url}`;
|
||||
await saveCatalogFileConfig(targetUrl);
|
||||
} else {
|
||||
saveConfig({
|
||||
type,
|
||||
type: 'repo',
|
||||
location: target,
|
||||
config: await generateEntityDefinitions(target),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isMounted()) return;
|
||||
const type = !parseGitUri(target).filepathtype ? 'repo' : 'file';
|
||||
if (type === 'repo') {
|
||||
await trySaveRepositoryConfig(target);
|
||||
} else {
|
||||
const data = await catalogApi.addLocation({ target });
|
||||
saveConfig({
|
||||
type,
|
||||
location: data.location.target,
|
||||
config: data.entities,
|
||||
});
|
||||
await saveCatalogFileConfig(target);
|
||||
}
|
||||
nextStep();
|
||||
} catch (e) {
|
||||
|
||||
@@ -77,8 +77,9 @@ export const ImportComponentPage = ({
|
||||
</Typography>
|
||||
<Typography variant="h6">GitHub Repo</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
If you already have code in a GitHub repository, enter the full
|
||||
URL to your repo and a new pull request with a sample Backstage
|
||||
If you already have code in a GitHub repository without
|
||||
Backstage metadata file set up for it, enter the full URL to
|
||||
your repo and a new pull request with a sample Backstage
|
||||
metadata Entity File (<code>catalog-info.yaml</code>) will be
|
||||
opened for you.
|
||||
</Typography>
|
||||
|
||||
@@ -28,12 +28,12 @@ export function useGithubRepos() {
|
||||
const api = useApi(catalogImportApiRef);
|
||||
const config = useApi(configApiRef);
|
||||
|
||||
const submitPrToRepo = async (selectedRepo: ConfigSpec) => {
|
||||
const getGithubIntegrationConfig = (location: string) => {
|
||||
const {
|
||||
name: repoName,
|
||||
owner: ownerName,
|
||||
resource: hostname,
|
||||
} = parseGitUri(selectedRepo.location);
|
||||
} = parseGitUri(location);
|
||||
|
||||
const configs = readGitHubIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.github') ?? [],
|
||||
@@ -41,9 +41,22 @@ export function useGithubRepos() {
|
||||
const githubIntegrationConfig = configs.find(v => v.host === hostname);
|
||||
if (!githubIntegrationConfig) {
|
||||
throw new Error(
|
||||
`Unable to locate github-integration for repo-location: ${selectedRepo.location}`,
|
||||
`Unable to locate github-integration for repo-location: ${location}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
repoName,
|
||||
ownerName,
|
||||
githubIntegrationConfig,
|
||||
};
|
||||
};
|
||||
|
||||
const submitPrToRepo = async (selectedRepo: ConfigSpec) => {
|
||||
const {
|
||||
repoName,
|
||||
ownerName,
|
||||
githubIntegrationConfig,
|
||||
} = getGithubIntegrationConfig(selectedRepo.location);
|
||||
const submitPRResponse = await api
|
||||
.submitPrToRepo({
|
||||
owner: ownerName,
|
||||
@@ -68,8 +81,28 @@ export function useGithubRepos() {
|
||||
return submitPRResponse;
|
||||
};
|
||||
|
||||
const checkForExistingCatalogInfo = async (location: string) => {
|
||||
const {
|
||||
repoName,
|
||||
ownerName,
|
||||
githubIntegrationConfig,
|
||||
} = getGithubIntegrationConfig(location);
|
||||
return await api
|
||||
.checkForExistingCatalogInfo({
|
||||
owner: ownerName,
|
||||
repo: repoName,
|
||||
githubIntegrationConfig,
|
||||
})
|
||||
.catch(e => {
|
||||
throw new Error(
|
||||
`Failed to inspect repository for existing catalog-info.yaml:\n${e.message}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
submitPrToRepo,
|
||||
checkForExistingCatalogInfo,
|
||||
generateEntityDefinitions: (repo: string) =>
|
||||
api.generateEntityDefinitions({ repo }),
|
||||
addLocation: (location: string) =>
|
||||
|
||||
Reference in New Issue
Block a user