From 7d1a522abc464a53b6f8fd6d2a27392c5ff383cf Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 31 Oct 2021 16:49:16 -0500 Subject: [PATCH 1/6] Added getting builds by definition name Signed-off-by: Andre Wanlin --- .../src/api/AzureDevOpsApi.ts | 72 ++++++++++++++-- .../src/service/router.test.ts | 85 ++++++++++++++++++- .../src/service/router.ts | 27 ++++++ 3 files changed, 175 insertions(+), 9 deletions(-) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 53dc19c68d..7acc5fc066 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -35,7 +35,7 @@ import { getArtifactId, } from '../utils'; -import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Logger } from 'winston'; import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces'; import { TeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; @@ -60,19 +60,46 @@ export class AzureDevOpsApi { return client.getRepository(repoName, projectName); } - public async getBuildList( + public async getBuildDefinitions( projectName: string, - repoId: string, - top: number, - ): Promise { + definitionName: string, + ): Promise { this.logger?.debug( - `Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`, + `Calling Azure DevOps REST API, getting Build Definitions for ${definitionName} in Project ${projectName}`, ); + const client = await this.webApi.getBuildApi(); + return client.getDefinitions( + projectName, + definitionName, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + } + + public async getBuildList( + projectName: string, + definitions?: number[], + repoId?: string, + top?: number, + ): Promise { const client = await this.webApi.getBuildApi(); return client.getBuilds( projectName, - undefined, + definitions, undefined, undefined, undefined, @@ -91,7 +118,7 @@ export class AzureDevOpsApi { undefined, undefined, repoId, - 'TfsGit', + repoId ? 'TfsGit' : undefined, ); } @@ -107,6 +134,7 @@ export class AzureDevOpsApi { const gitRepository = await this.getGitRepository(projectName, repoName); const buildList = await this.getBuildList( projectName, + undefined, gitRepository.id as string, top, ); @@ -118,6 +146,34 @@ export class AzureDevOpsApi { return repoBuilds; } + public async getDefinitionBuilds( + projectName: string, + definitionName: string, + top: number, + ) { + this.logger?.debug( + `Calling Azure DevOps REST API, getting up to ${top} Builds for ${definitionName} in Project ${projectName}`, + ); + + const buildDefinitions = await this.getBuildDefinitions( + projectName, + definitionName, + ); + const definitions = buildDefinitions.map(bd => bd.id) as number[]; + const buildList = await this.getBuildList( + projectName, + definitions, + undefined, + top, + ); + + const repoBuilds: RepoBuild[] = buildList.map(build => { + return mappedRepoBuild(build); + }); + + return repoBuilds; + } + public async getPullRequests( projectName: string, repoName: string, diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index ce5f712392..82d4df1265 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-azure-devops-common'; import { AzureDevOpsApi } from '../api'; -import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { ConfigReader } from '@backstage/config'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { createRouter } from './router'; @@ -39,7 +39,9 @@ describe('createRouter', () => { azureDevOpsApi = { getGitRepository: jest.fn(), getBuildList: jest.fn(), + getBuildDefinitions: jest.fn(), getRepoBuilds: jest.fn(), + getDefinitionBuilds: jest.fn(), getPullRequests: jest.fn(), } as any; const router = await createRouter({ @@ -136,6 +138,7 @@ describe('createRouter', () => { expect(azureDevOpsApi.getBuildList).toHaveBeenCalledWith( 'myProject', + undefined, 'af4ae3af-e747-4129-9bbc-d1329f6b0998', 40, ); @@ -144,6 +147,32 @@ describe('createRouter', () => { }); }); + describe('GET /build-definitions/:projectName/:definitionName', () => { + it('fetches a list of build definitions', async () => { + const inputDefinition: BuildDefinitionReference = { + id: 1, + name: 'myBuildDefinition', + }; + + const inputDefinitions: BuildDefinitionReference[] = [inputDefinition]; + + azureDevOpsApi.getBuildDefinitions.mockResolvedValueOnce( + inputDefinitions, + ); + + const response = await request(app).get( + '/build-definitions/myProject/myBuildDefinition', + ); + + expect(azureDevOpsApi.getBuildDefinitions).toHaveBeenCalledWith( + 'myProject', + 'myBuildDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(inputDefinitions); + }); + }); + describe('GET /repo-builds/:projectName/:repoName', () => { it('fetches a list of repo builds', async () => { const firstRepoBuild: RepoBuild = { @@ -198,6 +227,60 @@ describe('createRouter', () => { }); }); + describe('GET /definition-builds/:projectName/:definitionName', () => { + it('fetches a list of repo builds', async () => { + const firstRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: undefined, + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondRepoBuild: RepoBuild = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: undefined, + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdRepoBuild: RepoBuild = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: undefined, + source: 'refs/heads/develop (9bedf678)', + }; + + const repoBuilds: RepoBuild[] = [ + firstRepoBuild, + secondRepoBuild, + thirdRepoBuild, + ]; + + azureDevOpsApi.getDefinitionBuilds.mockResolvedValueOnce(repoBuilds); + + const response = await request(app) + .get('/definition-builds/myProject/myDefinition') + .query({ top: '30' }); + + expect(azureDevOpsApi.getDefinitionBuilds).toHaveBeenCalledWith( + 'myProject', + 'myDefinition', + 30, + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(repoBuilds); + }); + }); + describe('GET /pull-requests/:projectName/:repoName', () => { it('fetches a list of pull requests', async () => { const firstPullRequest: PullRequest = { diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 57d54df18f..5e6369bb11 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -73,12 +73,25 @@ export async function createRouter( const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; const buildList = await azureDevOpsApi.getBuildList( projectName, + undefined, repoId, top, ); res.status(200).json(buildList); }); + router.get( + '/build-definitions/:projectName/:definitionName', + async (req, res) => { + const { projectName, definitionName } = req.params; + const buildDefinitionList = await azureDevOpsApi.getBuildDefinitions( + projectName, + definitionName, + ); + res.status(200).json(buildDefinitionList); + }, + ); + router.get('/repo-builds/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; @@ -93,6 +106,20 @@ export async function createRouter( res.status(200).json(gitRepository); }); + router.get( + '/definition-builds/:projectName/:definitionName', + async (req, res) => { + const { projectName, definitionName } = req.params; + const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; + const gitRepository = await azureDevOpsApi.getDefinitionBuilds( + projectName, + definitionName, + top, + ); + res.status(200).json(gitRepository); + }, + ); + router.get('/pull-requests/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; From a77526afcdb23a019a6a925552bde4b02f9b2178 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 31 Oct 2021 16:54:08 -0500 Subject: [PATCH 2/6] Added changeset and updated API Report Signed-off-by: Andre Wanlin --- .changeset/rare-toes-burn.md | 5 +++++ plugins/azure-devops-backend/api-report.md | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/rare-toes-burn.md diff --git a/.changeset/rare-toes-burn.md b/.changeset/rare-toes-burn.md new file mode 100644 index 0000000000..2524df1daa --- /dev/null +++ b/.changeset/rare-toes-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops-backend': patch +--- + +Added getting builds by definition name diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 19d96dc728..9fd1e43bf6 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -25,8 +25,9 @@ export class AzureDevOpsApi { // (undocumented) getBuildList( projectName: string, - repoId: string, - top: number, + definitions?: number[], + repoId?: string, + top?: number, ): Promise; // (undocumented) getDashboardPullRequests( From 868f1dae99086ebeb43f96282d746a41e6ad6664 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 13 Nov 2021 11:25:46 -0600 Subject: [PATCH 3/6] Refactored to provide better API for Builds Signed-off-by: Andre Wanlin --- plugins/azure-devops-backend/api-report.md | 7 +- .../src/api/AzureDevOpsApi.test.ts | 691 ++++++++++++------ .../src/api/AzureDevOpsApi.ts | 190 +++-- .../src/service/router.test.ts | 227 +++--- .../src/service/router.ts | 53 +- plugins/azure-devops-common/api-report.md | 23 + plugins/azure-devops-common/src/types.ts | 16 + 7 files changed, 801 insertions(+), 406 deletions(-) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 9fd1e43bf6..cbb512d00e 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -4,6 +4,8 @@ ```ts import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { BuildRun } from '@backstage/plugin-azure-devops-common'; import { Config } from '@backstage/config'; import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; import express from 'express'; @@ -25,9 +27,8 @@ export class AzureDevOpsApi { // (undocumented) getBuildList( projectName: string, - definitions?: number[], - repoId?: string, - top?: number, + repoId: string, + top: number, ): Promise; // (undocumented) getDashboardPullRequests( diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index 2cb7d93c0b..76b5089bbc 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -29,277 +29,510 @@ import { GitPullRequest, GitRepository, } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi'; +import { + mappedBuildRun, + mappedPullRequest, + mappedRepoBuild, +} from './AzureDevOpsApi'; import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; describe('AzureDevOpsApi', () => { describe('mappedRepoBuild', () => { - it('should return RepoBuild from Build', () => { - const inputBuildDefinition: DefinitionReference = { - name: 'My Build Definition', - }; + describe('mappedRepoBuild happy path', () => { + it('should return RepoBuild from Build', () => { + const inputBuildDefinition: DefinitionReference = { + name: 'My Build Definition', + }; - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: inputBuildDefinition, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: inputBuildDefinition, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'My Build Definition - Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with no Build definition name', () => { - it('should return RepoBuild with only Build Number for title', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with no Build definition name', () => { + it('should return RepoBuild with only Build Number for title', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined status', () => { - it('should return BuildStatus of None for status', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with undefined status', () => { + it('should return BuildStatus of None for status', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: undefined, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: undefined, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.None, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.None, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined result', () => { - it('should return BuildResult of None for result', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with undefined result', () => { + it('should return BuildResult of None for result', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.InProgress, - result: undefined, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined link', () => { - it('should return empty string for link', () => { - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + describe('mappedRepoBuild with undefined link', () => { + it('should return empty string for link', () => { + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.InProgress, - result: undefined, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: undefined, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: undefined, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: '', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: '', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); }); describe('mappedPullRequest', () => { - it('should return PullRequest from GitPullRequest', () => { - const inputGitRepository: GitRepository = { - name: 'super-feature-repo', - }; + describe('mappedPullRequest happy path', () => { + it('should return PullRequest from GitPullRequest', () => { + const inputGitRepository: GitRepository = { + name: 'super-feature-repo', + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputPullRequest: GitPullRequest = { - pullRequestId: 7181, - repository: inputGitRepository, - title: 'My Awesome New Feature', - createdBy: inputIdentityRef, - creationDate: new Date('2020-09-12T06:10:23.932Z'), - sourceRefName: 'refs/heads/topic/super-awesome-feature', - targetRefName: 'refs/heads/main', - status: PullRequestStatus.Active, - isDraft: false, - }; + const inputPullRequest: GitPullRequest = { + pullRequestId: 7181, + repository: inputGitRepository, + title: 'My Awesome New Feature', + createdBy: inputIdentityRef, + creationDate: new Date('2020-09-12T06:10:23.932Z'), + sourceRefName: 'refs/heads/topic/super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + }; - const inputBaseUrl = - 'https://host.com/myOrg/_git/super-feature-repo/pullrequest'; + const inputBaseUrl = + 'https://host.com/myOrg/_git/super-feature-repo/pullrequest'; - const outputPullRequest: PullRequest = { - pullRequestId: 7181, - repoName: 'super-feature-repo', - title: 'My Awesome New Feature', - uniqueName: 'DOMAIN\\jdoe', - createdBy: 'Jane Doe', - creationDate: '2020-09-12T06:10:23.932Z', - sourceRefName: 'refs/heads/topic/super-awesome-feature', - targetRefName: 'refs/heads/main', - status: PullRequestStatus.Active, - isDraft: false, - link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7181', - }; + const outputPullRequest: PullRequest = { + pullRequestId: 7181, + repoName: 'super-feature-repo', + title: 'My Awesome New Feature', + uniqueName: 'DOMAIN\\jdoe', + createdBy: 'Jane Doe', + creationDate: '2020-09-12T06:10:23.932Z', + sourceRefName: 'refs/heads/topic/super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7181', + }; - expect(mappedPullRequest(inputPullRequest, inputBaseUrl)).toEqual( - outputPullRequest, - ); + expect(mappedPullRequest(inputPullRequest, inputBaseUrl)).toEqual( + outputPullRequest, + ); + }); + }); + }); + + describe('mappedBuildRun', () => { + describe('mappedBuildRun happy path', () => { + it('should return RepoBuild from Build', () => { + const inputBuildDefinition: DefinitionReference = { + name: 'My Build Definition', + }; + + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: inputBuildDefinition, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with no Build definition name', () => { + it('should return RepoBuild with only Build Number for title', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined status', () => { + it('should return BuildStatus of None for status', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: undefined, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.None, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined result', () => { + it('should return BuildResult of None for result', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined link', () => { + it('should return empty string for link', () => { + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: undefined, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: '', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); }); }); }); diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 7acc5fc066..c98092b5ac 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -14,8 +14,13 @@ * limitations under the License. */ +import { + Build, + BuildDefinitionReference, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { BuildResult, + BuildRun, BuildStatus, DashboardPullRequest, Policy, @@ -35,7 +40,6 @@ import { getArtifactId, } from '../utils'; -import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Logger } from 'winston'; import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces'; import { TeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; @@ -60,46 +64,19 @@ export class AzureDevOpsApi { return client.getRepository(repoName, projectName); } - public async getBuildDefinitions( - projectName: string, - definitionName: string, - ): Promise { - this.logger?.debug( - `Calling Azure DevOps REST API, getting Build Definitions for ${definitionName} in Project ${projectName}`, - ); - - const client = await this.webApi.getBuildApi(); - return client.getDefinitions( - projectName, - definitionName, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); - } - public async getBuildList( projectName: string, - definitions?: number[], - repoId?: string, - top?: number, + repoId: string, + top: number, ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`, + ); + const client = await this.webApi.getBuildApi(); return client.getBuilds( projectName, - definitions, + undefined, undefined, undefined, undefined, @@ -118,7 +95,7 @@ export class AzureDevOpsApi { undefined, undefined, repoId, - repoId ? 'TfsGit' : undefined, + 'TfsGit', ); } @@ -134,7 +111,6 @@ export class AzureDevOpsApi { const gitRepository = await this.getGitRepository(projectName, repoName); const buildList = await this.getBuildList( projectName, - undefined, gitRepository.id as string, top, ); @@ -146,34 +122,6 @@ export class AzureDevOpsApi { return repoBuilds; } - public async getDefinitionBuilds( - projectName: string, - definitionName: string, - top: number, - ) { - this.logger?.debug( - `Calling Azure DevOps REST API, getting up to ${top} Builds for ${definitionName} in Project ${projectName}`, - ); - - const buildDefinitions = await this.getBuildDefinitions( - projectName, - definitionName, - ); - const definitions = buildDefinitions.map(bd => bd.id) as number[]; - const buildList = await this.getBuildList( - projectName, - definitions, - undefined, - top, - ); - - const repoBuilds: RepoBuild[] = buildList.map(build => { - return mappedRepoBuild(build); - }); - - return repoBuilds; - } - public async getPullRequests( projectName: string, repoName: string, @@ -313,6 +261,101 @@ export class AzureDevOpsApi { return teamMembers .map(teamMember => teamMember.identity?.id) .filter((id): id is string => Boolean(id)); + public async getBuildDefinitions( + projectName: string, + definitionName: string, + ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting Build Definitions for ${definitionName} in Project ${projectName}`, + ); + + const client = await this.webApi.getBuildApi(); + return client.getDefinitions( + projectName, + definitionName, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + } + + public async getBuilds( + projectName: string, + top: number, + repoId?: string, + definitions?: number[], + ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`, + ); + + const client = await this.webApi.getBuildApi(); + return client.getBuilds( + projectName, + definitions, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + top, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + repoId, + repoId ? 'TfsGit' : undefined, + ); + } + + public async getBuildRuns( + projectName: string, + top: number, + repoName?: string, + definitionName?: string, + ) { + let repoId: string | undefined; + let definitions: number[] | undefined; + + if (repoName) { + const gitRepository = await this.getGitRepository(projectName, repoName); + repoId = gitRepository.id; + } + + if (definitionName) { + const buildDefinitions = await this.getBuildDefinitions( + projectName, + definitionName, + ); + definitions = buildDefinitions.map(bd => bd.id) as number[]; + } + + const builds = await this.getBuilds(projectName, top, repoId, definitions); + + const buildRuns: BuildRun[] = builds.map(build => { + return mappedBuildRun(build); + }); + + return buildRuns; } } @@ -351,3 +394,20 @@ export function mappedPullRequest( link: `${linkBaseUrl}/${pullRequest.pullRequestId}`, }; } + +export function mappedBuildRun(build: Build): BuildRun { + return { + id: build.id, + title: [build.definition?.name, build.buildNumber] + .filter(Boolean) + .join(' - '), + link: build._links?.web.href ?? '', + status: build.status ?? BuildStatus.None, + result: build.result ?? BuildResult.None, + queueTime: build.queueTime?.toISOString(), + startTime: build.startTime?.toISOString(), + finishTime: build.finishTime?.toISOString(), + source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, + uniqueName: build.requestedFor?.uniqueName ?? 'N/A', + }; +} diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 82d4df1265..87135781e6 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -14,8 +14,13 @@ * limitations under the License. */ +import { + Build, + BuildDefinitionReference, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { BuildResult, + BuildRun, BuildStatus, PullRequest, PullRequestStatus, @@ -23,7 +28,6 @@ import { } from '@backstage/plugin-azure-devops-common'; import { AzureDevOpsApi } from '../api'; -import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { ConfigReader } from '@backstage/config'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { createRouter } from './router'; @@ -43,6 +47,8 @@ describe('createRouter', () => { getRepoBuilds: jest.fn(), getDefinitionBuilds: jest.fn(), getPullRequests: jest.fn(), + getBuilds: jest.fn(), + getBuildRuns: jest.fn(), } as any; const router = await createRouter({ azureDevOpsApi, @@ -138,7 +144,6 @@ describe('createRouter', () => { expect(azureDevOpsApi.getBuildList).toHaveBeenCalledWith( 'myProject', - undefined, 'af4ae3af-e747-4129-9bbc-d1329f6b0998', 40, ); @@ -147,32 +152,6 @@ describe('createRouter', () => { }); }); - describe('GET /build-definitions/:projectName/:definitionName', () => { - it('fetches a list of build definitions', async () => { - const inputDefinition: BuildDefinitionReference = { - id: 1, - name: 'myBuildDefinition', - }; - - const inputDefinitions: BuildDefinitionReference[] = [inputDefinition]; - - azureDevOpsApi.getBuildDefinitions.mockResolvedValueOnce( - inputDefinitions, - ); - - const response = await request(app).get( - '/build-definitions/myProject/myBuildDefinition', - ); - - expect(azureDevOpsApi.getBuildDefinitions).toHaveBeenCalledWith( - 'myProject', - 'myBuildDefinition', - ); - expect(response.status).toEqual(200); - expect(response.body).toEqual(inputDefinitions); - }); - }); - describe('GET /repo-builds/:projectName/:repoName', () => { it('fetches a list of repo builds', async () => { const firstRepoBuild: RepoBuild = { @@ -227,60 +206,6 @@ describe('createRouter', () => { }); }); - describe('GET /definition-builds/:projectName/:definitionName', () => { - it('fetches a list of repo builds', async () => { - const firstRepoBuild: RepoBuild = { - id: 1, - title: 'My Build Definition - Build 1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.PartiallySucceeded, - queueTime: undefined, - source: 'refs/heads/develop (f4f78b31)', - }; - - const secondRepoBuild: RepoBuild = { - id: 2, - title: 'My Build Definition - Build 2', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: undefined, - source: 'refs/heads/develop (13c988d4)', - }; - - const thirdRepoBuild: RepoBuild = { - id: 3, - title: 'My Build Definition - Build 3', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: undefined, - source: 'refs/heads/develop (9bedf678)', - }; - - const repoBuilds: RepoBuild[] = [ - firstRepoBuild, - secondRepoBuild, - thirdRepoBuild, - ]; - - azureDevOpsApi.getDefinitionBuilds.mockResolvedValueOnce(repoBuilds); - - const response = await request(app) - .get('/definition-builds/myProject/myDefinition') - .query({ top: '30' }); - - expect(azureDevOpsApi.getDefinitionBuilds).toHaveBeenCalledWith( - 'myProject', - 'myDefinition', - 30, - ); - expect(response.status).toEqual(200); - expect(response.body).toEqual(repoBuilds); - }); - }); - describe('GET /pull-requests/:projectName/:repoName', () => { it('fetches a list of pull requests', async () => { const firstPullRequest: PullRequest = { @@ -343,4 +268,142 @@ describe('createRouter', () => { expect(response.body).toEqual(pullRequests); }); }); + + describe('GET /build-definitions/:projectName/:definitionName', () => { + it('fetches a list of build definitions', async () => { + const inputDefinition: BuildDefinitionReference = { + id: 1, + name: 'myBuildDefinition', + }; + + const inputDefinitions: BuildDefinitionReference[] = [inputDefinition]; + + azureDevOpsApi.getBuildDefinitions.mockResolvedValueOnce( + inputDefinitions, + ); + + const response = await request(app).get( + '/build-definitions/myProject/myBuildDefinition', + ); + + expect(azureDevOpsApi.getBuildDefinitions).toHaveBeenCalledWith( + 'myProject', + 'myBuildDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(inputDefinitions); + }); + }); + + describe('GET /builds/:projectName', () => { + describe('GET /builds/:projectName with repoName', () => { + it('fetches a list of build runs using repoName', async () => { + const firstBuildRun: BuildRun = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondBuildRun: BuildRun = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdBuildRun: BuildRun = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (9bedf678)', + }; + + const buildRuns: BuildRun[] = [ + firstBuildRun, + secondBuildRun, + thirdBuildRun, + ]; + + azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); + + const response = await request(app) + .get('/builds/myProject') + .query({ top: '50', repoName: 'myRepo' }); + + expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( + 'myProject', + 50, + 'myRepo', + undefined, + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(buildRuns); + }); + }); + + describe('GET /builds/:projectName with definitionName', () => { + it('fetches a list of build runs using definitionName', async () => { + const firstBuildRun: BuildRun = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondBuildRun: BuildRun = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdBuildRun: BuildRun = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (9bedf678)', + }; + + const buildRuns: BuildRun[] = [ + firstBuildRun, + secondBuildRun, + thirdBuildRun, + ]; + + azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); + + const response = await request(app) + .get('/builds/myProject') + .query({ top: '50', definitionName: 'myDefinition' }); + + expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( + 'myProject', + 50, + undefined, + 'myDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(buildRuns); + }); + }); + }); }); diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 5e6369bb11..99c140a424 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -73,25 +73,12 @@ export async function createRouter( const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; const buildList = await azureDevOpsApi.getBuildList( projectName, - undefined, repoId, top, ); res.status(200).json(buildList); }); - router.get( - '/build-definitions/:projectName/:definitionName', - async (req, res) => { - const { projectName, definitionName } = req.params; - const buildDefinitionList = await azureDevOpsApi.getBuildDefinitions( - projectName, - definitionName, - ); - res.status(200).json(buildDefinitionList); - }, - ); - router.get('/repo-builds/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; @@ -106,20 +93,6 @@ export async function createRouter( res.status(200).json(gitRepository); }); - router.get( - '/definition-builds/:projectName/:definitionName', - async (req, res) => { - const { projectName, definitionName } = req.params; - const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; - const gitRepository = await azureDevOpsApi.getDefinitionBuilds( - projectName, - definitionName, - top, - ); - res.status(200).json(gitRepository); - }, - ); - router.get('/pull-requests/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; @@ -171,6 +144,32 @@ export async function createRouter( res.status(200).json(allTeams); }); + router.get( + '/build-definitions/:projectName/:definitionName', + async (req, res) => { + const { projectName, definitionName } = req.params; + const buildDefinitionList = await azureDevOpsApi.getBuildDefinitions( + projectName, + definitionName, + ); + res.status(200).json(buildDefinitionList); + }, + ); + + router.get('/builds/:projectName', async (req, res) => { + const { projectName } = req.params; + const repoName = req.query.repoName?.toString(); + const definitionName = req.query.definitionName?.toString(); + const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; + const builds = await azureDevOpsApi.getBuildRuns( + projectName, + top, + repoName, + definitionName, + ); + res.status(200).json(builds); + }); + router.use(errorHandler()); return router; } diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 17cccb05cb..bb458f169a 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -14,6 +14,29 @@ export enum BuildResult { Succeeded = 2, } +// Warning: (ae-missing-release-tag) "BuildRun" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BuildRun = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: string; + startTime?: string; + finishTime?: string; + source: string; + uniqueName?: string; +}; + +// Warning: (ae-missing-release-tag) "BuildRunOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BuildRunOptions = { + top?: number; +}; + // Warning: (ae-missing-release-tag) "BuildStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index da41e08d1f..e6bd174079 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -251,3 +251,19 @@ export enum PullRequestVoteStatus { WaitingForAuthor = -5, Rejected = -10, } +export type BuildRun = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: string; + startTime?: string; + finishTime?: string; + source: string; + uniqueName?: string; +}; + +export type BuildRunOptions = { + top?: number; +}; From 5b41684f6aca83d499dca3ff599cab2718b4873f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 26 Nov 2021 07:09:05 -0600 Subject: [PATCH 4/6] Updated changeset to minor Signed-off-by: Andre Wanlin --- .changeset/rare-toes-burn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rare-toes-burn.md b/.changeset/rare-toes-burn.md index 2524df1daa..264576eb58 100644 --- a/.changeset/rare-toes-burn.md +++ b/.changeset/rare-toes-burn.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops-backend': minor --- Added getting builds by definition name From 07ac314fb2f891be0313d5d89ab3bd45f2b5b361 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 4 Dec 2021 11:50:38 -0600 Subject: [PATCH 5/6] Improvements based on feedback Signed-off-by: Andre Wanlin --- plugins/azure-devops-backend/api-report.md | 19 ++++++++++++ .../src/api/AzureDevOpsApi.ts | 30 +++++-------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index cbb512d00e..9574215e3e 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -25,12 +25,31 @@ export class AzureDevOpsApi { // (undocumented) getAllTeams(): Promise; // (undocumented) + getBuildDefinitions( + projectName: string, + definitionName: string, + ): Promise; + // (undocumented) getBuildList( projectName: string, repoId: string, top: number, ): Promise; // (undocumented) + getBuildRuns( + projectName: string, + top: number, + repoName?: string, + definitionName?: string, + ): Promise; + // (undocumented) + getBuilds( + projectName: string, + top: number, + repoId?: string, + definitions?: number[], + ): Promise; + // (undocumented) getDashboardPullRequests( projectName: string, options: PullRequestOptions, diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index c98092b5ac..6e45a5f4e5 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -261,6 +261,8 @@ export class AzureDevOpsApi { return teamMembers .map(teamMember => teamMember.identity?.id) .filter((id): id is string => Boolean(id)); + } + public async getBuildDefinitions( projectName: string, definitionName: string, @@ -270,25 +272,7 @@ export class AzureDevOpsApi { ); const client = await this.webApi.getBuildApi(); - return client.getDefinitions( - projectName, - definitionName, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); + return client.getDefinitions(projectName, definitionName); } public async getBuilds( @@ -346,14 +330,14 @@ export class AzureDevOpsApi { projectName, definitionName, ); - definitions = buildDefinitions.map(bd => bd.id) as number[]; + definitions = buildDefinitions + .map(bd => bd.id) + .filter((bd): bd is number => Boolean(bd)); } const builds = await this.getBuilds(projectName, top, repoId, definitions); - const buildRuns: BuildRun[] = builds.map(build => { - return mappedBuildRun(build); - }); + const buildRuns: BuildRun[] = builds.map(mappedBuildRun); return buildRuns; } From fa0b8ab6a89db893183f4de1bf27c654db0ea850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 8 Dec 2021 13:47:04 +0100 Subject: [PATCH 6/6] patch level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/rare-toes-burn.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/rare-toes-burn.md b/.changeset/rare-toes-burn.md index 264576eb58..2865eed51f 100644 --- a/.changeset/rare-toes-burn.md +++ b/.changeset/rare-toes-burn.md @@ -1,5 +1,6 @@ --- -'@backstage/plugin-azure-devops-backend': minor +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops-common': patch --- Added getting builds by definition name