diff --git a/.changeset/fluffy-tomatoes-rescue.md b/.changeset/fluffy-tomatoes-rescue.md
new file mode 100644
index 0000000000..4d446070a0
--- /dev/null
+++ b/.changeset/fluffy-tomatoes-rescue.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-gocd': patch
+---
+
+Add GoCD plugin for CI/CD.
diff --git a/app-config.yaml b/app-config.yaml
index 2837d97d2f..b43a229ae9 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -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
diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md
index c3af6caf2d..26a8710820 100644
--- a/docs/features/software-catalog/well-known-annotations.md
+++ b/docs/features/software-catalog/well-known-annotations.md
@@ -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
diff --git a/microsite/data/plugins/gocd.yaml b/microsite/data/plugins/gocd.yaml
new file mode 100644
index 0000000000..7bf24d75fd
--- /dev/null
+++ b/microsite/data/plugins/gocd.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
diff --git a/packages/app/package.json b/packages/app/package.json
index bbfd903484..b83c43674b 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -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",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 455cd188fb..60dc0dc979 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -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 = (
+
+
+
+
diff --git a/plugins/gocd/.eslintrc.js b/plugins/gocd/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/gocd/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/gocd/README.md b/plugins/gocd/README.md
new file mode 100644
index 0000000000..105d93e830
--- /dev/null
+++ b/plugins/gocd/README.md
@@ -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 = (
+
+ {/* Place the following section where you want the tab to appear */}
+
+
+
+```
+
+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: '[,]'
+```
+
+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/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:
+```
diff --git a/plugins/gocd/api-report.md b/plugins/gocd/api-report.md
new file mode 100644
index 0000000000..de2c76e06b
--- /dev/null
+++ b/plugins/gocd/api-report.md
@@ -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
+///
+
+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)
+```
diff --git a/plugins/gocd/config.d.ts b/plugins/gocd/config.d.ts
new file mode 100644
index 0000000000..6f4f4f3dc3
--- /dev/null
+++ b/plugins/gocd/config.d.ts
@@ -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;
+ };
+}
diff --git a/plugins/gocd/dev/index.tsx b/plugins/gocd/dev/index.tsx
new file mode 100644
index 0000000000..e821173774
--- /dev/null
+++ b/plugins/gocd/dev/index.tsx
@@ -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();
diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json
new file mode 100644
index 0000000000..d07931916d
--- /dev/null
+++ b/plugins/gocd/package.json
@@ -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"
+}
diff --git a/plugins/gocd/src/api/gocdApi.client.ts b/plugins/gocd/src/api/gocdApi.client.ts
new file mode 100644
index 0000000000..1f4955a80d
--- /dev/null
+++ b/plugins/gocd/src/api/gocdApi.client.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 {
+ 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();
+ }
+}
diff --git a/plugins/gocd/src/api/gocdApi.model.ts b/plugins/gocd/src/api/gocdApi.model.ts
new file mode 100644
index 0000000000..8bb9a481e6
--- /dev/null
+++ b/plugins/gocd/src/api/gocdApi.model.ts
@@ -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;
+}
diff --git a/plugins/gocd/src/api/gocdApi.ts b/plugins/gocd/src/api/gocdApi.ts
new file mode 100644
index 0000000000..a9883a0c66
--- /dev/null
+++ b/plugins/gocd/src/api/gocdApi.ts
@@ -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;
+}
diff --git a/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.test.tsx b/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.test.tsx
new file mode 100644
index 0000000000..7f16f40a6a
--- /dev/null
+++ b/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.test.tsx
@@ -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(
+
+
+
+
+ ,
+ );
+
+ it('should display an empty state if an app annotation is missing', async () => {
+ const rendered = await renderComponent();
+
+ expect(await rendered.findByText('Missing Annotation')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.tsx b/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.tsx
new file mode 100644
index 0000000000..46e5d09260
--- /dev/null
+++ b/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.tsx
@@ -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(
+ rawPipelines ? rawPipelines[0] : '',
+ );
+
+ const {
+ value: pipelineHistory,
+ loading,
+ error,
+ } = useAsync(async (): Promise => {
+ 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 (
+
+ );
+ }
+
+ if (isError(pipelineHistory)) {
+ return (
+
+ );
+ }
+
+ if (!loading && !pipelineHistory) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/gocd/src/components/GoCdBuildsComponent/index.ts b/plugins/gocd/src/components/GoCdBuildsComponent/index.ts
new file mode 100644
index 0000000000..662d2b4038
--- /dev/null
+++ b/plugins/gocd/src/components/GoCdBuildsComponent/index.ts
@@ -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';
diff --git a/plugins/gocd/src/components/GoCdBuildsTable/GoCdBuildsTable.tsx b/plugins/gocd/src/components/GoCdBuildsTable/GoCdBuildsTable.tsx
new file mode 100644
index 0000000000..51ba2b6e8c
--- /dev/null
+++ b/plugins/gocd/src/components/GoCdBuildsTable/GoCdBuildsTable.tsx
@@ -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}
+
+ {build.author}
+ >
+ );
+ return ;
+};
+
+const renderStages = (build: GoCdBuildResult): React.ReactNode => {
+ return build.stages.map(s => {
+ switch (s.status) {
+ case GoCdBuildResultStatus.successful: {
+ return (
+ <>
+ {s.text}
+
+
+ >
+ );
+ }
+ case GoCdBuildResultStatus.error: {
+ return (
+ <>
+ {s.text}
+
+
+ >
+ );
+ }
+ case GoCdBuildResultStatus.running: {
+ return (
+ <>
+ {s.text}
+
+
+ >
+ );
+ }
+ case GoCdBuildResultStatus.aborted: {
+ return (
+ <>
+ {s.text}
+
+
+ >
+ );
+ }
+ case GoCdBuildResultStatus.pending: {
+ return (
+ <>
+ {s.text}
+
+
+ >
+ );
+ }
+ default: {
+ return (
+ <>
+ {s.text}
+
+
+ >
+ );
+ }
+ }
+ });
+};
+
+const renderSource = (build: GoCdBuildResult): React.ReactNode => {
+ return (
+ }
+ >
+ {build.commitHash}
+
+ );
+};
+
+const renderId = (
+ goCdBaseUrl: string,
+ build: GoCdBuildResult,
+): React.ReactNode => {
+ const goCdBuildUrl = `${goCdBaseUrl}/go/pipelines/value_stream_map/${build.buildSlug}`;
+ return (
+
+ {build.id}
+
+ }
+ subvalue={
+ build.triggerTime &&
+ DateTime.fromMillis(build.triggerTime).toLocaleString(
+ DateTime.DATETIME_MED,
+ )
+ }
+ />
+ );
+};
+
+const renderError = (error: Error): JSX.Element => {
+ return {error.message};
+};
+
+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('');
+
+ const columns: TableColumn[] = [
+ {
+ 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 && (
+
+ setSelectedSearchTerm(searchTerm)
+ }
+ />
+ )}
+ >
+ );
+};
diff --git a/plugins/gocd/src/components/Select/Select.tsx b/plugins/gocd/src/components/Select/Select.tsx
new file mode 100644
index 0000000000..6362844902
--- /dev/null
+++ b/plugins/gocd/src/components/Select/Select.tsx
@@ -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 (
+
+ );
+ });
+};
+
+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 (
+
+ {label}
+
+
+ );
+};
diff --git a/plugins/gocd/src/components/Select/index.ts b/plugins/gocd/src/components/Select/index.ts
new file mode 100644
index 0000000000..1503c54cdf
--- /dev/null
+++ b/plugins/gocd/src/components/Select/index.ts
@@ -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';
diff --git a/plugins/gocd/src/extensions.ts b/plugins/gocd/src/extensions.ts
new file mode 100644
index 0000000000..55624c9d7e
--- /dev/null
+++ b/plugins/gocd/src/extensions.ts
@@ -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,
+ ),
+ },
+ }),
+);
diff --git a/plugins/gocd/src/index.ts b/plugins/gocd/src/index.ts
new file mode 100644
index 0000000000..c92e899546
--- /dev/null
+++ b/plugins/gocd/src/index.ts
@@ -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';
diff --git a/plugins/gocd/src/plugin.test.ts b/plugins/gocd/src/plugin.test.ts
new file mode 100644
index 0000000000..02b52a00b6
--- /dev/null
+++ b/plugins/gocd/src/plugin.test.ts
@@ -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();
+ });
+});
diff --git a/plugins/gocd/src/plugin.ts b/plugins/gocd/src/plugin.ts
new file mode 100644
index 0000000000..01081978f4
--- /dev/null
+++ b/plugins/gocd/src/plugin.ts
@@ -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({
+ 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);
+ },
+ }),
+ ],
+});
diff --git a/plugins/gocd/src/routes.ts b/plugins/gocd/src/routes.ts
new file mode 100644
index 0000000000..71e431c040
--- /dev/null
+++ b/plugins/gocd/src/routes.ts
@@ -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',
+});
diff --git a/plugins/gocd/src/setupTests.ts b/plugins/gocd/src/setupTests.ts
new file mode 100644
index 0000000000..fc6dbd98f8
--- /dev/null
+++ b/plugins/gocd/src/setupTests.ts
@@ -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';