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(managers/custom): generic manager for json files #32784

Merged
merged 48 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
cb0dd89
implement jsonata manager
RahulGautamSingh Nov 26, 2024
6caecbd
validation
RahulGautamSingh Nov 28, 2024
739b28f
Merge branch 'main' into feat/generic-manager
RahulGautamSingh Nov 28, 2024
0432ece
fix tests
RahulGautamSingh Nov 28, 2024
8f88d8d
Merge branch 'feat/generic-manager' of https://github.com/RahulGautam…
RahulGautamSingh Nov 28, 2024
754324d
update docs
RahulGautamSingh Nov 28, 2024
c497553
fix tests
RahulGautamSingh Nov 28, 2024
f8a32ea
fix lint issue
RahulGautamSingh Nov 28, 2024
a375c51
refactor
RahulGautamSingh Nov 28, 2024
0b54c6d
fix ci issues
RahulGautamSingh Nov 28, 2024
4c14ac6
remove duplicate code
RahulGautamSingh Nov 28, 2024
caf4513
add jsonata to customType allowed values
RahulGautamSingh Nov 28, 2024
55fc985
Apply Suggestions
RahulGautamSingh Nov 29, 2024
69f6745
refactor: remove unused types
RahulGautamSingh Nov 29, 2024
dabb627
docs: refactor
RahulGautamSingh Nov 29, 2024
7f60365
Update docs/usage/configuration-options.md
RahulGautamSingh Nov 29, 2024
bfd00e1
apply suggestions
RahulGautamSingh Nov 30, 2024
292c91f
docs: redo structure
RahulGautamSingh Nov 30, 2024
1324d83
apply suggestions
RahulGautamSingh Dec 5, 2024
ff13a21
Apply Suggestions
RahulGautamSingh Dec 7, 2024
c8db815
docs: remove redundant codeblock
RahulGautamSingh Dec 7, 2024
c92ad2b
refactor: tests
RahulGautamSingh Dec 7, 2024
28e64e5
fix: docs
RahulGautamSingh Dec 11, 2024
cb25557
fix: types
RahulGautamSingh Dec 11, 2024
d4cfb2d
feat: add new field fileFormat
RahulGautamSingh Dec 11, 2024
96c0024
refactor: simplify logic for handleMatching()
RahulGautamSingh Dec 11, 2024
1ef8764
refactor: apply DRY concept
RahulGautamSingh Dec 11, 2024
ffd6228
fix(types): indentation
RahulGautamSingh Dec 11, 2024
c746d87
Merge branch 'main' into feat/generic-manager
RahulGautamSingh Dec 17, 2024
81d9121
Merge branch 'main' into feat/generic-manager
RahulGautamSingh Dec 18, 2024
4e4739c
validation for JSONata manager
RahulGautamSingh Dec 18, 2024
e66b916
docs(customManagers): fileFormat
RahulGautamSingh Dec 18, 2024
e28ba81
Apply Suggestion
RahulGautamSingh Dec 18, 2024
76b0cc4
fix issues
RahulGautamSingh Dec 18, 2024
0e48b18
apply suggestions
RahulGautamSingh Dec 18, 2024
f4ec390
Apply Suggestions
RahulGautamSingh Dec 19, 2024
66f2577
fix test
RahulGautamSingh Dec 19, 2024
a632dc6
Merge branch 'main' into feat/generic-manager
RahulGautamSingh Dec 19, 2024
d1da658
rebase
RahulGautamSingh Dec 19, 2024
e9ab58a
matchStrings: update description
RahulGautamSingh Dec 19, 2024
a36550e
update docs
RahulGautamSingh Dec 19, 2024
3788661
fix test
RahulGautamSingh Dec 21, 2024
cd68238
Apply Suggestions
RahulGautamSingh Jan 19, 2025
211927d
fix tests
RahulGautamSingh Jan 19, 2025
6f4fa17
fix lint isue
RahulGautamSingh Jan 19, 2025
9a09159
Update configuration-options.md
rarkins Jan 27, 2025
db0fb20
Apply Suggestions
RahulGautamSingh Jan 29, 2025
bd24a50
merge
RahulGautamSingh Jan 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,20 +797,33 @@
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.

