feat: add readme card plugin on azure devops

Signed-off-by: Alisson Fabiano <afabiano@eshopworld.com>
This commit is contained in:
Alisson Fabiano
2022-08-11 16:07:10 +01:00
parent af1e11b744
commit bd34a1e7d4
10 changed files with 333 additions and 2 deletions
@@ -42,6 +42,7 @@ import {
convertDashboardPullRequest,
convertPolicy,
getArtifactId,
replaceReadme,
} from '../utils';
import { TeamMember as AdoTeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
@@ -52,11 +53,13 @@ import {
TeamProjectReference,
WebApiTeam,
} from 'azure-devops-node-api/interfaces/CoreInterfaces';
import { UrlReader } from '@backstage/backend-common';
export class AzureDevOpsApi {
public constructor(
private readonly logger: Logger,
private readonly webApi: WebApi,
private readonly urlReader: UrlReader,
) {}
public async getProjects(): Promise<Project[]> {
@@ -393,6 +396,24 @@ export class AzureDevOpsApi {
return buildRuns;
}
public async getReadme(
host: string,
organization: string,
projectName: string,
repoName: string,
): Promise<string> {
const getContentFile = async (
path: string,
encoding?: BufferEncoding,
): Promise<string> => {
const url = `https://${host}/${organization}/${projectName}/_git/${repoName}?path=${path}`;
const response = await this.urlReader.read(url);
return Buffer.from(response).toString(encoding);
};
const readmeContent = await getContentFile('README.md');
return await replaceReadme(readmeContent, getContentFile);
}
}
export function mappedRepoBuild(build: Build): RepoBuild {
@@ -53,6 +53,7 @@ describe('createRouter', () => {
getBuildRuns: jest.fn(),
getAllTeams: jest.fn(),
getTeamMembers: jest.fn(),
getReadme: jest.fn(),
} as any;
const router = await createRouter({
azureDevOpsApi,
@@ -455,4 +456,61 @@ describe('createRouter', () => {
expect(response.status).toEqual(200);
});
});
describe('GET /readme/:projectName/:repoName', () => {
it('fetches readme file', async () => {
const readmeMock = getReadmeMock();
azureDevOpsApi.getReadme.mockResolvedValueOnce(readmeMock);
const response = await request(app).get(
'/readme/myProject/myRepo?path=README.md',
);
expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith(
'host.com',
'myOrg',
'myProject',
'myRepo',
);
expect(response.status).toEqual(200);
expect(response.body).toEqual({
content: readmeMock,
url: `https://host.com/myOrg/myProject/_git/myRepo?path=README.md`,
});
});
});
});
function getReadmeMock() {
return `
# Introduction
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
# Getting Started
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
1. Installation process
2. Software dependencies
3. Latest releases
4. API references
# Build and Test
TODO: Describe and show how to build your code and run the tests.
# Contribute
TODO: Explain how other users and developers can contribute to make your code better.
If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:
- [ASP.NET Core](https://github.com/aspnet/Home)
- [Visual Studio Code](https://github.com/Microsoft/vscode)
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
- ![Imagem 1](./images/image1.jpg)
- ![Imagem 2](./images/image2.png)
- ![Imagem 3](./images/image3.jpg)
- ![Imagem 4](./images/image4.webp)
- ![Imagem 5](./images/image5.png)
- ![Imagem 6](/images/image6.png)
- ![Imagem 7](/images/image-7.jpg)
- ![Imagem 8](./images/image-8.gif)
- ![Imagem 9](/images/image9.png)
`;
}
@@ -26,7 +26,7 @@ import { Config } from '@backstage/config';
import { Logger } from 'winston';
import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider';
import Router from 'express-promise-router';
import { errorHandler } from '@backstage/backend-common';
import { errorHandler, UrlReaders } from '@backstage/backend-common';
import express from 'express';
const DEFAULT_TOP = 10;
@@ -46,12 +46,13 @@ export async function createRouter(
const token = config.getString('token');
const host = config.getString('host');
const organization = config.getString('organization');
const reader = UrlReaders.default(options);
const authHandler = getPersonalAccessTokenHandler(token);
const webApi = new WebApi(`https://${host}/${organization}`, authHandler);
const azureDevOpsApi =
options.azureDevOpsApi || new AzureDevOpsApi(logger, webApi);
options.azureDevOpsApi || new AzureDevOpsApi(logger, webApi, reader);
const pullRequestsDashboardProvider =
await PullRequestsDashboardProvider.create(logger, azureDevOpsApi);
@@ -191,6 +192,20 @@ export async function createRouter(
res.status(200).json(teamIds);
});
router.get('/readme/:projectName/:repoName', async (req, res) => {
const { projectName, repoName } = req.params;
const content = await azureDevOpsApi.getReadme(
host,
organization,
projectName,
repoName,
);
res.status(200).json({
content,
url: `https://${host}/${organization}/${projectName}/_git/${repoName}?path=README.md`,
});
});
router.use(errorHandler());
return router;
}
@@ -33,6 +33,7 @@ import {
import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import PathUtils from 'path';
export function convertDashboardPullRequest(
pullRequest: GitPullRequest,
@@ -205,6 +206,29 @@ export function convertPolicy(
};
}
export async function replaceReadme(
content: string,
getContentFile: (path: string, encoding?: BufferEncoding) => Promise<string>,
) {
const regExp =
/\[([^\[\]]*)\]\((?!https?:\/\/)(.*?)(\.png|\.jpg|\.jpeg|\.gif|\.webp)(.*)\)/gim;
const filesPath = content.match(regExp) || [];
if (filesPath.length === 0) return content;
let replacedContent = content;
for (const filePath of filesPath) {
const { label, path, ext } = getPartsFromFilePath(filePath);
const mime = getMimeByExtension(ext);
const base64 = await getContentFile(path, 'base64');
replacedContent = replacedContent.replace(
filePath,
`[${label}](data:${mime};base64,${base64})`,
);
}
return replacedContent;
}
function convertReviewer(
identityRef?: IdentityRefWithVote,
): Reviewer | undefined {
@@ -263,3 +287,29 @@ function convertCreatedBy(identityRef?: IdentityRef): CreatedBy | undefined {
function hasAutoComplete(pullRequest: GitPullRequest): boolean {
return pullRequest.isDraft !== true && !!pullRequest.completionOptions;
}
function getPartsFromFilePath(content: string): {
label: string;
path: string;
ext: string;
} {
const regExp = /\[(.*?)\]\(([^)]+)\)/;
const [_, label, path] = regExp.exec(content) || [];
return {
label,
path: path.startsWith('.') ? path.substring(1, path.length) : path,
ext: PathUtils.extname(path),
};
}
function getMimeByExtension(ext: string): string {
return (
{
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
}[ext] || ''
);
}