diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 22564f7dce069e7fcbedf30c0926df0a17340d63..27c819537385f3aa80ff67a0a231c739ee56c339 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -418,6 +418,16 @@ const options = [ cli: false, env: false, }, + { + name: 'matchCurrentVersion', + description: + 'A version or version range to match against the current version of a package. Valid only within `packageRules` object', + type: 'string', + stage: 'depType', + mergeable: true, + cli: false, + env: false, + }, // Version behaviour { name: 'allowedVersions', diff --git a/lib/config/validation.js b/lib/config/validation.js index 3b7d31db5dec109b5cbfba964a2345e0f6d9ad59..cea56a8f1a951909c2bc2cac8876857e3220650c 100644 --- a/lib/config/validation.js +++ b/lib/config/validation.js @@ -169,6 +169,17 @@ async function validateConfig(config, isPreset, parentPath) { message, }); } + if ( + resolvedRule.matchCurrentVersion && + !isValidSemver(resolvedRule.matchCurrentVersion) + ) { + errors.push({ + depName: 'Configuration Error', + message: `${currentPath}: ${ + resolvedRule.matchCurrentVersion + } isn't a valid semver`, + }); + } } else { errors.push({ depName: 'Configuration Error', @@ -197,7 +208,7 @@ async function validateConfig(config, isPreset, parentPath) { } } if ( - selectors.includes(key) && + (selectors.includes(key) || key === 'matchCurrentVersion') && !(parentPath && parentPath.match(/packageRules\[\d+\]$/)) && // Inside a packageRule (parentPath || !isPreset) // top level in a preset ) { diff --git a/lib/util/semver.js b/lib/util/semver.js index b4e054df13329e20257610059670a1c081ca44c9..7c0f41224ce5009bc73b9846b32e001dbd1ce800 100644 --- a/lib/util/semver.js +++ b/lib/util/semver.js @@ -10,6 +10,7 @@ const { parseRange, parse: parseVersion, stringifyRange } = semverUtils; const { compare: semverSort, gt: isGreaterThan, + intersects: intersectsSemver, maxSatisfying: maxSatisfyingVersion, minSatisfying: minSatisfyingVersion, minor: getMinor, @@ -34,6 +35,7 @@ module.exports = { getMajor, getMinor, getPatch, + intersectsSemver, isGreaterThan, isRange, isStable, diff --git a/lib/workers/package-file/dep-type.js b/lib/workers/package-file/dep-type.js index 3e88cf53744da2e69af9a30bc2a4b0d5e577950b..45d22cea6ec2dc1a7872b578b5066edff9c4cec3 100644 --- a/lib/workers/package-file/dep-type.js +++ b/lib/workers/package-file/dep-type.js @@ -1,6 +1,7 @@ const configParser = require('../../config'); const pkgWorker = require('./package'); const { extractDependencies } = require('../../manager'); +const { intersectsSemver } = require('../../util/semver'); module.exports = { renovateDepType, @@ -62,6 +63,7 @@ function getDepConfig(depTypeConfig, dep) { excludePackagePatterns, packageNames, packagePatterns, + matchCurrentVersion, } = packageRule; let applyRule; if ( @@ -104,6 +106,9 @@ function getDepConfig(depTypeConfig, dep) { } } } + if (applyRule !== false && matchCurrentVersion) { + applyRule = intersectsSemver(dep.currentVersion, matchCurrentVersion); + } if (applyRule !== false && depTypeList && depTypeList.length) { applyRule = depTypeList.includes(dep.depType); } @@ -114,6 +119,8 @@ function getDepConfig(depTypeConfig, dep) { delete depConfig.packagePatterns; delete depConfig.excludePackageNames; delete depConfig.excludePackagePatterns; + delete depConfig.depTypeList; + delete depConfig.matchCurrentVersion; } }); } diff --git a/test/config/__snapshots__/index.spec.js.snap b/test/config/__snapshots__/index.spec.js.snap index 99616eb51a24e09fdaab45f15691337e354094cd..15bad95a23f141d823224d2c158e5dd1bf209824 100644 --- a/test/config/__snapshots__/index.spec.js.snap +++ b/test/config/__snapshots__/index.spec.js.snap @@ -114,6 +114,7 @@ Object { "logLevel": "error", "major": Object {}, "managerBranchPrefix": "", + "matchCurrentVersion": null, "meteor": Object {}, "minor": Object {}, "mirrorMode": false, diff --git a/test/config/__snapshots__/validation.spec.js.snap b/test/config/__snapshots__/validation.spec.js.snap index ec7d7a889a0c65479bf7ff469752d4739b490902..f5b8df6ac42031e6789b0befb9781657ffe85612 100644 --- a/test/config/__snapshots__/validation.spec.js.snap +++ b/test/config/__snapshots__/validation.spec.js.snap @@ -68,6 +68,15 @@ Array [ ] `; +exports[`config/validation validateConfig(config) invalid matchCurrentVersion triggers an error 1`] = ` +Array [ + Object { + "depName": "Configuration Error", + "message": "packageRules: >= 2.-1.4 isn't a valid semver", + }, +] +`; + exports[`config/validation validateConfig(config) returns nested errors 1`] = ` Array [ Object { diff --git a/test/config/validation.spec.js b/test/config/validation.spec.js index 136f1ed8d58bbeeb2be0a21ed2d6e46b35295fcc..d010aaf5979842b786526b0c7e16d7bd7f0b5d96 100644 --- a/test/config/validation.spec.js +++ b/test/config/validation.spec.js @@ -107,5 +107,21 @@ describe('config/validation', () => { expect(errors).toMatchSnapshot(); expect(errors).toHaveLength(0); }); + it('invalid matchCurrentVersion triggers an error', async () => { + const config = { + packageRules: [ + { + packageNames: ['angular'], + matchCurrentVersion: '>= 2.-1.4', + }, + ], + }; + const { warnings, errors } = await configValidation.validateConfig( + config + ); + expect(warnings).toHaveLength(0); + expect(errors).toMatchSnapshot(); + expect(errors).toHaveLength(1); + }); }); }); diff --git a/test/workers/package-file/dep-type.spec.js b/test/workers/package-file/dep-type.spec.js index e0108164524f750b6c784963159168e4557227e2..9bcfb3be4388119d24943face5602b289f2bd2fc 100644 --- a/test/workers/package-file/dep-type.spec.js +++ b/test/workers/package-file/dep-type.spec.js @@ -260,5 +260,53 @@ describe('lib/workers/package-file/dep-type', () => { expect(res.x).toBeUndefined(); expect(res.packageRules).toBeUndefined(); }); + it('checks if matchCurrentVersion selector is valid and satisfies the condition on range overlap', () => { + const config = { + packageRules: [ + { + packageNames: ['test'], + matchCurrentVersion: '<= 2.0.0', + x: 1, + }, + ], + }; + const res1 = depTypeWorker.getDepConfig(config, { + depName: 'test', + currentVersion: '^1.0.0', + }); + expect(res1.x).toBeDefined(); + }); + it('checks if matchCurrentVersion selector is valid and satisfies the condition on pinned to range overlap', () => { + const config = { + packageRules: [ + { + packageNames: ['test'], + matchCurrentVersion: '>= 2.0.0', + x: 1, + }, + ], + }; + const res1 = depTypeWorker.getDepConfig(config, { + depName: 'test', + currentVersion: '2.4.6', + }); + expect(res1.x).toBeDefined(); + }); + it('checks if matchCurrentVersion selector works with static values', () => { + const config = { + packageRules: [ + { + packageNames: ['test'], + matchCurrentVersion: '4.6.0', + x: 1, + }, + ], + }; + const res1 = depTypeWorker.getDepConfig(config, { + depName: 'test', + currentVersion: '4.6.0', + }); + expect(res1.x).toBeDefined(); + }); }); }); diff --git a/website/docs/_posts/2017-10-05-configuration-options.md b/website/docs/_posts/2017-10-05-configuration-options.md index 303b02ff45da662f5918fa3eeea4278b4917a991..fe1c5afc2ec4556f9c994c7e2420a00b1d7d8b6e 100644 --- a/website/docs/_posts/2017-10-05-configuration-options.md +++ b/website/docs/_posts/2017-10-05-configuration-options.md @@ -618,6 +618,16 @@ Prefix to be added after `branchPrefix` for distinguishing between branches for This value defaults to empty string, as historically no prefix was necessary for when Renovate was JS-only. Now - for example - we use `docker-` for Docker branches, so they may look like `renovate/docker-ubuntu-16.x`. +## matchCurrentVersion + +If set in a packageRule, the rule will be applied only if the current version of the package matches against this version or range. + +| name | value | +| ---- | ------ | +| type | string | + +`matchCurrentVersion` can be an exact semver version or a semver range. + ## meteor Configuration specific for meteor updates.