Merge pull request #22791 from DavidRobertsOrbis/azd-readme-location
azure-devops plugin: Ability to fetch the README file from a different AZD path
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
---
|
||||
'@backstage/plugin-azure-devops-backend': minor
|
||||
'@backstage/plugin-azure-devops-common': minor
|
||||
'@backstage/plugin-azure-devops': minor
|
||||
---
|
||||
|
||||
Ability to fetch the README file from a different Azure DevOps path.
|
||||
|
||||
Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path`
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
dev.azure.com/readme-path: /my-path/README.md
|
||||
```
|
||||
@@ -124,6 +124,7 @@ export class AzureDevOpsApi {
|
||||
org: string,
|
||||
project: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
): Promise<{
|
||||
url: string;
|
||||
content: string;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-azure-devops-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
|
||||
@@ -527,11 +527,12 @@ export class AzureDevOpsApi {
|
||||
org: string,
|
||||
project: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
): Promise<{
|
||||
url: string;
|
||||
content: string;
|
||||
}> {
|
||||
const url = buildEncodedUrl(host, org, project, repo, 'README.md');
|
||||
const url = buildEncodedUrl(host, org, project, repo, path);
|
||||
const response = await this.urlReader.readUrl(url);
|
||||
const buffer = await response.buffer();
|
||||
const content = await replaceReadme(
|
||||
|
||||
@@ -482,7 +482,7 @@ describe('createRouter', () => {
|
||||
});
|
||||
|
||||
describe('GET /readme/:projectName/:repoName', () => {
|
||||
it('fetches readme file', async () => {
|
||||
it('fetches default default readme file', async () => {
|
||||
const content = getReadmeMock();
|
||||
const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README.md`;
|
||||
|
||||
@@ -491,14 +491,13 @@ describe('createRouter', () => {
|
||||
url,
|
||||
});
|
||||
|
||||
const response = await request(app).get(
|
||||
'/readme/myProject/myRepo?path=README.md',
|
||||
);
|
||||
const response = await request(app).get('/readme/myProject/myRepo');
|
||||
expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith(
|
||||
'host.com',
|
||||
'myOrg',
|
||||
'myProject',
|
||||
'myRepo',
|
||||
'README.md',
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
@@ -507,6 +506,80 @@ describe('createRouter', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /readme/:projectName/:repoName with readme filename', () => {
|
||||
it('fetches specified readme file', async () => {
|
||||
const content = getReadmeMock();
|
||||
const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README_NOT_DEFAULT.md`;
|
||||
|
||||
azureDevOpsApi.getReadme.mockResolvedValueOnce({
|
||||
content,
|
||||
url,
|
||||
});
|
||||
|
||||
const response = await request(app).get(
|
||||
'/readme/myProject/myRepo?path=README_NOT_DEFAULT.md',
|
||||
);
|
||||
expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith(
|
||||
'host.com',
|
||||
'myOrg',
|
||||
'myProject',
|
||||
'myRepo',
|
||||
'README_NOT_DEFAULT.md',
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
content,
|
||||
url,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /readme/:projectName/:repoName with readme path', () => {
|
||||
it('fetches specified readme file from subfolder', async () => {
|
||||
const content = getReadmeMock();
|
||||
const url = `https://host.com/myOrg/myProject/_git/myRepo?path=/my-path/README.md`;
|
||||
|
||||
azureDevOpsApi.getReadme.mockResolvedValueOnce({
|
||||
content,
|
||||
url,
|
||||
});
|
||||
|
||||
const response = await request(app).get(
|
||||
'/readme/myProject/myRepo?path=/my-path/README.md',
|
||||
);
|
||||
expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith(
|
||||
'host.com',
|
||||
'myOrg',
|
||||
'myProject',
|
||||
'myRepo',
|
||||
'/my-path/README.md',
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
content,
|
||||
url,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /readme/:projectName/:repoName with a bad readme path (multiple values)', () => {
|
||||
it('throws InputError', async () => {
|
||||
const response = await request(app).get(
|
||||
'/readme/myProject/myRepo?path=1&path=2',
|
||||
);
|
||||
expect(azureDevOpsApi.getReadme).not.toHaveBeenCalled();
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /readme/:projectName/:repoName with a bad readme path (empty string)', () => {
|
||||
it('throws InputError', async () => {
|
||||
const response = await request(app).get('/readme/myProject/myRepo?path=');
|
||||
expect(azureDevOpsApi.getReadme).not.toHaveBeenCalled();
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getReadmeMock() {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Logger } from 'winston';
|
||||
import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider';
|
||||
import Router from 'express-promise-router';
|
||||
import { errorHandler, UrlReader } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import express from 'express';
|
||||
|
||||
const DEFAULT_TOP = 10;
|
||||
@@ -216,12 +217,28 @@ export async function createRouter(
|
||||
req.query.host?.toString() ?? config.getString('azureDevOps.host');
|
||||
const org =
|
||||
req.query.org?.toString() ?? config.getString('azureDevOps.organization');
|
||||
let path = req.query.path;
|
||||
|
||||
if (path === undefined) {
|
||||
// if the annotation is missing, default to the previous behaviour (look for README.md in the root of the repo)
|
||||
path = 'README.md';
|
||||
}
|
||||
|
||||
if (typeof path !== 'string') {
|
||||
throw new InputError('Invalid path param');
|
||||
}
|
||||
|
||||
if (path === '') {
|
||||
throw new InputError('If present, the path param should not be empty');
|
||||
}
|
||||
|
||||
const { projectName, repoName } = req.params;
|
||||
const readme = await azureDevOpsApi.getReadme(
|
||||
host,
|
||||
org,
|
||||
projectName,
|
||||
repoName,
|
||||
path,
|
||||
);
|
||||
res.status(200).json(readme);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,9 @@ export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org';
|
||||
// @public (undocumented)
|
||||
export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project';
|
||||
|
||||
// @public (undocumented)
|
||||
export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path';
|
||||
|
||||
// @public (undocumented)
|
||||
export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo';
|
||||
|
||||
@@ -228,6 +231,8 @@ export interface ReadmeConfig {
|
||||
// (undocumented)
|
||||
org?: string;
|
||||
// (undocumented)
|
||||
path?: string;
|
||||
// (undocumented)
|
||||
project: string;
|
||||
// (undocumented)
|
||||
repo: string;
|
||||
|
||||
@@ -22,6 +22,8 @@ export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org';
|
||||
/** @public */
|
||||
export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project';
|
||||
/** @public */
|
||||
export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path';
|
||||
/** @public */
|
||||
export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo';
|
||||
/** @public */
|
||||
export const AZURE_DEVOPS_DEFAULT_TOP: number = 10;
|
||||
|
||||
@@ -212,6 +212,7 @@ export interface ReadmeConfig {
|
||||
repo: string;
|
||||
host?: string;
|
||||
org?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -63,13 +63,19 @@ spec:
|
||||
|
||||
#### Mono repos
|
||||
|
||||
If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity.
|
||||
If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity, like this:
|
||||
|
||||
```yaml
|
||||
dev.azure.com/project-repo: <my-project>/<my-repo>
|
||||
dev.azure.com/build-definition: <build-definition-name>
|
||||
```
|
||||
|
||||
Then to display the `README` file that belongs to each entity you would do this:
|
||||
|
||||
```yaml
|
||||
dev.azure.com/readme-path: /<path-to>/<my-readme-file>.md
|
||||
```
|
||||
|
||||
#### Pipeline in different project to repo
|
||||
|
||||
If your pipeline is in a different project to the source code, you will need to specify this in the project annotation.
|
||||
|
||||
@@ -189,6 +189,9 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
|
||||
if (opts.org) {
|
||||
queryString.append('org', opts.org);
|
||||
}
|
||||
if (opts.path) {
|
||||
queryString.append('path', opts.path);
|
||||
}
|
||||
return await this.get(
|
||||
`readme/${encodeURIComponent(opts.project)}/${encodeURIComponent(
|
||||
opts.repo,
|
||||
|
||||
@@ -30,8 +30,15 @@ export function useReadme(entity: Entity): {
|
||||
const api = useApi(azureDevOpsApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
const { project, repo, host, org } = getAnnotationValuesFromEntity(entity);
|
||||
return api.getReadme({ project, repo: repo as string, host, org });
|
||||
const { project, repo, host, org, readmePath } =
|
||||
getAnnotationValuesFromEntity(entity);
|
||||
return api.getReadme({
|
||||
project,
|
||||
repo: repo as string,
|
||||
host,
|
||||
org,
|
||||
path: readmePath,
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('getAnnotationValuesFromEntity', () => {
|
||||
project: 'projectName',
|
||||
repo: 'repoName',
|
||||
definition: undefined,
|
||||
readmePath: undefined,
|
||||
host: undefined,
|
||||
org: undefined,
|
||||
});
|
||||
@@ -149,6 +150,7 @@ describe('getAnnotationValuesFromEntity', () => {
|
||||
project: 'projectName',
|
||||
repo: undefined,
|
||||
definition: 'buildDefinitionName',
|
||||
readmePath: undefined,
|
||||
host: undefined,
|
||||
org: undefined,
|
||||
});
|
||||
@@ -220,6 +222,7 @@ describe('getAnnotationValuesFromEntity', () => {
|
||||
project: 'projectName',
|
||||
repo: 'repoName',
|
||||
definition: undefined,
|
||||
readmePath: undefined,
|
||||
host: 'hostName',
|
||||
org: 'organizationName',
|
||||
});
|
||||
@@ -246,6 +249,7 @@ describe('getAnnotationValuesFromEntity', () => {
|
||||
project: 'projectName',
|
||||
repo: undefined,
|
||||
definition: 'buildDefinitionName',
|
||||
readmePath: undefined,
|
||||
host: 'hostName',
|
||||
org: 'organizationName',
|
||||
});
|
||||
@@ -344,6 +348,7 @@ describe('getAnnotationValuesFromEntity', () => {
|
||||
project: 'projectName',
|
||||
repo: 'repoName',
|
||||
definition: undefined,
|
||||
readmePath: undefined,
|
||||
host: 'company.com/tfs',
|
||||
org: 'organizationName',
|
||||
});
|
||||
@@ -417,6 +422,7 @@ describe('getAnnotationValuesFromEntity', () => {
|
||||
project: 'projectName',
|
||||
repo: 'repoName',
|
||||
definition: 'buildDefinitionName',
|
||||
readmePath: undefined,
|
||||
host: undefined,
|
||||
org: undefined,
|
||||
});
|
||||
@@ -443,6 +449,87 @@ describe('getAnnotationValuesFromEntity', () => {
|
||||
project: 'projectName',
|
||||
repo: undefined,
|
||||
definition: 'buildDefinitionName',
|
||||
readmePath: undefined,
|
||||
host: undefined,
|
||||
org: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('definition, project and readme', () => {
|
||||
it('returns with the readme path', () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'project-repo',
|
||||
annotations: {
|
||||
'dev.azure.com/project': 'projectName',
|
||||
'dev.azure.com/build-definition': 'buildDefinitionName',
|
||||
'dev.azure.com/readme-path': 'readme/path.md',
|
||||
},
|
||||
},
|
||||
};
|
||||
const values = getAnnotationValuesFromEntity(entity);
|
||||
expect(values).toEqual({
|
||||
project: 'projectName',
|
||||
repo: undefined,
|
||||
definition: 'buildDefinitionName',
|
||||
readmePath: 'readme/path.md',
|
||||
host: undefined,
|
||||
org: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('definition, projectRepo and readme', () => {
|
||||
it('returns with the readme path', () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'project-repo',
|
||||
annotations: {
|
||||
'dev.azure.com/project-repo': 'projectName/repoName',
|
||||
'dev.azure.com/build-definition': 'buildDefinitionName',
|
||||
'dev.azure.com/readme-path': 'readme/path.md',
|
||||
},
|
||||
},
|
||||
};
|
||||
const values = getAnnotationValuesFromEntity(entity);
|
||||
expect(values).toEqual({
|
||||
project: 'projectName',
|
||||
repo: 'repoName',
|
||||
definition: 'buildDefinitionName',
|
||||
readmePath: 'readme/path.md',
|
||||
host: undefined,
|
||||
org: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectRepo and readme', () => {
|
||||
it('returns with the readme path', () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'project-repo',
|
||||
annotations: {
|
||||
'dev.azure.com/project-repo': 'projectName/repoName',
|
||||
'dev.azure.com/readme-path': 'readme/path.md',
|
||||
},
|
||||
},
|
||||
};
|
||||
const values = getAnnotationValuesFromEntity(entity);
|
||||
expect(values).toEqual({
|
||||
project: 'projectName',
|
||||
repo: 'repoName',
|
||||
definition: undefined,
|
||||
readmePath: 'readme/path.md',
|
||||
host: undefined,
|
||||
org: undefined,
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
AZURE_DEVOPS_PROJECT_ANNOTATION,
|
||||
AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION,
|
||||
AZURE_DEVOPS_README_ANNOTATION,
|
||||
AZURE_DEVOPS_REPO_ANNOTATION,
|
||||
AZURE_DEVOPS_HOST_ORG_ANNOTATION,
|
||||
} from '@backstage/plugin-azure-devops-common';
|
||||
@@ -28,6 +29,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): {
|
||||
definition?: string;
|
||||
host?: string;
|
||||
org?: string;
|
||||
readmePath?: string;
|
||||
} {
|
||||
const hostOrg = getHostOrg(entity.metadata.annotations);
|
||||
const projectRepo = getProjectRepo(entity.metadata.annotations);
|
||||
@@ -35,12 +37,15 @@ export function getAnnotationValuesFromEntity(entity: Entity): {
|
||||
entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION];
|
||||
const definition =
|
||||
entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION];
|
||||
const readmePath =
|
||||
entity.metadata.annotations?.[AZURE_DEVOPS_README_ANNOTATION];
|
||||
|
||||
if (definition) {
|
||||
if (project) {
|
||||
return {
|
||||
project,
|
||||
definition,
|
||||
readmePath: readmePath,
|
||||
...hostOrg,
|
||||
};
|
||||
}
|
||||
@@ -49,6 +54,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): {
|
||||
project: projectRepo.project,
|
||||
repo: projectRepo.repo,
|
||||
definition,
|
||||
readmePath: readmePath,
|
||||
...hostOrg,
|
||||
};
|
||||
}
|
||||
@@ -60,6 +66,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): {
|
||||
return {
|
||||
project: projectRepo.project,
|
||||
repo: projectRepo.repo,
|
||||
readmePath: readmePath,
|
||||
...hostOrg,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4933,6 +4933,7 @@ __metadata:
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/plugin-azure-devops-common": "workspace:^"
|
||||
"@backstage/plugin-catalog-common": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user