## matchQueries

Each `matchQueries` must be a valid regular expression, optionally with named capture groups.

Example:

````json
{
"matchQueries": [
"packages.{ \"depName\": package, \"currentValue\": version }"
]
}

### matchStrings

Each `matchStrings` must be a valid regular expression, optionally with named capture groups.

Example:

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

Check failure on line 826 in docs/usage/configuration-options.md

View workflow job for this annotation

GitHub Actions / lint-docs

Invalid JSON in fenced code block

Unexpected non-whitespace character after JSON at position 96 (line 7 column 1). Fix this manually by ensuring each block is a valid, complete JSON document.

Check failure on line 826 in docs/usage/configuration-options.md

View workflow job for this annotation

GitHub Actions / lint-docs

Invalid JSON in fenced code block

Unexpected non-whitespace character after JSON at position 96 (line 7 column 1). Fix this manually by ensuring each block is a valid, complete JSON document.
### matchStringsStrategy

`matchStringsStrategy` controls behavior when multiple `matchStrings` values are provided.
Expand Down
21 changes: 21 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2749,6 +2749,27 @@ const options: RenovateOptions[] = [
cli: false,
env: false,
},
{
name: 'matchQueries',
description:
'JSON query to use. Valid only within a `customManagers` object of type `jsonata`.',
type: 'array',
subType: 'string',
parents: ['customManagers'],
cli: false,
env: false,
},
{
name: 'matchStrings',
description:
'Regex capture rule to use. Valid only within a `customManagers` object.',
type: 'array',
subType: 'string',
format: 'regex',
parents: ['customManagers'],
cli: false,
env: false,
},
{
name: 'matchStringsStrategy',
description: 'Strategy how to interpret matchStrings.',
Expand Down
1 change: 1 addition & 0 deletions lib/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ export async function validateConfig(
'customType',
'description',
'fileMatch',
'matchQueries',
'matchStrings',
'matchStringsStrategy',
'depNameTemplate',
Expand Down
2 changes: 2 additions & 0 deletions lib/modules/manager/custom/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { ManagerApi } from '../types';
import * as jsonata from './jsonata';
import * as regex from './regex';

const api = new Map<string, ManagerApi>();
export default api;

api.set('regex', regex);
api.set('jsonata', jsonata);
2 changes: 2 additions & 0 deletions lib/modules/manager/custom/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ describe('modules/manager/custom/index', () => {
expect(customManager.isCustomManager('npm')).toBe(false);
expect(customManager.isCustomManager('regex')).toBe(true);
expect(customManager.isCustomManager('custom.regex')).toBe(false);
expect(customManager.isCustomManager('jsonata')).toBe(true);
expect(customManager.isCustomManager('custom.jsonata')).toBe(false);
});
});
});
268 changes: 268 additions & 0 deletions lib/modules/manager/custom/jsonata/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { logger } from '../../../../logger';
import type { JsonataExtractConfig } from './types';
import { defaultConfig, extractPackageFile } from '.';

