Added pull requests to backend

Signed-off-by: Andre Wanlin <awanlin@rapidrtc.com>
This commit is contained in:
Andre Wanlin
2021-10-09 13:14:26 -05:00
parent db2666b980
commit b7c0585471
9 changed files with 252 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops-backend': patch
---
Expands the Azure DevOps backend plugin to provide pull request data to be used by the front end plugin
@@ -10,6 +10,7 @@ import { Config } from '@backstage/config';
import express from 'express';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { Logger as Logger_2 } from 'winston';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { WebApi } from 'azure-devops-node-api';
// Warning: (ae-missing-release-tag) "AzureDevOpsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -29,6 +30,13 @@ export class AzureDevOpsApi {
repoName: string,
): Promise<GitRepository>;
// (undocumented)
getPullRequests(
projectName: string,
repoName: string,
top: number,
status: PullRequestStatus,
): Promise<PullRequest[]>;
// (undocumented)
getRepoBuilds(
projectName: string,
repoName: string,
@@ -41,6 +49,22 @@ export class AzureDevOpsApi {
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PullRequest = {
pullRequestId?: number;
repoName?: string;
title?: string;
createdBy?: string;
creationDate?: Date;
sourceRefName?: string;
targetRefName?: string;
status?: PullRequestStatus;
isDraft?: boolean;
link: string;
};
// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -13,17 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { repoBuildFromBuild } from './AzureDevOpsApi';
import { RepoBuild } from './types';
import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi';
import { PullRequest, RepoBuild } from './types';
import {
Build,
BuildResult,
BuildStatus,
DefinitionReference,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
import {
GitPullRequest,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { GitRepository } from 'azure-devops-node-api/interfaces/TfvcInterfaces';
import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
describe('AzureDevOpsApi', () => {
describe('repoBuildFromBuild', () => {
describe('mappedRepoBuild', () => {
it('should return RepoBuild from Build', () => {
const inputBuildDefinition: DefinitionReference = {
name: 'My Build Definition',
@@ -57,11 +63,11 @@ describe('AzureDevOpsApi', () => {
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with no Build definition name', () => {
describe('mappedRepoBuild with no Build definition name', () => {
it('should return RepoBuild with only Build Number for title', () => {
const inputLinks: any = {
web: {
@@ -91,11 +97,11 @@ describe('AzureDevOpsApi', () => {
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with undefined status', () => {
describe('mappedRepoBuild with undefined status', () => {
it('should return BuildStatus of None for status', () => {
const inputLinks: any = {
web: {
@@ -125,11 +131,11 @@ describe('AzureDevOpsApi', () => {
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with undefined result', () => {
describe('mappedRepoBuild with undefined result', () => {
it('should return BuildResult of None for result', () => {
const inputLinks: any = {
web: {
@@ -159,11 +165,11 @@ describe('AzureDevOpsApi', () => {
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with undefined link', () => {
describe('mappedRepoBuild with undefined link', () => {
it('should return empty string for link', () => {
const inputBuild: Build = {
id: 1,
@@ -187,7 +193,51 @@ describe('AzureDevOpsApi', () => {
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('mappedPullRequest', () => {
it('should return PullRequest from GitPullRequest', () => {
const inputGitRepository: GitRepository = {
name: 'super-feature-repo',
};
const inputIdentityRef: IdentityRef = {
displayName: 'Jane Doe',
};
const inputPullRequest: GitPullRequest = {
pullRequestId: 7181,
repository: inputGitRepository,
title: 'My Awesome New Feature',
createdBy: inputIdentityRef,
creationDate: new Date('2020-09-12T06:10:23.9325232Z'),
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 outputPullRequest: PullRequest = {
pullRequestId: 7181,
repoName: 'super-feature-repo',
title: 'My Awesome New Feature',
createdBy: 'Jane Doe',
creationDate: new Date('2020-09-12T06:10:23.9325232Z'),
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,
);
});
});
});
@@ -16,12 +16,17 @@
import { Logger } from 'winston';
import { WebApi } from 'azure-devops-node-api';
import { RepoBuild } from './types';
import {
Build,
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
import {
GitPullRequest,
GitPullRequestSearchCriteria,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { PullRequest, RepoBuild } from './types';
export class AzureDevOpsApi {
constructor(
@@ -88,14 +93,47 @@ export class AzureDevOpsApi {
);
const repoBuilds: RepoBuild[] = buildList.map(build => {
return repoBuildFromBuild(build);
return mappedRepoBuild(build);
});
return repoBuilds;
}
async getPullRequests(
projectName: string,
repoName: string,
top: number,
status: PullRequestStatus,
) {
if (this.logger) {
this.logger.debug(
`Calling Azure DevOps REST API, getting up to ${top} Pull Requests for Repository ${repoName} for Project ${projectName}`,
);
}
const gitRepository = await this.getGitRepository(projectName, repoName);
const client = await this.webApi.getGitApi();
const searchCriteria: GitPullRequestSearchCriteria = {
status: status,
};
const gitPullRequests = await client.getPullRequests(
gitRepository.id as string,
searchCriteria,
projectName,
undefined,
undefined,
top,
);
const linkBaseUrl = `${this.webApi.serverUrl}/${projectName}/_git/${repoName}/pullrequest`;
const pullRequests: PullRequest[] = gitPullRequests.map(gitPullRequest => {
return mappedPullRequest(gitPullRequest, linkBaseUrl);
});
return pullRequests;
}
}
export function repoBuildFromBuild(build: Build) {
export function mappedRepoBuild(build: Build) {
return {
id: build.id,
title: [build.definition?.name, build.buildNumber]
@@ -108,3 +146,21 @@ export function repoBuildFromBuild(build: Build) {
source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`,
};
}
export function mappedPullRequest(
pullRequest: GitPullRequest,
linkBaseUrl: string,
) {
return {
pullRequestId: pullRequest.pullRequestId,
repoName: pullRequest.repository?.name,
title: pullRequest.title,
createdBy: pullRequest.createdBy?.displayName,
creationDate: pullRequest.creationDate,
sourceRefName: pullRequest.sourceRefName,
targetRefName: pullRequest.targetRefName,
status: pullRequest.status,
isDraft: pullRequest.isDraft,
link: `${linkBaseUrl}/${pullRequest.pullRequestId}`,
};
}
@@ -15,4 +15,4 @@
*/
export { AzureDevOpsApi } from './AzureDevOpsApi';
export type { RepoBuild } from './types';
export type { RepoBuild, PullRequest } from './types';
@@ -18,6 +18,7 @@ import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
export type RepoBuild = {
id?: number;
@@ -28,3 +29,16 @@ export type RepoBuild = {
queueTime?: Date;
source: string;
};
export type PullRequest = {
pullRequestId?: number;
repoName?: string;
title?: string;
createdBy?: string;
creationDate?: Date;
sourceRefName?: string;
targetRefName?: string;
status?: PullRequestStatus;
isDraft?: boolean;
link: string;
};
+1 -1
View File
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export { AzureDevOpsApi } from './api';
export type { RepoBuild } from './api';
export type { RepoBuild, PullRequest } from './api';
export * from './service/router';
@@ -20,8 +20,11 @@ import express from 'express';
import request from 'supertest';
import { AzureDevOpsApi } from '../api';
import { createRouter } from './router';
import { RepoBuild } from '../api/types';
import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { PullRequest, RepoBuild } from '../api/types';
import {
GitRepository,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
Build,
BuildResult,
@@ -37,6 +40,7 @@ describe('createRouter', () => {
getGitRepository: jest.fn(),
getBuildList: jest.fn(),
getRepoBuilds: jest.fn(),
getPullRequests: jest.fn(),
} as any;
const router = await createRouter({
azureDevOpsApi,
@@ -193,4 +197,68 @@ describe('createRouter', () => {
expect(response.body).toEqual(repoBuilds);
});
});
describe('GET /pull-requests/:projectName/:repoName', () => {
it('fetches a list of pull requests', async () => {
const firstPullRequest: PullRequest = {
pullRequestId: 7181,
repoName: 'super-feature-repo',
title: 'My Awesome New Feature',
createdBy: 'Jane Doe',
creationDate: undefined,
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 secondPullRequest: PullRequest = {
pullRequestId: 7182,
repoName: 'super-feature-repo',
title: 'Refactoring My Awesome New Feature',
createdBy: 'Jane Doe',
creationDate: undefined,
sourceRefName: 'refs/heads/topic/refactor-super-awesome-feature',
targetRefName: 'refs/heads/main',
status: PullRequestStatus.Active,
isDraft: false,
link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7182',
};
const thirdPullRequest: PullRequest = {
pullRequestId: 7183,
repoName: 'super-feature-repo',
title: 'Bug Fix for My Awesome New Feature',
createdBy: 'Jane Doe',
creationDate: undefined,
sourceRefName: 'refs/heads/topic/fix-super-awesome-feature',
targetRefName: 'refs/heads/main',
status: PullRequestStatus.Active,
isDraft: false,
link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7183',
};
const pullRequests: PullRequest[] = [
firstPullRequest,
secondPullRequest,
thirdPullRequest,
];
azureDevOpsApi.getPullRequests.mockResolvedValueOnce(pullRequests);
const response = await request(app)
.get('/pull-requests/myProject/myRepo')
.query({ top: '50', status: 1 });
expect(azureDevOpsApi.getPullRequests).toHaveBeenCalledWith(
'myProject',
'myRepo',
50,
1,
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(pullRequests);
});
});
});
@@ -21,6 +21,7 @@ import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api';
import { AzureDevOpsApi } from '../api';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
const DEFAULT_TOP: number = 10;
@@ -84,6 +85,21 @@ export async function createRouter(
res.status(200).json(gitRepository);
});
router.get('/pull-requests/:projectName/:repoName', async (req, res) => {
const { projectName, repoName } = req.params;
const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP;
const status = req.query.status
? Number(req.query.status)
: PullRequestStatus.Active;
const gitPullRequest = await azureDevOpsApi.getPullRequests(
projectName,
repoName,
top,
status,
);
res.status(200).json(gitPullRequest);
});
router.use(errorHandler());
return router;
}