Skip to content

Commit

Permalink
feat(managers/custom): generic manager for json files (#32784)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <[email protected]>
Co-authored-by: HonkingGoose <[email protected]>
  • Loading branch information
3 people authored Jan 30, 2025
1 parent a3f4098 commit fc8b8f9
Show file tree
Hide file tree
Showing 14 changed files with 964 additions and 40 deletions.
99 changes: 87 additions & 12 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,22 +706,35 @@ You can define custom managers to handle:
- Proprietary file formats or conventions
- Popular file formats not yet supported as a manager by Renovate

Currently we only have one custom manager.
The `regex` manager which is based on using Regular Expression named capture groups.
Renovate has two custom managers:

You must have a named capture group matching (e.g. `(?<depName>.*)`) _or_ configure its corresponding template (e.g. `depNameTemplate`) for these fields:
| Custom manager | Matching engine |
| -------------- | ---------------------------------------------- |
| `regex` | Regular Expression, with named capture groups. |
| `jsonata` | JSONata query. |

To use a custom manager, you need give some information:

1. `fileMatch`: name/pattern of the file to extract deps from
1. `matchStrings`: `regex` patterns or `jsonata` queries used to process the file

The `matchStrings` must capture/extract the following three fields:

- `datasource`
- `depName` and / or `packageName`
- `currentValue`

Use named capture group matching _or_ set a corresponding template.
We recommend you use only _one_ of these methods, or you'll get confused.
Alteratively, you could also use corresponding templates (e.g. `depNameTemplate`) for these fields.
But, we recommend you use only _one_ of these methods, or you'll get confused.

We recommend that you also tell Renovate what `versioning` to use.
If the `versioning` field is missing, then Renovate defaults to using `semver` versioning.
Also, we recommend you explicitly set which `versioning` Renovate should use.

For more details and examples about it, see our [documentation for the `regex` manager](modules/manager/regex/index.md).
Renovate defaults to `semver-coerced` versioning if _both_ condition are met:

- The `versioning` field is missing in the custom manager config
- The Renovate datasource does _not_ set its own default versioning

For more details and examples regarding each custom manager, see our documentation for the [`regex` manager](modules/manager/regex/index.md) and the [`JSONata` manager](modules/manager/jsonata/index.md).
For template fields, use the triple brace `{{{ }}}` notation to avoid Handlebars escaping any special characters.

<!-- prettier-ignore -->
Expand Down Expand Up @@ -763,13 +776,19 @@ This will lead to following update where `1.21-alpine` is the newest version of
image: my.new.registry/aRepository/andImage:1.21-alpine
```
<!-- prettier-ignore -->
!!! note
Can only be used with the custom regex manager.
### currentValueTemplate
If the `currentValue` for a dependency is not captured with a named group then it can be defined in config using this field.
It will be compiled using Handlebars and the regex `groups` result.

### customType

It specifies which custom manager to use. There are two available options: `regex` and `jsonata`.

Example:

```json
Expand All @@ -786,9 +805,24 @@ Example:
}
```

```json title="Parsing a JSON file with a custom manager"
{
"customManagers": [
{
"customType": "jsonata",
"fileFormat": "json",
"fileMatch": ["file.json"],
"matchStrings": [
"packages.{ \"depName\": package, \"currentValue\": version }"
]
}
]
}
```

### datasourceTemplate

If the `datasource` for a dependency is not captured with a named group then it can be defined in config using this field.
If the `datasource` for a dependency is not captured with a named group, then it can be defined in config using this field.
It will be compiled using Handlebars and the regex `groups` result.

### depNameTemplate
Expand All @@ -803,23 +837,60 @@ It will be compiled using Handlebars and the regex `groups` result.

### extractVersionTemplate

If `extractVersion` cannot be captured with a named capture group in `matchString` then it can be defined manually using this field.
If `extractVersion` cannot be captured with a named capture group in `matchString`, then it can be defined manually using this field.
It will be compiled using Handlebars and the regex `groups` result.

### fileFormat

<!-- prettier-ignore -->
!!! note
Can only be used with the custom jsonata manager.

It specifies the syntax of the package file that's managed by the custom `jsonata` manager.
This setting helps the system correctly parse and interpret the configuration file's contents.

Only the `json` format is supported.

```json title="Parsing a JSON file with a custom manager"
{
"customManagers": [
{
"customType": "jsonata",
"fileFormat": "json",
"fileMatch": [".renovaterc"],
"matchStrings": [
"packages.{ \"depName\": package, \"currentValue\": version }"
]
}
]
}
```

### matchStrings

Each `matchStrings` must be a valid regular expression, optionally with named capture groups.
Each `matchStrings` must be one of the following:

1. A valid regular expression, which may optionally include named capture groups (if using `customType=regex`)
2. Or, a valid, escaped [JSONata](https://docs.jsonata.org/overview.html) query (if using `customType=json`)

Example:

```json
```json title="matchStrings with a valid regular expression"
{
"matchStrings": [
"ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)\\s"
]
}
```

