mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 02:00:10 +00:00
874f3e3953
* feat(presets): support relative `extends` references inside presets
Presets which are fetched from a repository can now reference sibling
presets relatively, instead of repeating their own source, repository and
tag:
- `./x` resolves against the directory of the preset file with the
reference
- `../x` resolves against the parent directory of that file
- `/x` resolves against the root of the preset repository
The source, repository and tag of the preset which holds the reference are
inherited, so `./system/registries` inside `github>org/repo#v2.0.0`
resolves to `github>org/repo//system/registries#v2.0.0`. A reference which
resolves to the `default` preset at the repository root is emitted in the
plain repository form, so `github>org/repo#v2.0.0`, which keeps it
deduplicated against the way consumers spell the very same preset.
A reference must stay inside its repository, its final path segment must
name a preset file rather than a directory, and it can carry parameters
but neither its own `#tag` nor a `:subPreset`. Parameters may contain a
Handlebars template, including a sub-expression such as
`./group({{ lower (env.TEAM) }})`, while a templated path cannot be
resolved and is left alone. A reference must also not carry a source
prefix, so strings such as `local>./x` or `github>../x` are rejected as
invalid.
References are canonicalized lazily, while the preset which holds them is
fetched in `getPreset()`, after migration and massaging. Only presets from
a repository hosted source, so `github`, `gitlab`, `gitea`, `forgejo` and
`local`, are walked. A relative reference which has no parent preset, for
example in a repository config, an inherited config or in `globalExtends`,
is reported as a validation error for that entry.
Relative entries of a preset's `ignorePresets` are resolved in the same
way, so a preset can neutralize a preset which it references relatively,
as long as the consuming repository does not set `ignorePresets` itself.
A reference which cannot be resolved is kept raw and logged, which lets
consumers neutralize it with `ignorePresets`, and it only fails per entry.
Because relative references cannot be resolved while a config is
validated, they are stripped from a `packageRules` entry before its
presets are resolved. A rule which is then left without any selector is
reported as a warning rather than as an error, since the stripped preset
may still provide the missing selectors. Rules without relative references
keep failing hard as before.
The `renovate-config` manager reports relative and unparsable `extends`
entries with a skipReason, instead of dropping them, and ignores templated
entries which can only be resolved at runtime.
Behavior change: an `extends` string which starts with `./`, `../` or `/`,
such as `./x`, `../x` or `/x`, is now read as a relative reference. Before
it was parsed as a local repository name, which in rare cases could
accidentally resolve through the dot segment normalization of the request
URL. For the same reason a source prefixed relative looking string, such
as `local>/org/repo`, `github>./x` or `npm>./x`, is now rejected as an
invalid preset.
* fix(presets): parse scoped npm presets with explicit `npm>` prefix
`npm>@scope/name` was parsed as a local preset, because the heuristic for
local presets matched before the `npm>` prefix was stripped.
Every other use of `npm>` keeps its current behavior, so `npm>foo` still
falls back to the npm package `renovate-config-foo`, `npm>owner/repo` is
still resolved as a local preset and nested prefixes such as
`local>npm>foo` keep working.
* refactor: remove duplicated preset source
* docs: streamline new docs
* add more docs for skipReason and add requested changes
* Apply suggestions from code review
Co-authored-by: Jamie Tanna <github@jamietanna.co.uk>
* test(presets): drop object literal type casts in relative preset spec
---------
Co-authored-by: Jamie Tanna <github@jamietanna.co.uk>
687 lines
18 KiB
TypeScript
687 lines
18 KiB
TypeScript
import { codeBlock } from 'common-tags';
|
|
import { logger } from '~test/util.ts';
|
|
import { extractPackageFile } from './index.ts';
|
|
|
|
describe('modules/manager/renovate-config/extract', () => {
|
|
describe('extractPackageFile()', () => {
|
|
it('returns null for empty file', () => {
|
|
expect(extractPackageFile('', 'renovate.json')).toBeNull();
|
|
});
|
|
|
|
it('returns null for invalid file', () => {
|
|
expect(
|
|
extractPackageFile('this-is-not-json-object', 'renovate.json'),
|
|
).toBeNull();
|
|
});
|
|
|
|
describe('presets', () => {
|
|
it('returns null for a config file without presets', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"draftPR": true
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toBeNull();
|
|
expect(logger.logger.warn).not.toHaveBeenCalled();
|
|
expect(logger.logger.info).not.toHaveBeenCalled();
|
|
expect(logger.logger.debug).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns null for a config file only contains built-in presets', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"extends": ["config:recommended", ":label(test)", "helpers:pinGitHubActionDigests"]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toBeNull();
|
|
expect(logger.logger.warn).not.toHaveBeenCalled();
|
|
expect(logger.logger.info).not.toHaveBeenCalled();
|
|
expect(logger.logger.debug).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('provides skipReason for unsupported preset sources', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"extends": [
|
|
"fastcore",
|
|
"http://my.server/users/me/repos/renovate-presets/raw/default.json",
|
|
"local>renovate/presets",
|
|
"local>renovate/presets2#1.2.3"
|
|
]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).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 relative presets', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"extends": ["./system/registries", "../shared", "/security/base"]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
depName: './system/registries',
|
|
skipReason: 'unsupported-datasource',
|
|
},
|
|
{
|
|
depName: '../shared',
|
|
skipReason: 'unsupported-datasource',
|
|
},
|
|
{
|
|
depName: '/security/base',
|
|
skipReason: 'unsupported-datasource',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('provides skipReason for malformed presets', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"extends": ["./foo#1.2.3", "github>abc/foo#1.2.3"]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
depName: './foo#1.2.3',
|
|
skipReason: 'invalid-value',
|
|
},
|
|
{
|
|
datasource: 'github-tags',
|
|
depName: 'abc/foo',
|
|
currentValue: '1.2.3',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
|
{ preset: './foo#1.2.3', err: expect.any(Error) },
|
|
'Failed to parse preset',
|
|
);
|
|
});
|
|
|
|
it('ignores templated presets', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"extends": ["local>{{ env.PRESET_REPO }}:python-312", "github>abc/foo#1.2.3"]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-tags',
|
|
depName: 'abc/foo',
|
|
currentValue: '1.2.3',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('provides skipReason for presets without versions', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"extends": [
|
|
"github>abc/foo",
|
|
"gitlab>abc/bar:xyz",
|
|
"gitea>cde/foo//path/xyz"
|
|
]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).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(
|
|
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"
|
|
]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).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(
|
|
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"
|
|
]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).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(
|
|
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"
|
|
]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).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(
|
|
codeBlock`
|
|
{
|
|
// comments are permitted
|
|
"extends": [
|
|
"github>abc/foo#1.2.3",
|
|
],
|
|
}
|
|
`,
|
|
'renovate.json5',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-tags',
|
|
depName: 'abc/foo',
|
|
currentValue: '1.2.3',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('constraints', () => {
|
|
it('returns null for a config file without constraints', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"draftPR": true
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('returns null for a config file has an empty constraints', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"constraints": {}
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('extracts known `ToolName`s with explicit versions', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"constraints": {
|
|
"bazelisk": "1.2.3",
|
|
"maven": "4.0.0"
|
|
}
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'bazelisk',
|
|
packageName: 'bazelbuild/bazelisk',
|
|
versioning: 'semver',
|
|
currentValue: '1.2.3',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'maven',
|
|
packageName: 'containerbase/maven-prebuild',
|
|
versioning: 'maven',
|
|
currentValue: '4.0.0',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('extracts known `ToolName`s with ranges versions', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"constraints": {
|
|
"bazelisk": ">= 1.2.3",
|
|
"maven": "< 4.0.0"
|
|
}
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'bazelisk',
|
|
packageName: 'bazelbuild/bazelisk',
|
|
versioning: 'semver',
|
|
currentValue: '>= 1.2.3',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'maven',
|
|
packageName: 'containerbase/maven-prebuild',
|
|
versioning: 'maven',
|
|
currentValue: '< 4.0.0',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('extracts `ToolName`s from packageRules', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"constraints": {
|
|
"golang": "1.20.5"
|
|
},
|
|
"packageRules": [
|
|
{
|
|
"matchFileNames": ["go.mod"],
|
|
"constraints": {
|
|
"golang": "1.26.0",
|
|
"gomodMod": "1.2.0"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'golang',
|
|
packageName: 'containerbase/golang-prebuild',
|
|
versioning: 'npm',
|
|
currentValue: '1.20.5',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'golang',
|
|
packageName: 'containerbase/golang-prebuild',
|
|
versioning: 'npm',
|
|
currentValue: '1.26.0',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
{
|
|
depName: 'gomodMod',
|
|
skipReason: 'unsupported',
|
|
currentValue: '1.2.0',
|
|
depType: 'constraint',
|
|
commitMessageTopic: '{{{depName}}} constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('handles no `constraints` in packageRules', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"constraints": {
|
|
"golang": "1.20.5"
|
|
},
|
|
"packageRules": [
|
|
{}
|
|
]
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'golang',
|
|
packageName: 'containerbase/golang-prebuild',
|
|
versioning: 'npm',
|
|
currentValue: '1.20.5',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('sets skipReason=unsupported for a constraint that is not a tool', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"constraints": {
|
|
"gomodMod": "1.2.0"
|
|
}
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
depName: 'gomodMod',
|
|
skipReason: 'unsupported',
|
|
currentValue: '1.2.0',
|
|
depType: 'constraint',
|
|
commitMessageTopic: '{{{depName}}} constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('extracts known `ToolName`s with ranges versions', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
"constraints": {
|
|
"bazelisk": ">= 1.2.3",
|
|
"maven": "< 4.0.0"
|
|
}
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'bazelisk',
|
|
packageName: 'bazelbuild/bazelisk',
|
|
versioning: 'semver',
|
|
currentValue: '>= 1.2.3',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'maven',
|
|
packageName: 'containerbase/maven-prebuild',
|
|
versioning: 'maven',
|
|
currentValue: '< 4.0.0',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('supports JSON5', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
codeBlock`
|
|
{
|
|
// comments are permitted
|
|
"constraints": {
|
|
// and no quotes around keys
|
|
gleam: "3.4.5", // and trailing comma
|
|
}
|
|
}
|
|
`,
|
|
'renovate.json5',
|
|
),
|
|
).toEqual({
|
|
deps: [
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'gleam',
|
|
packageName: 'gleam-lang/gleam',
|
|
versioning: 'semver',
|
|
currentValue: '3.4.5',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
it('extracts all types of configuration', () => {
|
|
expect(
|
|
extractPackageFile(
|
|
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"
|
|
],
|
|
"constraints": {
|
|
"bazelisk": ">= 1.2.3",
|
|
"maven": "< 4.0.0"
|
|
}
|
|
}
|
|
`,
|
|
'renovate.json',
|
|
),
|
|
).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',
|
|
},
|
|
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'bazelisk',
|
|
packageName: 'bazelbuild/bazelisk',
|
|
versioning: 'semver',
|
|
currentValue: '>= 1.2.3',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
{
|
|
datasource: 'github-releases',
|
|
depName: 'maven',
|
|
packageName: 'containerbase/maven-prebuild',
|
|
versioning: 'maven',
|
|
currentValue: '< 4.0.0',
|
|
depType: 'tool-constraint',
|
|
commitMessageTopic: '{{{depName}}} tool constraint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
});
|