Merge pull request #19616 from awanlin/topic/refactor-annotation-values

Refactored annotation values
This commit is contained in:
Fredrik Adelöw
2023-10-23 14:31:50 +02:00
committed by GitHub
19 changed files with 982 additions and 146 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops': patch
---
Consolidated getting the annotation values into a single function to help with future changes
@@ -41,6 +41,7 @@ import {
EntityAzurePullRequestsContent,
isAzureDevOpsAvailable,
isAzurePipelinesAvailable,
EntityAzureReadmeCard,
} from '@backstage/plugin-azure-devops';
import {
isOctopusDeployAvailable,
@@ -415,6 +416,14 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
<Grid item md={6}>
<EntityAzureReadmeCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={2}>
<InfoCard title="Rate this entity">
<LikeDislikeButtons />
@@ -16,14 +16,14 @@
import { BuildTable } from '../BuildTable/BuildTable';
import React from 'react';
import { getAnnotationFromEntity } from '../../utils/getAnnotationFromEntity';
import { getAnnotationValuesFromEntity } from '../../utils/getAnnotationValuesFromEntity';
import { useBuildRuns } from '../../hooks/useBuildRuns';
import { useEntity } from '@backstage/plugin-catalog-react';
export const EntityPageAzurePipelines = (props: { defaultLimit?: number }) => {
const { entity } = useEntity();
const { project, repo, definition } = getAnnotationFromEntity(entity);
const { project, repo, definition } = getAnnotationValuesFromEntity(entity);
const { items, loading, error } = useBuildRuns(
project,
@@ -23,11 +23,9 @@ import {
ErrorPanel,
} from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useProjectRepoFromEntity } from '../../hooks';
import { useApi } from '@backstage/core-plugin-api';
import React from 'react';
import { azureDevOpsApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
import { useReadme } from '../../hooks';
const useStyles = makeStyles(theme => ({
readMe: {
@@ -85,18 +83,9 @@ const ReadmeCardError = ({ error }: ErrorProps) => {
export const ReadmeCard = (props: Props) => {
const classes = useStyles();
const api = useApi(azureDevOpsApiRef);
const { entity } = useEntity();
const { project, repo } = useProjectRepoFromEntity(entity);
const { loading, error, value } = useAsync(
() =>
api.getReadme({
project,
repo,
}),
[api, project, repo, entity],
);
const { loading, error, item: value } = useReadme(entity);
if (loading) {
return <Progress />;
+1
View File
@@ -16,6 +16,7 @@
export const AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION =
'dev.azure.com/build-definition';
export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org';
export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project';
export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo';
export const AZURE_DEVOPS_DEFAULT_TOP: number = 10;
+1 -1
View File
@@ -16,8 +16,8 @@
export * from './useAllTeams';
export * from './useDashboardPullRequests';
export * from './useProjectRepoFromEntity';
export * from './usePullRequests';
export * from './useRepoBuilds';
export * from './useUserEmail';
export * from './useUserTeamIds';
export * from './useReadme';
@@ -0,0 +1,130 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { useGitTags } from './useGitTags';
import { Entity } from '@backstage/catalog-model';
import { GitTag } from '@backstage/plugin-azure-devops-common';
import { TestApiProvider } from '@backstage/test-utils';
import { AzureDevOpsApi, azureDevOpsApiRef } from '../api';
describe('useGitTags', () => {
const azureDevOpsApiMock = {
getGitTags: jest.fn(),
};
const azureDevOpsApi =
azureDevOpsApiMock as Partial<AzureDevOpsApi> as AzureDevOpsApi;
const Wrapper = (props: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[azureDevOpsApiRef, azureDevOpsApi]]}>
{props.children}
</TestApiProvider>
);
it('should provide an array of GitTag', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
const tags: GitTag[] = [
{
objectId: 'tag-1',
peeledObjectId: 'tag-1',
name: 'tag-1',
createdBy: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-1',
},
{
objectId: 'tag-2',
peeledObjectId: 'tag-2',
name: 'tag-2',
createdBy: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-2',
},
{
objectId: 'tag-3',
peeledObjectId: 'tag-3',
name: 'tag-3',
createdBy: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-3',
},
];
azureDevOpsApiMock.getGitTags.mockResolvedValue({ items: tags });
const { result } = renderHook(() => useGitTags(entity), {
wrapper: Wrapper,
});
expect(result.current.loading).toEqual(true);
await waitFor(() => {
expect(result.current).toEqual({
error: undefined,
items: tags,
loading: false,
});
});
});
it('should return throw when annotation missing', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
},
};
expect(() =>
renderHook(() => useGitTags(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
});
it('should return throw when annotation invalid', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'fake',
},
},
};
expect(() =>
renderHook(() => useGitTags(entity), {
wrapper: Wrapper,
}),
).toThrow(
'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: <project-name>/<repo-name>, found: "fake"',
);
});
});
+4 -4
View File
@@ -20,7 +20,7 @@ import { Entity } from '@backstage/catalog-model';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
import { getAnnotationValuesFromEntity } from '../utils';
export function useGitTags(entity: Entity): {
items?: GitTag[];
@@ -28,10 +28,10 @@ export function useGitTags(entity: Entity): {
error?: Error;
} {
const api = useApi(azureDevOpsApiRef);
const { project, repo } = useProjectRepoFromEntity(entity);
const { project, repo } = getAnnotationValuesFromEntity(entity);
const { value, loading, error } = useAsync(() => {
return api.getGitTags(project, repo);
const { value, loading, error } = useAsync(async () => {
return await api.getGitTags(project, repo as string);
}, [api, project, repo]);
return {
@@ -1,47 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Entity } from '@backstage/catalog-model';
import { AZURE_DEVOPS_REPO_ANNOTATION } from '../constants';
export function useProjectRepoFromEntity(entity: Entity): {
project: string;
repo: string;
} {
const [project, repo] = (
entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION] ?? ''
).split('/');
if (!project && !repo) {
throw new Error(
'Value for annotation dev.azure.com/project-repo was not in the correct format: <project-name>/<repo-name>',
);
}
if (!project) {
throw new Error(
'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: <project-name>/<repo-name>',
);
}
if (!repo) {
throw new Error(
'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: <project-name>/<repo-name>',
);
}
return { project, repo };
}
@@ -0,0 +1,129 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import { PullRequest } from '@backstage/plugin-azure-devops-common';
import { TestApiProvider } from '@backstage/test-utils';
import { AzureDevOpsApi, azureDevOpsApiRef } from '../api';
import { usePullRequests } from './usePullRequests';
describe('usePullRequests', () => {
const azureDevOpsApiMock = {
getPullRequests: jest.fn(),
};
const azureDevOpsApi =
azureDevOpsApiMock as Partial<AzureDevOpsApi> as AzureDevOpsApi;
const Wrapper = (props: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[azureDevOpsApiRef, azureDevOpsApi]]}>
{props.children}
</TestApiProvider>
);
it('should provide an array of PullRequest', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
const pullRequests: PullRequest[] = [
{
pullRequestId: 1,
repoName: 'repo',
title: 'title-1',
createdBy: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
},
{
pullRequestId: 2,
repoName: 'repo',
title: 'title-2',
createdBy: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
},
{
pullRequestId: 3,
repoName: 'repo',
title: 'title-3',
createdBy: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
},
];
azureDevOpsApiMock.getPullRequests.mockResolvedValue({
items: pullRequests,
});
const { result } = renderHook(() => usePullRequests(entity), {
wrapper: Wrapper,
});
expect(result.current.loading).toEqual(true);
await waitFor(() => {
expect(result.current).toEqual({
error: undefined,
items: pullRequests,
loading: false,
});
});
});
it('should return throw when annotation missing', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
},
};
expect(() =>
renderHook(() => usePullRequests(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
});
it('should return throw when annotation invalid', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'fake',
},
},
};
expect(() =>
renderHook(() => usePullRequests(entity), {
wrapper: Wrapper,
}),
).toThrow(
'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: <project-name>/<repo-name>, found: "fake"',
);
});
});
@@ -25,7 +25,7 @@ import { Entity } from '@backstage/catalog-model';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
import { getAnnotationValuesFromEntity } from '../utils';
export function usePullRequests(
entity: Entity,
@@ -44,10 +44,10 @@ export function usePullRequests(
};
const api = useApi(azureDevOpsApiRef);
const { project, repo } = useProjectRepoFromEntity(entity);
const { project, repo } = getAnnotationValuesFromEntity(entity);
const { value, loading, error } = useAsync(() => {
return api.getPullRequests(project, repo, options);
const { value, loading, error } = useAsync(async () => {
return await api.getPullRequests(project, repo as string, options);
}, [api, project, repo, top, status]);
return {
@@ -0,0 +1,108 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import { Readme } from '@backstage/plugin-azure-devops-common';
import { TestApiProvider } from '@backstage/test-utils';
import { AzureDevOpsApi, azureDevOpsApiRef } from '../api';
import { useReadme } from './useReadme';
describe('useReadme', () => {
const azureDevOpsApiMock = {
getReadme: jest.fn(),
};
const azureDevOpsApi =
azureDevOpsApiMock as Partial<AzureDevOpsApi> as AzureDevOpsApi;
const Wrapper = (props: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[azureDevOpsApiRef, azureDevOpsApi]]}>
{props.children}
</TestApiProvider>
);
it('should provide a Readme', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
const readme: Readme = {
url: 'https://dev.azure.com/org/project/repo',
content: 'This is some fake README content',
};
azureDevOpsApiMock.getReadme.mockResolvedValue({
item: readme,
});
const { result } = renderHook(() => useReadme(entity), {
wrapper: Wrapper,
});
expect(result.current.loading).toEqual(true);
await waitFor(() => {
expect(result.current.item).toEqual({
item: readme,
});
});
});
it('should return throw when annotation missing', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
},
};
expect(() =>
renderHook(() => useReadme(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
});
it('should return throw when annotation invalid', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'fake',
},
},
};
expect(() =>
renderHook(() => useReadme(entity), {
wrapper: Wrapper,
}),
).toThrow(
'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: <project-name>/<repo-name>, found: "fake"',
);
});
});
@@ -0,0 +1,42 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Readme } from '@backstage/plugin-azure-devops-common';
import { Entity } from '@backstage/catalog-model';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { getAnnotationValuesFromEntity } from '../utils';
export function useReadme(entity: Entity): {
item?: Readme;
loading: boolean;
error?: Error;
} {
const api = useApi(azureDevOpsApiRef);
const { project, repo } = getAnnotationValuesFromEntity(entity);
const { value, loading, error } = useAsync(async () => {
return await api.getReadme({ project, repo: repo as string });
}, [api, project, repo]);
return {
item: value,
loading,
error,
};
}
@@ -0,0 +1,126 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import { RepoBuild } from '@backstage/plugin-azure-devops-common';
import { TestApiProvider } from '@backstage/test-utils';
import { AzureDevOpsApi, azureDevOpsApiRef } from '../api';
import { useRepoBuilds } from './useRepoBuilds';
describe('useRepoBuilds', () => {
const azureDevOpsApiMock = {
getRepoBuilds: jest.fn(),
};
const azureDevOpsApi =
azureDevOpsApiMock as Partial<AzureDevOpsApi> as AzureDevOpsApi;
const Wrapper = (props: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[azureDevOpsApiRef, azureDevOpsApi]]}>
{props.children}
</TestApiProvider>
);
it('should provide an array of RepoBuild', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
const repoBuilds: RepoBuild[] = [
{
id: 1,
title: 'title-1',
source: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
},
{
id: 2,
title: 'title-2',
source: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
},
{
id: 3,
title: 'title-3',
source: 'awanlin',
link: 'https://dev.azure.com/org/project/repo',
},
];
azureDevOpsApiMock.getRepoBuilds.mockResolvedValue({
items: repoBuilds,
});
const { result } = renderHook(() => useRepoBuilds(entity), {
wrapper: Wrapper,
});
expect(result.current.loading).toEqual(true);
await waitFor(() => {
expect(result.current).toEqual({
error: undefined,
items: repoBuilds,
loading: false,
});
});
});
it('should return throw when annotation missing', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
},
};
expect(() =>
renderHook(() => useRepoBuilds(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
});
it('should return throw when annotation invalid', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'fake',
},
},
};
expect(() =>
renderHook(() => useRepoBuilds(entity), {
wrapper: Wrapper,
}),
).toThrow(
'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: <project-name>/<repo-name>, found: "fake"',
);
});
});
@@ -24,7 +24,7 @@ import { Entity } from '@backstage/catalog-model';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
import { getAnnotationValuesFromEntity } from '../utils';
export function useRepoBuilds(
entity: Entity,
@@ -40,10 +40,10 @@ export function useRepoBuilds(
};
const api = useApi(azureDevOpsApiRef);
const { project, repo } = useProjectRepoFromEntity(entity);
const { project, repo } = getAnnotationValuesFromEntity(entity);
const { value, loading, error } = useAsync(() => {
return api.getRepoBuilds(project, repo, options);
const { value, loading, error } = useAsync(async () => {
return await api.getRepoBuilds(project, repo as string, options);
}, [api, project, repo, entity]);
return {
@@ -1,69 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION,
AZURE_DEVOPS_PROJECT_ANNOTATION,
AZURE_DEVOPS_REPO_ANNOTATION,
} from '../constants';
import { Entity } from '@backstage/catalog-model';
export function getAnnotationFromEntity(entity: Entity): {
project: string;
repo?: string;
definition?: string;
} {
const annotation =
entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION];
if (annotation) {
const { project, repo } = getProjectRepo(annotation);
const definition = undefined;
return { project, repo, definition };
}
const project =
entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION];
if (!project) {
throw new Error('Value for annotation dev.azure.com/project was not found');
}
const definition =
entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION];
if (!definition) {
throw new Error(
'Value for annotation dev.azure.com/build-definition was not found',
);
}
const repo = undefined;
return { project, repo, definition };
}
function getProjectRepo(annotation: string): {
project: string;
repo: string;
} {
const [project, repo] = annotation.split('/');
if (!project && !repo) {
throw new Error(
'Value for annotation dev.azure.com/project-repo was not in the correct format: <project-name>/<repo-name>',
);
}
return { project, repo };
}
@@ -0,0 +1,310 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Entity } from '@backstage/catalog-model';
import { getAnnotationValuesFromEntity } from './getAnnotationValuesFromEntity';
describe('getAnnotationValuesFromEntity', () => {
describe('with valid project-repo annotation', () => {
it('should return project and repo', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: 'repoName',
definition: undefined,
host: undefined,
org: undefined,
});
});
});
describe('with invalid project-repo annotation', () => {
it('should throw incorrect format error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'project',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: <project-name>/<repo-name>, found: "project"',
);
});
});
describe('with project-repo annotation missing project', () => {
it('should throw missing project error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': '/repo',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: <project-name>/<repo-name>, found: "/repo"',
);
});
});
describe('with project-repo annotation missing repo', () => {
it('should throw missing repo error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'project/',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: <project-name>/<repo-name>, found: "project/"',
);
});
});
describe('with valid project and build-definition annotations', () => {
it('should return project and definition', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-build-definition',
annotations: {
'dev.azure.com/project': 'projectName',
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: undefined,
definition: 'buildDefinitionName',
host: undefined,
org: undefined,
});
});
});
describe('with only project annotation', () => {
it('should should throw annotation not found error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project',
annotations: {
'dev.azure.com/project': 'projectName',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Value for annotation "dev.azure.com/build-definition" was not found',
);
});
});
describe('with only build-definition annotation', () => {
it('should should throw annotation not found error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'build-definition',
annotations: {
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Value for annotation "dev.azure.com/project" was not found',
);
});
});
describe('with valid project-repo and host-org annotations', () => {
it('should return project, repo, host, and org', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
'dev.azure.com/host-org': 'hostName/organizationName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: 'repoName',
definition: undefined,
host: 'hostName',
org: 'organizationName',
});
});
});
describe('with valid project, build-definition, and host-org annotations', () => {
it('should return project, definition, host and org', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-build-definition',
annotations: {
'dev.azure.com/project': 'projectName',
'dev.azure.com/build-definition': 'buildDefinitionName',
'dev.azure.com/host-org': 'hostName/organizationName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: undefined,
definition: 'buildDefinitionName',
host: 'hostName',
org: 'organizationName',
});
});
});
describe('with invalid host-org annotation', () => {
it('should throw incorrect format error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'host-org',
annotations: {
'dev.azure.com/host-org': 'host',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Invalid value for annotation "dev.azure.com/host-org"; expected format is: <host-name>/<organization-name>, found: "host"',
);
});
});
describe('with host-org annotation missing host', () => {
it('should throw missing project error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'host-org',
annotations: {
'dev.azure.com/host-org': '/org',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Invalid value for annotation "dev.azure.com/host-org"; expected format is: <host-name>/<organization-name>, found: "/org"',
);
});
});
describe('with host-org annotation missing org', () => {
it('should throw missing repo error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'host-org',
annotations: {
'dev.azure.com/host-org': 'host/',
},
},
};
const test = () => {
return getAnnotationValuesFromEntity(entity);
};
expect(test).toThrow(
'Invalid value for annotation "dev.azure.com/host-org"; expected format is: <host-name>/<organization-name>, found: "host/"',
);
});
});
});
@@ -0,0 +1,103 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION,
AZURE_DEVOPS_HOST_ORG_ANNOTATION,
AZURE_DEVOPS_PROJECT_ANNOTATION,
AZURE_DEVOPS_REPO_ANNOTATION,
} from '../constants';
import { Entity } from '@backstage/catalog-model';
export function getAnnotationValuesFromEntity(entity: Entity): {
project: string;
repo?: string;
definition?: string;
host?: string;
org?: string;
} {
const { host, org } = getHostOrg(entity.metadata.annotations);
const projectRepoValues = getProjectRepo(entity.metadata.annotations);
if (projectRepoValues.project && projectRepoValues.repo) {
return {
project: projectRepoValues.project,
repo: projectRepoValues.repo,
host,
org,
};
}
const project =
entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION];
if (!project) {
throw new Error(
`Value for annotation "${AZURE_DEVOPS_PROJECT_ANNOTATION}" was not found`,
);
}
const definition =
entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION];
if (!definition) {
throw new Error(
`Value for annotation "${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION}" was not found`,
);
}
return { project, definition, host, org };
}
function getProjectRepo(annotations?: Record<string, string>): {
project?: string;
repo?: string;
} {
const annotation = annotations?.[AZURE_DEVOPS_REPO_ANNOTATION];
if (!annotation) {
return { project: undefined, repo: undefined };
}
if (annotation.includes('/')) {
const [project, repo] = annotation.split('/');
if (project && repo) {
return { project, repo };
}
}
throw new Error(
`Invalid value for annotation "${AZURE_DEVOPS_REPO_ANNOTATION}"; expected format is: <project-name>/<repo-name>, found: "${annotation}"`,
);
}
function getHostOrg(annotations?: Record<string, string>): {
host?: string;
org?: string;
} {
const annotation = annotations?.[AZURE_DEVOPS_HOST_ORG_ANNOTATION];
if (!annotation) {
return { host: undefined, org: undefined };
}
if (annotation.includes('/')) {
const [host, org] = annotation.split('/');
if (host && org) {
return { host, org };
}
}
throw new Error(
`Invalid value for annotation "${AZURE_DEVOPS_HOST_ORG_ANNOTATION}"; expected format is: <host-name>/<organization-name>, found: "${annotation}"`,
);
}
+1 -1
View File
@@ -17,4 +17,4 @@
export * from './arrayHas';
export * from './equalsIgnoreCase';
export * from './getDurationFromDates';
export * from './getAnnotationFromEntity';
export * from './getAnnotationValuesFromEntity';