```json title="matchStrings with a valid JSONata query"
{
"matchStrings": [
"packages.{ \"depName\": package, \"currentValue\": version }"
]
}
```

### matchStringsStrategy

`matchStringsStrategy` controls behavior when multiple `matchStrings` values are provided.
Expand All @@ -829,6 +900,10 @@ Three options are available:
- `recursive`
- `combination`

<!--prettier-ignore-->
!!! note
`matchStringsStrategy` can only be used in a custom regex manager config!

#### any

Each provided `matchString` will be matched individually to the content of the `packageFile`.
Expand Down
16 changes: 12 additions & 4 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2735,18 +2735,26 @@ const options: RenovateOptions[] = [
description:
'Custom manager to use. Valid only within a `customManagers` object.',
type: 'string',
allowedValues: ['regex'],
allowedValues: ['jsonata', 'regex'],
parents: ['customManagers'],
cli: false,
env: false,
},
{
name: 'matchStrings',
name: 'fileFormat',
description:
'Regex capture rule to use. Valid only within a `customManagers` object.',
'It specifies the syntax of the package file being managed by the custom JSONata manager.',
type: 'string',
allowedValues: ['json'],
parents: ['customManagers'],
cli: false,
env: false,
},
{
name: 'matchStrings',
description: 'Queries to use. Valid only within a `customManagers` object.',
type: 'array',
subType: 'string',
format: 'regex',
parents: ['customManagers'],
cli: false,
env: false,
Expand Down
74 changes: 63 additions & 11 deletions lib/config/validation-helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import is from '@sindresorhus/is';
import jsonata from 'jsonata';
import { logger } from '../../logger';
import type {
RegexManagerConfig,
RegexManagerTemplates,
} from '../../modules/manager/custom/regex/types';
import type { RegexManagerTemplates } from '../../modules/manager/custom/regex/types';
import type { CustomManager } from '../../modules/manager/custom/types';
import { regEx } from '../../util/regex';
import type { ValidationMessage } from '../types';

Expand Down Expand Up @@ -78,21 +77,20 @@ export function isFalseGlobal(
return false;
}

function hasField(
customManager: Partial<RegexManagerConfig>,
field: string,
): boolean {
function hasField(customManager: CustomManager, field: string): boolean {
const templateField = `${field}Template` as keyof RegexManagerTemplates;
const fieldStr =
customManager.customType === 'regex' ? `(?<${field}>` : field;
return !!(
customManager[templateField] ??
customManager.matchStrings?.some((matchString) =>
matchString.includes(`(?<${field}>`),
matchString.includes(fieldStr),
)
);
}

export function validateRegexManagerFields(
customManager: Partial<RegexManagerConfig>,
customManager: CustomManager,
currentPath: string,
errors: ValidationMessage[],
): void {
Expand All @@ -114,7 +112,8 @@ export function validateRegexManagerFields(
} else {
errors.push({
topic: 'Configuration Error',
message: `Each Custom Manager must contain a non-empty matchStrings array`,
message:
'Each Custom Manager `matchStrings` array must have at least one item.',
});
}

Expand All @@ -136,3 +135,56 @@ export function validateRegexManagerFields(
});
}
}

export function validateJSONataManagerFields(
customManager: CustomManager,
currentPath: string,
errors: ValidationMessage[],
): void {
if (!is.nonEmptyString(customManager.fileFormat)) {
errors.push({
topic: 'Configuration Error',
message: 'Each JSONata manager must contain a fileFormat field.',
});
}

if (is.nonEmptyArray(customManager.matchStrings)) {
for (const matchString of customManager.matchStrings) {
try {
jsonata(matchString);
} catch (err) {
logger.debug(
{ err },
'customManager.matchStrings JSONata query validation error',
);
errors.push({
topic: 'Configuration Error',
message: `Invalid JSONata query for ${currentPath}: \`${matchString}\``,
});
}
}
} else {
errors.push({
topic: 'Configuration Error',
message: `Each Custom Manager must contain a non-empty matchStrings array`,
});
}

const mandatoryFields = ['currentValue', 'datasource'];
for (const field of mandatoryFields) {
if (!hasField(customManager, field)) {
errors.push({
topic: 'Configuration Error',
message: `JSONata Managers must contain ${field}Template configuration or ${field} in the query `,
});
}
}

const nameFields = ['depName', 'packageName'];
if (!nameFields.some((field) => hasField(customManager, field))) {
errors.push({
topic: 'Configuration Error',
message: `JSONata Managers must contain depName or packageName in the query or their templates`,
});
}
}
Loading

0 comments on commit fc8b8f9

Please sign in to comment.