Remove the short-form annotation and only support the full dir:. option
Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
@@ -3,22 +3,21 @@
|
||||
'@backstage/plugin-techdocs-backend': minor
|
||||
---
|
||||
|
||||
Introduce the annotation `backstage.io/techdocs-ref: <relative-target>` as an alias for `backstage.io/techdocs-ref: dir:<relative-target>`.
|
||||
This annotation works with both the basic and the recommended flow, however, it will be most useful with the basic approach.
|
||||
Improve the annotation `backstage.io/techdocs-ref: dir:<relative-target>` that links to a path that is relative to the source of the annotated entity.
|
||||
This annotation works with the basic and the recommended flow, however, it will be most useful with the basic approach.
|
||||
|
||||
This change remove the deprecation of the `dir` reference and provides first-class support for it.
|
||||
In addition, this change removes the support of the deprecated `github`, `gitlab`, and `azure/api` locations from the `dir` reference preparer.
|
||||
|
||||
#### Example Usage
|
||||
|
||||
The new annotation is convenient if the documentation is stored in the same location, i.e. the same git repository, as the `catalog-info.yaml`.
|
||||
The annotation is convenient if the documentation is stored in the same location, i.e. the same git repository, as the `catalog-info.yaml`.
|
||||
While it is still supported to add full URLs such as `backstage.io/techdocs-ref: url:https://...` for custom setups, documentation is mostly stored in the same repository as the entity definition.
|
||||
By automatically resolving the target relative to the registration location of the entity, the configuration overhead for this default setup is minimized.
|
||||
Since it leverages the `@backstage/integrations` package for the URL resolution, this is compatible with every supported source.
|
||||
|
||||
Consider the following examples:
|
||||
|
||||
> Note that the short version `<target>` is only an alias for the still supported `dir:<target>`.
|
||||
|
||||
1. "I have a repository with a single `catalog-info.yaml` and a TechDocs page in the root folder!"
|
||||
|
||||
```
|
||||
@@ -29,7 +28,7 @@ https://github.com/backstage/example/tree/main/
|
||||
| > metadata:
|
||||
| > name: example
|
||||
| > annotations:
|
||||
| > backstage.io/techdocs-ref: . # -> same folder
|
||||
| > backstage.io/techdocs-ref: dir:. # -> same folder
|
||||
| > spec: {}
|
||||
|- docs/
|
||||
|- mkdocs.yml
|
||||
@@ -45,7 +44,7 @@ https://bitbucket.org/my-owner/my-project/src/master/
|
||||
| > metadata:
|
||||
| > name: example
|
||||
| > annotations:
|
||||
| > backstage.io/techdocs-ref: ./some-folder # -> subfolder
|
||||
| > backstage.io/techdocs-ref: dir:./some-folder # -> subfolder
|
||||
| > spec: {}
|
||||
|- some-folder/
|
||||
|- docs/
|
||||
@@ -63,7 +62,7 @@ https://dev.azure.com/organization/project/_git/repository
|
||||
| > metadata:
|
||||
| > name: my-1st-module
|
||||
| > annotations:
|
||||
| > backstage.io/techdocs-ref: . # -> same folder
|
||||
| > backstage.io/techdocs-ref: dir:. # -> same folder
|
||||
| > spec: {}
|
||||
|- docs/
|
||||
|- mkdocs.yml
|
||||
@@ -74,7 +73,7 @@ https://dev.azure.com/organization/project/_git/repository
|
||||
| > metadata:
|
||||
| > name: my-2nd-module
|
||||
| > annotations:
|
||||
| > backstage.io/techdocs-ref: . # -> same folder
|
||||
| > backstage.io/techdocs-ref: dir:. # -> same folder
|
||||
| > spec: {}
|
||||
|- docs/
|
||||
|- mkdocs.yml
|
||||
|
||||
@@ -137,18 +137,6 @@ export const getDefaultBranch: (
|
||||
config: Config,
|
||||
) => Promise<string>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getDirLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const getDirLocation: (
|
||||
entity: Entity,
|
||||
) =>
|
||||
| {
|
||||
type: 'dir';
|
||||
target: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getDocFilesFromRepository" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -382,6 +370,7 @@ export type TechDocsMetadata = {
|
||||
// @public
|
||||
export const transformDirLocation: (
|
||||
entity: Entity,
|
||||
dirAnnotation: ParsedLocationAnnotation,
|
||||
scmIntegrations: ScmIntegrationRegistry,
|
||||
) => {
|
||||
type: 'dir' | 'url';
|
||||
|
||||
@@ -26,7 +26,6 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import {
|
||||
getDirLocation,
|
||||
getDocFilesFromRepository,
|
||||
getLocationForEntity,
|
||||
parseReferenceAnnotation,
|
||||
@@ -126,76 +125,11 @@ describe('parseReferenceAnnotation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDirLocation', () => {
|
||||
it.each`
|
||||
techdocsRef | responseTarget
|
||||
${undefined} | ${undefined}
|
||||
${16} | ${undefined}
|
||||
${'.'} | ${'.'}
|
||||
${'dir:.'} | ${'.'}
|
||||
${'./relative'} | ${'./relative'}
|
||||
${'dir:./relative'} | ${'./relative'}
|
||||
${'dir:https://github.com...'} | ${'https://github.com...'}
|
||||
${'url:https://github.com...'} | ${undefined}
|
||||
`(
|
||||
'should handle "backstage.io/techdocs-ref: $techdocsRef" correctly',
|
||||
({ techdocsRef, responseTarget }) => {
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': techdocsRef,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getDirLocation(entity);
|
||||
|
||||
expect(result).toEqual(
|
||||
responseTarget && { type: 'dir', target: responseTarget },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('Reject https urls and hint to the url: location type', async () => {
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': 'https://github.com/backstage/backstage',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => getDirLocation(entity)).toThrow(
|
||||
/please prefix it with 'url:'/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformDirLocation', () => {
|
||||
it('should reject missing annotation', () => {
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => transformDirLocation(entity, scmIntegrations)).toThrow(
|
||||
/No techdocs location annotation provided in entity: component:default\/test/,
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
techdocsRef | target
|
||||
${'.'} | ${'https://my-url/folder/'}
|
||||
${'./sub-folder'} | ${'https://my-url/folder/sub-folder'}
|
||||
techdocsRef | target
|
||||
${'dir:.'} | ${'https://my-url/folder/'}
|
||||
${'dir:./sub-folder'} | ${'https://my-url/folder/sub-folder'}
|
||||
`(
|
||||
'should transform "$techdocsRef" for url type locations',
|
||||
({ techdocsRef, target }) => {
|
||||
@@ -215,16 +149,20 @@ describe('transformDirLocation', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformDirLocation(entity, scmIntegrations);
|
||||
const result = transformDirLocation(
|
||||
entity,
|
||||
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
|
||||
scmIntegrations,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ type: 'url', target });
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
techdocsRef | target
|
||||
${'.'} | ${path.join(rootDir, 'working-copy')}
|
||||
${'./sub-folder'} | ${path.join(rootDir, 'working-copy', 'sub-folder')}
|
||||
techdocsRef | target
|
||||
${'dir:.'} | ${path.join(rootDir, 'working-copy')}
|
||||
${'dir:./sub-folder'} | ${path.join(rootDir, 'working-copy', 'sub-folder')}
|
||||
`(
|
||||
'should transform "$techdocsRef" for file type locations',
|
||||
({ techdocsRef, target }) => {
|
||||
@@ -244,7 +182,11 @@ describe('transformDirLocation', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformDirLocation(entity, scmIntegrations);
|
||||
const result = transformDirLocation(
|
||||
entity,
|
||||
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
|
||||
scmIntegrations,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ type: 'dir', target });
|
||||
},
|
||||
@@ -262,12 +204,18 @@ describe('transformDirLocation', () => {
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': '..',
|
||||
'backstage.io/techdocs-ref': 'dir:..',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => transformDirLocation(entity, scmIntegrations)).toThrow(
|
||||
expect(() =>
|
||||
transformDirLocation(
|
||||
entity,
|
||||
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
|
||||
scmIntegrations,
|
||||
),
|
||||
).toThrow(
|
||||
/Relative path is not allowed to refer to a directory outside its parent/,
|
||||
);
|
||||
});
|
||||
@@ -284,43 +232,22 @@ describe('transformDirLocation', () => {
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': '.',
|
||||
'backstage.io/techdocs-ref': 'dir:.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => transformDirLocation(entity, scmIntegrations)).toThrow(
|
||||
/Unable to resolve location type other/,
|
||||
);
|
||||
expect(() =>
|
||||
transformDirLocation(
|
||||
entity,
|
||||
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
|
||||
scmIntegrations,
|
||||
),
|
||||
).toThrow(/Unable to resolve location type other/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocationForEntity', () => {
|
||||
it('should handle implicit dir locations', () => {
|
||||
(getEntitySourceLocation as jest.Mock).mockReturnValue({
|
||||
type: 'url',
|
||||
target: 'https://my-url/folder/',
|
||||
});
|
||||
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': '.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const parsedLocationAnnotation = getLocationForEntity(
|
||||
entity,
|
||||
scmIntegrations,
|
||||
);
|
||||
expect(parsedLocationAnnotation.type).toBe('url');
|
||||
expect(parsedLocationAnnotation.target).toBe('https://my-url/folder/');
|
||||
});
|
||||
|
||||
it('should handle dir locations', () => {
|
||||
(getEntitySourceLocation as jest.Mock).mockReturnValue({
|
||||
type: 'url',
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Entity,
|
||||
getEntitySourceLocation,
|
||||
parseLocationReference,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
@@ -60,44 +59,6 @@ export const parseReferenceAnnotation = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the entity provides a `backstage.io/techdocs-ref` annotation of type `dir`
|
||||
* and return the annotation. It accepts the two variants `<target>` and `dir:<target>`.
|
||||
*
|
||||
* @param entity - the entity to check
|
||||
*/
|
||||
export const getDirLocation = (
|
||||
entity: Entity,
|
||||
): { type: 'dir'; target: string } | undefined => {
|
||||
const annotation = entity.metadata.annotations?.['backstage.io/techdocs-ref'];
|
||||
|
||||
if (!annotation) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof annotation !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// if the string doesn't contain `:`, interpret it as the target of a dir type
|
||||
if (!annotation.includes(':')) {
|
||||
return { type: 'dir', target: annotation };
|
||||
}
|
||||
|
||||
// note that `backstage.io/techdocs-ref: https://...` is invalid and will throw
|
||||
const reference = parseLocationReference(annotation);
|
||||
|
||||
if (reference.type === 'dir') {
|
||||
return {
|
||||
type: 'dir',
|
||||
target: reference.target,
|
||||
};
|
||||
}
|
||||
|
||||
// ignore any other types
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* TechDocs references of type `dir` are relative the source location of the entity.
|
||||
* This function transforms relative references to absolute ones, based on the
|
||||
@@ -107,30 +68,22 @@ export const getDirLocation = (
|
||||
* an absolute `dir` location.
|
||||
*
|
||||
* @param entity - the entity with annotations
|
||||
* @param scmIntegrations - access to the scmIntegrationt to do url transformations
|
||||
* @param dirAnnotation - the parsed techdocs-ref annotation of type 'dir'
|
||||
* @param scmIntegrations - access to the scmIntegration to do url transformations
|
||||
* @throws if the entity doesn't specify a `dir` location or is ingested from an unsupported location.
|
||||
* @returns the transformed location with an absolute target.
|
||||
*/
|
||||
export const transformDirLocation = (
|
||||
entity: Entity,
|
||||
dirAnnotation: ParsedLocationAnnotation,
|
||||
scmIntegrations: ScmIntegrationRegistry,
|
||||
): { type: 'dir' | 'url'; target: string } => {
|
||||
const dirLocation = getDirLocation(entity);
|
||||
|
||||
if (!dirLocation) {
|
||||
throw new InputError(
|
||||
`No techdocs location annotation provided in entity: ${stringifyEntityRef(
|
||||
entity,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const location = getEntitySourceLocation(entity);
|
||||
|
||||
switch (location.type) {
|
||||
case 'url': {
|
||||
const target = scmIntegrations.resolveUrl({
|
||||
url: dirLocation.target,
|
||||
url: dirAnnotation.target,
|
||||
base: location.target,
|
||||
});
|
||||
|
||||
@@ -144,7 +97,7 @@ export const transformDirLocation = (
|
||||
// only permit targets in the same folder as the target of the `file` location!
|
||||
const target = resolveSafeChildPath(
|
||||
path.dirname(location.target),
|
||||
dirLocation.target,
|
||||
dirAnnotation.target,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -162,24 +115,21 @@ export const getLocationForEntity = (
|
||||
entity: Entity,
|
||||
scmIntegration: ScmIntegrationRegistry,
|
||||
): ParsedLocationAnnotation => {
|
||||
// try to resolve relative references first
|
||||
if (getDirLocation(entity) !== undefined) {
|
||||
return transformDirLocation(entity, scmIntegration);
|
||||
}
|
||||
|
||||
const { type, target } = parseReferenceAnnotation(
|
||||
const annotation = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
switch (annotation.type) {
|
||||
case 'github':
|
||||
case 'gitlab':
|
||||
case 'azure/api':
|
||||
case 'url':
|
||||
return { type, target };
|
||||
return annotation;
|
||||
case 'dir':
|
||||
return transformDirLocation(entity, annotation, scmIntegration);
|
||||
default:
|
||||
throw new Error(`Invalid reference annotation ${type}`);
|
||||
throw new Error(`Invalid reference annotation ${annotation.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { Logger } from 'winston';
|
||||
import { transformDirLocation } from '../../helpers';
|
||||
import { parseReferenceAnnotation, transformDirLocation } from '../../helpers';
|
||||
import { PreparerBase, PreparerResponse } from './types';
|
||||
|
||||
export class DirectoryPreparer implements PreparerBase {
|
||||
@@ -39,18 +39,29 @@ export class DirectoryPreparer implements PreparerBase {
|
||||
entity: Entity,
|
||||
options?: { logger?: Logger; etag?: string },
|
||||
): Promise<PreparerResponse> {
|
||||
const { type, target } = transformDirLocation(entity, this.scmIntegrations);
|
||||
const annotation = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
const { type, target } = transformDirLocation(
|
||||
entity,
|
||||
annotation,
|
||||
this.scmIntegrations,
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case 'url': {
|
||||
options?.logger?.info(`Download documentation from ${target}`);
|
||||
options?.logger?.debug(`Reading files from ${target}`);
|
||||
// the target is an absolute url since it has already been transformed
|
||||
const response = await this.reader.readTree(target, {
|
||||
etag: options?.etag,
|
||||
});
|
||||
const preparedDir = await response.dir();
|
||||
|
||||
options?.logger?.debug(`Tree downloaded and stored at ${preparedDir}`);
|
||||
|
||||
return {
|
||||
preparedDir: await response.dir(),
|
||||
preparedDir,
|
||||
etag: response.etag,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
*/
|
||||
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Entity, parseLocationReference } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Logger } from 'winston';
|
||||
import { getDirLocation } from '../../helpers';
|
||||
import { parseReferenceAnnotation } from '../../helpers';
|
||||
import { CommonGitPreparer } from './commonGit';
|
||||
import { DirectoryPreparer } from './dir';
|
||||
import { PreparerBase, PreparerBuilder, RemoteProtocol } from './types';
|
||||
@@ -63,20 +62,10 @@ export class Preparers implements PreparerBuilder {
|
||||
}
|
||||
|
||||
get(entity: Entity): PreparerBase {
|
||||
const annotation =
|
||||
entity.metadata.annotations?.['backstage.io/techdocs-ref'];
|
||||
if (!annotation) {
|
||||
throw new InputError(
|
||||
`No location annotation provided in entity: ${entity.metadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
// the dir processor handles both `<target>` and `dir:<target>`
|
||||
if (getDirLocation(entity) !== undefined) {
|
||||
return this.preparerMap.get('dir')!;
|
||||
}
|
||||
|
||||
const { type } = parseLocationReference(annotation);
|
||||
const { type } = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
const preparer = this.preparerMap.get(type as RemoteProtocol);
|
||||
|
||||
if (!preparer) {
|
||||
|
||||
Reference in New Issue
Block a user