Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datasource): implement custom datasource #23147

Merged
merged 7 commits into from Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/usage/configuration-options.md
Expand Up @@ -633,6 +633,38 @@ When using with `npm`, we recommend you:
- Use `constraintsFiltering` on `dependencies`, not `devDependencies` (usually you do not need to be strict about development dependencies)
- Do _not_ enable `rollbackPrs` at the same time (otherwise your _current_ version may be rolled back if it's incompatible)

## customDatasources

Use `customDatasources` to fetch releases from APIs or statically hosted sites and Renovate has no own datasource.
These datasources can be referred by RegexManagers or can be used to overwrite default datasources.

For more details see the [`custom` datasource documentation](/modules/datasource/custom/).

### defaultRegistryUrlTemplate

`registryUrl` which is used, if none is return by extraction.
As this is a template it can be dynamically set. E.g. add the `packageName` as part of the URL:

```json5
{
customDatasources: {
foo: {
defaultRegistryUrlTemplate: 'https://exmaple.foo.bar/v1/{{ packageName }}',
},
},
}
```

### format

Defines which format the API is returning.
Only `json` is supported, but more are planned for future.

### transformTemplates

`transformTemplates` is a list of [jsonata rules](https://docs.jsonata.org/simple) which get applied serially.
Use this if the API does not return a Renovate compatible schema.

## defaultRegistryUrls

Override a datasource's default registries with this config option.
Expand Down
35 changes: 35 additions & 0 deletions lib/config/options/index.ts
Expand Up @@ -68,6 +68,16 @@ const options: RenovateOptions[] = [
default: ['**/*'],
cli: false,
},
{
name: 'format',
description: 'Format of the custom datasource',
type: 'string',
parent: 'customDatasources',
default: 'json',
allowedValues: ['json'],
cli: false,
env: false,
},
{
name: 'executionMode',
description:
Expand Down Expand Up @@ -334,6 +344,13 @@ const options: RenovateOptions[] = [
type: 'object',
default: {},
},
{
name: 'customDatasources',
description: 'Defines custom datasources for usage by managers',
type: 'object',
experimental: true,
default: {},
},
{
name: 'dockerChildPrefix',
description:
Expand Down Expand Up @@ -923,6 +940,16 @@ const options: RenovateOptions[] = [
cli: false,
env: false,
},
{
name: 'defaultRegistryUrlTemplate',
description:
'Template for generating a defaultRegistryUrl for custom datasource',
type: 'string',
default: '',
parent: 'customDatasources',
rarkins marked this conversation as resolved.
Show resolved Hide resolved
cli: false,
env: false,
},
{
name: 'registryUrls',
description:
Expand Down Expand Up @@ -1741,6 +1768,14 @@ const options: RenovateOptions[] = [
type: 'boolean',
default: false,
},
{
name: 'transformTemplates',
description: 'List of jsonata transformation rules',
type: 'array',
subType: 'string',
parent: 'customDatasources',
default: [],
},
{
name: 'transitiveRemediation',
description: 'Enable remediation of transitive dependencies.',
Expand Down
14 changes: 13 additions & 1 deletion lib/config/types.ts
Expand Up @@ -260,6 +260,7 @@ export interface RenovateConfig
osvVulnerabilityAlerts?: boolean;
vulnerabilitySeverity?: string;
regexManagers?: RegExManager[];
customDatasources?: Record<string, CustomDatasourceConfig>;

fetchReleaseNotes?: FetchReleaseNotesOptions;
secrets?: Record<string, string>;
Expand All @@ -272,6 +273,12 @@ export interface RenovateConfig
checkedBranches?: string[];
}

export interface CustomDatasourceConfig {
defaultRegistryUrlTemplate?: string;
format?: 'json';
transformTemplates?: string[];
}

export interface AllConfig
extends RenovateConfig,
GlobalOnlyConfig,
Expand Down Expand Up @@ -379,7 +386,12 @@ export interface RenovateOptionBase {

name: string;

parent?: 'hostRules' | 'packageRules' | 'postUpgradeTasks' | 'regexManagers';
parent?:
| 'customDatasources'
| 'hostRules'
| 'packageRules'
| 'postUpgradeTasks'
| 'regexManagers';

// used by tests
relatedOptions?: string[];
Expand Down
42 changes: 42 additions & 0 deletions lib/config/validation.spec.ts
Expand Up @@ -117,6 +117,48 @@ describe('config/validation', () => {
expect(errors).toMatchSnapshot();
});

it('catches invalid customDatasources content', async () => {
const config = {
customDatasources: {
foo: {
randomKey: '',
defaultRegistryUrlTemplate: [],
transformTemplates: [{}],
},
},
} as any;
const { errors } = await configValidation.validateConfig(config);
expect(errors).toMatchObject([
{
message:
'Invalid `customDatasources.customDatasources.defaultRegistryUrlTemplate` configuration: is a string',
},
{
message:
'Invalid `customDatasources.customDatasources.randomKey` configuration: key is not allowed',
},
{
message:
'Invalid `customDatasources.customDatasources.transformTemplates` configuration: is not an array of string',
},
]);
});

it('catches invalid customDatasources record type', async () => {
const config = {
customDatasources: {
randomKey: '',
},
} as any;
const { errors } = await configValidation.validateConfig(config);
expect(errors).toMatchObject([
{
message:
'Invalid `customDatasources.randomKey` configuration: customDatasource is not an object',
},
]);
});

it('catches invalid baseBranches regex', async () => {
const config = {
baseBranches: ['/***$}{]][/'],
Expand Down
41 changes: 41 additions & 0 deletions lib/config/validation.ts
Expand Up @@ -568,6 +568,47 @@ export async function validateConfig(
message: `Invalid \`${currentPath}.${key}.${res}\` configuration: value is not a string`,
});
}
} else if (key === 'customDatasources') {
const allowedKeys = [
'description',
'defaultRegistryUrlTemplate',
'format',
'transformTemplates',
];
for (const [
customDatasourceName,
customDatasourceValue,
] of Object.entries(val)) {
if (!is.plainObject(customDatasourceValue)) {
errors.push({
topic: 'Configuration Error',
message: `Invalid \`${currentPath}.${customDatasourceName}\` configuration: customDatasource is not an object`,
});
continue;
}
for (const [subKey, subValue] of Object.entries(
customDatasourceValue
)) {
if (!allowedKeys.includes(subKey)) {
errors.push({
topic: 'Configuration Error',
message: `Invalid \`${currentPath}.${key}.${subKey}\` configuration: key is not allowed`,
});
} else if (subKey === 'transformTemplates') {
if (!is.array(subValue, is.string)) {
errors.push({
topic: 'Configuration Error',
message: `Invalid \`${currentPath}.${key}.${subKey}\` configuration: is not an array of string`,
});
}
} else if (!is.string(subValue)) {
errors.push({
topic: 'Configuration Error',
message: `Invalid \`${currentPath}.${key}.${subKey}\` configuration: is a string`,
});
}
}
}
} else if (
['customEnvVariables', 'migratePresets', 'secrets'].includes(key)
) {
Expand Down
2 changes: 2 additions & 0 deletions lib/modules/datasource/api.ts
Expand Up @@ -11,6 +11,7 @@ import { ConanDatasource } from './conan';
import { CondaDatasource } from './conda';
import { CpanDatasource } from './cpan';
import { CrateDatasource } from './crate';
import { CustomDatasource } from './custom';
import { DartDatasource } from './dart';
import { DartVersionDatasource } from './dart-version';
import { DenoDatasource } from './deno';
Expand Down Expand Up @@ -72,6 +73,7 @@ api.set(ConanDatasource.id, new ConanDatasource());
api.set(CondaDatasource.id, new CondaDatasource());
api.set(CpanDatasource.id, new CpanDatasource());
api.set(CrateDatasource.id, new CrateDatasource());
api.set(CustomDatasource.id, new CustomDatasource());
api.set(DartDatasource.id, new DartDatasource());
api.set(DartVersionDatasource.id, new DartVersionDatasource());
api.set(DenoDatasource.id, new DenoDatasource());
Expand Down