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

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
42 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
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
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,
},
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
{
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 = `
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
{
"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": "http://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);
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
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/')),
Fixed Show fixed Hide fixed
).toHaveLength(1);
expect(res?.deps.filter((dep) => dep.depType === 'dev')).toHaveLength(1);
});

it('applies templates', async () => {
const json = `
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
{
"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": "http://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}}http://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);
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved

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/')),
Fixed Show fixed Hide fixed
).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('http://default.registry.url/'),
Fixed Show fixed Hide fixed
),
).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);
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
});

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);
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
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