From 7f4cb4aa579f1415a20094884cd38814185a3c12 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Thu, 17 May 2018 07:16:13 +0200 Subject: [PATCH] feat: rangeStrategy (#1954) This PR replaces the existing `pinVersions`, `upgradeInRange` and `versionStrategy` settings with a single one: `rangeStrategy`. Previously: - `pinVersions` could be `true` or `false`, but defaulted to `null`, which meant that Renovate would decide. `true` meant that Renovate would replace existing ranges like `^1.0.0` with an exact/pinned version such as `1.2.0`. - `upgradeInRange` could be true or false, default to false. If `true`, it would mean Renovate would replace an existing range like `^1.0.0` with something like `^1.2.0` - `versionStrategy` could be `replace` or `widen` and was mainly used for `peerDependencies` to widen existing ranges, e.g. from `^1.0.0` to `^1.0.0 || ^2.0.0` It was possible to set conflicting settings, e.g. configuring `pinVersions=true` and `upgradeInRange=true`. Now, we combine them into a single setting: `rangeStrategy`: - `auto` = Renovate decides (this will be done on a manager-by-manager basis) - `pin` = convert ranges to exact versions - `bump` = same as `upgradeInRange` previously, e.g. bump the range even if the new version satisifies the existing range - `replace` = Same as pinVersions === false && upgradeInRange === false, i.e. only replace the range if the new version falls outside it - `widen` = Same as previous versionStrategy==='widen' --- docs/design-decisions.md | 8 -- lib/config/definitions.js | 21 +---- lib/config/migration.js | 20 +++++ lib/manager/npm/package.js | 41 +++++++++- lib/manager/npm/versions.js | 29 ++++--- lib/manager/travis/package.js | 5 +- lib/workers/repository/process/fetch.js | 16 +--- .../config/file-with-repo-presets.js | 4 +- test/config/__snapshots__/index.spec.js.snap | 8 +- .../__snapshots__/migration.spec.js.snap | 19 +++-- .../config/__snapshots__/presets.spec.js.snap | 8 +- test/config/index.spec.js | 8 +- test/config/migration.spec.js | 10 ++- test/config/presets.spec.js | 2 +- test/manager/npm/package.spec.js | 34 ++++++++ test/manager/npm/versions.spec.js | 80 +++++++++---------- test/workers/repository/process/fetch.spec.js | 2 +- website/docs/configuration-options.md | 59 +++++++------- 18 files changed, 222 insertions(+), 152 deletions(-) diff --git a/docs/design-decisions.md b/docs/design-decisions.md index 69b2d9eb2b..143e9712dd 100644 --- a/docs/design-decisions.md +++ b/docs/design-decisions.md @@ -94,14 +94,6 @@ already closed. This allows users to close unwelcome upgrade PRs and worry about them being recreated every run. Typically this is most useful for major upgrades. This option is configurable. -## Range handling - -`renovate` prefers pinned dependency versions, instead of maintaining ranges. -Even if the project is using tilde ranges, why not pin them for consistency if -you're also using `renovate` every day? - -This is now configurable via the `pinVersions` configuration option. - ## Rebasing Unmergeable Pull Requests With the default behaviour of one branch per dependency, it's often that case diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 7600539e49..ba46182ad6 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -405,13 +405,6 @@ const options = [ stage: 'package', type: 'boolean', }, - { - name: 'pinVersions', - description: 'Convert ranged versions to pinned versions', - stage: 'package', - type: 'boolean', - default: false, - }, { name: 'separateMajorReleases', description: @@ -456,20 +449,12 @@ const options = [ type: 'boolean', }, { - name: 'upgradeInRange', - description: - 'Upgrade ranges to latest version even if latest version satisfies existing range', - stage: 'package', - type: 'boolean', - default: false, - }, - { - name: 'versionStrategy', - description: - 'Strategy for how to modify/update existing versions/semver. Possible values: auto, replace, or widen', + name: 'rangeStrategy', + description: 'Policy for how to modify/update existing ranges.', stage: 'package', type: 'string', default: 'auto', + allowedValues: ['auto', 'pin', 'bump', 'replace', 'widen'], cli: false, env: false, }, diff --git a/lib/config/migration.js b/lib/config/migration.js index 2a384f960b..75955ea27a 100644 --- a/lib/config/migration.js +++ b/lib/config/migration.js @@ -91,6 +91,26 @@ function migrateConfig(config) { delete depTypePackageRule.packageRules; migratedConfig.packageRules.push(depTypePackageRule); delete migratedConfig[key]; + } else if (key === 'pinVersions') { + isMigrated = true; + delete migratedConfig.pinVersions; + if (val === true) { + migratedConfig.rangeStrategy = 'pin'; + } else if (val === false) { + migratedConfig.rangeStrategy = 'replace'; + } + } else if (key === 'upgradeInRange') { + isMigrated = true; + delete migratedConfig.upgradeInRange; + if (val === true) { + migratedConfig.rangeStrategy = 'bump'; + } + } else if (key === 'versionStrategy') { + isMigrated = true; + delete migratedConfig.versionStrategy; + if (val === 'widen') { + migratedConfig.rangeStrategy = 'widen'; + } } else if (key === 'semanticPrefix') { isMigrated = true; delete migratedConfig.semanticPrefix; diff --git a/lib/manager/npm/package.js b/lib/manager/npm/package.js index 2ceb48f93f..92173142b1 100644 --- a/lib/manager/npm/package.js +++ b/lib/manager/npm/package.js @@ -2,11 +2,46 @@ const npmApi = require('../../datasource/npm'); const versions = require('./versions'); const { isValidSemver } = require('../../util/semver'); const nodeManager = require('../_helpers/node/package'); +const { parseRange } = require('../../util/semver'); module.exports = { + getRangeStrategy, getPackageUpdates, }; +function getRangeStrategy(config) { + const { + depType, + depName, + packageJsonType, + currentVersion, + rangeStrategy, + } = config; + if (rangeStrategy !== 'auto') { + return rangeStrategy; + } + if (depType === 'devDependencies') { + // Always pin devDependencies + logger.debug({ depName }, 'Pinning devDependency'); + return 'pin'; + } + if (depType === 'dependencies' && packageJsonType === 'app') { + // Pin dependencies if we're pretty sure it's not a browser library + logger.debug('Pinning app dependency'); + return 'pin'; + } + if (depType === 'peerDependencies') { + // Widen peer dependencies + logger.debug('Widening peer dependencies'); + return 'widen'; + } + const semverParsed = parseRange(currentVersion); + if (semverParsed.length > 1) { + return 'widen'; + } + return 'replace'; +} + async function getPackageUpdates(config) { logger.trace({ config }, `npm.getPackageUpdates()`); const { depType, depName, currentVersion } = config; @@ -34,13 +69,17 @@ async function getPackageUpdates(config) { logger.debug(results[0].message); return results; } + const rangeStrategy = getRangeStrategy(config); npmApi.setNpmrc( config.npmrc, config.global ? config.global.exposeEnv : false ); const npmDep = await npmApi.getDependency(depName); if (npmDep) { - results = await versions.determineUpgrades(npmDep, config); + results = await versions.determineUpgrades(npmDep, { + ...config, + rangeStrategy, + }); if (results.length > 0) { logger.info( { dependency: depName }, diff --git a/lib/manager/npm/versions.js b/lib/manager/npm/versions.js index a6c32a4b20..7e17b7648c 100644 --- a/lib/manager/npm/versions.js +++ b/lib/manager/npm/versions.js @@ -29,7 +29,7 @@ function determineUpgrades(npmDep, config) { const result = { type: 'warning', }; - const { lockedVersion, pinVersions, allowedVersions } = config; + const { lockedVersion, rangeStrategy, allowedVersions } = config; const { versions } = npmDep; if (!versions || Object.keys(versions).length === 0) { result.message = `No versions returned from registry for this package`; @@ -54,17 +54,16 @@ function determineUpgrades(npmDep, config) { isPastLatest(npmDep, version) === false // if the version is less than or equal to latest ); let rangeOperator; - if (config.upgradeInRange && isRange(currentVersion)) { - logger.debug({ currentVersion }, 'upgradeInRange is true'); + if (config.rangeStrategy === 'bump' && isRange(currentVersion)) { + logger.debug({ currentVersion }, 'bumping current range'); const parsedRange = parseRange(currentVersion); if (parsedRange && parsedRange.length === 1) { const [range] = parsedRange; if (range.major && range.minor && range.patch) { if (range.operator === '^' || range.operator === '~') { - logger.debug('Applying upgradeInRange'); + logger.debug('Applying in-range bump'); currentVersion = `${range.major}.${range.minor}.${range.patch}`; currentVersion += range.release ? `-${range.release}` : ''; - logger.debug({ currentVersion }, 'upgradeInRange currentVersion'); rangeOperator = range.operator; } else { logger.debug({ currentVersion }, 'Unsupported range type'); @@ -80,7 +79,11 @@ function determineUpgrades(npmDep, config) { // Check for a current range and pin it if (isRange(currentVersion)) { let newVersion; - if (pinVersions && lockedVersion && isPinnedVersion(lockedVersion)) { + if ( + rangeStrategy === 'pin' && + lockedVersion && + isPinnedVersion(lockedVersion) + ) { newVersion = lockedVersion; } else { // Pin ranges to their maximum satisfying version @@ -219,7 +222,11 @@ function determineUpgrades(npmDep, config) { } // Return now if array is empty, or we can keep pinned version upgrades - if (upgrades.length === 0 || config.pinVersions || !isRange(currentVersion)) { + if ( + upgrades.length === 0 || + config.rangeStrategy === 'pin' || + !isRange(currentVersion) + ) { return rangeOperator ? upgrades.map(upgrade => ({ ...upgrade, @@ -268,10 +275,10 @@ function determineUpgrades(npmDep, config) { .map(upgrade => ({ ...upgrade, ...{ isRange: true } })) .map(upgrade => { const { major, minor } = parseVersion(upgrade.newVersion); - const canReplace = config.versionStrategy !== 'widen'; - const forceReplace = config.versionStrategy === 'replace'; - const canWiden = config.versionStrategy !== 'replace'; - const forceWiden = config.versionStrategy === 'widen'; + const canReplace = config.rangeStrategy !== 'widen'; + const forceReplace = config.rangeStrategy === 'replace'; + const canWiden = config.rangeStrategy !== 'replace'; + const forceWiden = config.rangeStrategy === 'widen'; if ( lastSemver.operator === '~' && canReplace && diff --git a/lib/manager/travis/package.js b/lib/manager/travis/package.js index 340970594d..4ecc30c318 100644 --- a/lib/manager/travis/package.js +++ b/lib/manager/travis/package.js @@ -34,7 +34,10 @@ async function getPackageUpdates(config) { .sort() // sort combined array .reverse() // we want to order latest to oldest .map(version => `${version}`); // convert to strings - if (config.pinVersions || isPinnedVersion(config.currentVersion[0])) { + if ( + config.rangeStrategy === 'pin' || + isPinnedVersion(config.currentVersion[0]) + ) { const releases = await getRepoReleases('nodejs/node'); newVersion = newVersion.map(version => maxSatisfyingVersion(releases, version).replace(/^v/, '') diff --git a/lib/workers/repository/process/fetch.js b/lib/workers/repository/process/fetch.js index f7d7e69cf0..33276a4716 100644 --- a/lib/workers/repository/process/fetch.js +++ b/lib/workers/repository/process/fetch.js @@ -12,7 +12,7 @@ module.exports = { async function fetchDepUpdates(packageFileConfig, dep) { /* eslint-disable no-param-reassign */ const { manager, packageFile } = packageFileConfig; - const { depType, depName, currentVersion } = dep; + const { depName, currentVersion } = dep; let depConfig = mergeChildConfig(packageFileConfig, dep); depConfig = applyPackageRules(depConfig); dep.updates = []; @@ -25,25 +25,13 @@ async function fetchDepUpdates(packageFileConfig, dep) { ) { logger.debug( { depName: dep.depName }, - 'Dependency is ignored as part of monorepo' + 'Dependency is ignored due to being internal' ); dep.skipReason = 'internal-package'; } else if (depConfig.enabled === false) { logger.debug({ depName: dep.depName }, 'Dependency is disabled'); dep.skipReason = 'disabled'; } else { - if (depConfig.pinVersions === null && !depConfig.upgradeInRange) { - if (depType === 'devDependencies') { - // Always pin devDependencies - logger.debug({ depName }, 'Pinning devDependency'); - depConfig.pinVersions = true; - } - if (depType === 'dependencies' && depConfig.packageJsonType === 'app') { - // Pin dependencies if we're pretty sure it's not a browser library - logger.debug('Pinning app dependency'); - depConfig.pinVersions = true; - } - } dep.updates = await getPackageUpdates(manager, depConfig); logger.debug({ packageFile, diff --git a/test/_fixtures/config/file-with-repo-presets.js b/test/_fixtures/config/file-with-repo-presets.js index 823610546c..d6ce9aae86 100644 --- a/test/_fixtures/config/file-with-repo-presets.js +++ b/test/_fixtures/config/file-with-repo-presets.js @@ -1,13 +1,13 @@ module.exports = { logLevel: 'error', extends: [':prHourlyLimit1', ':automergePatch'], - upgradeInRange: true, + automerge: true, separatePatchReleases: true, repositories: [ 'bar/baz', { repository: 'foo/bar', - upgradeInRange: false, + automerge: false, }, { repository: 'renovateapp/renovate', diff --git a/test/config/__snapshots__/index.spec.js.snap b/test/config/__snapshots__/index.spec.js.snap index 09afdc585a..b3d948e769 100644 --- a/test/config/__snapshots__/index.spec.js.snap +++ b/test/config/__snapshots__/index.spec.js.snap @@ -2,8 +2,8 @@ exports[`config/index .parseConfigs(env, defaultArgv) resolves all presets: foo/bar 1`] = ` Object { + "automerge": false, "repository": "foo/bar", - "upgradeInRange": false, } `; @@ -180,7 +180,6 @@ Object { "unpublishSafe": false, }, "pinDigests": true, - "pinVersions": false, "pip_requirements": Object { "enabled": false, "fileMatch": Array [ @@ -197,6 +196,7 @@ Object { "prTitle": null, "privateKey": null, "python": Object {}, + "rangeStrategy": "auto", "rebaseStalePrs": null, "recreateClosed": false, "renovateFork": false, @@ -224,8 +224,6 @@ Object { "unstablePattern": null, "updateLockFiles": true, "updateNotScheduled": true, - "upgradeInRange": false, - "versionStrategy": "auto", "yarnrc": null, } `; @@ -245,7 +243,7 @@ Object { "description": Array [ "Use version pinning (maintain a single version only and not semver ranges)", ], - "pinVersions": true, + "rangeStrategy": "pin", "repository": "renovateapp/renovate", } `; diff --git a/test/config/__snapshots__/migration.spec.js.snap b/test/config/__snapshots__/migration.spec.js.snap index 93d4d03a83..6025070470 100644 --- a/test/config/__snapshots__/migration.spec.js.snap +++ b/test/config/__snapshots__/migration.spec.js.snap @@ -77,6 +77,12 @@ Object { "ang", ], }, + Object { + "depTypeList": Array [ + "peerDependencies", + ], + "rangeStrategy": "widen", + }, Object { "depTypeList": Array [ "devDependencies", @@ -107,6 +113,7 @@ Object { "automerge": true, }, "prTitle": "{{#if semanticCommitType}}{{semanticCommitType}}{{#if semanticCommitScope}}({{semanticCommitScope}}){{/if}}: {{/if}}some pr title", + "rangeStrategy": "bump", "schedule": "on the first day of the month", "semanticCommitScope": "deps", "semanticCommitType": "fix", @@ -129,7 +136,7 @@ Object { "paths": Array [ "package.json", ], - "pinVersions": true, + "rangeStrategy": "pin", }, Object { "depTypeList": Array [ @@ -138,7 +145,7 @@ Object { "paths": Array [ "package.json", ], - "pinVersions": true, + "rangeStrategy": "pin", }, ], } @@ -171,13 +178,13 @@ Object { "paths": Array [ "backend/package.json", ], - "pinVersions": false, + "rangeStrategy": "replace", }, Object { "paths": Array [ "frontend/package.json", ], - "pinVersions": true, + "rangeStrategy": "pin", }, Object { "depTypeList": Array [ @@ -186,7 +193,7 @@ Object { "paths": Array [ "other/package.json", ], - "pinVersions": true, + "rangeStrategy": "pin", }, Object { "depTypeList": Array [ @@ -195,7 +202,7 @@ Object { "paths": Array [ "other/package.json", ], - "pinVersions": true, + "rangeStrategy": "pin", }, ], } diff --git a/test/config/__snapshots__/presets.spec.js.snap b/test/config/__snapshots__/presets.spec.js.snap index af43c227c4..f02db8cdfb 100644 --- a/test/config/__snapshots__/presets.spec.js.snap +++ b/test/config/__snapshots__/presets.spec.js.snap @@ -76,7 +76,7 @@ Object { "description": Array [ "Use version pinning (maintain a single version only and not semver ranges)", ], - "pinVersions": true, + "rangeStrategy": "pin", } `; @@ -287,13 +287,13 @@ Object { "packagePatterns": Array [ "*", ], - "pinVersions": false, + "rangeStrategy": "replace", }, Object { "depTypeList": Array [ "devDependencies", ], - "pinVersions": true, + "rangeStrategy": "pin", }, ], "prCreation": "immediate", @@ -670,6 +670,6 @@ Object { "Use version pinning (maintain a single version only and not semver ranges)", ], "foo": 1, - "pinVersions": true, + "rangeStrategy": "pin", } `; diff --git a/test/config/index.spec.js b/test/config/index.spec.js index af69477e66..43d15fca82 100644 --- a/test/config/index.spec.js +++ b/test/config/index.spec.js @@ -199,7 +199,7 @@ describe('config/index', () => { expect(get.mock.calls.length).toBe(0); }); it('resolves all presets', async () => { - defaultArgv.push('--pr-hourly-limit=10', '--upgrade-in-range=false'); + defaultArgv.push('--pr-hourly-limit=10', '--automerge=false'); const env = { GITHUB_TOKEN: 'abc', RENOVATE_CONFIG_FILE: require.resolve( @@ -218,7 +218,7 @@ describe('config/index', () => { expect(actual.minor.automerge).toBeUndefined(); expect(actual.major.automerge).toBeUndefined(); expect(actual.prHourlyLimit).toBe(10); - expect(actual.upgradeInRange).toBe(false); + expect(actual.automerge).toBe(false); actual.repositories.forEach(repo => { if (typeof repo === 'object') { expect(repo).toMatchSnapshot(repo.repository); @@ -233,7 +233,7 @@ describe('config/index', () => { const parentConfig = { ...defaultConfig }; const childConfig = { foo: 'bar', - pinVersions: false, + rangeStrategy: 'replace', lockFileMaintenance: { schedule: ['on monday'], }, @@ -241,7 +241,7 @@ describe('config/index', () => { const configParser = require('../../lib/config/index.js'); const config = configParser.mergeChildConfig(parentConfig, childConfig); expect(config.foo).toEqual('bar'); - expect(config.pinVersions).toBe(false); + expect(config.rangeStrategy).toEqual('replace'); expect(config.lockFileMaintenance.schedule).toEqual(['on monday']); expect(config.lockFileMaintenance).toMatchSnapshot(); }); diff --git a/test/config/migration.spec.js b/test/config/migration.spec.js index 9249b03aeb..49c483c99f 100644 --- a/test/config/migration.spec.js +++ b/test/config/migration.spec.js @@ -13,6 +13,7 @@ describe('config/migration', () => { automergeMajor: false, automergeMinor: true, automergePatch: true, + upgradeInRange: true, baseBranch: 'next', ignoreNodeModules: true, node: { @@ -30,6 +31,9 @@ describe('config/migration', () => { extends: ['foo'], }, ], + peerDependencies: { + versionStrategy: 'widen', + }, packageRules: [ { packagePatterns: '^(@angular|typescript)', @@ -81,7 +85,7 @@ describe('config/migration', () => { expect(isMigrated).toBe(true); expect(migratedConfig.depTypes).not.toBeDefined(); expect(migratedConfig.automerge).toEqual(false); - expect(migratedConfig.packageRules).toHaveLength(6); + expect(migratedConfig.packageRules).toHaveLength(7); }); it('migrates before and after schedules', () => { const config = { @@ -283,8 +287,8 @@ describe('config/migration', () => { expect(migratedConfig.includePaths).toHaveLength(4); expect(migratedConfig.packageFiles).toBeUndefined(); expect(migratedConfig.packageRules).toHaveLength(4); - expect(migratedConfig.packageRules[0].pinVersions).toBe(false); - expect(migratedConfig.packageRules[1].pinVersions).toBe(true); + expect(migratedConfig.packageRules[0].rangeStrategy).toBe('replace'); + expect(migratedConfig.packageRules[1].rangeStrategy).toBe('pin'); }); it('it migrates more packageFiles', () => { const config = { diff --git a/test/config/presets.spec.js b/test/config/presets.spec.js index f417924f6c..e4b8727840 100644 --- a/test/config/presets.spec.js +++ b/test/config/presets.spec.js @@ -107,7 +107,7 @@ describe('config/presets', () => { config.extends = [':pinVersions']; const res = await presets.resolveConfigPresets(config); expect(res).toMatchSnapshot(); - expect(res.pinVersions).toBe(true); + expect(res.rangeStrategy).toEqual('pin'); }); it('throws if valid and invalid', async () => { config.foo = 1; diff --git a/test/manager/npm/package.spec.js b/test/manager/npm/package.spec.js index 5bfc66071f..80241a5b68 100644 --- a/test/manager/npm/package.spec.js +++ b/test/manager/npm/package.spec.js @@ -8,6 +8,40 @@ jest.mock('../../../lib/manager/_helpers/node/package'); npmApi.getDependency = jest.fn(); describe('lib/manager/npm/package', () => { + describe('getRangeStrategy', () => { + it('returns same if not auto', () => { + const config = { rangeStrategy: 'widen' }; + expect(npm.getRangeStrategy(config)).toEqual('widen'); + }); + it('pins devDependencies', () => { + const config = { rangeStrategy: 'auto', depType: 'devDependencies' }; + expect(npm.getRangeStrategy(config)).toEqual('pin'); + }); + it('pins app dependencies', () => { + const config = { + rangeStrategy: 'auto', + depType: 'dependencies', + packageJsonType: 'app', + }; + expect(npm.getRangeStrategy(config)).toEqual('pin'); + }); + it('widens peerDependencies', () => { + const config = { rangeStrategy: 'auto', depType: 'peerDependencies' }; + expect(npm.getRangeStrategy(config)).toEqual('widen'); + }); + it('widens complex ranges', () => { + const config = { + rangeStrategy: 'auto', + depType: 'dependencies', + currentVersion: '^1.6.0 || ^2.0.0', + }; + expect(npm.getRangeStrategy(config)).toEqual('widen'); + }); + it('defaults to replace', () => { + const config = { rangeStrategy: 'auto', depType: 'dependencies' }; + expect(npm.getRangeStrategy(config)).toEqual('replace'); + }); + }); describe('getPackageUpdates', () => { let config; beforeEach(() => { diff --git a/test/manager/npm/versions.spec.js b/test/manager/npm/versions.spec.js index 1e67a13bef..16948c573d 100644 --- a/test/manager/npm/versions.spec.js +++ b/test/manager/npm/versions.spec.js @@ -10,7 +10,7 @@ let config; describe('manager/npm/versions', () => { beforeEach(() => { config = { ...require('../../../lib/config/defaults').getConfig() }; - config.pinVersions = true; + config.rangeStrategy = 'pin'; }); describe('.determineUpgrades(npmDep, config)', () => { @@ -120,27 +120,23 @@ describe('manager/npm/versions', () => { expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('widens minor ranged versions if configured', () => { - config.pinVersions = false; config.currentVersion = '~1.3.0'; - config.versionStrategy = 'widen'; + config.rangeStrategy = 'widen'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('replaces minor complex ranged versions if configured', () => { - config.pinVersions = false; config.currentVersion = '~1.2.0 || ~1.3.0'; - config.versionStrategy = 'replace'; + config.rangeStrategy = 'replace'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('widens major ranged versions if configured', () => { - config.pinVersions = false; config.currentVersion = '^2.0.0'; - config.versionStrategy = 'widen'; + config.rangeStrategy = 'widen'; expect(versions.determineUpgrades(webpackJson, config)).toMatchSnapshot(); }); it('replaces major complex ranged versions if configured', () => { - config.pinVersions = false; config.currentVersion = '^1.0.0 || ^2.0.0'; - config.versionStrategy = 'replace'; + config.rangeStrategy = 'replace'; expect(versions.determineUpgrades(webpackJson, config)).toMatchSnapshot(); }); it('pins minor ranged versions', () => { @@ -153,7 +149,7 @@ describe('manager/npm/versions', () => { expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('ignores minor ranged versions when not pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '^1.0.0'; expect(versions.determineUpgrades(qJson, config)).toHaveLength(0); }); @@ -166,115 +162,115 @@ describe('manager/npm/versions', () => { expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades tilde ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '~1.3.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades .x major ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '0.x'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades .x minor ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '1.3.x'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades shorthand major ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades shorthand minor ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '1.3'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades multiple tilde ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '~0.7.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades multiple caret ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '^0.7.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('supports complex ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'widen'; config.currentVersion = '^0.7.0 || ^0.8.0'; const res = versions.determineUpgrades(qJson, config); expect(res).toHaveLength(2); expect(res[0]).toMatchSnapshot(); }); it('supports complex major ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'widen'; config.currentVersion = '^1.0.0 || ^2.0.0'; const res = versions.determineUpgrades(webpackJson, config); expect(res).toMatchSnapshot(); }); it('supports complex major hyphen ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'widen'; config.currentVersion = '1.x - 2.x'; const res = versions.determineUpgrades(webpackJson, config); expect(res).toMatchSnapshot(); }); it('widens .x OR ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'widen'; config.currentVersion = '1.x || 2.x'; const res = versions.determineUpgrades(webpackJson, config); expect(res).toMatchSnapshot(); }); it('widens stanndalone major OR ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'widen'; config.currentVersion = '1 || 2'; const res = versions.determineUpgrades(webpackJson, config); expect(res).toMatchSnapshot(); }); it('supports complex tilde ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'widen'; config.currentVersion = '~1.2.0 || ~1.3.0'; const res = versions.determineUpgrades(qJson, config); expect(res).toMatchSnapshot(); }); it('returns nothing for greater than ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '>= 0.7.0'; expect(versions.determineUpgrades(qJson, config)).toHaveLength(0); }); it('upgrades less than equal ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '<= 0.7.2'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades less than ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '< 0.7.2'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('upgrades major less than equal ranges', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '<= 1.0.0'; const res = versions.determineUpgrades(qJson, config); expect(res).toMatchSnapshot(); expect(res[0].newVersion).toEqual('<= 2.0.0'); }); it('upgrades major less than ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '< 1.0.0'; const res = versions.determineUpgrades(qJson, config); expect(res).toMatchSnapshot(); expect(res[0].newVersion).toEqual('< 2.0.0'); }); it('upgrades major greater than less than ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '>= 0.5.0 < 1.0.0'; const res = versions.determineUpgrades(qJson, config); expect(res).toMatchSnapshot(); expect(res[0].newVersion).toEqual('>= 0.5.0 < 2.0.0'); }); it('upgrades minor greater than less than ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '>= 0.5.0 <0.8'; const res = versions.determineUpgrades(qJson, config); expect(res).toMatchSnapshot(); @@ -282,7 +278,7 @@ describe('manager/npm/versions', () => { expect(res[1].newVersion).toEqual('>= 0.5.0 <1.5'); }); it('upgrades minor greater than less than equals ranges without pinning', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '>= 0.5.0 <= 0.8.0'; const res = versions.determineUpgrades(qJson, config); expect(res).toMatchSnapshot(); @@ -290,7 +286,7 @@ describe('manager/npm/versions', () => { expect(res[1].newVersion).toEqual('>= 0.5.0 <= 1.5.0'); }); it('rejects reverse ordered less than greater than', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '<= 0.8.0 >= 0.5.0'; const res = versions.determineUpgrades(qJson, config); expect(res).toMatchSnapshot(); @@ -335,12 +331,12 @@ describe('manager/npm/versions', () => { ).toMatchSnapshot(); }); it('should treat zero zero tilde ranges as 0.0.x', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '~0.0.34'; expect(versions.determineUpgrades(helmetJson, config)).toEqual([]); }); it('should treat zero zero caret ranges as pinned', () => { - config.pinVersions = false; + config.rangeStrategy = 'replace'; config.currentVersion = '^0.0.34'; expect(versions.determineUpgrades(helmetJson, config)).toMatchSnapshot(); }); @@ -363,36 +359,32 @@ describe('manager/npm/versions', () => { }); it('does not jump major unstable', () => { config.currentVersion = '^4.4.0-canary.3'; - config.pinVersions = false; + config.rangeStrategy = 'replace'; const res = versions.determineUpgrades(nextJson, config); expect(res).toHaveLength(0); }); it('supports in-range updates', () => { - config.upgradeInRange = true; + config.rangeStrategy = 'bump'; config.currentVersion = '~1.0.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('rejects in-range unsupported operator', () => { - config.upgradeInRange = true; - config.pinVersions = false; + config.rangeStrategy = 'bump'; config.currentVersion = '>=1.0.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('rejects non-fully specified in-range updates', () => { - config.upgradeInRange = true; - config.pinVersions = false; + config.rangeStrategy = 'bump'; config.currentVersion = '1.x'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('rejects complex range in-range updates', () => { - config.upgradeInRange = true; - config.pinVersions = false; + config.rangeStrategy = 'bump'; config.currentVersion = '^0.9.0 || ^1.0.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); it('rejects non-range in-range updates', () => { - config.upgradeInRange = true; - config.pinVersions = false; + config.rangeStrategy = 'bump'; config.currentVersion = '1.0.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); diff --git a/test/workers/repository/process/fetch.spec.js b/test/workers/repository/process/fetch.spec.js index 2f5e6a91fb..4bb48f9248 100644 --- a/test/workers/repository/process/fetch.spec.js +++ b/test/workers/repository/process/fetch.spec.js @@ -51,7 +51,7 @@ describe('workers/repository/process/fetch', () => { expect(packageFiles.npm[0].deps[2].updates).toHaveLength(0); }); it('fetches updates', async () => { - config.pinVersions = null; + config.rangeStrategy = 'auto'; const packageFiles = { npm: [ { diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md index c8cd70e4b4..aa33e40d17 100644 --- a/website/docs/configuration-options.md +++ b/website/docs/configuration-options.md @@ -415,11 +415,11 @@ Use this field if you want to have one or more exact name matches in your packag ``` "packageRules": [{ "packageNames": ["angular"], - "pinVersions": true + "rangeStrategy": "pin" }] ``` -The above will enable `pinVersions` for the package `angular`. +The above will enable set `rangeStrategy` to `pin` only for the package `angular`. ### packagePatterns @@ -428,11 +428,11 @@ Use this field if you want to have one or more package names patterns in your pa ``` "packageRules": [{ "packageNames": ["^angular"], - "pinVersions": false + "rangeStrategy": "replace" }] ``` -The above will enable `pinVersions` for any package starting with `angular`. +The above will set `rangeStrategy` to `replace` for any package starting with `angular`. ## patch @@ -448,10 +448,6 @@ Add to this object if you wish to define rules that apply only to PRs that pin d By default, Renovate will add sha256 digests to Docker source images so that they are then "immutable". Set this to false to continue using only tags to identify source images. -## pinVersions - -This is a very important feature to consider, because not every repository's requirements are the same. The default value within the tool itself is false, which means no existing ranges are pinned. However if you are using the suggested preset `"config:base"`, then it changes the default of pinVersions to `null`, which means Renovate attempts to autodetect what's best for the project. In such cases `devDependencies` in `package.json` will alway be pinned, but `dependencies` will only be pinned if the package is `private` or has no `main` entry defined - both indicators that it is not intended to be published and consumed by other packages. To override the `"config:base"` setting, add the preset `":preserveSemverRanges"` to your `extends` array. - ## pip_requirements ## prBody @@ -494,6 +490,32 @@ The PR title is important for some of Renovate's matching algorithms (e.g. deter ## python +## rangeStrategy + +Behaviour: + +* `auto` = Renovate decides (this will be done on a manager-by-manager basis) +* `pin` = convert ranges to exact versions, e.g. `^1.0.0` -> `1.1.0` +* `bump` = e.g. bump the range even if the new version satisifies the existing range, e.g. `^1.0.0` -> `^1.1.0` +* `replace` = Replace the range with a newer one if the new version falls outside it, e.g. `^1.0.0` -> `^2.0.0` +* `widen` = Widen the range with newer one, e.g. `^1.0.0` -> `^1.0.0 || ^2.0.0` + +Renovate's "auto" strategy works like this for npm: + +1. Always pin `devDependencies` +2. Pin `dependencies` if we detect that it's an app and not a library +3. Widen `peerDependencies` +4. If an existing range already ends with an "or" operator - e.g. `"^1.0.0 || ^2.0.0"` - then Renovate will widen it, e.g. making it into `"^1.0.0 || ^2.0.0 || ^3.0.0"`. +5. Otherwise, replace the range. e.g. `"^2.0.0"` would be replaced by `"^3.0.0"` + +**bump** + +By default, Renovate assumes that if you are using ranges then it's because you want them to be wide/open. As such, Renovate won't deliberately "narrow" any range by increasing the semver value inside. + +For example, if your `package.json` specifies a value for `left-pad` of `^1.0.0` and the latest version on npmjs is `1.2.0`, then Renovate won't change anything because `1.2.0` satisfies the range. If instead you'd prefer to be updated to `^1.2.0` in cases like this, then set `rangeStrategy` to `bump` in your Renovate config. + +This feature supports simple caret (`^`) and tilde (`~`) ranges only, like `^1.0.0` and `~1.0.0`. It is not compatible with `pinVersions=true`. + ## rebaseStalePrs This field is defaulted to `null` because it has a potential to create a lot of noise and additional builds to your repository. If you enable it to true, it means each Renovate branch will be updated whenever the base branch has changed. If enabled, this also means that whenever a Renovate PR is merged (whether by automerge or manually via GitHub web) then any other existing Renovate PRs will then need to get rebased and retested. @@ -629,25 +651,4 @@ When schedules are in use, it generally means "no updates". However there are ca This is default true, meaning that Renovate will perform certain "desirable" updates to _existing_ PRs even when outside of schedule. If you wish to disable all updates outside of scheduled hours then set this field to false. -## upgradeInRange - -By default, Renovate assumes that if you are using ranges then it's because you want them to be wide/open. As such, Renovate won't deliberately "narrow" the range by increasing the semver value inside. - -For example, if your `package.json` specifies a value for `left-pad` of `^1.0.0` and the latest version on npmjs is `1.2.0`, then Renovate won't change anything. If instead you'd prefer to be updated to `^1.2.0` in cases like this, then set `upgradeInRange` to `true` in your Renovate config. - -This feature supports simple caret (`^`) and tilde (`~`) ranges only, like `^1.0.0` and `~1.0.0`. It is not compatible with `pinVersions=true`. - -## versionStrategy - -npm-only. - -Renovate's "auto" strategy for updating versions is like this: - -1. If the existing version already ends with an "or" operator - e.g. `"^1.0.0 || ^2.0.0"` - then Renovate will widen it, e.g. making it into `"^1.0.0 || ^2.0.0 || ^3.0.0"`. -2. Otherwise, replace it. e.g. `"^2.0.0"` would be replaced by `"^3.0.0"` - -You can override logic either way, by setting it to `replace` or `widen`. e.g. if the currentVersion is `"^1.0.0 || ^2.0.0"` but you configure `versionStrategy=replace` then the result will be `"^3.0.0"`. - -Or for example if you configure all `peerDependencies` with `versionStrategy=widen` and have `"react": "^15.0.0"` as current version then it will be updated to `"react": "^15.0.0 || ^16.0.0"`. - ## yarnrc -- GitLab