diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 7bfc318654358a94e63fd81183a08a8de040bfe2..52eb00d15e82d5186f3171045b88b2c9a018baff 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -593,7 +593,14 @@ const options = [ description: 'Policy for how to modify/update existing ranges.', type: 'string', default: 'replace', - allowedValues: ['auto', 'pin', 'bump', 'replace', 'widen'], + allowedValues: [ + 'auto', + 'pin', + 'bump', + 'replace', + 'widen', + 'update-lockfile', + ], cli: false, env: false, }, @@ -825,7 +832,7 @@ const options = [ description: 'Branch topic', type: 'string', default: - '{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x', + '{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}', cli: false, }, { diff --git a/lib/manager/npm/post-update/index.js b/lib/manager/npm/post-update/index.js index fb76bb53279ea5351ac2cbd7a6457eea42d90ee3..4e5579ef40eff6364667ece0ae58c122f54bc155 100644 --- a/lib/manager/npm/post-update/index.js +++ b/lib/manager/npm/post-update/index.js @@ -39,11 +39,16 @@ function determineLockFileDirs(config, packageFiles) { } continue; // eslint-disable-line no-continue } + if (upgrade.isLockfileUpdate) { + yarnLockDirs.push(upgrade.yarnLock); + npmLockDirs.push(upgrade.npmLock); + } } if ( config.upgrades.every( - upgrade => upgrade.updateType === 'lockFileMaintenance' + upgrade => + upgrade.updateType === 'lockFileMaintenance' || upgrade.isLockfileUpdate ) ) { return { @@ -366,12 +371,16 @@ async function getAdditionalFiles(config, packageFiles) { const lockFileDir = path.dirname(lockFile); const fileName = path.basename(lockFile); logger.debug(`Generating ${fileName} for ${lockFileDir}`); + const upgrades = config.upgrades.filter( + upgrade => upgrade.npmLock === lockFile + ); const res = await npm.generateLockFile( upath.join(config.localDir, lockFileDir), env, fileName, config.skipInstalls, - config.binarySource + config.binarySource, + upgrades ); if (res.error) { // istanbul ignore if @@ -415,10 +424,14 @@ async function getAdditionalFiles(config, packageFiles) { const lockFileDir = path.dirname(lockFile); logger.debug(`Generating yarn.lock for ${lockFileDir}`); const lockFileName = upath.join(lockFileDir, 'yarn.lock'); + const upgrades = config.upgrades.filter( + upgrade => upgrade.yarnLock === lockFile + ); const res = await yarn.generateLockFile( upath.join(config.localDir, lockFileDir), env, - config + config, + upgrades ); if (res.error) { // istanbul ignore if diff --git a/lib/manager/npm/post-update/npm.js b/lib/manager/npm/post-update/npm.js index 3238f1b7f67d463695d479571649b3c21d19c547..c9e11dc06703e416435510ca9ff19da5c6ccc6d0 100644 --- a/lib/manager/npm/post-update/npm.js +++ b/lib/manager/npm/post-update/npm.js @@ -12,7 +12,8 @@ async function generateLockFile( env, filename, skipInstalls, - binarySource + binarySource, + upgrades = [] ) { logger.debug(`Spawning npm install to create ${cwd}/${filename}`); let lockFile = null; @@ -76,6 +77,19 @@ async function generateLockFile( })); logger.debug(`npm stdout:\n${stdout}`); logger.debug(`npm stderr:\n${stderr}`); + const lockUpdates = upgrades.filter(upgrade => upgrade.isLockfileUpdate); + if (lockUpdates.length) { + const updateCmd = + cmd + + lockUpdates + .map(update => ` ${update.depName}@${update.toVersion}`) + .join(''); + ({ stdout, stderr } = await exec(updateCmd, { + cwd, + shell: true, + env, + })); + } const duration = process.hrtime(startTime); const seconds = Math.round(duration[0] + duration[1] / 1e9); lockFile = await fs.readFile(upath.join(cwd, filename), 'utf8'); diff --git a/lib/manager/npm/post-update/yarn.js b/lib/manager/npm/post-update/yarn.js index 573aa39b70ca7cab905f0cf73bca299e4ba63618..2fc57c6501039892cf9a5fba65f4f6eede0c6fa1 100644 --- a/lib/manager/npm/post-update/yarn.js +++ b/lib/manager/npm/post-update/yarn.js @@ -7,7 +7,7 @@ module.exports = { generateLockFile, }; -async function generateLockFile(cwd, env, config = {}) { +async function generateLockFile(cwd, env, config = {}, upgrades = []) { const { binarySource, yarnIntegrity } = config; logger.debug(`Spawning yarn install to create ${cwd}/yarn.lock`); let lockFile = null; @@ -73,22 +73,37 @@ async function generateLockFile(cwd, env, config = {}) { cmd = 'yarn'; } logger.debug(`Using yarn: ${cmd}`); - cmd += ' install'; - cmd += ' --ignore-scripts'; - cmd += ' --ignore-engines'; - cmd += ' --ignore-platform'; - cmd += process.env.YARN_MUTEX_FILE + let cmdExtras = ''; + cmdExtras += ' --ignore-scripts'; + cmdExtras += ' --ignore-engines'; + cmdExtras += ' --ignore-platform'; + cmdExtras += process.env.YARN_MUTEX_FILE ? ` --mutex file:${process.env.YARN_MUTEX_FILE}` : ' --mutex network:31879'; - + const installCmd = cmd + ' install' + cmdExtras; // TODO: Switch to native util.promisify once using only node 8 - ({ stdout, stderr } = await exec(cmd, { + ({ stdout, stderr } = await exec(installCmd, { cwd, shell: true, env, })); logger.debug(`yarn stdout:\n${stdout}`); logger.debug(`yarn stderr:\n${stderr}`); + const lockUpdates = upgrades + .filter(upgrade => upgrade.isLockfileUpdate) + .map(upgrade => upgrade.depName); + if (lockUpdates.length) { + const updateCmd = + cmd + + ' upgrade' + + lockUpdates.map(depName => ` ${depName}`).join('') + + cmdExtras; + ({ stdout, stderr } = await exec(updateCmd, { + cwd, + shell: true, + env, + })); + } const duration = process.hrtime(startTime); const seconds = Math.round(duration[0] + duration[1] / 1e9); lockFile = await fs.readFile(upath.join(cwd, 'yarn.lock'), 'utf8'); diff --git a/lib/versioning/npm/range.js b/lib/versioning/npm/range.js index 05ef352bf84c5723fea475e548499e4066bde54c..2f1c77fbbb95f703cf1a91a6b815440f4370149a 100644 --- a/lib/versioning/npm/range.js +++ b/lib/versioning/npm/range.js @@ -4,6 +4,7 @@ const { minor, patch, prerelease, + satisfies, valid: isVersion, } = require('semver'); const { parseRange } = require('semver-utils'); @@ -16,6 +17,12 @@ function getNewValue(currentValue, rangeStrategy, fromVersion, toVersion) { if (rangeStrategy === 'pin' || isVersion(currentValue)) { return toVersion; } + if (rangeStrategy === 'lockfile-update') { + if (satisfies(toVersion, currentValue)) { + return currentValue; + } + return getNewValue(currentValue, 'replace', fromVersion, toVersion); + } const parsedRange = parseRange(currentValue); const element = parsedRange[parsedRange.length - 1]; if (rangeStrategy === 'widen') { diff --git a/lib/workers/repository/onboarding/pr/pr-list.js b/lib/workers/repository/onboarding/pr/pr-list.js index 391f15578c683157327fc92bdfc3e5b42ece301a..87f02c452299ed3ab58d298cc3c80909e2dc13ce 100644 --- a/lib/workers/repository/onboarding/pr/pr-list.js +++ b/lib/workers/repository/onboarding/pr/pr-list.js @@ -41,7 +41,9 @@ function getPrList(config, branches) { } else { text += upgrade.depName.replace(prTitleRe, '@​$1'); } - text += ` to \`${upgrade.newDigest || upgrade.newValue}\``; + text += upgrade.isLockfileUpdate + ? ` to \`${upgrade.toVersion}\`` + : ` to \`${upgrade.newDigest || upgrade.newValue}\``; text += '\n'; } if (!seen.includes(text)) { diff --git a/lib/workers/repository/process/lookup/index.js b/lib/workers/repository/process/lookup/index.js index 415deb7fddd67d4b233ed9c168e5b5ee64750a47..92ebe130395ace32027c6f6efb9163e8e95012f2 100644 --- a/lib/workers/repository/process/lookup/index.js +++ b/lib/workers/repository/process/lookup/index.js @@ -13,7 +13,7 @@ module.exports = { }; async function lookupUpdates(config) { - const { depName, currentValue } = config; + const { depName, currentValue, lockedVersion } = config; logger.trace({ dependency: depName, currentValue }, 'lookupUpdates'); const { equals, @@ -126,10 +126,15 @@ async function lookupUpdates(config) { newMajor: getMajor(fromVersion), }); } + let filterStart = fromVersion; + if (lockedVersion && rangeStrategy === 'lockfile-update') { + // Look for versions greater than the current locked version that still satisfy the package.json range + filterStart = lockedVersion; + } // Filter latest, unstable, etc const filteredVersions = filterVersions( config, - fromVersion, + filterStart, dependency.latestVersion, allVersions, releases @@ -147,12 +152,19 @@ async function lookupUpdates(config) { toVersion ); if (!update.newValue || update.newValue === currentValue) { - continue; // eslint-disable-line no-continue + if (!config.lockedVersion) { + continue; // eslint-disable-line no-continue + } + update.updateType = 'lockfileUpdate'; + update.fromVersion = lockedVersion; + update.isSingleVersion = true; } update.newMajor = getMajor(toVersion); update.newMinor = getMinor(toVersion); - update.updateType = getType(config, fromVersion, toVersion); - update.isSingleVersion = !!isSingleVersion(update.newValue); + update.updateType = + update.updateType || getType(config, update.fromVersion, toVersion); + update.isSingleVersion = + update.isSingleVersion || !!isSingleVersion(update.newValue); if (!isVersion(update.newValue)) { update.isRange = true; } @@ -186,7 +198,7 @@ async function lookupUpdates(config) { release => filteredVersions.length && (filteredVersions.includes(release.version) || - release.version === fromVersion) + release.version === filterStart) ); } else if (!currentValue) { res.skipReason = 'unsupported-value'; @@ -245,8 +257,8 @@ async function lookupUpdates(config) { } for (const update of res.updates) { const { updateType, fromVersion, toVersion } = update; - if (updateType === 'bump') { - update.isBump = true; + if (['bump', 'lockfileUpdate'].includes(updateType)) { + update[updateType === 'bump' ? 'isBump' : 'isLockfileUpdate'] = true; if (getMajor(toVersion) > getMajor(fromVersion)) { update.updateType = 'major'; } else if ( @@ -265,6 +277,7 @@ async function lookupUpdates(config) { .filter( update => update.newValue !== config.currentValue || + update.isLockfileUpdate || (update.newDigest && !update.newDigest.startsWith(config.currentDigest)) ); if (res.updates.some(update => update.updateType === 'pin')) { @@ -301,6 +314,9 @@ function getType(config, fromVersion, toVersion) { function getBucket(config, update) { const { separateMajorMinor, separateMultipleMajor } = config; const { updateType, newMajor } = update; + if (updateType === 'lockfileUpdate') { + return updateType; + } if ( !separateMajorMinor || config.major.automerge === true || diff --git a/test/workers/branch/lock-files/npm.spec.js b/test/workers/branch/lock-files/npm.spec.js index 15a233b676bd3131735e69563f96affe1253826c..1e36155ff5b926d555e4a4b743c28c5c197dadac 100644 --- a/test/workers/branch/lock-files/npm.spec.js +++ b/test/workers/branch/lock-files/npm.spec.js @@ -29,6 +29,33 @@ describe('generateLockFile', () => { expect(res.error).not.toBeDefined(); expect(res.lockFile).toEqual('package-lock-contents'); }); + it('performs lock file updates', async () => { + getInstalledPath.mockReturnValueOnce('node_modules/npm'); + exec.mockReturnValueOnce({ + stdout: '', + stderror: '', + }); + exec.mockReturnValueOnce({ + stdout: '', + stderror: '', + }); + fs.readFile = jest.fn(() => 'package-lock-contents'); + const skipInstalls = true; + const updates = [ + { depName: 'some-dep', toVersion: '1.0.1', isLockfileUpdate: true }, + ]; + const res = await npmHelper.generateLockFile( + 'some-dir', + {}, + 'package-lock.json', + skipInstalls, + null, + updates + ); + expect(fs.readFile.mock.calls.length).toEqual(1); + expect(res.error).not.toBeDefined(); + expect(res.lockFile).toEqual('package-lock-contents'); + }); it('performs full install', async () => { getInstalledPath.mockReturnValueOnce('node_modules/npm'); exec.mockReturnValueOnce({ diff --git a/test/workers/branch/lock-files/yarn.spec.js b/test/workers/branch/lock-files/yarn.spec.js index 6f487f9c43ae488b97b88264be8f84018615e553..ad03a7eb8498fc410193e5544d1e8d110b7fa298 100644 --- a/test/workers/branch/lock-files/yarn.spec.js +++ b/test/workers/branch/lock-files/yarn.spec.js @@ -22,6 +22,22 @@ describe('generateLockFile', () => { expect(fs.readFile.mock.calls.length).toEqual(1); expect(res.lockFile).toEqual('package-lock-contents'); }); + it('performs lock file updates', async () => { + getInstalledPath.mockReturnValueOnce('node_modules/yarn'); + exec.mockReturnValueOnce({ + stdout: '', + stderror: '', + }); + exec.mockReturnValueOnce({ + stdout: '', + stderror: '', + }); + fs.readFile = jest.fn(() => 'package-lock-contents'); + const res = await yarnHelper.generateLockFile('some-dir', {}, {}, [ + { depName: 'some-dep', isLockfileUpdate: true }, + ]); + expect(res.lockFile).toEqual('package-lock-contents'); + }); it('catches errors', async () => { getInstalledPath.mockReturnValueOnce('node_modules/yarn'); exec.mockReturnValueOnce({ diff --git a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap index 8e3d6cf62e052cd484fa6c1c6034106c513e4c9a..6bf543f53acd0c054cbb0685546eb3be36b59c6b 100644 --- a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap +++ b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap @@ -1143,6 +1143,48 @@ Array [ ] `; +exports[`workers/repository/process/lookup .lookupUpdates() supports lock file updates mixed with regular updates 1`] = ` +Array [ + Object { + "canBeUnpublished": false, + "fromVersion": "0.4.0", + "isLockfileUpdate": true, + "isRange": true, + "isSingleVersion": true, + "newMajor": 0, + "newMinor": 4, + "newValue": "^0.4.0", + "releaseTimestamp": "2011-06-10T17:20:04.719Z", + "toVersion": "0.4.4", + "updateType": "minor", + }, + Object { + "canBeUnpublished": false, + "fromVersion": "0.4.4", + "isRange": true, + "isSingleVersion": false, + "newMajor": 0, + "newMinor": 9, + "newValue": "^0.9.0", + "releaseTimestamp": "2013-09-04T17:07:22.948Z", + "toVersion": "0.9.7", + "updateType": "minor", + }, + Object { + "canBeUnpublished": false, + "fromVersion": "0.4.4", + "isRange": true, + "isSingleVersion": false, + "newMajor": 1, + "newMinor": 4, + "newValue": "^1.0.0", + "releaseTimestamp": "2015-05-17T04:25:07.299Z", + "toVersion": "1.4.1", + "updateType": "major", + }, +] +`; + exports[`workers/repository/process/lookup .lookupUpdates() supports majorgte updates 1`] = ` Array [ Object { diff --git a/test/workers/repository/process/lookup/index.spec.js b/test/workers/repository/process/lookup/index.spec.js index 570f5e6b7a821581d80c77459426b9c13cac6d79..1683b30e536169c30bdc7001069528eb9993f84d 100644 --- a/test/workers/repository/process/lookup/index.spec.js +++ b/test/workers/repository/process/lookup/index.spec.js @@ -54,6 +54,17 @@ describe('workers/repository/process/lookup', () => { .reply(200, qJson); expect((await lookup.lookupUpdates(config)).updates).toMatchSnapshot(); }); + it('supports lock file updates mixed with regular updates', async () => { + config.currentValue = '^0.4.0'; + config.rangeStrategy = 'lockfile-update'; + config.depName = 'q'; + config.purl = 'pkg:npm/q'; + config.lockedVersion = '0.4.0'; + nock('https://registry.npmjs.org') + .get('/q') + .reply(200, qJson); + expect((await lookup.lookupUpdates(config)).updates).toMatchSnapshot(); + }); it('returns multiple updates if grouping but separateMajorMinor=true', async () => { config.groupName = 'somegroup'; config.currentValue = '0.4.0'; diff --git a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap index ed3ac155df8e09c9e8fddd099b54f92e17261ebd..b8747b96ba46783efefd41d70ac79c246275426d 100644 --- a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap +++ b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap @@ -10,7 +10,7 @@ Array [ "binarySource": "bundled", "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}", "branchPrefix": "renovate/", - "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x", + "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}", "bumpVersion": null, "commitBody": null, "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}", @@ -107,7 +107,7 @@ Array [ "binarySource": "bundled", "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}", "branchPrefix": "renovate/", - "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x", + "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}", "bumpVersion": null, "commitBody": null, "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}", @@ -301,7 +301,7 @@ Array [ "binarySource": "bundled", "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}", "branchPrefix": "renovate/", - "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x", + "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}", "bumpVersion": null, "commitBody": null, "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}", @@ -495,7 +495,7 @@ Array [ "binarySource": "bundled", "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}", "branchPrefix": "renovate/", - "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x", + "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}", "bumpVersion": null, "commitBody": null, "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}", @@ -592,7 +592,7 @@ Array [ "binarySource": "bundled", "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}", "branchPrefix": "renovate/", - "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x", + "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}", "bumpVersion": null, "commitBody": null, "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}", @@ -689,7 +689,7 @@ Array [ "binarySource": "bundled", "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}", "branchPrefix": "renovate/", - "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x", + "branchTopic": "{{{depNameSanitized}}}-{{{newMajor}}}{{#if isPatch}}.{{{newMinor}}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}", "bumpVersion": null, "commitBody": null, "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}", diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md index 9634e07dcee6dc1b809298cff437638286d0682d..797cf91afa9139ffd8d80114624961901e0eadfc 100644 --- a/website/docs/configuration-options.md +++ b/website/docs/configuration-options.md @@ -758,6 +758,7 @@ Behaviour: - `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` +- `update-lockfile` = Update the lock file when in-range updates are available, otherwise 'replace' for updates out of range Renovate's "auto" strategy works like this for npm: