diff --git a/.changeset/eighty-forks-fry.md b/.changeset/eighty-forks-fry.md new file mode 100644 index 0000000000..4a25dd128a --- /dev/null +++ b/.changeset/eighty-forks-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Use docs/README.md or README.md as fallback if docs/index.md is missing diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index fcf506098e..fdb164f44a 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -27,6 +27,7 @@ import { getGeneratorKey, getMkdocsYml, getRepoUrlFromLocationAnnotation, + patchIndexPreBuild, patchMkdocsYmlPreBuild, storeEtagMetadata, validateMkdocsYaml, @@ -65,6 +66,8 @@ const mkdocsYmlWithComments = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'), ); const mockLogger = getVoidLogger(); +const warn = jest.spyOn(mockLogger, 'warn'); + const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({})); @@ -286,6 +289,78 @@ describe('helpers', () => { }); }); + describe('patchIndexPreBuild', () => { + afterEach(() => { + warn.mockClear(); + }); + it('should have no effect if docs/index.md exists', async () => { + mockFs({ + '/docs/index.md': 'index.md content', + '/docs/README.md': 'docs/README.md content', + }); + + await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + + expect(fs.readFileSync('/docs/index.md', 'utf-8')).toEqual( + 'index.md content', + ); + expect(warn).not.toHaveBeenCalledWith(); + mockFs.restore(); + }); + + it("should use docs/README.md if docs/index.md doesn't exists", async () => { + mockFs({ + '/docs/README.md': 'docs/README.md content', + '/README.md': 'main README.md content', + }); + + await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + + expect(fs.readFileSync('/docs/index.md', 'utf-8')).toEqual( + 'docs/README.md content', + ); + expect(warn.mock.calls).toEqual([['docs/index.md not found.']]); + mockFs.restore(); + }); + + it('should use README.md if neither docs/index.md or docs/README.md exist', async () => { + mockFs({ + '/README.md': 'main README.md content', + }); + + await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + + expect(fs.readFileSync('/docs/index.md', 'utf-8')).toEqual( + 'main README.md content', + ); + expect(warn.mock.calls).toEqual([ + ['docs/index.md not found.'], + ['docs/README.md not found.'], + ['docs/readme.md not found.'], + ]); + mockFs.restore(); + }); + + it('should not use any file as index.md if no one matches the requirements', async () => { + mockFs({}); + + await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + + expect(() => fs.readFileSync('/docs/index.md', 'utf-8')).toThrow(); + expect(warn.mock.calls).toEqual([ + ['docs/index.md not found.'], + ['docs/README.md not found.'], + ['docs/readme.md not found.'], + ['README.md not found.'], + ['readme.md not found.'], + [ + "Could not find any techdocs' index file. Please make sure at least one of /docs/index.md /docs/README.md /docs/readme.md /README.md /readme.md exists.", + ], + ]); + mockFs.restore(); + }); + }); + describe('addBuildTimestampMetadata', () => { beforeEach(() => { mockFs.restore(); @@ -406,7 +481,7 @@ describe('helpers', () => { it('should return true on when a valid docs_dir is present', async () => { await expect( validateMkdocsYaml(inputDir, mkdocsYmlWithValidDocDir.toString()), - ).resolves.toBeUndefined(); + ).resolves.toBe('docs/'); }); it('should return false on absolute doc_dir path', async () => { diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 94746c1aaa..f77b557076 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -176,24 +176,31 @@ export const getMkdocsYml = async ( * @param {string} inputDir base dir to be used as a docs_dir path validity check * @param {string} mkdocsYmlFileString The string contents of the loaded * mkdocs.yml or equivalent of a docs site + * @returns the parsed docs_dir or undefined */ export const validateMkdocsYaml = async ( inputDir: string, mkdocsYmlFileString: string, -) => { - const mkdocsYml: any = yaml.load(mkdocsYmlFileString, { +): Promise => { + const mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA, }); + if (mkdocsYml === null || typeof mkdocsYml !== 'object') { + return undefined; + } + + const parsedMkdocsYml: Record = mkdocsYml; if ( - mkdocsYml.docs_dir && - !isChildPath(inputDir, resolvePath(inputDir, mkdocsYml.docs_dir)) + parsedMkdocsYml.docs_dir && + !isChildPath(inputDir, resolvePath(inputDir, parsedMkdocsYml.docs_dir)) ) { throw new Error( `docs_dir configuration value in mkdocs can't be an absolute directory or start with ../ for security reasons. Use relative paths instead which are resolved relative to your mkdocs.yml file location.`, ); } + return parsedMkdocsYml.docs_dir; }; /** @@ -291,6 +298,52 @@ export const patchMkdocsYmlPreBuild = async ( } }; +/** + * Update docs/index.md file before TechDocs generator uses it to generate docs site, + * falling back to docs/README.md or README.md in case a default docs/index.md + * is not provided. + */ +export const patchIndexPreBuild = async ({ + inputDir, + logger, + docsDir = 'docs', +}: { + inputDir: string; + logger: Logger; + docsDir?: string; +}) => { + const docsPath = path.join(inputDir, docsDir); + const indexMdPath = path.join(docsPath, 'index.md'); + + if (await fs.pathExists(indexMdPath)) { + return; + } + logger.warn(`${path.join(docsDir, 'index.md')} not found.`); + const fallbacks = [ + path.join(docsPath, 'README.md'), + path.join(docsPath, 'readme.md'), + path.join(inputDir, 'README.md'), + path.join(inputDir, 'readme.md'), + ]; + + await fs.ensureDir(docsPath); + for (const filePath of fallbacks) { + try { + await fs.copyFile(filePath, indexMdPath); + return; + } catch (error) { + logger.warn(`${path.relative(inputDir, filePath)} not found.`); + } + } + + logger.warn( + `Could not find any techdocs' index file. Please make sure at least one of ${[ + indexMdPath, + ...fallbacks, + ].join(' ')} exists.`, + ); +}; + /** * Update the techdocs_metadata.json to add a new build timestamp metadata. Create the .json file if it doesn't exist. * diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 6b9a2817be..e87278c2d4 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -25,6 +25,7 @@ import { import { addBuildTimestampMetadata, getMkdocsYml, + patchIndexPreBuild, patchMkdocsYmlPreBuild, runCommand, storeEtagMetadata, @@ -94,7 +95,7 @@ export class TechdocsGenerator implements GeneratorBase { const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir); // validate the docs_dir first - await validateMkdocsYaml(inputDir, content); + const docsDir = await validateMkdocsYaml(inputDir, content); if (parsedLocationAnnotation) { await patchMkdocsYmlPreBuild( @@ -103,6 +104,7 @@ export class TechdocsGenerator implements GeneratorBase { parsedLocationAnnotation, this.scmIntegrations, ); + await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); } // Directories to bind on container