diff --git a/app-config.yaml b/app-config.yaml
index c24ac1701b..b92595df7b 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -436,3 +436,4 @@ azureDevOps:
host: dev.azure.com
token: my-token
organization: my-company
+ top: 25
diff --git a/packages/app/package.json b/packages/app/package.json
index d1892438d8..fe86077273 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -11,6 +11,7 @@
"@backstage/core-plugin-api": "^0.1.10",
"@backstage/integration-react": "^0.1.11",
"@backstage/plugin-api-docs": "^0.6.11",
+ "@backstage/plugin-azure-devops": "^0.1.0",
"@backstage/plugin-badges": "^0.2.12",
"@backstage/plugin-catalog": "^0.7.0",
"@backstage/plugin-catalog-graph": "^0.1.3",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 62fdcf4d3f..b79bd2c4aa 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -33,6 +33,10 @@ import {
EntityProvidedApisCard,
EntityProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
+import {
+ EntityAzureDevOpsContent,
+ isAzureDevOpsAvailable,
+} from '@backstage/plugin-azure-devops';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import {
EntityAboutCard,
@@ -183,6 +187,10 @@ export const cicdContent = (
+
+
+
+
{
+ return await createRouter({ logger, config });
+ }
+ ```
+
+3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
+
+ ```ts
+ import azureDevOps from './plugins/azuredevops';
+ // ...
+ async function main() {
+ // ...
+ const azureDevOpsEnv = useHotMemoize(module, () => createEnv('azure-devops'));
+ apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv));
+ ```
+
+4. Now run `yarn start-backend` from the repo root
+5. Finally open `http://localhost:7000/api/azure-devops/health` in a browser and it should return `{"status":"ok"}`
+
+### Frontend
+
+To get the frontend working you'll need to do the following two steps:
+
+1. First we need to add the @backstage/plugin-azure-devops package to your frontend app:
+
+ ```bash
+ # From your Backstage root directory
+ cd packages/app
+ yarn add @backstage/plugin-azure-devops
+ ```
+
+2. Second we need to add the `EntityAzureDevOpsContent` extension to the entity page in your app:
+
+```tsx
+// In packages/app/src/components/catalog/EntityPage.tsx
+import {
+ EntityAzureDevOpsContent,
+ isAzureDevOpsAvailable,
+} from '@backstage/plugin-azure-devops';
+
+// For example in the CI/CD section
+const cicdContent = (
+
+ // ...
+
+
+
+ // ...
+
+```
+
+### Entity Annotation
+
+You need to add the following annotation to any entities you want to be able to use the Azure Devops plugin with:
+
+```yaml
+dev.azure.com/project-repo: /
+```
+
+Let's break this down a little: `` will be the name of your Team Project and `` will be the name of your repository which needs to be part of the Team Project you entered for ``.
+
+Here's what that will look like in action:
+
+```yaml
+# Example catalog-info.yaml entity definition file
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ # ...
+ annotations:
+ dev.azure.com/project-repo: my-project/my-repo
+spec:
+ type: service
+ # ...
+```
+
+## Features
+
+- Lists the top _n_ builds for a given repository where _n_ is the value configured for `top`
+
+## Limitations
+
+- Currently multiple organizations is not supported
diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md
new file mode 100644
index 0000000000..11de164276
--- /dev/null
+++ b/plugins/azure-devops/api-report.md
@@ -0,0 +1,35 @@
+## API Report File for "@backstage/plugin-azure-devops"
+
+> 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';
+import { RouteRef } from '@backstage/core-plugin-api';
+
+// Warning: (ae-missing-release-tag) "azureDevOpsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const azureDevOpsPlugin: BackstagePlugin<
+ {
+ entityContent: RouteRef;
+ },
+ {}
+>;
+
+// Warning: (ae-missing-release-tag) "EntityAzureDevOpsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const EntityAzureDevOpsContent: (_props: {
+ entity?: Entity | undefined;
+}) => JSX.Element;
+
+// Warning: (ae-missing-release-tag) "isAzureDevOpsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const isAzureDevOpsAvailable: (entity: Entity) => boolean;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/azure-devops/config.d.ts b/plugins/azure-devops/config.d.ts
new file mode 100644
index 0000000000..827e64d5bd
--- /dev/null
+++ b/plugins/azure-devops/config.d.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 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 {
+ /** Configuration options for the azure-devops-backend plugin */
+ azureDevOps: {
+ /**
+ * The max number of items to return - applies to builds
+ * @visibility frontend
+ */
+ top: number;
+ };
+}
diff --git a/plugins/azure-devops/dev/index.tsx b/plugins/azure-devops/dev/index.tsx
new file mode 100644
index 0000000000..8d1c3ef6ea
--- /dev/null
+++ b/plugins/azure-devops/dev/index.tsx
@@ -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 { createDevApp } from '@backstage/dev-utils';
+import { azureDevOpsPlugin } from '../src/plugin';
+
+createDevApp().registerPlugin(azureDevOpsPlugin).render();
diff --git a/plugins/azure-devops/doc/azure-devops-builds.png b/plugins/azure-devops/doc/azure-devops-builds.png
new file mode 100644
index 0000000000..89a777059f
Binary files /dev/null and b/plugins/azure-devops/doc/azure-devops-builds.png differ
diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json
new file mode 100644
index 0000000000..fb59d5eb38
--- /dev/null
+++ b/plugins/azure-devops/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "@backstage/plugin-azure-devops",
+ "version": "0.1.0",
+ "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"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/azure-devops"
+ },
+ "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.3",
+ "@backstage/core-components": "^0.6.0",
+ "@backstage/core-plugin-api": "^0.1.9",
+ "@backstage/plugin-catalog-react": "^0.5.1",
+ "@backstage/theme": "^0.2.10",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.57",
+ "azure-devops-node-api": "^11.0.1",
+ "moment": "^2.29.1",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "react-router": "6.0.0-beta.0",
+ "react-use": "^17.2.4"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.7.13",
+ "@backstage/core-app-api": "^0.1.14",
+ "@backstage/dev-utils": "^0.2.10",
+ "@backstage/test-utils": "^0.1.17",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^11.2.5",
+ "@testing-library/user-event": "^13.1.8",
+ "@types/jest": "*",
+ "@types/node": "*",
+ "msw": "^0.29.0",
+ "cross-fetch": "^3.0.6"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts
new file mode 100644
index 0000000000..424999441d
--- /dev/null
+++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { RepoBuild } from './types';
+import { createApiRef } from '@backstage/core-plugin-api';
+
+export const azureDevOpsApiRef = createApiRef({
+ id: 'plugin.azure-devops.service',
+ description:
+ 'Used by the Azure DevOps plugin to make requests to accompanying backend',
+});
+
+export interface AzureDevOpsApi {
+ getRepoBuilds(
+ projectName: string,
+ repoName: string,
+ top: number,
+ ): Promise;
+}
diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
new file mode 100644
index 0000000000..c9e44109aa
--- /dev/null
+++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { AzureDevOpsApi } from './AzureDevOpsApi';
+import { RepoBuild } from './types';
+import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
+
+export class AzureDevOpsClient implements AzureDevOpsApi {
+ private readonly discoveryApi: DiscoveryApi;
+ private readonly identityApi: IdentityApi;
+
+ constructor(options: {
+ discoveryApi: DiscoveryApi;
+ identityApi: IdentityApi;
+ }) {
+ this.discoveryApi = options.discoveryApi;
+ this.identityApi = options.identityApi;
+ }
+
+ async getRepoBuilds(
+ projectName: string,
+ repoName: string,
+ top: number,
+ ): Promise {
+ return await this.get(`/repo-builds/${projectName}/${repoName}?top=${top}`);
+ }
+
+ private async get(path: string): Promise {
+ const url = `${await this.discoveryApi.getBaseUrl('azure-devops')}${path}`;
+ const idToken = await this.identityApi.getIdToken();
+ const response = await fetch(url, {
+ headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
+ });
+
+ if (!response.ok) {
+ const payload = await response.text();
+ const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
+ throw new Error(message);
+ }
+
+ return await response.json();
+ }
+}
diff --git a/plugins/azure-devops/src/api/index.ts b/plugins/azure-devops/src/api/index.ts
new file mode 100644
index 0000000000..c69636222f
--- /dev/null
+++ b/plugins/azure-devops/src/api/index.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 * from './AzureDevOpsApi';
+export * from './AzureDevOpsClient';
diff --git a/plugins/azure-devops/src/api/types.ts b/plugins/azure-devops/src/api/types.ts
new file mode 100644
index 0000000000..35d4e38ae1
--- /dev/null
+++ b/plugins/azure-devops/src/api/types.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 {
+ BuildResult,
+ BuildStatus,
+} from 'azure-devops-node-api/interfaces/BuildInterfaces';
+
+export type RepoBuild = {
+ id?: number;
+ title: string;
+ link: string;
+ status?: BuildStatus;
+ result?: BuildResult;
+ queueTime?: Date;
+ source: string;
+};
diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx
new file mode 100644
index 0000000000..26f365c5e9
--- /dev/null
+++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.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 from 'react';
+import moment from 'moment';
+import {
+ Table,
+ TableColumn,
+ StatusError,
+ StatusOK,
+ StatusWarning,
+ StatusAborted,
+} from '@backstage/core-components';
+import { Link, Box, Typography } from '@material-ui/core';
+import Alert from '@material-ui/lab/Alert';
+import { RepoBuild } from '../../api/types';
+import {
+ BuildResult,
+ BuildStatus,
+} from 'azure-devops-node-api/interfaces/BuildInterfaces';
+
+const getBuildResultComponent = (result: number | undefined = 0) => {
+ switch (result) {
+ case 0: // None
+ return ;
+ case 2: // Succeeded
+ return ;
+ case 4: // PartiallySucceeded
+ return ;
+ case 8: // Failed
+ return ;
+ case 32: // Canceled
+ return ;
+ default:
+ return ;
+ }
+};
+
+const columns: TableColumn[] = [
+ {
+ title: 'ID',
+ field: 'id',
+ highlight: false,
+ width: '80px',
+ },
+ {
+ title: 'Build',
+ field: 'title',
+ render: (row: Partial) => (
+
+ {row.title}
+
+ ),
+ },
+ {
+ title: 'Source',
+ field: 'source',
+ },
+ {
+ title: 'Status',
+ field: 'status',
+ render: (row: Partial) => (
+
+
+ {BuildStatus[row.status || 0]}
+
+ ),
+ },
+ {
+ title: 'Result',
+ field: 'result',
+ render: (row: Partial) => (
+
+ {getBuildResultComponent(row.result)}
+
+ {BuildResult[row.result || 0]}
+
+ ),
+ },
+ {
+ title: 'Date',
+ field: 'queueTime',
+ render: (row: Partial) => moment(row.queueTime).fromNow(),
+ },
+];
+
+type Props = {
+ items: RepoBuild[];
+ loading: boolean;
+ error?: any;
+};
+
+export const BuildTable = ({ items, loading, error }: Props) => {
+ if (error) {
+ return (
+
+ );
+};
diff --git a/plugins/azure-devops/src/components/BuildTable/index.ts b/plugins/azure-devops/src/components/BuildTable/index.ts
new file mode 100644
index 0000000000..acf20eb523
--- /dev/null
+++ b/plugins/azure-devops/src/components/BuildTable/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 { BuildTable } from './BuildTable';
diff --git a/plugins/azure-devops/src/components/EntityPageAzureDevOps/EntityPageAzureDevOps.tsx b/plugins/azure-devops/src/components/EntityPageAzureDevOps/EntityPageAzureDevOps.tsx
new file mode 100644
index 0000000000..fbeac53569
--- /dev/null
+++ b/plugins/azure-devops/src/components/EntityPageAzureDevOps/EntityPageAzureDevOps.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity } from '@backstage/catalog-model';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import React from 'react';
+import { useRepoBuilds } from '../../hooks/useRepoBuilds';
+import { BuildTable } from '../BuildTable/BuildTable';
+
+type Props = {
+ /** @deprecated The entity is now grabbed from context instead */
+ entity?: Entity;
+};
+
+export const EntityPageAzureDevOps = (_props: Props) => {
+ const { entity } = useEntity();
+ const { items, loading, error } = useRepoBuilds(entity);
+
+ return ;
+};
diff --git a/plugins/azure-devops/src/components/EntityPageAzureDevOps/index.ts b/plugins/azure-devops/src/components/EntityPageAzureDevOps/index.ts
new file mode 100644
index 0000000000..89ee4f199b
--- /dev/null
+++ b/plugins/azure-devops/src/components/EntityPageAzureDevOps/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 { EntityPageAzureDevOps } from './EntityPageAzureDevOps';
diff --git a/plugins/azure-devops/src/components/Router.tsx b/plugins/azure-devops/src/components/Router.tsx
new file mode 100644
index 0000000000..74cf0439f8
--- /dev/null
+++ b/plugins/azure-devops/src/components/Router.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 { Routes, Route } from 'react-router';
+import { azureDevOpsRouteRef } from '../routes';
+import { EntityPageAzureDevOps } from './EntityPageAzureDevOps';
+import { AZURE_DEVOPS_ANNOTATION } from '../constants';
+import { Entity } from '@backstage/catalog-model';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { MissingAnnotationEmptyState } from '@backstage/core-components';
+
+export const isAzureDevOpsAvailable = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]);
+
+type Props = {
+ /** @deprecated The entity is now grabbed from context instead */
+ entity?: Entity;
+};
+
+export const Router = (_props: Props) => {
+ const { entity } = useEntity();
+
+ if (!isAzureDevOpsAvailable(entity)) {
+ return ;
+ }
+
+ return (
+
+ }
+ />
+
+ );
+};
diff --git a/plugins/azure-devops/src/constants.ts b/plugins/azure-devops/src/constants.ts
new file mode 100644
index 0000000000..6abc6449e9
--- /dev/null
+++ b/plugins/azure-devops/src/constants.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 const AZURE_DEVOPS_ANNOTATION = 'dev.azure.com/project-repo';
diff --git a/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts
new file mode 100644
index 0000000000..721fa52781
--- /dev/null
+++ b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.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.
+ */
+
+import { Entity } from '@backstage/catalog-model';
+import { AZURE_DEVOPS_ANNOTATION } from '../constants';
+
+export function useProjectRepoFromEntity(entity: Entity) {
+ const [project, repo] = (
+ entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION] ?? ''
+ ).split('/');
+ return { project, repo };
+}
diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts
new file mode 100644
index 0000000000..e6d2964770
--- /dev/null
+++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { useAsync } from 'react-use';
+import { Entity } from '@backstage/catalog-model';
+import { useApi, configApiRef } from '@backstage/core-plugin-api';
+import { azureDevOpsApiRef } from '../api';
+import { RepoBuild } from '../api/types';
+import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
+
+const DEFAULT_TOP: number = 10;
+
+export function useRepoBuilds(entity: Entity) {
+ const config = useApi(configApiRef);
+ const top = config.getOptionalNumber('azureDevOps.top') ?? DEFAULT_TOP;
+ const api = useApi(azureDevOpsApiRef);
+ const { project, repo } = useProjectRepoFromEntity(entity);
+ const { value, loading, error } = useAsync(() => {
+ return api.getRepoBuilds(project, repo, top);
+ }, [api, project, repo, entity]);
+
+ return {
+ items: value as RepoBuild[],
+ loading,
+ error,
+ };
+}
diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts
new file mode 100644
index 0000000000..b654aebab7
--- /dev/null
+++ b/plugins/azure-devops/src/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 { azureDevOpsPlugin, EntityAzureDevOpsContent } from './plugin';
+export { isAzureDevOpsAvailable } from './components/Router';
diff --git a/plugins/azure-devops/src/plugin.test.ts b/plugins/azure-devops/src/plugin.test.ts
new file mode 100644
index 0000000000..f23639798f
--- /dev/null
+++ b/plugins/azure-devops/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 { azureDevOpsPlugin } from './plugin';
+
+describe('azure-devops', () => {
+ it('should export plugin', () => {
+ expect(azureDevOpsPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/azure-devops/src/plugin.ts b/plugins/azure-devops/src/plugin.ts
new file mode 100644
index 0000000000..f37fab6d62
--- /dev/null
+++ b/plugins/azure-devops/src/plugin.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { azureDevOpsApiRef } from './api/AzureDevOpsApi';
+import { AzureDevOpsClient } from './api/AzureDevOpsClient';
+import {
+ createApiFactory,
+ createPlugin,
+ createRoutableExtension,
+ createRouteRef,
+ discoveryApiRef,
+ identityApiRef,
+} from '@backstage/core-plugin-api';
+
+export const rootRouteRef = createRouteRef({
+ path: '',
+ title: 'AzureDevOps',
+});
+
+export const azureDevOpsPlugin = createPlugin({
+ id: 'azureDevOps',
+ apis: [
+ createApiFactory({
+ api: azureDevOpsApiRef,
+ deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
+ factory: ({ discoveryApi, identityApi }) =>
+ new AzureDevOpsClient({ discoveryApi, identityApi }),
+ }),
+ ],
+ routes: {
+ entityContent: rootRouteRef,
+ },
+});
+
+export const EntityAzureDevOpsContent = azureDevOpsPlugin.provide(
+ createRoutableExtension({
+ name: 'EntityAzureDevOpsContent',
+ component: () => import('./components/Router').then(m => m.Router),
+ mountPoint: rootRouteRef,
+ }),
+);
diff --git a/plugins/azure-devops/src/routes.ts b/plugins/azure-devops/src/routes.ts
new file mode 100644
index 0000000000..81f4b48fda
--- /dev/null
+++ b/plugins/azure-devops/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 azureDevOpsRouteRef = createRouteRef({
+ title: 'azure-devops',
+});
diff --git a/plugins/azure-devops/src/setupTests.ts b/plugins/azure-devops/src/setupTests.ts
new file mode 100644
index 0000000000..fc6dbd98f8
--- /dev/null
+++ b/plugins/azure-devops/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';