describe('modules/manager/custom/jsonata/index', () => {
it('has default config', () => {
expect(defaultConfig).toEqual({
fileMatch: [],
});
});

it('extracts data when no templates are used', async () => {
const json = `
{
"packages": [
{
"dep_name": "foo",
"package_name": "fii",
"current_value": "1.2.3",
"current_digest": "1234",
"data_source": "nuget",
"versioning": "maven",
"extract_version": "custom-extract-version",
"registry_url": "https://brr.brr",
"dep_type": "dev"
}
]
}`;
const config = {
matchQueries: [
`packages.{
"depName": dep_name,
"packageName": package_name,
"currentValue": current_value,
"currentDigest": current_digest,
"datasource": data_source,
"versioning": versioning,
"extractVersion": extract_version,
"registryUrl": registry_url,
"depType": dep_type
}`,
],
};
const res = await extractPackageFile(json, 'unused', config);

expect(res?.deps).toHaveLength(1);
expect(res?.deps.filter((dep) => dep.depName === 'foo')).toHaveLength(1);
expect(res?.deps.filter((dep) => dep.packageName === 'fii')).toHaveLength(
1,
);
expect(
res?.deps.filter((dep) => dep.currentValue === '1.2.3'),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.currentDigest === '1234'),
).toHaveLength(1);
expect(res?.deps.filter((dep) => dep.datasource === 'nuget')).toHaveLength(
1,
);
expect(res?.deps.filter((dep) => dep.versioning === 'maven')).toHaveLength(
1,
);
expect(
res?.deps.filter(
(dep) => dep.extractVersion === 'custom-extract-version',
),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.registryUrls?.includes('http://brr.brr/')),
).toHaveLength(1);

Check failure on line 70 in lib/modules/manager/custom/jsonata/index.spec.ts

View workflow job for this annotation

GitHub Actions / test (1/3)

modules/manager/custom/jsonata/index › extracts data when no templates are used

expect(received).toHaveLength(expected) Expected length: 1 Received length: 0 Received array: [] at Object.<anonymous> (lib/modules/manager/custom/jsonata/index.spec.ts:70:7)

Check failure on line 70 in lib/modules/manager/custom/jsonata/index.spec.ts

View workflow job for this annotation

GitHub Actions / test (3/16)

modules/manager/custom/jsonata/index › extracts data when no templates are used

expect(received).toHaveLength(expected) Expected length: 1 Received length: 0 Received array: [] at Object.<anonymous> (lib/modules/manager/custom/jsonata/index.spec.ts:70:7)
expect(res?.deps.filter((dep) => dep.depType === 'dev')).toHaveLength(1);
});

it('applies templates', async () => {
const json = `
{
"packages": [
{
"dep_name": "foo",
"package_name": "fii",
"current_value": "1.2.3",
"current_digest": "1234",
"data_source": "nuget",
"versioning": "maven",
"extract_version": "custom-extract-version",
"registry_url": "https://brr.brr",
"dep_type": "dev"
},
{
}]
}`;
const config = {
matchQueries: [
`packages.{
"depName": dep_name,
"packageName": package_name,
"currentValue": current_value,
"currentDigest": current_digest,
"datasource": data_source,
"versioning": versioning,
"extractVersion": extract_version,
"registryUrl": registry_url,
"depType": dep_type
}`,
],
depNameTemplate:
'{{#if depName}}{{depName}}{{else}}default-dep-name{{/if}}',
packageNameTemplate:
'{{#if packageName}}{{packageName}}{{else}}default-package-name{{/if}}',
currentValueTemplate:
'{{#if currentValue}}{{currentValue}}{{else}}default-current-value{{/if}}',
currentDigestTemplate:
'{{#if currentDigest}}{{currentDigest}}{{else}}default-current-digest{{/if}}',
datasourceTemplate:
'{{#if datasource}}{{datasource}}{{else}}default-datasource{{/if}}',
versioningTemplate:
'{{#if versioning}}{{versioning}}{{else}}default-versioning{{/if}}',
extractVersionTemplate:
'{{#if extractVersion}}{{extractVersion}}{{else}}default-extract-version{{/if}}',
registryUrlTemplate:
'{{#if registryUrl}}{{registryUrl}}{{else}}https://default.registry.url{{/if}}',
depTypeTemplate:
'{{#if depType}}{{depType}}{{else}}default-dep-type{{/if}}',
};
const res = await extractPackageFile(json, 'unused', config);

expect(res?.deps).toHaveLength(2);

expect(res?.deps.filter((dep) => dep.depName === 'foo')).toHaveLength(1);
expect(res?.deps.filter((dep) => dep.packageName === 'fii')).toHaveLength(
1,
);
expect(
res?.deps.filter((dep) => dep.currentValue === '1.2.3'),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.currentDigest === '1234'),
).toHaveLength(1);
expect(res?.deps.filter((dep) => dep.datasource === 'nuget')).toHaveLength(
1,
);
expect(res?.deps.filter((dep) => dep.versioning === 'maven')).toHaveLength(
1,
);
expect(
res?.deps.filter(
(dep) => dep.extractVersion === 'custom-extract-version',
),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.registryUrls?.includes('https://brr.brr/')),
).toHaveLength(1);
expect(res?.deps.filter((dep) => dep.depType === 'dev')).toHaveLength(1);

