github-deployments plugin

Signed-off-by: Andrew Johnson <ajohnson@gocardless.com>
This commit is contained in:
Andrew Johnson
2021-03-25 16:01:10 +00:00
parent f7a796e8aa
commit ec023e8286
17 changed files with 688 additions and 0 deletions
+1
View File
@@ -19,6 +19,7 @@
"@backstage/plugin-explore": "^0.3.2",
"@backstage/plugin-gcp-projects": "^0.2.5",
"@backstage/plugin-github-actions": "^0.4.2",
"@backstage/plugin-github-deployments": "^0.1.1",
"@backstage/plugin-gitops-profiles": "^0.2.6",
"@backstage/plugin-graphiql": "^0.2.9",
"@backstage/plugin-jenkins": "^0.4.1",
+1
View File
@@ -45,3 +45,4 @@ export { plugin as Org } from '@backstage/plugin-org';
export { plugin as Kafka } from '@backstage/plugin-kafka';
export { todoPlugin } from '@backstage/plugin-todo';
export { badgesPlugin } from '@backstage/plugin-badges';
export { plugin as githubDeploymentsPlugin } from '@backstage/plugin-github-deployments';
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+75
View File
@@ -0,0 +1,75 @@
# Github Deployments Plugin
The Github Deployments Plugin displays recent deployments from Github.
![github-deployments-card](./docs/github-deployments-card.png)
## Getting Started
1. Install the Github Deployments Plugin
```bash
# packages/app
yarn add @backstage/plugin-github-deployments
```
2. Add proxy and auth token for Github
```yaml
# app-config.yaml
proxy:
...
'/github/api':
target: https://api.github.com
changeOrigin: true
secure: true
headers:
Authorization:
# Content: 'token OAUTH-TOKEN'
$env: GITHUB_OAUTH_TOKEN
```
3. Add the plugin to the app
```typescript
// packages/app/src/plugins.ts
export { plugin as GithubDeployments } from '@backstage/plugin-github-deployments';
```
4. Add the ... to the EntityPage:
```typescript
// packages/app/src/components/catalog/EntityPage.tsx
import { EntityGithubDeploymentsCard } from '@backstage/plugin-github-deployments';
const OverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
// ...
<Grid item xs={12} sm={6} md={4}>
<EntityGithubDeploymentsCard entity={entity} />
</Grid>
// ...
</Grid>
);
```
5. Add the github.com/project-slug annotation to your catalog-info.yaml file:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage
description: |
Backstage is an open-source developer portal that puts the developer experience first.
annotations:
github.com/project-slug: YOUR_PROJECT_SLUG
spec:
type: library
owner: CNCF
lifecycle: experimental
```
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2021 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 { createDevApp } from '@backstage/dev-utils';
import { githubDeploymentsPlugin } from '../src/plugin';
createDevApp().registerPlugin(githubDeploymentsPlugin).render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+51
View File
@@ -0,0 +1,51 @@
{
"name": "@backstage/plugin-github-deployments",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.7.4",
"@backstage/core": "^0.7.1",
"@backstage/theme": "^0.2.4",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/graphql": "^4.6.1",
"moment": "^2.29.1",
"nock": "^13.0.11",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.4",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.8",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
]
}
@@ -0,0 +1,89 @@
/*
* Copyright 2021 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 { createApiRef, DiscoveryApi } from '@backstage/core';
import { graphql } from '@octokit/graphql';
export type GithubDeployment = {
environment: string;
state: string;
updatedAt: string;
commit: {
abbreviatedOid: string;
commitUrl: string;
};
};
export interface GithubDeploymentsApi {
listDeployments(options: {
owner: string;
repo: string;
last: number;
}): Promise<GithubDeployment[]>;
}
export const githubDeploymentsApiRef = createApiRef<GithubDeploymentsApi>({
id: 'plugin.github-deployments.service',
description: 'Used by the Github Deployments plugin to make requests',
});
export type Options = {
discoveryApi: DiscoveryApi;
proxyPath?: string;
};
const deploymentsQuery = `
query lastDeployments($owner: String!, $repo: String!, $last: Int) {
repository(owner: $owner, name: $repo) {
deployments(last: $last) {
nodes {
state
environment
updatedAt
commit {
abbreviatedOid
commitUrl
}
}
}
}
}
`;
export class GithubDeploymentsApiClient implements GithubDeploymentsApi {
private readonly discoveryApi: DiscoveryApi;
constructor(options: Options) {
this.discoveryApi = options.discoveryApi;
}
private async getProxyUrl() {
return await this.discoveryApi.getBaseUrl('proxy');
}
async listDeployments(options: {
owner: string;
repo: string;
last: number;
}): Promise<GithubDeployment[]> {
const proxyUrl = await this.getProxyUrl();
const graphQlWithBaseURL = graphql.defaults({
baseUrl: `${proxyUrl}/github/api`,
});
const response: any = await graphQlWithBaseURL(deploymentsQuery, options);
return response.repository?.deployments?.nodes?.reverse() || [];
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2021 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 React from 'react';
import {
ApiProvider,
ApiRegistry,
errorApiRef,
UrlPatternDiscovery,
configApiRef,
ConfigReader,
} from '@backstage/core';
import { render } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api';
import { githubDeploymentsPlugin } from '../plugin';
import { GithubDeploymentsCard } from './GithubDeploymentsCard';
import { entityStub, responseStub } from '../mocks/mocks';
const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com');
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
const apis = ApiRegistry.from([
[configApiRef, new ConfigReader({})],
[errorApiRef, errorApiMock],
[githubDeploymentsApiRef, new GithubDeploymentsApiClient({ discoveryApi })],
]);
describe('github-deployments', () => {
const worker = setupServer();
beforeAll(() => worker.listen());
afterAll(() => worker.close());
afterEach(() => worker.resetHandlers());
beforeEach(() => {
jest.resetAllMocks();
});
describe('export-plugin', () => {
it('should export plugin', () => {
expect(githubDeploymentsPlugin).toBeDefined();
});
});
describe('GithubDeploymentsCard', () => {
it('should display fetched data', async () => {
worker.use(rest.post('*', (_, res, ctx) => res(ctx.json(responseStub))));
const rendered = render(
<ApiProvider apis={apis}>
<GithubDeploymentsCard entity={entityStub} />
</ApiProvider>,
);
expect(await rendered.findByText('active')).toBeInTheDocument();
expect(await rendered.findByText('prd')).toBeInTheDocument();
expect(await rendered.findByText('12345')).toHaveAttribute(
'href',
'https://exampleapi.com/123456789',
);
expect(await rendered.findByText('pending')).toBeInTheDocument();
expect(await rendered.findByText('lab')).toBeInTheDocument();
expect(await rendered.findByText('54321')).toHaveAttribute(
'href',
'https://exampleapi.com/543212345',
);
});
});
});
@@ -0,0 +1,75 @@
/*
* Copyright 2021 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 React from 'react';
import { LinearProgress } from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import { InfoCard, MissingAnnotationEmptyState, useApi } from '@backstage/core';
import { useAsync } from 'react-use';
import { githubDeploymentsApiRef } from '../api';
import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable';
export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug';
export const isGithubDeploymentsAvailable = (entity: Entity) =>
Boolean(entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION]);
const GithubDeploymentsComponent = ({
entity,
last,
}: {
entity: Entity;
last: number;
}) => {
const api = useApi(githubDeploymentsApiRef);
const annotation =
entity.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] ?? '';
const [owner, repo] = annotation.split('/');
const { loading, value, error } = useAsync(
async () => await api.listDeployments({ owner, repo, last }),
);
if (loading) {
return (
<InfoCard title="Github Deployments">
<LinearProgress />
</InfoCard>
);
}
if (error) {
return (
<InfoCard title="Github Deployments">
Error occurred while fetching data.
</InfoCard>
);
}
return <GithubDeploymentsTable deployments={value || []} />;
};
export const GithubDeploymentsCard = ({
entity,
last,
}: {
entity: Entity;
last?: number;
}) => {
return !isGithubDeploymentsAvailable(entity) ? (
<MissingAnnotationEmptyState annotation={GITHUB_PROJECT_SLUG_ANNOTATION} />
) : (
<GithubDeploymentsComponent entity={entity} last={last || 10} />
);
};
@@ -0,0 +1,90 @@
/*
* Copyright 2021 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 React from 'react';
import { Table, TableColumn } from '@backstage/core';
import { GithubDeployment } from '../../api';
import moment from 'moment';
import { Box, Typography, Link } from '@material-ui/core';
const lastUpdated = (start: string): string => moment(start).fromNow();
const State = ({ value }: { value: string }) => {
const colorMap: Record<string, string> = {
PENDING: 'orange',
IN_PROGRESS: 'orange',
ACTIVE: 'green',
};
return (
<Box display="flex" alignItems="center">
<span
style={{
display: 'block',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: colorMap[value] || 'grey',
marginRight: '5px',
}}
/>
<Typography variant="caption">{value}</Typography>
</Box>
);
};
const columns: TableColumn[] = [
{
title: 'Environment',
field: 'environment',
highlight: true,
},
{
title: 'Status',
field: 'environment',
render: (row: any): React.ReactNode => <State value={row.state} />,
},
{
title: 'Commit',
render: (row: any): React.ReactNode => (
<Link href={row.commit.commitUrl} target="_blank" rel="noopener">
{row.commit.abbreviatedOid}
</Link>
),
},
{
title: 'Last Updated',
render: (row: any): React.ReactNode => lastUpdated(row.updatedAt),
},
];
type GithubDeploymentsTableProps = {
deployments: GithubDeployment[];
};
const GithubDeploymentsTable = ({
deployments,
}: GithubDeploymentsTableProps) => {
return (
<Table
columns={columns}
options={{ padding: 'dense', paging: true, search: false, pageSize: 5 }}
title="Github Deployments"
data={deployments}
/>
);
};
export default GithubDeploymentsTable;
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2021 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.
*/
export {
githubDeploymentsPlugin as plugin,
EntityGithubDeploymentsCard,
} from './plugin';
@@ -0,0 +1,67 @@
/*
* Copyright 2021 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.
*/
export const entityStub = {
metadata: {
namespace: 'default',
annotations: {
'github.com/project-slug': 'org/repo',
},
name: 'sample-service',
description: 'Sample service',
uid: 'g0h33dd9-56h7-835b-b63v-7x5da3j64851',
generation: 1,
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
spec: {
type: 'service',
lifecycle: 'experimental',
},
relations: [],
};
export const responseStub = {
data: {
repository: {
deployments: {
nodes: [
{
state: 'active',
environment: 'prd',
updatedAt: '2021-03-25T12:08:45Z',
commit: {
commitUrl: 'https://exampleapi.com/123456789',
abbreviatedOid: '12345',
},
},
{
state: 'pending',
environment: 'lab',
updatedAt: '2021-03-25T12:08:47Z',
commit: {
commitUrl: 'https://exampleapi.com/543212345',
abbreviatedOid: '54321',
},
},
],
},
},
},
};
export const noDataResponseStub = {
data: {},
};
@@ -0,0 +1,22 @@
/*
* Copyright 2021 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 { githubDeploymentsPlugin } from './plugin';
describe('graphiql', () => {
it('should export plugin', () => {
expect(githubDeploymentsPlugin).toBeDefined();
});
});
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright 2021 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 {
createApiFactory,
createComponentExtension,
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { githubDeploymentsApiRef, GithubDeploymentsApiClient } from './api';
export const githubDeploymentsPlugin = createPlugin({
id: 'github-deployments',
apis: [
createApiFactory({
api: githubDeploymentsApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) =>
new GithubDeploymentsApiClient({ discoveryApi }),
}),
],
});
export const EntityGithubDeploymentsCard = githubDeploymentsPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/GithubDeploymentsCard').then(
m => m.GithubDeploymentsCard,
),
},
}),
);
@@ -0,0 +1,17 @@
/*
* Copyright 2021 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 '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
+29
View File
@@ -4290,6 +4290,15 @@
"@octokit/types" "^6.0.3"
universal-user-agent "^6.0.0"
"@octokit/graphql@^4.6.1":
version "4.6.1"
resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.1.tgz#f975486a46c94b7dbe58a0ca751935edc7e32cc9"
integrity sha512-2lYlvf4YTDgZCTXTW4+OX+9WTLFtEUc6hGm4qM1nlZjzxj+arizM4aHWzBVBCxY9glh7GIs0WEuiSgbVzv8cmA==
dependencies:
"@octokit/request" "^5.3.0"
"@octokit/types" "^6.0.3"
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz#123e0438a0bc718ccdac3b5a2e69b3dd00daa85b"
@@ -17940,6 +17949,11 @@ lodash.once@^4.0.0, lodash.once@^4.1.1:
resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -19278,6 +19292,16 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
nock@^13.0.11:
version "13.0.11"
resolved "https://registry.npmjs.org/nock/-/nock-13.0.11.tgz#ba733252e720897ca50033205c39db0c7470f331"
integrity sha512-sKZltNkkWblkqqPAsjYW0bm3s9DcHRPiMOyKO/PkfJ+ANHZ2+LA2PLe22r4lLrKgXaiSaDQwW3qGsJFtIpQIeQ==
dependencies:
debug "^4.1.0"
json-stringify-safe "^5.0.1"
lodash.set "^4.3.2"
propagate "^2.0.0"
node-abi@^2.7.0:
version "2.19.3"
resolved "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d"
@@ -21648,6 +21672,11 @@ prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0,
object-assign "^4.1.1"
react-is "^16.8.1"
propagate@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
property-expr@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330"