Merge pull request #8622 from julioz/julioz/gocd-plugin
Add GoCD plugin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-gocd': patch
|
||||
---
|
||||
|
||||
Add GoCD plugin for CI/CD.
|
||||
@@ -107,6 +107,13 @@ proxy:
|
||||
headers:
|
||||
Authorization: ${AIRFLOW_BASIC_AUTH_HEADER}
|
||||
|
||||
'/gocd':
|
||||
target: https://your.gocd.instance.com/go/api
|
||||
allowedMethods: ['GET']
|
||||
allowedHeaders: ['Authorization']
|
||||
headers:
|
||||
Authorization: Basic ${GOCD_AUTH_CREDENTIALS}
|
||||
|
||||
organization:
|
||||
name: My Company
|
||||
|
||||
@@ -450,3 +457,6 @@ azureDevOps:
|
||||
|
||||
apacheAirflow:
|
||||
baseUrl: https://your.airflow.instance.com
|
||||
|
||||
gocd:
|
||||
baseUrl: https://your.gocd.instance.com
|
||||
|
||||
@@ -204,6 +204,24 @@ browser when viewing that user.
|
||||
This annotation can be used on a [User entity](descriptor-format.md#kind-user)
|
||||
to note that it originated from that user on GitHub.
|
||||
|
||||
### gocd.org/pipelines
|
||||
|
||||
```yaml
|
||||
# Example:
|
||||
metadata:
|
||||
annotations:
|
||||
gocd.org/pipelines: backstage,backstage-pr,backstage-builder
|
||||
```
|
||||
|
||||
The value of this annotation is a comma-separated list of the GoCD pipeline
|
||||
names to fetch CI/CD information for.
|
||||
|
||||
The pipeline name is usually defined in the `gocd.yml` file for the pipeline
|
||||
definition.
|
||||
|
||||
Specifying this annotation will enable GoCD related features in Backstage for
|
||||
that entity.
|
||||
|
||||
### sentry.io/project-slug
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: GoCD
|
||||
author: SoundCloud
|
||||
authorUrl: https://github.com/soundcloud
|
||||
category: CI/CD
|
||||
description: GoCD is an open-source tool which is used in software development to help teams and organizations automate the continuous delivery of software.
|
||||
documentation: https://github.com/backstage/backstage/tree/master/plugins/gocd
|
||||
iconUrl: https://pics.freeicons.io/uploads/icons/png/13646383971540553613-512.png
|
||||
npmPackageName: '@backstage/plugin-gocd'
|
||||
tags:
|
||||
- ci
|
||||
- cd
|
||||
@@ -27,6 +27,7 @@
|
||||
"@backstage/plugin-explore": "^0.3.24",
|
||||
"@backstage/plugin-gcp-projects": "^0.3.12",
|
||||
"@backstage/plugin-github-actions": "^0.4.30",
|
||||
"@backstage/plugin-gocd": "^0.1.0",
|
||||
"@backstage/plugin-graphiql": "^0.2.26",
|
||||
"@backstage/plugin-home": "^0.4.9",
|
||||
"@backstage/plugin-jenkins": "^0.5.16",
|
||||
|
||||
@@ -129,6 +129,7 @@ import {
|
||||
EntityNewRelicDashboardContent,
|
||||
EntityNewRelicDashboardCard,
|
||||
} from '@backstage/plugin-newrelic-dashboard';
|
||||
import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd';
|
||||
|
||||
import React, { ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
@@ -185,6 +186,10 @@ export const cicdContent = (
|
||||
<EntityTravisCIContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
<EntitySwitch.Case if={isGoCdAvailable}>
|
||||
<EntityGoCdContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
<EntitySwitch.Case if={isGithubActionsAvailable}>
|
||||
<EntityGithubActionsContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
# GoCD
|
||||
|
||||
Welcome to the GoCD plugin!
|
||||
|
||||
- View recent GoCD Builds
|
||||
|
||||
## Installation
|
||||
|
||||
GoCD Plugin exposes an entity tab component named `EntityGoCdContent`. You can include it in the
|
||||
[`EntityPage.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/catalog/EntityPage.tsx)`:
|
||||
|
||||
```tsx
|
||||
// At the top imports
|
||||
import { EntityGoCdContent } from '@backstage/plugin-gocd';
|
||||
|
||||
// Farther down at the component declaration
|
||||
const componentEntityPage = (
|
||||
<EntityLayout>
|
||||
{/* Place the following section where you want the tab to appear */}
|
||||
<EntityLayout.Route path="/go-cd" title="GoCD">
|
||||
<EntityGoCdContent />
|
||||
</EntityLayout.Route>
|
||||
```
|
||||
|
||||
Now your plugin should be visible as a tab at the top of the entity pages,
|
||||
specifically for components that are of the type `component`.
|
||||
However, it warns of a missing `gocd.org/pipelines` annotation.
|
||||
|
||||
Add the annotation to your component [catalog-info.yaml](https://github.com/backstage/backstage/blob/master/catalog-info.yaml). You can refer to multiple GoCD pipelines by defining their names separated by commas, as shown in the highlighted example below:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
annotations:
|
||||
gocd.org/pipelines: '<NAME OF THE PIPELINE 1>[,<NAME OF PIPELINE 2>]'
|
||||
```
|
||||
|
||||
The plugin requires to configure a GoCD API proxy with a `GOCD_AUTH_CREDENTIALS` for authentication in the [app-config.yaml](https://github.com/backstage/backstage/blob/master/app-config.yaml). Its value is an opaque token you can obtain directly from your GoCD instance, in the shape `base64(user + ':' + pass)`. For example, a user "root" and password "root" would become `base64('root:root') = cm9vdDpyb290`:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
'/gocd':
|
||||
target: '<go cd server host>/go/api'
|
||||
allowedMethods: ['GET']
|
||||
allowedHeaders: ['Authorization']
|
||||
headers:
|
||||
Authorization: Basic ${GOCD_AUTH_CREDENTIALS}
|
||||
```
|
||||
|
||||
You should also include the `gocd` section to allow for the plugin to redirect back to GoCD pipelines in your deployed instance:
|
||||
|
||||
```yaml
|
||||
gocd:
|
||||
baseUrl: <go cd server host>
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
## API Report File for "@backstage/plugin-gocd"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
// @public
|
||||
export const EntityGoCdContent: () => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const GOCD_PIPELINES_ANNOTATION = 'gocd.org/pipelines';
|
||||
|
||||
// @public
|
||||
export const gocdPlugin: BackstagePlugin<{}, {}>;
|
||||
|
||||
// @public
|
||||
export const isGoCdAvailable: (entity: Entity) => boolean;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Configurations for the GoCD plugin */
|
||||
gocd: {
|
||||
/**
|
||||
* The base url of the GoCD installation.
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseUrl: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { createDevApp } from '@backstage/dev-utils';
|
||||
import { gocdPlugin } from '../src/plugin';
|
||||
|
||||
createDevApp().registerPlugin(gocdPlugin).render();
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@backstage/plugin-gocd",
|
||||
"description": "A Backstage plugin that integrates towards GoCD",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/gocd"
|
||||
},
|
||||
"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.9.1",
|
||||
"@backstage/core-components": "^0.8.3",
|
||||
"@backstage/core-plugin-api": "^0.4.1",
|
||||
"@backstage/errors": "^0.1.4",
|
||||
"@backstage/plugin-catalog-react": "^0.6.0",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.0.2",
|
||||
"qs": "^6.10.1",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.10.5",
|
||||
"@backstage/core-app-api": "^0.3.1",
|
||||
"@backstage/dev-utils": "^0.2.16",
|
||||
"@backstage/test-utils": "^0.2.1",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/lodash": "^4.14.173",
|
||||
"@types/luxon": "^2.0.4",
|
||||
"@types/node": "^14.14.32",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.35.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 { GoCdApi } from './gocdApi';
|
||||
import { GoCdApiError, PipelineHistory } from './gocdApi.model';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
export class GoCdClientApi implements GoCdApi {
|
||||
constructor(private readonly discoveryApi: DiscoveryApi) {}
|
||||
|
||||
async getPipelineHistory(
|
||||
pipelineName: string,
|
||||
): Promise<PipelineHistory | GoCdApiError> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('proxy');
|
||||
const pipelineHistoryResponse = await fetch(
|
||||
`${baseUrl}/gocd/pipelines/${pipelineName}/history`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.go.cd+json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!pipelineHistoryResponse.ok) {
|
||||
throw await ResponseError.fromResponse(pipelineHistoryResponse);
|
||||
}
|
||||
|
||||
return await pipelineHistoryResponse.json();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export interface GoCdApiError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PipelineHistory {
|
||||
_links: Links;
|
||||
pipelines: Pipeline[];
|
||||
}
|
||||
|
||||
export interface Links {
|
||||
next: Next;
|
||||
}
|
||||
|
||||
export interface Next {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface Pipeline {
|
||||
name: string;
|
||||
counter: number;
|
||||
label: string;
|
||||
natural_order?: number;
|
||||
can_run?: boolean;
|
||||
preparing_to_schedule?: boolean;
|
||||
comment: string | null;
|
||||
scheduled_date?: number;
|
||||
build_cause?: BuildCause;
|
||||
stages: Stage[];
|
||||
}
|
||||
|
||||
export interface BuildCause {
|
||||
trigger_message: string;
|
||||
trigger_forced: boolean;
|
||||
approver: string;
|
||||
material_revisions: MaterialRevision[];
|
||||
}
|
||||
|
||||
export interface MaterialRevision {
|
||||
changed: boolean;
|
||||
material: Material;
|
||||
modifications: Modification[];
|
||||
}
|
||||
|
||||
export interface Material {
|
||||
name: string;
|
||||
fingerprint: string;
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Modification {
|
||||
revision: string;
|
||||
modified_time: number;
|
||||
user_name: string;
|
||||
comment: string | null;
|
||||
email_address: string | null;
|
||||
}
|
||||
|
||||
export interface Stage {
|
||||
result?: string;
|
||||
status: string;
|
||||
rerun_of_counter?: number | null;
|
||||
name: string;
|
||||
counter: string;
|
||||
scheduled: boolean;
|
||||
approval_type?: string | null;
|
||||
approved_by?: string | null;
|
||||
operate_permission?: boolean;
|
||||
can_run?: boolean;
|
||||
jobs: Job[];
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
name: string;
|
||||
scheduled_date?: number;
|
||||
state: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
export enum GoCdBuildResultStatus {
|
||||
running,
|
||||
successful,
|
||||
warning,
|
||||
aborted,
|
||||
error,
|
||||
pending,
|
||||
}
|
||||
|
||||
export interface GoCdBuildStageResult {
|
||||
status: GoCdBuildResultStatus;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface GoCdBuildResult {
|
||||
id: number;
|
||||
source: string;
|
||||
stages: GoCdBuildStageResult[];
|
||||
buildSlug: string;
|
||||
message: string;
|
||||
pipeline: string;
|
||||
author: string | undefined;
|
||||
commitHash: string;
|
||||
triggerTime: number | undefined;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { GoCdApiError, PipelineHistory } from './gocdApi.model';
|
||||
|
||||
export interface GoCdApi {
|
||||
getPipelineHistory(
|
||||
pipelineName: string,
|
||||
): Promise<PipelineHistory | GoCdApiError>;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
|
||||
import { GoCdBuildsComponent } from './GoCdBuildsComponent';
|
||||
import { gocdApiRef } from '../../plugin';
|
||||
import { GoCdApi } from '../../api/gocdApi';
|
||||
|
||||
const mockApiClient: GoCdApi = {
|
||||
getPipelineHistory: jest.fn(async () => ({
|
||||
_links: { next: { href: 'some-href' } },
|
||||
pipelines: [],
|
||||
})),
|
||||
};
|
||||
|
||||
describe('GoCdArtifactsComponent', () => {
|
||||
const configApi: ConfigApi = new ConfigReader({
|
||||
gocd: {
|
||||
baseUrl: 'gocd.baseurl.com',
|
||||
},
|
||||
});
|
||||
const entityValue = { entity: { metadata: {} } as Entity };
|
||||
|
||||
const renderComponent = () =>
|
||||
renderWithEffects(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[gocdApiRef, mockApiClient],
|
||||
[configApiRef, configApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entityValue.entity}>
|
||||
<GoCdBuildsComponent />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
it('should display an empty state if an app annotation is missing', async () => {
|
||||
const rendered = await renderComponent();
|
||||
|
||||
expect(await rendered.findByText('Missing Annotation')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
MissingAnnotationEmptyState,
|
||||
EmptyState,
|
||||
Page,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { useAsync } from 'react-use';
|
||||
import { gocdApiRef } from '../../plugin';
|
||||
import { GoCdBuildsTable } from '../GoCdBuildsTable/GoCdBuildsTable';
|
||||
import { GoCdApiError, PipelineHistory } from '../../api/gocdApi.model';
|
||||
import { Item, Select } from '../Select';
|
||||
|
||||
/**
|
||||
* Constant storing GoCD pipelines annotation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const GOCD_PIPELINES_ANNOTATION = 'gocd.org/pipelines';
|
||||
|
||||
/**
|
||||
* Returns true if GoCD annotation is present in the given entity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const isGoCdAvailable = (entity: Entity): boolean =>
|
||||
Boolean(entity.metadata.annotations?.[GOCD_PIPELINES_ANNOTATION]);
|
||||
|
||||
export const GoCdBuildsComponent = (): JSX.Element => {
|
||||
const { entity } = useEntity();
|
||||
const config = useApi(configApiRef);
|
||||
const rawPipelines: string[] | undefined = (
|
||||
entity.metadata.annotations?.[GOCD_PIPELINES_ANNOTATION] as
|
||||
| string
|
||||
| undefined
|
||||
)
|
||||
?.split(',')
|
||||
.map(p => p.trim());
|
||||
const gocdApi = useApi(gocdApiRef);
|
||||
|
||||
const [selectedPipeline, setSelectedPipeline] = useState<string>(
|
||||
rawPipelines ? rawPipelines[0] : '',
|
||||
);
|
||||
|
||||
const {
|
||||
value: pipelineHistory,
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async (): Promise<PipelineHistory | GoCdApiError> => {
|
||||
return await gocdApi.getPipelineHistory(selectedPipeline);
|
||||
}, [selectedPipeline]);
|
||||
|
||||
const onSelectedPipelineChanged = (pipeline: string) => {
|
||||
setSelectedPipeline(pipeline);
|
||||
};
|
||||
|
||||
const getSelectionItems = (pipelines: string[]): Item[] => {
|
||||
return pipelines.map(p => ({ label: p, value: p }));
|
||||
};
|
||||
|
||||
function isError(
|
||||
apiResult: PipelineHistory | GoCdApiError | undefined,
|
||||
): apiResult is GoCdApiError {
|
||||
return (apiResult as GoCdApiError)?.message !== undefined;
|
||||
}
|
||||
|
||||
if (!rawPipelines) {
|
||||
return (
|
||||
<MissingAnnotationEmptyState annotation={GOCD_PIPELINES_ANNOTATION} />
|
||||
);
|
||||
}
|
||||
|
||||
if (isError(pipelineHistory)) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="GoCD pipelines"
|
||||
description={`Could not fetch pipelines defined for entity ${entity.metadata.name}. Error: ${pipelineHistory.message}`}
|
||||
missing="content"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!loading && !pipelineHistory) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="GoCD pipelines"
|
||||
description={`We could not find pipelines defined for entity ${entity.metadata.name}.`}
|
||||
missing="data"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Content noPadding>
|
||||
<ContentHeader title={entity.metadata.name}>
|
||||
<Select
|
||||
value={selectedPipeline}
|
||||
onChange={pipeline => onSelectedPipelineChanged(pipeline)}
|
||||
label="Pipeline"
|
||||
items={getSelectionItems(rawPipelines)}
|
||||
/>
|
||||
</ContentHeader>
|
||||
<GoCdBuildsTable
|
||||
goCdBaseUrl={config.getString('gocd.baseUrl')}
|
||||
pipelineHistory={pipelineHistory}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export {
|
||||
GoCdBuildsComponent,
|
||||
isGoCdAvailable,
|
||||
GOCD_PIPELINES_ANNOTATION,
|
||||
} from './GoCdBuildsComponent';
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { Entity, getEntitySourceLocation } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { Button } from '@material-ui/core';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import {
|
||||
GoCdBuildResult,
|
||||
GoCdBuildResultStatus,
|
||||
PipelineHistory,
|
||||
Pipeline,
|
||||
} from '../../api/gocdApi.model';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
Table,
|
||||
TableColumn,
|
||||
Link,
|
||||
SubvalueCell,
|
||||
StatusOK,
|
||||
StatusWarning,
|
||||
StatusAborted,
|
||||
StatusError,
|
||||
StatusRunning,
|
||||
StatusPending,
|
||||
} from '@backstage/core-components';
|
||||
|
||||
type GoCdBuildsProps = {
|
||||
goCdBaseUrl: string;
|
||||
pipelineHistory: PipelineHistory | undefined;
|
||||
loading: boolean;
|
||||
error: Error | undefined;
|
||||
};
|
||||
|
||||
const renderTrigger = (build: GoCdBuildResult): React.ReactNode => {
|
||||
const subvalue = (
|
||||
<>
|
||||
{build.pipeline}
|
||||
<br />
|
||||
{build.author}
|
||||
</>
|
||||
);
|
||||
return <SubvalueCell value={build.message} subvalue={subvalue} />;
|
||||
};
|
||||
|
||||
const renderStages = (build: GoCdBuildResult): React.ReactNode => {
|
||||
return build.stages.map(s => {
|
||||
switch (s.status) {
|
||||
case GoCdBuildResultStatus.successful: {
|
||||
return (
|
||||
<>
|
||||
<StatusOK>{s.text}</StatusOK>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case GoCdBuildResultStatus.error: {
|
||||
return (
|
||||
<>
|
||||
<StatusError>{s.text}</StatusError>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case GoCdBuildResultStatus.running: {
|
||||
return (
|
||||
<>
|
||||
<StatusRunning>{s.text}</StatusRunning>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case GoCdBuildResultStatus.aborted: {
|
||||
return (
|
||||
<>
|
||||
<StatusAborted>{s.text}</StatusAborted>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case GoCdBuildResultStatus.pending: {
|
||||
return (
|
||||
<>
|
||||
<StatusPending>{s.text}</StatusPending>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<>
|
||||
<StatusWarning>{s.text}</StatusWarning>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderSource = (build: GoCdBuildResult): React.ReactNode => {
|
||||
return (
|
||||
<Button
|
||||
href={build.source}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
startIcon={<GitHubIcon />}
|
||||
>
|
||||
{build.commitHash}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderId = (
|
||||
goCdBaseUrl: string,
|
||||
build: GoCdBuildResult,
|
||||
): React.ReactNode => {
|
||||
const goCdBuildUrl = `${goCdBaseUrl}/go/pipelines/value_stream_map/${build.buildSlug}`;
|
||||
return (
|
||||
<SubvalueCell
|
||||
value={
|
||||
<Link to={goCdBuildUrl} target="_blank" rel="noreferrer">
|
||||
{build.id}
|
||||
</Link>
|
||||
}
|
||||
subvalue={
|
||||
build.triggerTime &&
|
||||
DateTime.fromMillis(build.triggerTime).toLocaleString(
|
||||
DateTime.DATETIME_MED,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderError = (error: Error): JSX.Element => {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
};
|
||||
|
||||
const toStageStatus = (status: string): GoCdBuildResultStatus => {
|
||||
switch (status.toLocaleLowerCase('en-US')) {
|
||||
case 'passed':
|
||||
return GoCdBuildResultStatus.successful;
|
||||
case 'failed':
|
||||
return GoCdBuildResultStatus.error;
|
||||
case 'aborted':
|
||||
return GoCdBuildResultStatus.aborted;
|
||||
case 'building':
|
||||
return GoCdBuildResultStatus.running;
|
||||
case 'pending':
|
||||
return GoCdBuildResultStatus.pending;
|
||||
default:
|
||||
return GoCdBuildResultStatus.aborted;
|
||||
}
|
||||
};
|
||||
|
||||
const toBuildResults = (
|
||||
entity: Entity,
|
||||
builds: Pipeline[],
|
||||
): GoCdBuildResult[] | undefined => {
|
||||
// TODO(julioz): What if not git 'type'?
|
||||
const entitySourceLocation =
|
||||
getEntitySourceLocation(entity).target.split('/tree')[0];
|
||||
return builds.map(build => ({
|
||||
id: build.counter,
|
||||
source: `${entitySourceLocation}/commit/${build.build_cause?.material_revisions[0]?.modifications[0].revision}`,
|
||||
stages: build.stages.map(s => ({
|
||||
status: toStageStatus(s.status),
|
||||
text: s.name,
|
||||
})),
|
||||
buildSlug: `${build.name}/${build.counter}`,
|
||||
message:
|
||||
build.build_cause?.material_revisions[0]?.modifications[0].comment ?? '',
|
||||
pipeline: build.name,
|
||||
author:
|
||||
build.build_cause?.material_revisions[0]?.modifications[0].user_name,
|
||||
commitHash: build.label,
|
||||
triggerTime: build.scheduled_date,
|
||||
}));
|
||||
};
|
||||
|
||||
export const GoCdBuildsTable = (props: GoCdBuildsProps): JSX.Element => {
|
||||
const { pipelineHistory, loading, error } = props;
|
||||
const { entity } = useEntity();
|
||||
const [, setSelectedSearchTerm] = useState<string>('');
|
||||
|
||||
const columns: TableColumn<GoCdBuildResult>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
field: 'id',
|
||||
highlight: true,
|
||||
render: build => renderId(props.goCdBaseUrl, build),
|
||||
},
|
||||
{
|
||||
title: 'Trigger',
|
||||
customFilterAndSearch: (query, row: GoCdBuildResult) =>
|
||||
`${row.message} ${row.pipeline} ${row.author}`
|
||||
.toLocaleUpperCase('en-US')
|
||||
.includes(query.toLocaleUpperCase('en-US')),
|
||||
field: 'message',
|
||||
highlight: true,
|
||||
render: build => renderTrigger(build),
|
||||
},
|
||||
{
|
||||
title: 'Stages',
|
||||
field: 'status',
|
||||
render: build => renderStages(build),
|
||||
},
|
||||
{
|
||||
title: 'Source',
|
||||
field: 'source',
|
||||
render: build => renderSource(build),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && renderError(error)}
|
||||
{!error && (
|
||||
<Table
|
||||
title="GoCD builds"
|
||||
isLoading={loading}
|
||||
options={{
|
||||
search: true,
|
||||
searchAutoFocus: true,
|
||||
debounceInterval: 800,
|
||||
paging: false,
|
||||
showFirstLastPageButtons: false,
|
||||
pageSize: 10,
|
||||
}}
|
||||
columns={columns}
|
||||
data={
|
||||
pipelineHistory
|
||||
? toBuildResults(entity, pipelineHistory.pipelines) || []
|
||||
: []
|
||||
}
|
||||
onSearchChange={(searchTerm: string) =>
|
||||
setSelectedSearchTerm(searchTerm)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 FormControl from '@material-ui/core/FormControl';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
|
||||
export type Item = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
formControl: {
|
||||
margin: theme.spacing(1),
|
||||
minWidth: 120,
|
||||
},
|
||||
selectEmpty: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
type SelectComponentProps = {
|
||||
value: string;
|
||||
items: Item[];
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const renderItems = (items: Item[]) => {
|
||||
return items.map(item => {
|
||||
return (
|
||||
<MenuItem value={item.value} key={item.label}>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const SelectComponent = ({
|
||||
value,
|
||||
items,
|
||||
label,
|
||||
onChange,
|
||||
}: SelectComponentProps): JSX.Element => {
|
||||
const classes = useStyles();
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
const val = event.target.value as string;
|
||||
onChange(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
variant="outlined"
|
||||
className={classes.formControl}
|
||||
disabled={items.length === 0}
|
||||
>
|
||||
<InputLabel>{label}</InputLabel>
|
||||
<Select label={label} value={value} onChange={handleChange}>
|
||||
{renderItems(items)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { SelectComponent as Select } from './Select';
|
||||
export type { Item } from './Select';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { gocdPlugin } from './plugin';
|
||||
import { createComponentExtension } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* GoCD builds table component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityGoCdContent = gocdPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'EntityGoCdContent',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/GoCdBuildsComponent').then(
|
||||
m => m.GoCdBuildsComponent,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { gocdPlugin } from './plugin';
|
||||
export { EntityGoCdContent } from './extensions';
|
||||
export {
|
||||
GOCD_PIPELINES_ANNOTATION,
|
||||
isGoCdAvailable,
|
||||
} from './components/GoCdBuildsComponent';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { gocdPlugin } from './plugin';
|
||||
|
||||
describe('gocd', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(gocdPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { GoCdClientApi } from './api/gocdApi.client';
|
||||
import { GoCdApi } from './api/gocdApi';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
createApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export const gocdApiRef = createApiRef<GoCdApi>({
|
||||
id: 'plugin.gocd.service',
|
||||
});
|
||||
|
||||
/**
|
||||
* Plugin definition.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const gocdPlugin = createPlugin({
|
||||
id: 'gocd',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: gocdApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => {
|
||||
return new GoCdClientApi(discoveryApi);
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'gocd',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
Reference in New Issue
Block a user