diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4a5bc1ad8..46c6a3bd1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -269,8 +269,8 @@ jobs: - name: Lint fenced code blocks run: yarn -s doc-fence-check - - name: Lint website docs - run: yarn -s lint-website-docs + - name: Lint documentation + run: yarn -s lint-documentation lint-other: needs: [setup] diff --git a/lib/config/types.ts b/lib/config/types.ts index 027f00952a..99cf44b617 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -396,9 +396,6 @@ export interface RenovateOptionBase { | 'postUpgradeTasks' | 'regexManagers'; - // used by tests - relatedOptions?: string[]; - stage?: RenovateConfigStage; experimental?: boolean; diff --git a/package.json b/package.json index 7f6e80d82b..d9fb273a83 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "debug": "cross-env NODE_OPTIONS=--inspect-brk ts-node lib/renovate.ts", "doc-fix": "run-s markdown-lint-fix prettier-fix", "doc-fence-check": "node tools/check-fenced-code.mjs", - "lint-website-docs": "jest --coverage false test/website-docs.spec.ts", + "lint-documentation": "jest --coverage false test/documentation.spec.ts", "eslint": "eslint . --cache --cache-location .cache/eslint --report-unused-disable-directives", "eslint-fix": "eslint --cache --cache-location .cache/eslint --fix . --report-unused-disable-directives", "eslint-ci": "eslint . --cache --cache-strategy content --cache-location .cache/eslint --format gha", diff --git a/test/documentation.spec.ts b/test/documentation.spec.ts new file mode 100644 index 0000000000..da63620d84 --- /dev/null +++ b/test/documentation.spec.ts @@ -0,0 +1,88 @@ +import fs from 'fs-extra'; +import { glob } from 'glob'; +import { getOptions } from '../lib/config/options'; +import { regEx } from '../lib/util/regex'; + +const options = getOptions(); +const markdownGlob = '{docs,lib}/**/*.md'; + +describe('documentation', () => { + it('has no invalid links', async () => { + const markdownFiles = await glob(markdownGlob); + + await Promise.all( + markdownFiles.map(async (markdownFile) => { + const markdownText = await fs.readFile(markdownFile, 'utf8'); + expect(markdownText).not.toMatch(regEx(/\.md\/#/)); + }) + ); + }); + + describe('website-documentation', () => { + describe('configuration-options', () => { + const doc = fs.readFileSync( + 'docs/usage/configuration-options.md', + 'utf8' + ); + + const headers = doc + .match(/\n## (.*?)\n/g) + ?.map((match) => match.substring(4, match.length - 1)); + + const expectedOptions = options + .filter((option) => !option.globalOnly) + .filter((option) => !option.parent) + .filter((option) => !option.autogenerated) + .map((option) => option.name) + .sort(); + + it('has doc headers sorted alphabetically', () => { + expect(headers).toEqual([...headers!].sort()); + }); + + it('has headers for every required option', () => { + expect(headers).toEqual(expectedOptions); + }); + + const subHeaders = doc + .match(/\n### (.*?)\n/g) + ?.map((match) => match.substring(5, match.length - 1)); + subHeaders!.sort(); + const expectedSubOptions = options + .filter((option) => option.stage !== 'global') + .filter((option) => !option.globalOnly) + .filter((option) => option.parent) + .map((option) => option.name) + .sort(); + expectedSubOptions.sort(); + + it('has headers for every required sub-option', () => { + expect(subHeaders).toEqual(expectedSubOptions); + }); + }); + + describe('self-hosted-configuration', () => { + const doc = fs.readFileSync( + 'docs/usage/self-hosted-configuration.md', + 'utf8' + ); + + const headers = doc + .match(/\n## (.*?)\n/g) + ?.map((match) => match.substring(4, match.length - 1)); + + const expectedOptions = options + .filter((option) => !!option.globalOnly) + .map((option) => option.name) + .sort(); + + it('has headers sorted alphabetically', () => { + expect(headers).toEqual([...headers!].sort()); + }); + + it('has headers for every required option', () => { + expect(headers).toEqual(expectedOptions); + }); + }); + }); +}); diff --git a/test/website-docs.spec.ts b/test/website-docs.spec.ts deleted file mode 100644 index 164a526992..0000000000 --- a/test/website-docs.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ -import fs from 'node:fs'; -import is from '@sindresorhus/is'; -import { getOptions } from '../lib/config/options'; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - type ContainsOption = T extends ArrayLike ? T[number] : unknown; - - interface Matchers { - /** - * only available in `test/website-docs.spec.js` - * @param arg Value which current values should contain - */ - toContainOption(arg: ContainsOption): void; - } - } -} - -const options = getOptions(); - -describe('website-docs', () => { - const doc = fs.readFileSync('docs/usage/configuration-options.md', 'utf8'); - const selfHostDoc = fs.readFileSync( - 'docs/usage/self-hosted-configuration.md', - 'utf8' - ); - const headers = doc - .match(/\n## (.*?)\n/g) - ?.map((match) => match.substring(4, match.length - 1)); - const selfHostHeaders = selfHostDoc - .match(/\n## (.*?)\n/g) - ?.map((match) => match.substring(4, match.length - 1)); - const expectedOptions = options - .filter((option) => !option.globalOnly) - .filter((option) => !option.parent) - .filter((option) => !option.autogenerated) - .map((option) => option.name) - .sort(); - - const selfHostExpectedOptions = options - .filter((option) => !!option.globalOnly) - .map((option) => option.name) - .sort(); - - it('has doc headers sorted alphabetically', () => { - expect(headers).toEqual([...headers!].sort()); - }); - - it('has headers for every required option', () => { - expect(headers).toEqual(expectedOptions); - }); - - it('has self hosted doc headers sorted alphabetically', () => { - expect(selfHostHeaders).toEqual([...selfHostHeaders!].sort()); - }); - - it('has headers (self hosted) for every required option', () => { - expect(selfHostHeaders).toEqual(selfHostExpectedOptions); - }); - - const headers3 = doc - .match(/\n### (.*?)\n/g) - ?.map((match) => match.substring(5, match.length - 1)); - headers3!.sort(); - const expectedOptions3 = options - .filter((option) => option.stage !== 'global') - .filter((option) => !option.globalOnly) - .filter((option) => option.parent) - .map((option) => option.name) - .sort(); - expectedOptions3.sort(); - - it('has headers for every required sub-option', () => { - expect(headers3).toEqual(expectedOptions3); - }); - - // Checking relatedOptions field in options - const relatedOptionsMatrix = options - .map((option) => option.relatedOptions) - .filter(is.truthy) - .sort(); - - let relatedOptions = ([] as string[]).concat(...relatedOptionsMatrix!); // Converts the matrix to an 1D array - relatedOptions = [...new Set(relatedOptions)]; // Makes all options unique - - /* - Matcher which checks if the argument is within the received array (or string) - on an error, it throws a custom message. - */ - expect.extend({ - toContainOption(received: T[], argument: T) { - if (received.includes(argument)) { - return { - message: (): string => - `Option "${argument}" should be within options`, - pass: true, - }; - } - return { - message: (): string => - `Option "${argument}" doesn't exist within options`, - pass: false, - }; - }, - }); - - const allOptionNames = options.map((option) => option.name).sort(); - - // Lists through each option in the relatedOptions array to be able to locate the exact element which causes error, in case of one - it('has valid relateOptions values', () => { - relatedOptions.forEach((relOption) => { - expect(allOptionNames).toContainOption(relOption); - }); - }); -});