From e4c938b7a57d8c87a93325341b8457b640329a6a Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Thu, 9 Dec 2021 21:12:49 +0100 Subject: [PATCH] docs: release as asset (#11429) * docs: prepare release as asset * chore: cleanup * fix: wrong extension * fix: wrong logger * fix: wrong path * chore: clean and create tmp * chore: fix types * fix: update generation * Update .github/workflows/build.yml Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> * Update lib/datasource/types.ts Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- .eslintignore | 1 + .github/workflows/build.yml | 6 ++ .prettierignore | 1 + .releaserc | 11 ++- docs/usage/gitlab-bot-security.md | 4 ++ docs/usage/modules/datasource.md | 4 ++ docs/usage/modules/manager.md | 4 ++ docs/usage/modules/platform.md | 4 ++ docs/usage/modules/versioning.md | 4 ++ docs/usage/templates.md | 6 ++ lib/datasource/types.ts | 7 +- lib/manager/types.ts | 4 +- lib/types/base.ts | 4 ++ lib/types/index.ts | 1 + package.json | 6 +- tools/docs/config.ts | 91 ++++++++++++++++++++++++ tools/docs/datasources.ts | 36 ++++++++++ tools/docs/manager.ts | 86 +++++++++++++++++++++++ tools/docs/modules.ts | 26 +++++++ tools/docs/platforms.ts | 27 ++++++++ tools/docs/presets.ts | 70 +++++++++++++++++++ tools/docs/schema.ts | 111 ++++++++++++++++++++++++++++++ tools/docs/templates.ts | 26 +++++++ tools/docs/utils.ts | 69 +++++++++++++++++++ tools/docs/versioning.ts | 34 +++++++++ tools/generate-docs.ts | 89 ++++++++++++++++++++++++ tools/utils/index.ts | 63 ++++++++++++++++- 27 files changed, 786 insertions(+), 9 deletions(-) create mode 100644 lib/types/base.ts create mode 100644 tools/docs/config.ts create mode 100644 tools/docs/datasources.ts create mode 100644 tools/docs/manager.ts create mode 100644 tools/docs/modules.ts create mode 100644 tools/docs/platforms.ts create mode 100644 tools/docs/presets.ts create mode 100644 tools/docs/schema.ts create mode 100644 tools/docs/templates.ts create mode 100644 tools/docs/utils.ts create mode 100644 tools/docs/versioning.ts create mode 100644 tools/generate-docs.ts diff --git a/.eslintignore b/.eslintignore index 423462ef5c92ca..775881bd7e3a83 100644 --- a/.eslintignore +++ b/.eslintignore @@ -16,3 +16,4 @@ coverage **/*.generated.ts /tools/dist /patches +tmp/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e82a7201f944f..57faf368cb4e99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -204,3 +204,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Upload docs + uses: actions/upload-artifact@v2.3.0 + with: + name: docs + path: tmp/docs diff --git a/.prettierignore b/.prettierignore index 8208517732120b..286760af2ee8bd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,3 +18,4 @@ bin/yarn* **/*.generated.ts /tools/dist /patches +**/tmp/ diff --git a/.releaserc b/.releaserc index 014655c3a4f5e9..3645aad973ca92 100644 --- a/.releaserc +++ b/.releaserc @@ -5,14 +5,21 @@ [ "@semantic-release/github", { - "releasedLabels": false + "releasedLabels": false, + "assets": [ + { + "path": "tmp/docs.tgz", + "label": "docs.tgz" + } + ] } ], [ "@semantic-release/exec", { "verifyConditionsCmd": "run-s verify", - "publishCmd": "run-s \"release -- {@}\" -- --release=${nextRelease.version} --sha=${nextRelease.gitHead} --tag=${nextRelease.channel}" + "prepareCmd": "run-s \"release:prepare -- {@}\" -- --release=${nextRelease.version} --sha=${nextRelease.gitHead} --tag=${nextRelease.channel}", + "publishCmd": "run-s \"release:publish -- {@}\" -- --release=${nextRelease.version} --sha=${nextRelease.gitHead} --tag=${nextRelease.channel}" } ] ], diff --git a/docs/usage/gitlab-bot-security.md b/docs/usage/gitlab-bot-security.md index 4f7b26f41118e3..f1e38c9111c53e 100644 --- a/docs/usage/gitlab-bot-security.md +++ b/docs/usage/gitlab-bot-security.md @@ -1,3 +1,7 @@ +--- +title: GitLab bot security +--- + # GitLab bot security You should understand GitLab's security model, before deciding to run a "bot" service like Renovate on GitLab, particularly the pipeline credentials. diff --git a/docs/usage/modules/datasource.md b/docs/usage/modules/datasource.md index fecf9a0a4bb654..4d33d70e60341d 100644 --- a/docs/usage/modules/datasource.md +++ b/docs/usage/modules/datasource.md @@ -1,3 +1,7 @@ +--- +title: Datasources +--- + # Datasources Once Renovate's manager is done scanning files and extracting dependencies, it will assign a `datasource` to each extracted package file and/or dependency so that Renovate then knows how to search for new versions. diff --git a/docs/usage/modules/manager.md b/docs/usage/modules/manager.md index c91d3df65242f2..b8fc9be8d9cc12 100644 --- a/docs/usage/modules/manager.md +++ b/docs/usage/modules/manager.md @@ -1,3 +1,7 @@ +--- +title: Managers +--- + # Managers Renovate is based around the concept of "package managers", or "managers" for short. diff --git a/docs/usage/modules/platform.md b/docs/usage/modules/platform.md index 9459d4003945f9..b96f915c72a15a 100644 --- a/docs/usage/modules/platform.md +++ b/docs/usage/modules/platform.md @@ -1,3 +1,7 @@ +--- +title: Platforms +--- + # Renovate Platforms Renovate aims to be platform-neutral, while also taking advantage of good platform-specific features. diff --git a/docs/usage/modules/versioning.md b/docs/usage/modules/versioning.md index df24d46c8e1dd5..708116b5777ae5 100644 --- a/docs/usage/modules/versioning.md +++ b/docs/usage/modules/versioning.md @@ -1,3 +1,7 @@ +--- +title: Versioning +--- + # Versioning Once Managers have extracted dependencies, and Datasources have located available versions, then Renovate will use a "Versioning" scheme to perform sorting and filtering of results. diff --git a/docs/usage/templates.md b/docs/usage/templates.md index 470c1d0efda945..de2ea87d46a6fa 100644 --- a/docs/usage/templates.md +++ b/docs/usage/templates.md @@ -15,10 +15,16 @@ Some are configuration options passed through, while others are generated as par ## Exposed config options + + + ## Other available fields + + + ## Additional Handlebars helpers diff --git a/lib/datasource/types.ts b/lib/datasource/types.ts index c03502dde6a2c5..2f803443fb7e9b 100644 --- a/lib/datasource/types.ts +++ b/lib/datasource/types.ts @@ -1,3 +1,5 @@ +import type { ModuleApi } from '../types'; + export interface Config { datasource?: string; depName?: string; @@ -66,7 +68,7 @@ export interface ReleaseResult { replacementVersion?: string; } -export interface DatasourceApi { +export interface DatasourceApi extends ModuleApi { id: string; getDigest?(config: DigestConfig, newValue?: string): Promise; getReleases(config: GetReleasesConfig): Promise; @@ -93,4 +95,7 @@ export interface DatasourceApi { * false: caching is not performed, or performed within the datasource implementation */ caching?: boolean; + + /** optional URLs to add to docs as references */ + urls?: string[]; } diff --git a/lib/manager/types.ts b/lib/manager/types.ts index 72acef11ce2d4b..036ae7ce5611b2 100644 --- a/lib/manager/types.ts +++ b/lib/manager/types.ts @@ -5,7 +5,7 @@ import type { ValidationMessage, } from '../config/types'; import type { ProgrammingLanguage } from '../constants'; -import type { RangeStrategy, SkipReason } from '../types'; +import type { ModuleApi, RangeStrategy, SkipReason } from '../types'; import type { File } from '../util/git/types'; export type Result = T | Promise; @@ -235,7 +235,7 @@ export interface GlobalManagerConfig { npmrcMerge?: boolean; } -export interface ManagerApi { +export interface ManagerApi extends ModuleApi { defaultConfig: Record; language?: ProgrammingLanguage; supportsLockFileMaintenance?: boolean; diff --git a/lib/types/base.ts b/lib/types/base.ts new file mode 100644 index 00000000000000..e55530ca3f8d28 --- /dev/null +++ b/lib/types/base.ts @@ -0,0 +1,4 @@ +export interface ModuleApi { + displayName?: string; + url?: string; +} diff --git a/lib/types/index.ts b/lib/types/index.ts index e1e0fa9d427199..f85b78de349197 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -4,3 +4,4 @@ export * from './versioning'; export * from './branch-status'; export * from './vulnerability-alert'; export * from './pr-state'; +export * from './base'; diff --git a/package.json b/package.json index 90074825c2395d..12cd683a37938f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "scripts": { "build": "run-s clean generate:* compile:*", - "clean": "rimraf dist", + "build:docs": "run-s \"release:prepare {@}\"", + "clean": "rimraf dist tmp", "clean-cache": "node bin/clean-cache.js", "compile:ts": "tsc -p tsconfig.app.json", "config-validator": "node -r ts-node/register/transpile-only -- lib/config-validator.ts", @@ -38,7 +39,8 @@ "pretest": "run-s generate:* ", "prettier": "prettier --check \"**/*.{ts,js,mjs,json,md,yml}\"", "prettier-fix": "prettier --write \"**/*.{ts,js,mjs,json,md,yml}\"", - "release": "node tools/release.mjs", + "release:prepare": "node -r ts-node/register/transpile-only -- tools/generate-docs.ts", + "release:publish": "node tools/release.mjs", "start": "node -r ts-node/register/transpile-only -- lib/renovate.ts", "test": "run-s lint test-schema type-check null-check jest", "test-dirty": "git diff --exit-code", diff --git a/tools/docs/config.ts b/tools/docs/config.ts new file mode 100644 index 00000000000000..a06fa355b536d3 --- /dev/null +++ b/tools/docs/config.ts @@ -0,0 +1,91 @@ +import table from 'markdown-table'; +import { getOptions } from '../../lib/config/options'; +import { getCliName } from '../../lib/workers/global/config/parse/cli'; +import { getEnvName } from '../../lib/workers/global/config/parse/env'; +import { readFile, updateFile } from '../utils/index'; + +const options = getOptions(); + +function genTable(obj: [string, string][], type: string, def: any): string { + const data = [['Name', 'Value']]; + const name = obj[0][1]; + const ignoredKeys = [ + 'name', + 'description', + 'default', + 'stage', + 'allowString', + 'cli', + 'env', + 'admin', + ]; + obj.forEach(([key, val]) => { + const el = [key, val]; + if ( + !ignoredKeys.includes(el[0]) || + (el[0] === 'default' && typeof el[1] !== 'object' && name !== 'prBody') + ) { + if (type === 'string' && el[0] === 'default') { + el[1] = `\`"${el[1]}"\``; + } + if (type === 'boolean' && el[0] === 'default') { + el[1] = `\`${el[1]}\``; + } + if (type === 'string' && el[0] === 'default' && el[1].length > 200) { + el[1] = `[template]`; + } + data.push(el); + } + }); + + if (type === 'list') { + data.push(['default', '`[]`']); + } + if (type === 'string' && def === undefined) { + data.push(['default', '`null`']); + } + if (type === 'boolean' && def === undefined) { + data.push(['default', '`true`']); + } + if (type === 'boolean' && def === null) { + data.push(['default', '`null`']); + } + return table(data); +} + +export async function generateConfig(dist: string, bot = false): Promise { + let configFile = `configuration-options.md`; + if (bot) { + configFile = `self-hosted-configuration.md`; + } + + const configOptionsRaw = (await readFile(`docs/usage/${configFile}`)).split( + '\n' + ); + + options + .filter((option) => option.releaseStatus !== 'unpublished') + .forEach((option) => { + const el: Record = { ...option }; + let headerIndex = configOptionsRaw.indexOf(`## ${option.name}`); + if (headerIndex === -1) { + headerIndex = configOptionsRaw.indexOf(`### ${option.name}`); + } + if (bot) { + el.cli = getCliName(el); + el.env = getEnvName(el); + if (el.cli === '') { + el.cli = `N/A`; + } + if (el.env === '') { + el.env = 'N/A'; + } + } + + configOptionsRaw[headerIndex] += + `\n${option.description}\n\n` + + genTable(Object.entries(el), option.type, option.default); + }); + + await updateFile(`${dist}/${configFile}`, configOptionsRaw.join('\n')); +} diff --git a/tools/docs/datasources.ts b/tools/docs/datasources.ts new file mode 100644 index 00000000000000..9589863a17b718 --- /dev/null +++ b/tools/docs/datasources.ts @@ -0,0 +1,36 @@ +import { getDatasources } from '../../lib/datasource'; +import { readFile, updateFile } from '../utils'; +import { + formatDescription, + formatUrls, + getDisplayName, + replaceContent, +} from './utils'; + +export async function generateDatasources(dist: string): Promise { + const dsList = getDatasources(); + let datasourceContent = + '\nSupported values for `datasource` are: ' + + [...dsList.keys()].map((v) => `\`${v}\``).join(', ') + + '.\n\n'; + for (const [datasource, definition] of dsList) { + const { id, urls, defaultConfig } = definition; + const displayName = getDisplayName(datasource, definition); + datasourceContent += `\n### ${displayName} Datasource\n\n`; + datasourceContent += `**Identifier**: \`${id}\`\n\n`; + datasourceContent += formatUrls(urls); + datasourceContent += await formatDescription('datasource', datasource); + + if (defaultConfig) { + datasourceContent += + '**Default configuration**:\n\n```json\n' + + JSON.stringify(defaultConfig, undefined, 2) + + '\n```\n'; + } + + datasourceContent += `\n----\n\n`; + } + let indexContent = await readFile(`docs/usage/modules/datasource.md`); + indexContent = replaceContent(indexContent, datasourceContent); + await updateFile(`${dist}/modules/datasource.md`, indexContent); +} diff --git a/tools/docs/manager.ts b/tools/docs/manager.ts new file mode 100644 index 00000000000000..bf3bbd4323f2da --- /dev/null +++ b/tools/docs/manager.ts @@ -0,0 +1,86 @@ +import type { RenovateConfig } from '../../lib/config/types'; +import { getManagers } from '../../lib/manager'; +import { readFile, updateFile } from '../utils'; +import { getDisplayName, getNameWithUrl, replaceContent } from './utils'; + +function getTitle(manager: string, displayName: string): string { + if (manager === 'regex') { + return `Custom Manager Support using Regex`; + } + return `Automated Dependency Updates for ${displayName}`; +} + +function getManagerLink(manager: string): string { + return `[\`${manager}\`](${manager}/)`; +} + +export async function generateManagers(dist: string): Promise { + const managers = getManagers(); + const allLanguages: Record = {}; + for (const [manager, definition] of managers) { + const language = definition.language || 'other'; + allLanguages[language] = allLanguages[language] || []; + allLanguages[language].push(manager); + const { defaultConfig } = definition; + const { fileMatch } = defaultConfig as RenovateConfig; + const displayName = getDisplayName(manager, definition); + let md = `--- +title: ${getTitle(manager, displayName)} +sidebar_label: ${displayName} +--- +`; + if (manager !== 'regex') { + const nameWithUrl = getNameWithUrl(manager, definition); + md += `Renovate supports updating ${nameWithUrl} dependencies.\n\n`; + if (defaultConfig.enabled === false) { + md += '## Enabling\n\n'; + md += `${displayName} functionality is currently in beta testing so you must opt in to test it out. To enable it, add a configuration like this to either your bot config or your \`renovate.json\`:\n\n`; + md += '```\n'; + md += `{\n "${manager}": {\n "enabled": true\n }\n}`; + md += '\n```\n\n'; + md += + 'If you encounter any bugs, please [raise a bug report](https://github.com/renovatebot/renovate/issues/new?template=3-Bug_report.md). If you find that it works well, then feedback on that would be welcome too.\n\n'; + } + md += '## File Matching\n\n'; + if (!Array.isArray(fileMatch) || fileMatch.length === 0) { + md += `Because file names for \`${manager}\` cannot be easily determined automatically, Renovate will not attempt to match any \`${manager}\` files by default. `; + } else { + md += `By default, Renovate will check any files matching `; + if (fileMatch.length === 1) { + md += `the following regular expression: \`${fileMatch[0]}\`.\n\n`; + } else { + md += `any of the following regular expressions:\n\n`; + md += '```\n'; + md += fileMatch.join('\n'); + md += '\n```\n\n'; + } + } + md += `For details on how to extend a manager's \`fileMatch\` value, please follow [this link](/modules/manager/#file-matching).\n\n`; + } + + const managerReadmeContent = await readFile( + `lib/manager/${manager}/readme.md` + ); + if (manager !== 'regex') { + md += '\n## Additional Information\n\n'; + } + md += managerReadmeContent + '\n\n'; + + await updateFile(`${dist}/modules/manager/${manager}/index.md`, md); + } + const languages = Object.keys(allLanguages).filter( + (language) => language !== 'other' + ); + languages.sort(); + languages.push('other'); + let languageText = '\n'; + + for (const language of languages) { + languageText += `**${language}**: `; + languageText += allLanguages[language].map(getManagerLink).join(', '); + languageText += '\n\n'; + } + let indexContent = await readFile(`docs/usage/modules/manager.md`); + indexContent = replaceContent(indexContent, languageText); + await updateFile(`${dist}/modules/manager.md`, indexContent); +} diff --git a/tools/docs/modules.ts b/tools/docs/modules.ts new file mode 100644 index 00000000000000..cc15d58d81eda9 --- /dev/null +++ b/tools/docs/modules.ts @@ -0,0 +1,26 @@ +// import shell from 'shelljs'; + +// import { readFile, updateFile } from '../utils/index.js'; +// // import { getDisplayName, getNameWithUrl, replaceContent } from './utils.js'; + +// const fileRe = /modules[/\\](.+?)\.md$/; +// const titleRe = /^#(.+?)$/m; + +export async function generateModules(_root: string): Promise { + // for (const f of shell.ls('../usage/modules/*.md')) { + // const [, tgt] = fileRe.exec(f); + + // let content = await readFile(f); + // content = content.replace( + // titleRe, + // `--- + // title: $1 + // --- + // ` + // ); + + // await updateFile(`./docs/modules/${tgt}.md`, content); + // } + + await Promise.resolve(); +} diff --git a/tools/docs/platforms.ts b/tools/docs/platforms.ts new file mode 100644 index 00000000000000..7bfd1ee0066642 --- /dev/null +++ b/tools/docs/platforms.ts @@ -0,0 +1,27 @@ +import { getPlatformList } from '../../lib/platform'; +import { readFile, updateFile } from '../utils'; +import { replaceContent } from './utils'; + +function getModuleLink(module: string, title: string): string { + return `[${title ?? module}](${module}/)`; +} + +export async function generatePlatforms(dist: string): Promise { + let platformContent = 'Supported values for `platform` are: '; + const platforms = getPlatformList(); + for (const platform of platforms) { + const readme = await readFile(`lib/platform/${platform}/index.md`); + await updateFile(`${dist}/modules/platform/${platform}/index.md`, readme); + } + + platformContent += platforms + .map((v) => getModuleLink(v, `\`${v}\``)) + .join(', '); + + platformContent += '.\n'; + + const indexFileName = `docs/usage/modules/platform.md`; + let indexContent = await readFile(indexFileName); + indexContent = replaceContent(indexContent, platformContent); + await updateFile(`${dist}/modules/platform.md`, indexContent); +} diff --git a/tools/docs/presets.ts b/tools/docs/presets.ts new file mode 100644 index 00000000000000..dfa7e67781666a --- /dev/null +++ b/tools/docs/presets.ts @@ -0,0 +1,70 @@ +import { groups as presetGroups } from '../../lib/config/presets/internal'; +import { logger } from '../../lib/logger'; +import { updateFile } from '../utils'; + +function jsUcfirst(string: string): string { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +/** + * @param {string} name + * @param {number} order + */ +function generateFrontMatter(name: string, order: number): string { + return `--- +date: 2017-12-07 +title: ${name} Presets +categories: + - config-presets +type: Document +order: ${order} +--- +`; +} + +export async function generatePresets(dist: string): Promise { + let index = 0; + for (const [name, presetConfig] of Object.entries(presetGroups)) { + index += 1; + const formattedName = jsUcfirst(name) + .replace('Js', 'JS') + .replace(/s$/, '') + .replace(/^Config$/, 'Full Config'); + const frontMatter = generateFrontMatter(formattedName, index); + let content = `\n`; + for (const [preset, value] of Object.entries(presetConfig)) { + let header = `\n### ${name === 'default' ? '' : name}:${preset}`; + let presetDescription = value.description; + delete value.description; + if (!presetDescription) { + if (value.packageRules?.[0].description) { + presetDescription = value.packageRules[0].description; + delete value.packageRules[0].description; + } + } + let body = ''; + if (presetDescription) { + body += `\n\n${presetDescription}\n`; + } else { + logger.warn(`Preset ${name}:${preset} has no description`); + } + body += '\n```\n'; + body += JSON.stringify(value, null, 2); + body += '\n```\n'; + body += '----\n'; + if (body.includes('{{arg0}}')) { + header += '(``'; + if (body.includes('{{arg1}}')) { + header += ', ``'; + if (body.includes('{{arg2}}')) { + header += ', ``'; + } + } + header += ')'; + body = body.replace(/{{(arg\d+)}}/g, '$1'); + } + content += header + body; + } + await updateFile(`${dist}/presets-${name}.md`, frontMatter + content); + } +} diff --git a/tools/docs/schema.ts b/tools/docs/schema.ts new file mode 100644 index 00000000000000..8576e14d0fbfc2 --- /dev/null +++ b/tools/docs/schema.ts @@ -0,0 +1,111 @@ +import { getOptions } from '../../lib/config/options'; +import type { RenovateOptions } from '../../lib/config/types'; +import { hasKey } from '../../lib/util/object'; +import { updateFile } from '../utils'; + +const schema = { + title: 'JSON schema for Renovate config files (https://renovatebot.com/)', + $schema: 'http://json-schema.org/draft-04/schema#', + type: 'object', + properties: {}, +}; +const options = getOptions(); +options.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; +}); +const properties = schema.properties as Record; + +function createSingleConfig(option: RenovateOptions): Record { + const temp = {} as Record & RenovateOptions; + if (option.description) { + temp.description = option.description; + } + temp.type = option.type; + if (option.type === 'array') { + if (option.subType) { + temp.items = { + type: option.subType, + }; + if (hasKey('format', option) && option.format) { + temp.items.format = option.format; + } + if (option.allowedValues) { + temp.items.enum = option.allowedValues; + } + } + if (option.subType === 'string' && option.allowString === true) { + const items = temp.items; + delete temp.items; + delete temp.type; + temp.oneOf = [{ type: 'array', items }, { ...items }]; + } + } else { + if (hasKey('format', option) && option.format) { + temp.format = option.format; + } + if (option.allowedValues) { + temp.enum = option.allowedValues; + } + } + if (option.default !== undefined) { + temp.default = option.default; + } + if ( + hasKey('additionalProperties', option) && + option.additionalProperties !== undefined + ) { + temp.additionalProperties = option.additionalProperties; + } + if (temp.type === 'object' && !option.freeChoice) { + temp.$ref = '#'; + } + return temp; +} + +function createSchemaForParentConfigs(): void { + for (const option of options) { + if (!option.parent) { + properties[option.name] = createSingleConfig(option); + } + } +} + +function addChildrenArrayInParents(): void { + for (const option of options) { + if (option.parent) { + properties[option.parent].items = { + allOf: [ + { + type: 'object', + properties: {}, + }, + ], + }; + } + } +} + +function createSchemaForChildConfigs(): void { + for (const option of options) { + if (option.parent) { + properties[option.parent].items.allOf[0].properties[option.name] = + createSingleConfig(option); + } + } +} + +export async function generateSchema(dist: string): Promise { + createSchemaForParentConfigs(); + addChildrenArrayInParents(); + createSchemaForChildConfigs(); + await updateFile( + `${dist}/renovate-schema.json`, + `${JSON.stringify(schema, null, 2)}\n` + ); +} diff --git a/tools/docs/templates.ts b/tools/docs/templates.ts new file mode 100644 index 00000000000000..1867c0f3986a6f --- /dev/null +++ b/tools/docs/templates.ts @@ -0,0 +1,26 @@ +import { allowedFields, exposedConfigOptions } from '../../lib/util/template'; +import { readFile, updateFile } from '../utils'; +import { replaceContent } from './utils'; + +export async function generateTemplates(dist: string): Promise { + let exposedConfigOptionsText = + 'The following configuration options are passed through for templating: '; + exposedConfigOptionsText += + exposedConfigOptions + .map( + (field) => `[${field}](/configuration-options/#${field.toLowerCase()})` + ) + .join(', ') + '.'; + + let runtimeText = + 'The following runtime values are passed through for templating: \n\n'; + for (const [field, description] of Object.entries(allowedFields)) { + runtimeText += ` - \`${field}\`: ${description}\n`; + } + runtimeText += '\n\n'; + + let templateContent = await readFile('docs/usage/templates.md'); + templateContent = replaceContent(templateContent, exposedConfigOptionsText); + templateContent = replaceContent(templateContent, runtimeText); + await updateFile(`${dist}/templates.md`, templateContent); +} diff --git a/tools/docs/utils.ts b/tools/docs/utils.ts new file mode 100644 index 00000000000000..c8c8877dfcdaee --- /dev/null +++ b/tools/docs/utils.ts @@ -0,0 +1,69 @@ +import { logger } from '../../lib/logger'; +import type { ModuleApi } from '../../lib/types'; +import { readFile } from '../utils'; + +const replaceStart = + ''; +const replaceStop = ''; + +export function capitalize(input: string): string { + // console.log(input); + return input[0].toUpperCase() + input.slice(1); +} + +export function formatName(input: string): string { + return input.split('-').map(capitalize).join(' '); +} + +export function getDisplayName( + moduleName: string, + moduleDefinition: ModuleApi +): string { + return moduleDefinition.displayName || formatName(moduleName); +} + +export function getNameWithUrl( + moduleName: string, + moduleDefinition: ModuleApi +): string { + const displayName = getDisplayName(moduleName, moduleDefinition); + if (moduleDefinition.url) { + return `[${displayName}](${moduleDefinition.url})`; + } + return displayName; +} + +export function replaceContent(content: string, txt: string): string { + const replaceStartIndex = content.indexOf(replaceStart); + const replaceStopIndex = content.indexOf(replaceStop); + + if (replaceStartIndex < 0) { + logger.error('Missing replace placeholder'); + return content; + } + return ( + content.slice(0, replaceStartIndex) + + txt + + content.slice(replaceStopIndex + replaceStop.length) + ); +} + +export function formatUrls(urls: string[] | null | undefined): string { + if (Array.isArray(urls) && urls.length) { + return `**References**:\n\n${urls + .map((url) => ` - [${url}](${url})`) + .join('\n')}\n\n`; + } + return ''; +} + +export async function formatDescription( + type: string, + name: string +): Promise { + const content = await readFile(`lib/${type}/${name}/readme.md`); + if (!content) { + return ''; + } + return `**Description**:\n\n${content}\n`; +} diff --git a/tools/docs/versioning.ts b/tools/docs/versioning.ts new file mode 100644 index 00000000000000..780fa8c90c72b1 --- /dev/null +++ b/tools/docs/versioning.ts @@ -0,0 +1,34 @@ +import { getVersioningList } from '../../lib/versioning'; +import { readFile, updateFile } from '../utils'; +import { formatDescription, formatUrls, replaceContent } from './utils'; + +export async function generateVersioning(dist: string): Promise { + const versioningList = getVersioningList(); + let versioningContent = + '\nSupported values for `versioning` are: ' + + versioningList.map((v) => `\`${v}\``).join(', ') + + '.\n\n'; + for (const versioning of versioningList) { + const definition = await import(`../../lib/versioning/${versioning}`); + const { id, displayName, urls, supportsRanges, supportedRangeStrategies } = + definition; + versioningContent += `\n### ${displayName} Versioning\n\n`; + versioningContent += `**Identifier**: \`${id}\`\n\n`; + versioningContent += formatUrls(urls); + versioningContent += `**Ranges/Constraints:**\n\n`; + if (supportsRanges) { + versioningContent += `✅ Ranges are supported.\n\nValid \`rangeStrategy\` values are: ${( + supportedRangeStrategies || [] + ) + .map((strategy: string) => `\`${strategy}\``) + .join(', ')}\n\n`; + } else { + versioningContent += `❌ No range support.\n\n`; + } + versioningContent += await formatDescription('versioning', versioning); + versioningContent += `\n----\n\n`; + } + let indexContent = await readFile(`docs/usage/modules/versioning.md`); + indexContent = replaceContent(indexContent, versioningContent); + await updateFile(`${dist}/modules/versioning.md`, indexContent); +} diff --git a/tools/generate-docs.ts b/tools/generate-docs.ts new file mode 100644 index 00000000000000..4e23ebcf747a80 --- /dev/null +++ b/tools/generate-docs.ts @@ -0,0 +1,89 @@ +import { ERROR } from 'bunyan'; +import shell from 'shelljs'; +import { getProblems, logger } from '../lib/logger'; +import { generateConfig } from './docs/config'; +import { generateDatasources } from './docs/datasources'; +import { generateManagers } from './docs/manager'; +import { generateModules } from './docs/modules'; +import { generatePlatforms } from './docs/platforms'; +import { generatePresets } from './docs/presets'; +import { generateSchema } from './docs/schema'; +import { generateTemplates } from './docs/templates'; +import { generateVersioning } from './docs/versioning'; + +process.on('unhandledRejection', (err) => { + // Will print "unhandledRejection err is not defined" + logger.error({ err }, 'unhandledRejection'); + process.exit(-1); +}); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + try { + const dist = 'tmp/docs'; + let r: shell.ShellString; + + logger.info('generating docs'); + + r = shell.mkdir('-p', `${dist}/`); + if (r.code) { + return; + } + + logger.info('* static'); + r = shell.cp('-r', 'docs/usage/*', `${dist}/`); + if (r.code) { + return; + } + + logger.info('* modules'); + await generateModules(dist); + + logger.info('* platforms'); + await generatePlatforms(dist); + + // versionigs + logger.info('* versionigs'); + await generateVersioning(dist); + + // datasources + logger.info('* datasources'); + await generateDatasources(dist); + + // managers + logger.info('* managers'); + await generateManagers(dist); + + // presets + logger.info('* presets'); + await generatePresets(dist); + + // templates + logger.info('* templates'); + await generateTemplates(dist); + + // configuration-options + logger.info('* configuration-options'); + await generateConfig(dist); + + // self-hosted-configuration + logger.info('* self-hosted-configuration'); + await generateConfig(dist, true); + + // json-schema + logger.info('* json-schema'); + await generateSchema(dist); + + r = shell.exec('tar -czf ./tmp/docs.tgz -C ./tmp/docs .'); + if (r.code) { + return; + } + } catch (err) { + logger.error({ err }, 'Unexpected error'); + } finally { + const loggerErrors = getProblems().filter((p) => p.level >= ERROR); + if (loggerErrors.length) { + shell.exit(1); + } + } +})(); diff --git a/tools/utils/index.ts b/tools/utils/index.ts index ff4ef7fe74e6b7..d17bf464ec047c 100644 --- a/tools/utils/index.ts +++ b/tools/utils/index.ts @@ -1,8 +1,67 @@ +import fs from 'fs-extra'; +import { logger } from '../../lib/logger'; + +export const newFiles = new Set(); + /** * Get environment variable or empty string. * Used for easy mocking. - * @param key variable name + * @param {string} key variable name + * @returns {string} */ export function getEnv(key: string): string { - return process.env[key] ?? ''; + return process.env[key] || ''; +} + +/** + * Find all module directories. + * @param {string} dirname dir to search in + * @returns {string[]} + */ +export function findModules(dirname: string): string[] { + return fs + .readdirSync(dirname, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name) + .filter((name) => !name.startsWith('__')) + .sort(); +} + +/** + * @param {string} input + * @returns {string} + */ +export function camelCase(input: string): string { + return input + .replace(/(?:^\w|[A-Z]|\b\w)/g, (char, index) => + index === 0 ? char.toLowerCase() : char.toUpperCase() + ) + .replace(/-/g, ''); +} + +/** + * @param {string } file + * @param {string} code + * @returns {Promise} + */ +export async function updateFile(file: string, code: string): Promise { + const oldCode = fs.existsSync(file) ? await fs.readFile(file, 'utf8') : null; + if (code !== oldCode) { + if (!code) { + logger.error({ file }, 'Missing content'); + } + await fs.outputFile(file, code ?? '', { encoding: 'utf8' }); + } + newFiles.add(file); +} + +/** + * @param {string } file + * @returns {Promise} + */ +export function readFile(file: string): Promise { + if (fs.existsSync(file)) { + return fs.readFile(file, 'utf8'); + } + return Promise.resolve(''); }