expect(
res?.deps.filter((dep) => dep.depName === 'default-dep-name'),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.packageName === 'default-package-name'),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.currentValue === 'default-current-value'),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.currentDigest === 'default-current-digest'),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.datasource === 'default-datasource'),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.versioning === 'default-versioning'),
).toHaveLength(1);
expect(
res?.deps.filter(
(dep) => dep.extractVersion === 'default-extract-version',
),
).toHaveLength(1);
expect(
res?.deps.filter((dep) =>
dep.registryUrls?.includes('https://default.registry.url/'),
),
).toHaveLength(1);
expect(
res?.deps.filter((dep) => dep.depType === 'default-dep-type'),
).toHaveLength(1);
});

it('returns null when content is not json', async () => {
jest.mock('renovate/lib/logger');
const res = await extractPackageFile(
'not-json',
'foo-file',
{} as JsonataExtractConfig,
);
expect(res).toBeNull();
expect(logger.warn).toHaveBeenCalledWith(
expect.anything(),
`error parsing 'foo-file'`,
);
});

it('returns null if no dependencies found', async () => {
const config = {
matchQueries: [
'packages.{ "depName": package, "currentValue": version, "versioning ": versioning }',
],
};
const res = await extractPackageFile('{}', 'unused', config);
expect(res).toBeNull();
});

it('returns null if invalid template', async () => {
jest.mock('renovate/lib/logger');
const config = {
matchQueries: [`{"depName": "foo"}`],
versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver', // invalid template
};
const res = await extractPackageFile('{}', 'unused', config);
expect(res).toBeNull();
expect(logger.warn).toHaveBeenCalledWith(
expect.anything(),
'Error compiling template for JSONata manager',
);
});

it('extracts and does not apply a registryUrlTemplate if the result is an invalid url', async () => {
jest.mock('renovate/lib/logger');
const config = {
matchQueries: [`{"depName": "foo"}`],
registryUrlTemplate: 'this-is-not-a-valid-url-{{depName}}',
};
const res = await extractPackageFile('{}', 'unused', config);
expect(res).not.toBeNull();
expect(logger.warn).toHaveBeenCalledWith(
{ value: 'this-is-not-a-valid-url-foo' },
'Invalid json manager registryUrl',
);
});

it('extracts multiple dependencies with multiple matchQueries', async () => {
const config = {
matchQueries: [`{"depName": "foo"}`, `{"depName": "bar"}`],
};
const res = await extractPackageFile('{}', 'unused', config);
expect(res?.deps).toHaveLength(2);
});

it('excludes and warns if invalid jsonata query found', async () => {
const config = {
matchQueries: ['{', `{"depName": "foo"}`, `{"depName": "bar"}`],
};
const res = await extractPackageFile('{}', 'unused', config);
expect(res?.deps).toHaveLength(2);
expect(logger.warn).toHaveBeenCalledWith(
{ err: expect.any(Object) },
`Failed to compile JSONata query: {. Excluding it from queries.`,
);
});

it('extracts dependency with autoReplaceStringTemplate', async () => {
const config = {
matchQueries: [`{"depName": "foo"}`],
autoReplaceStringTemplate: 'auto-replace-string-template',
};
const res = await extractPackageFile('{}', 'values.yaml', config);
expect(res?.autoReplaceStringTemplate).toBe('auto-replace-string-template');
});
});
Loading
Loading