feat: Created new endpoints for getting dashboard PRs and all teams.

Signed-off-by: Marley Powell <Marley.Powell@exclaimer.com>
This commit is contained in:
Marley Powell
2021-11-26 15:32:37 +00:00
parent 2439a1acbd
commit 5c3941970e
4 changed files with 439 additions and 1 deletions
@@ -17,19 +17,30 @@
import {
BuildResult,
BuildStatus,
DashboardPullRequest,
Policy,
PullRequest,
PullRequestOptions,
RepoBuild,
Team,
} from '@backstage/plugin-azure-devops-common';
import {
GitPullRequest,
GitPullRequestSearchCriteria,
GitRepository,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
convertDashboardPullRequest,
convertPolicy,
getArtifactId,
} from '../utils';
import { Build } 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';
import { WebApi } from 'azure-devops-node-api';
import { WebApiTeam } from 'azure-devops-node-api/interfaces/CoreInterfaces';
export class AzureDevOpsApi {
public constructor(
@@ -138,6 +149,115 @@ export class AzureDevOpsApi {
return pullRequests;
}
public async getDashboardPullRequests(
projectName: string,
options: PullRequestOptions,
): Promise<DashboardPullRequest[]> {
this.logger?.debug(
`Getting dashboard pull requests for project '${projectName}'.`,
);
const client = await this.webApi.getGitApi();
const searchCriteria: GitPullRequestSearchCriteria = {
status: options.status,
};
const gitPullRequests: GitPullRequest[] =
await client.getPullRequestsByProject(
projectName,
searchCriteria,
undefined,
undefined,
options.top,
);
return Promise.all(
gitPullRequests.map(async gitPullRequest => {
const projectId = gitPullRequest.repository?.project?.id;
const prId = gitPullRequest.pullRequestId;
let policies: Policy[] | undefined;
if (projectId && prId) {
policies = await this.getPullRequestPolicies(
projectName,
projectId,
prId,
);
}
return convertDashboardPullRequest(
gitPullRequest,
this.webApi.serverUrl,
policies,
);
}),
);
}
private async getPullRequestPolicies(
projectName: string,
projectId: string,
pullRequestId: number,
): Promise<Policy[]> {
this.logger?.debug(
`Getting pull request policies for pull request id '${pullRequestId}'.`,
);
const client = await this.webApi.getPolicyApi();
const artifactId = getArtifactId(projectId, pullRequestId);
const policyEvaluationRecords: PolicyEvaluationRecord[] =
await client.getPolicyEvaluations(projectName, artifactId);
return policyEvaluationRecords
.map(convertPolicy)
.filter((policy): policy is Policy => Boolean(policy));
}
public async getAllTeams(): Promise<Team[]> {
this.logger?.debug('Getting all teams.');
const client = await this.webApi.getCoreApi();
const webApiTeams: WebApiTeam[] = await client.getAllTeams();
const teams: Team[] = await Promise.all(
webApiTeams.map(async team => ({
id: team.id,
name: team.name,
memberIds: await this.getTeamMemberIds(team),
})),
);
return teams.sort((a, b) =>
a.name && b.name ? a.name.localeCompare(b.name) : 0,
);
}
private async getTeamMemberIds(
team: WebApiTeam,
): Promise<string[] | undefined> {
this.logger?.debug(`Getting team member ids for team '${team.name}'.`);
if (!team.projectId || !team.id) {
return undefined;
}
const client = await this.webApi.getCoreApi();
const teamMembers: TeamMember[] =
await client.getTeamMembersWithExtendedProperties(
team.projectId,
team.id,
);
return teamMembers
.map(teamMember => teamMember.identity?.id)
.filter((id): id is string => Boolean(id));
}
}
export function mappedRepoBuild(build: Build): RepoBuild {
@@ -15,6 +15,7 @@
*/
import {
DashboardPullRequest,
PullRequestOptions,
PullRequestStatus,
} from '@backstage/plugin-azure-devops-common';
@@ -27,7 +28,7 @@ import Router from 'express-promise-router';
import { errorHandler } from '@backstage/backend-common';
import express from 'express';
const DEFAULT_TOP: number = 10;
const DEFAULT_TOP = 10;
export interface RouterOptions {
azureDevOpsApi?: AzureDevOpsApi;
@@ -80,33 +81,69 @@ export async function createRouter(
router.get('/repo-builds/:projectName/:repoName', async (req, res) => {
const { projectName, repoName } = req.params;
const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP;
const gitRepository = await azureDevOpsApi.getRepoBuilds(
projectName,
repoName,
top,
);
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 pullRequestOptions: PullRequestOptions = {
top: top,
status: status,
};
const gitPullRequest = await azureDevOpsApi.getPullRequests(
projectName,
repoName,
pullRequestOptions,
);
res.status(200).json(gitPullRequest);
});
router.get('/dashboard-pull-requests/:projectName', async (req, res) => {
const { projectName } = 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 pullRequestOptions: PullRequestOptions = {
top: top,
status: status,
};
const pullRequests: DashboardPullRequest[] =
await azureDevOpsApi.getDashboardPullRequests(
projectName,
pullRequestOptions,
);
res.status(200).json(pullRequests);
});
router.get('/all-teams', async (_req, res) => {
const allTeams = Object.values(await azureDevOpsApi.getAllTeams());
res.status(200).json(allTeams);
});
router.use(errorHandler());
return router;
}
@@ -0,0 +1,264 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CreatedBy,
DashboardPullRequest,
Policy,
PolicyEvaluationStatus,
PolicyType,
PolicyTypeId,
PullRequestVoteStatus,
Repository,
Reviewer,
} from '@backstage/plugin-azure-devops-common';
import {
GitPullRequest,
GitRepository,
IdentityRefWithVote,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
export function convertDashboardPullRequest(
pullRequest: GitPullRequest,
baseUrl: string,
policies: Policy[] | undefined,
): DashboardPullRequest {
return {
pullRequestId: pullRequest.pullRequestId,
title: pullRequest.title,
description: pullRequest.description,
repository: convertRepository(pullRequest.repository),
createdBy: convertCreatedBy(pullRequest.createdBy),
hasAutoComplete: hasAutoComplete(pullRequest),
policies,
reviewers: convertReviewers(pullRequest.reviewers),
creationDate: pullRequest.creationDate?.toISOString(),
status: pullRequest.status,
isDraft: pullRequest.isDraft,
link: getPullRequestLink(baseUrl, pullRequest),
};
}
export function getPullRequestLink(
baseUrl: string,
pullRequest: GitPullRequest,
): string | undefined {
const projectName = pullRequest.repository?.project?.name;
const repoName = pullRequest.repository?.name;
const pullRequestId = pullRequest.pullRequestId;
if (!projectName || !repoName || !pullRequestId) {
return undefined;
}
const encodedProjectName = encodeURIComponent(projectName);
const encodedRepoName = encodeURIComponent(repoName);
return `${baseUrl}/${encodedProjectName}/_git/${encodedRepoName}/pullrequest/${pullRequestId}`;
}
/**
* Tries to get the avatar from the new property if not then falls-back to deprecated `imageUrl`.
* https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-requests-by-project?view=azure-devops-rest-6.0#identityref
*/
export function getAvatarUrl(identity: IdentityRef): string | undefined {
return identity._links?.avatar?.href ?? identity.imageUrl;
}
export function getArtifactId(
projectId: string,
pullRequestId: number,
): string {
return `vstfs:///CodeReview/CodeReviewId/${projectId}/${pullRequestId}`;
}
export function convertPolicy(
policyEvaluationRecord: PolicyEvaluationRecord,
): Policy | undefined {
const policyConfig = policyEvaluationRecord.configuration;
const policyStatus = policyEvaluationRecord.status;
if (!policyConfig) {
return undefined;
}
if (
!(
policyConfig.isEnabled &&
!policyConfig.isDeleted &&
(policyConfig.isBlocking ||
policyConfig.type?.id === PolicyType.Status) && // Optional "Status" policies are actually required for automatic completion.
policyStatus !== PolicyEvaluationStatus.Approved
)
) {
return undefined;
}
const policyTypeId = policyConfig.type?.id;
if (!policyTypeId) {
return undefined;
}
const policyType: PolicyType | undefined = (
{
[PolicyTypeId.Build]: PolicyType.Build,
[PolicyTypeId.Status]: PolicyType.Status,
[PolicyTypeId.MinimumReviewers]: PolicyType.MinimumReviewers,
[PolicyTypeId.Comments]: PolicyType.Comments,
[PolicyTypeId.RequiredReviewers]: PolicyType.RequiredReviewers,
[PolicyTypeId.MergeStrategy]: PolicyType.MergeStrategy,
} as Record<string, PolicyType | undefined>
)[policyTypeId];
if (!policyType) {
return undefined;
}
const policyConfigSettings = policyConfig.settings;
let policyText = policyConfig.type?.displayName;
let policyLink: string | undefined;
switch (policyType) {
case PolicyType.Build: {
const buildDisplayName = policyConfigSettings.displayName;
if (buildDisplayName) {
policyText += `: ${buildDisplayName}`;
}
const buildId = policyEvaluationRecord.context?.buildId;
const policyConfigUrl = policyConfig.url;
if (buildId && policyConfigUrl) {
policyLink = policyConfigUrl.replace(
`_apis/policy/configurations/${policyConfig.id}`,
`_build/results?buildId=${buildId}`,
);
}
if (!policyStatus) {
break;
}
const buildExpired = Boolean(policyConfigSettings.isExpired);
const buildPolicyStatus =
(
{
[PolicyEvaluationStatus.Queued]: buildExpired
? 'expired'
: 'queued',
[PolicyEvaluationStatus.Rejected]: 'failed',
} as Record<PolicyEvaluationStatus, string | undefined>
)[policyStatus] ?? PolicyEvaluationStatus[policyStatus].toLowerCase();
policyText += ` (${buildPolicyStatus})`;
break;
}
case PolicyType.Status: {
const statusGenre = policyConfigSettings.statusGenre;
const statusName = policyConfigSettings.statusGenre;
if (statusName) {
policyText += `: ${statusGenre}/${statusName}`;
}
break;
}
case PolicyType.MinimumReviewers: {
const minimumApproverCount = policyConfigSettings.minimumApproverCount;
policyText += ` (${minimumApproverCount})`;
break;
}
case PolicyType.Comments:
break;
case PolicyType.RequiredReviewers:
break;
case PolicyType.MergeStrategy:
default:
return undefined;
}
return {
id: policyConfig.id,
type: policyType,
status: policyStatus,
text: policyText,
link: policyLink,
};
}
function convertReviewer(
identityRef?: IdentityRefWithVote,
): Reviewer | undefined {
if (!identityRef) {
return undefined;
}
return {
id: identityRef.id,
displayName: identityRef.displayName,
imageUrl: getAvatarUrl(identityRef),
isRequired: identityRef.isRequired,
isContainer: identityRef.isContainer,
voteStatus: (identityRef.vote ?? 0) as PullRequestVoteStatus,
};
}
function convertReviewers(
identityRefs?: IdentityRefWithVote[],
): Reviewer[] | undefined {
if (!identityRefs) {
return undefined;
}
return identityRefs
.map(convertReviewer)
.filter((reviewer): reviewer is Reviewer => Boolean(reviewer));
}
function convertRepository(repository?: GitRepository): Repository | undefined {
if (!repository) {
return undefined;
}
return {
id: repository.id,
name: repository.name,
url: repository.url,
};
}
function convertCreatedBy(identityRef?: IdentityRef): CreatedBy | undefined {
if (!identityRef) {
return undefined;
}
return {
id: identityRef.id,
displayName: identityRef.displayName,
uniqueName: identityRef.uniqueName,
imageUrl: getAvatarUrl(identityRef),
};
}
function hasAutoComplete(pullRequest: GitPullRequest): boolean {
return pullRequest.isDraft !== true && !!pullRequest.completionOptions;
}
@@ -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 * from './azure-devops-utils';