Merge pull request #6634 from backstage/mob/yaml-support

[TechDocs] Add support for mkdocs.yaml
This commit is contained in:
Eric Peterson
2021-07-28 16:18:05 +02:00
committed by GitHub
4 changed files with 85 additions and 31 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/techdocs-common': patch
---
TechDocs generator stage now supports `mkdocs.yaml` file, in addition to `.yml`
depending on whichever is present at the time of generation. (Assumes the
latest `spotify/techdocs` container, running mkdocs `v1.2.2` or greater).
@@ -23,6 +23,7 @@ import { RemoteProtocol } from '../prepare/types';
import {
addBuildTimestampMetadata,
getGeneratorKey,
getMkdocsYml,
getRepoUrlFromLocationAnnotation,
isValidRepoUrlForMkdocs,
patchMkdocsYmlPreBuild,
@@ -53,6 +54,9 @@ const mkdocsYmlWithValidDocDir = fs.readFileSync(
const mkdocsYmlWithInvalidDocDir = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir.yml'),
);
const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir2.yml'),
);
const mockLogger = getVoidLogger();
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
@@ -334,48 +338,68 @@ describe('helpers', () => {
});
});
describe('validateMkdocsYaml', () => {
beforeEach(() => {
mockFs({
'/mkdocs.yml': mkdocsYml,
'/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions,
'/mkdocs_valid_doc_dir.yml': mkdocsYmlWithValidDocDir,
'/mkdocs_invalid_doc_dir.yml': mkdocsYmlWithInvalidDocDir,
});
});
describe('getMkdocsYml', () => {
afterEach(() => {
mockFs.restore();
});
const inputDir = resolvePath(__filename, '../__fixtures__/');
it('returns expected contents when .yml file is present', async () => {
const key = path.join(inputDir, 'mkdocs.yml');
mockFs({ [key]: mkdocsYml });
const { path: mkdocsPath, content } = await getMkdocsYml(inputDir);
expect(mkdocsPath).toBe(key);
expect(content).toBe(mkdocsYml.toString());
});
it('returns expected contents when .yaml file is present', async () => {
const key = path.join(inputDir, 'mkdocs.yaml');
mockFs({ [key]: mkdocsYml });
const { path: mkdocsPath, content } = await getMkdocsYml(inputDir);
expect(mkdocsPath).toBe(key);
expect(content).toBe(mkdocsYml.toString());
});
it('throws when neither .yml nor .yaml file is present', async () => {
const invalidInputDir = resolvePath(__filename);
await expect(getMkdocsYml(invalidInputDir)).rejects.toThrowError(
/Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation/,
);
});
});
describe('validateMkdocsYaml', () => {
const inputDir = resolvePath(__filename, '../__fixtures__/');
it('should return true on when no docs_dir present', async () => {
await expect(
validateMkdocsYaml(inputDir, '/mkdocs.yml'),
validateMkdocsYaml(inputDir, mkdocsYml.toString()),
).resolves.toBeUndefined();
});
it('should return true on when a valid docs_dir is present', async () => {
await expect(
validateMkdocsYaml(inputDir, '/mkdocs_valid_doc_dir.yml'),
validateMkdocsYaml(inputDir, mkdocsYmlWithValidDocDir.toString()),
).resolves.toBeUndefined();
});
it('should return false on absolute doc_dir path', async () => {
await expect(
validateMkdocsYaml(inputDir, '/mkdocs_invalid_doc_dir.yml'),
validateMkdocsYaml(inputDir, mkdocsYmlWithInvalidDocDir.toString()),
).rejects.toThrow();
});
it('should return false on doc_dir path that traverses directory structure backwards', async () => {
await expect(
validateMkdocsYaml(inputDir, '/mkdocs_invalid_doc_dir2.yml'),
validateMkdocsYaml(inputDir, mkdocsYmlWithInvalidDocDir2.toString()),
).rejects.toThrow();
});
it('should validate files with custom yaml tags', async () => {
await expect(
validateMkdocsYaml(inputDir, '/mkdocs_with_extensions.yml'),
validateMkdocsYaml(inputDir, mkdocsYmlWithExtensions.toString()),
).resolves.toBeUndefined();
});
});
@@ -19,7 +19,7 @@ import { isChildPath } from '@backstage/backend-common';
import { spawn } from 'child_process';
import fs from 'fs-extra';
import yaml, { DEFAULT_SCHEMA, Type } from 'js-yaml';
import { resolve as resolvePath } from 'path';
import path, { resolve as resolvePath } from 'path';
import { PassThrough, Writable } from 'stream';
import { Logger } from 'winston';
import { ParsedLocationAnnotation } from '../../helpers';
@@ -155,26 +155,50 @@ const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([
}),
]);
/**
* Finds and loads the contents of either an mkdocs.yml or mkdocs.yaml file,
* depending on which is present (MkDocs supports both as of v1.2.2).
*
* @param {string} inputDir base dir to be searched for either an mkdocs.yml or
* mkdocs.yaml file.
*/
export const getMkdocsYml = async (
inputDir: string,
): Promise<{ path: string; content: string }> => {
let mkdocsYmlPath: string;
let mkdocsYmlFileString: string;
try {
mkdocsYmlPath = path.join(inputDir, 'mkdocs.yaml');
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
} catch {
try {
mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml');
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
} catch (error) {
throw new Error(
`Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation: ${error.message}`,
);
}
}
return {
path: mkdocsYmlPath,
content: mkdocsYmlFileString,
};
};
/**
* Validating mkdocs config file for incorrect/insecure values
* Throws on invalid configs
*
* @param {string} inputDir base dir to be used as a docs_dir path validity check
* @param {string} mkdocsYmlPath Absolute path to mkdocs.yml or equivalent of a docs site
* @param {string} mkdocsYmlFileString The string contents of the loaded
* mkdocs.yml or equivalent of a docs site
*/
export const validateMkdocsYaml = async (
inputDir: string,
mkdocsYmlPath: string,
mkdocsYmlFileString: string,
) => {
let mkdocsYmlFileString;
try {
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
} catch (error) {
throw new Error(
`Could not read MkDocs YAML config file ${mkdocsYmlPath} before for validation: ${error.message}`,
);
}
const mkdocsYml: any = yaml.load(mkdocsYmlFileString, {
schema: MKDOCS_SCHEMA,
});
@@ -20,6 +20,7 @@ import path from 'path';
import { Logger } from 'winston';
import {
addBuildTimestampMetadata,
getMkdocsYml,
patchMkdocsYmlPreBuild,
runCommand,
storeEtagMetadata,
@@ -71,10 +72,8 @@ export class TechdocsGenerator implements GeneratorBase {
logger: childLogger,
logStream,
}: GeneratorRunOptions): Promise<void> {
// TODO: In future mkdocs.yml can be mkdocs.yaml. So, use a config variable here to find out
// the correct file name.
// Do some updates to mkdocs.yml before generating docs e.g. adding repo_url
const mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml');
const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir);
if (parsedLocationAnnotation) {
await patchMkdocsYmlPreBuild(
mkdocsYmlPath,
@@ -83,7 +82,7 @@ export class TechdocsGenerator implements GeneratorBase {
);
}
await validateMkdocsYaml(inputDir, mkdocsYmlPath);
await validateMkdocsYaml(inputDir, content);
// Directories to bind on container
const mountDirs = {