diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index e8522f73326a9e..d3b6f471b90516 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -82,6 +82,7 @@ import * as preCommit from './pre-commit'; import * as pub from './pub'; import * as puppet from './puppet'; import * as pyenv from './pyenv'; +import * as renovateConfigPresets from './renovate-config-presets'; import * as rubyVersion from './ruby-version'; import * as runtimeVersion from './runtime-version'; import * as sbt from './sbt'; @@ -188,6 +189,7 @@ api.set('pre-commit', preCommit); api.set('pub', pub); api.set('puppet', puppet); api.set('pyenv', pyenv); +api.set('renovate-config-presets', renovateConfigPresets); api.set('ruby-version', rubyVersion); api.set('runtime-version', runtimeVersion); api.set('sbt', sbt); diff --git a/lib/modules/manager/renovate-config-presets/extract.spec.ts b/lib/modules/manager/renovate-config-presets/extract.spec.ts new file mode 100644 index 00000000000000..3d0e716f90e92a --- /dev/null +++ b/lib/modules/manager/renovate-config-presets/extract.spec.ts @@ -0,0 +1,233 @@ +import { codeBlock } from 'common-tags'; +import { extractPackageFile } from '.'; + +const noPresets = codeBlock` + { + "draftPR": true + } +`; + +const builtInPresets = codeBlock` + { + "extends": ["config:recommended", ":label(test)", "helpers:pinGitHubActionDigests"] + } +`; + +const unsupportedPresets = codeBlock` + { + "extends": [ + "fastcore", + "http://my.server/users/me/repos/renovate-presets/raw/default.json", + "local>renovate/presets", + "local>renovate/presets2#1.2.3" + ] + } +`; + +const presetsWithoutVersions = codeBlock` + { + "extends": [ + "github>abc/foo", + "gitlab>abc/bar:xyz", + "gitea>cde/foo//path/xyz" + ] + } +`; + +const githubPresets = codeBlock` + { + "extends": [ + "github>abc/foo#1.2.3", + "github>abc/bar:xyz#1.2.3", + "github>cde/foo//path/xyz#1.2.3", + "github>cde/bar:xyz/sub#1.2.3" + ] + } +`; + +const gitlabPresets = codeBlock` + { + "extends": [ + "gitlab>abc/foo#1.2.3", + "gitlab>abc/bar:xyz#1.2.3", + "gitlab>cde/foo//path/xyz#1.2.3", + "gitlab>cde/bar:xyz/sub#1.2.3" + ] + } +`; + +const giteaPresets = codeBlock` + { + "extends": [ + "gitea>abc/foo#1.2.3", + "gitea>abc/bar:xyz#1.2.3", + "gitea>cde/foo//path/xyz#1.2.3", + "gitea>cde/bar:xyz/sub#1.2.3" + ] + } +`; + +const json5Config = codeBlock` + { + // comments are permitted + "extends": [ + "github>abc/foo#1.2.3", + ], + } +`; + +describe('modules/manager/renovate-config-presets/extract', () => { + describe('extractPackageFile()', () => { + it('returns null for empty file', () => { + expect(extractPackageFile('', '')).toBeNull(); + }); + + it('returns null for invalid file', () => { + expect(extractPackageFile('this-is-not-json', '')).toBeNull(); + }); + + it('returns null for a config file without presets', () => { + expect(extractPackageFile(noPresets, '')).toBeNull(); + }); + + it('returns null for a config file only contains built-in presets', () => { + expect(extractPackageFile(builtInPresets, '')).toBeNull(); + }); + + it('provides skipReason for unsupported preset sources', () => { + expect(extractPackageFile(unsupportedPresets, '')).toEqual({ + deps: [ + { + depName: 'renovate-config-fastcore', + skipReason: 'unsupported-datasource', + }, + { + depName: + 'http://my.server/users/me/repos/renovate-presets/raw/default.json', + skipReason: 'unsupported-datasource', + }, + { + depName: 'renovate/presets', + skipReason: 'unsupported-datasource', + }, + { + depName: 'renovate/presets2', + skipReason: 'unsupported-datasource', + }, + ], + }); + }); + + it('provides skipReason for presets without versions', () => { + expect(extractPackageFile(presetsWithoutVersions, '')).toEqual({ + deps: [ + { + depName: 'abc/foo', + skipReason: 'unspecified-version', + }, + { + depName: 'abc/bar', + skipReason: 'unspecified-version', + }, + { + depName: 'cde/foo', + skipReason: 'unspecified-version', + }, + ], + }); + }); + + it('extracts from a config file with GitHub hosted presets', () => { + expect(extractPackageFile(githubPresets, '')).toEqual({ + deps: [ + { + datasource: 'github-tags', + depName: 'abc/foo', + currentValue: '1.2.3', + }, + { + datasource: 'github-tags', + depName: 'abc/bar', + currentValue: '1.2.3', + }, + { + datasource: 'github-tags', + depName: 'cde/foo', + currentValue: '1.2.3', + }, + { + datasource: 'github-tags', + depName: 'cde/bar', + currentValue: '1.2.3', + }, + ], + }); + }); + + it('extracts from a config file with GitLab hosted presets', () => { + expect(extractPackageFile(gitlabPresets, '')).toEqual({ + deps: [ + { + datasource: 'gitlab-tags', + depName: 'abc/foo', + currentValue: '1.2.3', + }, + { + datasource: 'gitlab-tags', + depName: 'abc/bar', + currentValue: '1.2.3', + }, + { + datasource: 'gitlab-tags', + depName: 'cde/foo', + currentValue: '1.2.3', + }, + { + datasource: 'gitlab-tags', + depName: 'cde/bar', + currentValue: '1.2.3', + }, + ], + }); + }); + + it('extracts from a config file with Gitea hosted presets', () => { + expect(extractPackageFile(giteaPresets, '')).toEqual({ + deps: [ + { + datasource: 'gitea-tags', + depName: 'abc/foo', + currentValue: '1.2.3', + }, + { + datasource: 'gitea-tags', + depName: 'abc/bar', + currentValue: '1.2.3', + }, + { + datasource: 'gitea-tags', + depName: 'cde/foo', + currentValue: '1.2.3', + }, + { + datasource: 'gitea-tags', + depName: 'cde/bar', + currentValue: '1.2.3', + }, + ], + }); + }); + + it('supports JSON5', () => { + expect(extractPackageFile(json5Config, '')).toEqual({ + deps: [ + { + datasource: 'github-tags', + depName: 'abc/foo', + currentValue: '1.2.3', + }, + ], + }); + }); + }); +}); diff --git a/lib/modules/manager/renovate-config-presets/extract.ts b/lib/modules/manager/renovate-config-presets/extract.ts new file mode 100644 index 00000000000000..9589279755762b --- /dev/null +++ b/lib/modules/manager/renovate-config-presets/extract.ts @@ -0,0 +1,82 @@ +import is from '@sindresorhus/is'; +import JSON5 from 'json5'; +import { parsePreset } from '../../../config/presets'; +import type { RenovateConfig } from '../../../config/types'; +import { logger } from '../../../logger'; +import { GiteaTagsDatasource } from '../../datasource/gitea-tags'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { GitlabTagsDatasource } from '../../datasource/gitlab-tags'; +import type { PackageDependency, PackageFileContent } from '../types'; + +const supportedPresetSources: { + source: string; + datasource: string; +}[] = [ + { + source: 'github', + datasource: GithubTagsDatasource.id, + }, + { + source: 'gitlab', + datasource: GitlabTagsDatasource.id, + }, + { + source: 'gitea', + datasource: GiteaTagsDatasource.id, + }, +]; + +export function extractPackageFile( + content: string, + packageFile: string, +): PackageFileContent | null { + logger.trace(`renovate-config-presets.extractPackageFile(${packageFile})`); + let config: RenovateConfig; + try { + config = JSON5.parse(content); + } catch { + logger.debug({ packageFile }, 'Invalid JSON5'); + return null; + } + + const deps: PackageDependency[] = []; + + for (const preset of config.extends ?? []) { + const parsedPreset = parsePreset(preset); + if ( + !supportedPresetSources.some( + (source) => source.source === parsedPreset.presetSource, + ) + ) { + if (parsedPreset.presetSource !== 'internal') { + deps.push({ + depName: parsedPreset.repo, + skipReason: 'unsupported-datasource', + }); + } + continue; + } + + if (is.nullOrUndefined(parsedPreset.tag)) { + deps.push({ + depName: parsedPreset.repo, + skipReason: 'unspecified-version', + }); + continue; + } + + const datasource = supportedPresetSources.find( + (source) => source.source === parsedPreset.presetSource, + )?.datasource; + if (!datasource) { + throw new Error(`Datasource not found for ${parsedPreset.presetSource}`); + } + deps.push({ + depName: parsedPreset.repo, + datasource, + currentValue: parsedPreset.tag, + }); + } + + return is.nonEmptyArray(deps) ? { deps } : null; +} diff --git a/lib/modules/manager/renovate-config-presets/index.ts b/lib/modules/manager/renovate-config-presets/index.ts new file mode 100644 index 00000000000000..67bfbbd53b25a8 --- /dev/null +++ b/lib/modules/manager/renovate-config-presets/index.ts @@ -0,0 +1,20 @@ +import { configFileNames } from '../../../config/app-strings'; +import { GiteaTagsDatasource } from '../../datasource/gitea-tags'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { GitlabTagsDatasource } from '../../datasource/gitlab-tags'; + +export { extractPackageFile } from './extract'; + +export const url = 'https://docs.renovatebot.com/config-presets/'; + +export const defaultConfig = { + fileMatch: configFileNames + .filter((name) => name !== 'package.json') + .map((name) => `^${name.replaceAll('.', '\\.')}$`), +}; + +export const supportedDatasources = [ + GithubTagsDatasource.id, + GitlabTagsDatasource.id, + GiteaTagsDatasource.id, +]; diff --git a/lib/modules/manager/renovate-config-presets/readme.md b/lib/modules/manager/renovate-config-presets/readme.md new file mode 100644 index 00000000000000..852d9c4d8df549 --- /dev/null +++ b/lib/modules/manager/renovate-config-presets/readme.md @@ -0,0 +1,7 @@ +### Unsupported Config + +- [Local presets](https://docs.renovatebot.com/config-presets/#local-presets) +- [HTTP URLs presets](https://docs.renovatebot.com/config-presets/#fetching-presets-from-an-http-server) +- [`package.json` file config](https://docs.renovatebot.com/configuration-options/) (deprecated) +- [`npm` hosted presets](https://docs.renovatebot.com/config-presets/#npm-hosted-presets) (deprecated) +- `package.json` hosted presets (deprecated)