diff --git a/lib/manager/composer/extract.js b/lib/manager/composer/extract.js index 7b0fc76580719ffe723c3bf853cd3993de165169..4d62f63cc86f9f56895f095318e982c0ab30721f 100644 --- a/lib/manager/composer/extract.js +++ b/lib/manager/composer/extract.js @@ -4,7 +4,7 @@ module.exports = { extractDependencies, }; -function extractDependencies(content, packageFile) { +async function extractDependencies(content, packageFile) { logger.debug('composer.extractDependencies()'); let packageJson; try { @@ -50,5 +50,14 @@ function extractDependencies(content, packageFile) { if (!deps.length) { return null; } - return { deps }; + let composerLock = false; + const filePath = packageFile.replace(/\.json$/, '.lock'); + if (await platform.getFile(filePath)) { + logger.debug( + { packageFile: packageFile.packageFile }, + 'Found composer.lock' + ); + composerLock = filePath; + } + return { deps, composerLock }; } diff --git a/lib/manager/composer/index.js b/lib/manager/composer/index.js index 9fd44f840d937f71850f246879fbcbfc9b91fd34..5d7233f2472d3e7c128fcceb0d15d11f02868449 100644 --- a/lib/manager/composer/index.js +++ b/lib/manager/composer/index.js @@ -1,10 +1,12 @@ const { extractDependencies } = require('./extract'); const { updateDependency } = require('../npm/update'); +const { getLockFile } = require('./lock-file'); const language = 'php'; module.exports = { extractDependencies, + getLockFile, language, updateDependency, // TODO: support this diff --git a/lib/manager/composer/lock-file.js b/lib/manager/composer/lock-file.js new file mode 100644 index 0000000000000000000000000000000000000000..003fa92e37c1dec447d51c40c5716c5c68c383de --- /dev/null +++ b/lib/manager/composer/lock-file.js @@ -0,0 +1,69 @@ +const { exec } = require('child-process-promise'); +const fs = require('fs-extra'); +const tmp = require('tmp-promise'); +const upath = require('upath'); + +module.exports = { + getLockFile, +}; + +async function getLockFile( + packageFileName, + updatedDeps, + newPackageFileContent +) { + logger.debug(`composer.getLockFile(${packageFileName})`); + const composerLockPath = packageFileName.replace(/\.json$/, '.lock'); + const existingComposerLockContent = await platform.getFile(composerLockPath); + if (!existingComposerLockContent) { + logger.debug('No composer.lock found'); + return null; + } + const tmpDir = await tmp.dir({ unsafeCleanup: true }); + let stdout; + let stderr; + try { + const composerJsonFileName = upath.join(tmpDir.path, 'composer.json'); + await fs.outputFile(composerJsonFileName, newPackageFileContent); + const composerLockFileName = upath.join(tmpDir.path, 'composer.lock'); + await fs.outputFile(composerLockFileName, existingComposerLockContent); + const env = { HOME: process.env.HOME, PATH: process.env.PATH }; + const startTime = process.hrtime(); + const cmd = ('composer update ' + updatedDeps.join(' ')).trim(); + logger.debug({ cmd }); + ({ stdout, stderr } = await exec(cmd, { + cwd: tmpDir.path, + shell: true, + env, + })); + logger.debug(`composer stdout:\n${stdout}`); + logger.debug(`composer stderr:\n${stderr}`); + const duration = process.hrtime(startTime); + const seconds = Math.round(duration[0] + duration[1] / 1e9); + const newComposerLockContent = await fs.readFile( + composerLockFileName, + 'utf8' + ); + logger.info( + { seconds, type: 'composer.lock', stdout, stderr }, + 'Generated lockfile' + ); + if (newComposerLockContent === existingComposerLockContent) { + logger.debug('composer.lock is unchanged'); + return null; + } + logger.debug('Returning updated composer.lock'); + return { + name: composerLockPath, + contents: newComposerLockContent, + }; + } catch (err) { + logger.warn( + { err, message: err.message }, + 'Failed to generate composer.lock' + ); + return null; + } finally { + tmpDir.cleanup(); + } +} diff --git a/lib/workers/branch/get-updated.js b/lib/workers/branch/get-updated.js index cf26300da395fc8a6ae27c11f21f6628d543a7bd..bb58f945470d6e381a72e267975fd71a466c0bbb 100644 --- a/lib/workers/branch/get-updated.js +++ b/lib/workers/branch/get-updated.js @@ -8,9 +8,15 @@ async function getUpdatedPackageFiles(config) { logger.debug('manager.getUpdatedPackageFiles()'); logger.trace({ config }); const updatedFileContents = {}; + const packageFileManagers = {}; + const packageFileUpdatedDeps = {}; for (const upgrade of config.upgrades) { - const { manager, packageFile } = upgrade; + const { manager, packageFile, depName } = upgrade; + packageFileManagers[packageFile] = manager; + packageFileUpdatedDeps[packageFile] = + packageFileUpdatedDeps[packageFile] || []; + packageFileUpdatedDeps[packageFile].push(depName); if (upgrade.updateType !== 'lockFileMaintenance') { const existingContent = updatedFileContents[packageFile] || @@ -46,8 +52,25 @@ async function getUpdatedPackageFiles(config) { name, contents: updatedFileContents[name], })); + const updatedLockFiles = []; + for (const packageFile of updatedPackageFiles) { + const manager = packageFileManagers[packageFile.name]; + const updatedDeps = packageFileUpdatedDeps[packageFile.name]; + const getLockFile = get(manager, 'getLockFile'); + if (getLockFile) { + const updatedLockFile = await getLockFile( + packageFile.name, + updatedDeps, + packageFile.contents + ); + if (updatedLockFile) { + updatedLockFiles.push(updatedLockFile); + } + } + } return { parentBranch: config.parentBranch, // Need to overwrite original config updatedPackageFiles, + updatedLockFiles, }; } diff --git a/test/manager/composer/__snapshots__/extract.spec.js.snap b/test/manager/composer/__snapshots__/extract.spec.js.snap index 0ca7e75d761eb8addc22ed6e187f2b69ab9325e0..1cc39fd69b4abfe1abca5ef06299cf36f0fe1585 100644 --- a/test/manager/composer/__snapshots__/extract.spec.js.snap +++ b/test/manager/composer/__snapshots__/extract.spec.js.snap @@ -1,7 +1,258 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`lib/manager/pip_requirements/extract extractDependencies() extracts dependencies 1`] = ` +exports[`lib/manager/composer/extract extractDependencies() extracts dependencies with lock file 1`] = ` Object { + "composerLock": "composer.lock", + "deps": Array [ + Object { + "currentValue": ">=5.3.2", + "depName": "php", + "depType": "require", + "purl": "pkg:packagist/php", + "skipReason": "unsupported", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "symfony/assetic-bundle", + "depType": "require", + "purl": "pkg:packagist/symfony/assetic-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "symfony/monolog-bundle", + "depType": "require", + "purl": "pkg:packagist/symfony/monolog-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "symfony/swiftmailer-bundle", + "depType": "require", + "purl": "pkg:packagist/symfony/swiftmailer-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "2.1.*", + "depName": "symfony/symfony", + "depType": "require", + "purl": "pkg:packagist/symfony/symfony", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "2.2.2", + "depName": "doctrine/common", + "depType": "require", + "purl": "pkg:packagist/doctrine/common", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "doctrine/doctrine-bundle", + "depType": "require", + "purl": "pkg:packagist/doctrine/doctrine-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "doctrine/doctrine-fixtures-bundle", + "depType": "require", + "purl": "pkg:packagist/doctrine/doctrine-fixtures-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "2.2.x-dev", + "depName": "doctrine/orm", + "depType": "require", + "purl": "pkg:packagist/doctrine/orm", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "exercise/elastica-bundle", + "depType": "require", + "purl": "pkg:packagist/exercise/elastica-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "friendsofsymfony/rest-bundle", + "depType": "require", + "purl": "pkg:packagist/friendsofsymfony/rest-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "*", + "depName": "friendsofsymfony/user-bundle", + "depType": "require", + "purl": "pkg:packagist/friendsofsymfony/user-bundle", + "skipReason": "any-version", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "*", + "depName": "fzaninotto/faker", + "depType": "require", + "purl": "pkg:packagist/fzaninotto/faker", + "skipReason": "any-version", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "1.0.1", + "depName": "jms/di-extra-bundle", + "depType": "require", + "purl": "pkg:packagist/jms/di-extra-bundle", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "*", + "depName": "jms/payment-core-bundle", + "depType": "require", + "purl": "pkg:packagist/jms/payment-core-bundle", + "skipReason": "any-version", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "1.1.0", + "depName": "jms/security-extra-bundle", + "depType": "require", + "purl": "pkg:packagist/jms/security-extra-bundle", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "knplabs/knp-menu-bundle", + "depType": "require", + "purl": "pkg:packagist/knplabs/knp-menu-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "knplabs/knp-paginator-bundle", + "depType": "require", + "purl": "pkg:packagist/knplabs/knp-paginator-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "liip/imagine-bundle", + "depType": "require", + "purl": "pkg:packagist/liip/imagine-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "merk/dough-bundle", + "depType": "require", + "purl": "pkg:packagist/merk/dough-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "sensio/distribution-bundle", + "depType": "require", + "purl": "pkg:packagist/sensio/distribution-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "sensio/framework-extra-bundle", + "depType": "require", + "purl": "pkg:packagist/sensio/framework-extra-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "sensio/generator-bundle", + "depType": "require", + "purl": "pkg:packagist/sensio/generator-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "simplethings/entity-audit-bundle", + "depType": "require", + "purl": "pkg:packagist/simplethings/entity-audit-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "stof/doctrine-extensions-bundle", + "depType": "require", + "purl": "pkg:packagist/stof/doctrine-extensions-bundle", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "dev-master", + "depName": "twig/extensions", + "depType": "require", + "purl": "pkg:packagist/twig/extensions", + "skipReason": "unsupported-constraint", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "2.3.*", + "depName": "behat/behat", + "depType": "require-dev", + "purl": "pkg:packagist/behat/behat", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "*", + "depName": "behat/behat-bundle", + "depType": "require-dev", + "purl": "pkg:packagist/behat/behat-bundle", + "skipReason": "any-version", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "*", + "depName": "behat/mink-bundle", + "depType": "require-dev", + "purl": "pkg:packagist/behat/mink-bundle", + "skipReason": "any-version", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "*", + "depName": "behat/sahi-client", + "depType": "require-dev", + "purl": "pkg:packagist/behat/sahi-client", + "skipReason": "any-version", + "versionScheme": "semverComposer", + }, + Object { + "currentValue": "*", + "depName": "behat/common-contexts", + "depType": "require-dev", + "purl": "pkg:packagist/behat/common-contexts", + "skipReason": "any-version", + "versionScheme": "semverComposer", + }, + ], +} +`; + +exports[`lib/manager/composer/extract extractDependencies() extracts dependencies with no lock file 1`] = ` +Object { + "composerLock": false, "deps": Array [ Object { "currentValue": ">=5.3.2", diff --git a/test/manager/composer/extract.spec.js b/test/manager/composer/extract.spec.js index 62869a20549afacfc6d2e370f37b8c1980e3625f..cf4a7e5e01155255ac1a6ee57262ac5246699ab2 100644 --- a/test/manager/composer/extract.spec.js +++ b/test/manager/composer/extract.spec.js @@ -8,20 +8,25 @@ const requirements1 = fs.readFileSync( 'utf8' ); -describe('lib/manager/pip_requirements/extract', () => { +describe('lib/manager/composer/extract', () => { describe('extractDependencies()', () => { - let config; + let packageFile; beforeEach(() => { - config = {}; + packageFile = 'composer.json'; }); - it('returns null for invalid json', () => { - expect(extractDependencies('nothing here', config)).toBe(null); + it('returns null for invalid json', async () => { + expect(await extractDependencies('nothing here', packageFile)).toBe(null); }); - it('returns null for empty deps', () => { - expect(extractDependencies('{}', config)).toBe(null); + it('returns null for empty deps', async () => { + expect(await extractDependencies('{}', packageFile)).toBe(null); }); - it('extracts dependencies', () => { - const res = extractDependencies(requirements1, config); + it('extracts dependencies with no lock file', async () => { + const res = await extractDependencies(requirements1, packageFile); + expect(res).toMatchSnapshot(); + }); + it('extracts dependencies with lock file', async () => { + platform.getFile.mockReturnValueOnce('some content'); + const res = await extractDependencies(requirements1, packageFile); expect(res).toMatchSnapshot(); }); }); diff --git a/test/manager/composer/lock-file.spec.js b/test/manager/composer/lock-file.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..95d52b4cf919f252e4b5a563d976fe655768f749 --- /dev/null +++ b/test/manager/composer/lock-file.spec.js @@ -0,0 +1,42 @@ +jest.mock('fs-extra'); +jest.mock('child-process-promise'); + +const fs = require('fs-extra'); +const { exec } = require('child-process-promise'); +const composer = require('../../../lib/manager/composer/lock-file'); + +describe('.getLockFile()', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('returns if no composer.lock found', async () => { + expect(await composer.getLockFile('composer.json', [], '{}')).toBeNull(); + }); + it('returns null if unchanged', async () => { + platform.getFile.mockReturnValueOnce('Current composer.lock'); + exec.mockReturnValueOnce({ + stdout: '', + stderror: '', + }); + fs.readFile = jest.fn(() => 'Current composer.lock'); + expect(await composer.getLockFile('composer.json', [], '{}')).toBeNull(); + }); + it('returns updated composer.lock', async () => { + platform.getFile.mockReturnValueOnce('Current composer.lock'); + exec.mockReturnValueOnce({ + stdout: '', + stderror: '', + }); + fs.readFile = jest.fn(() => 'New composer.lock'); + expect( + await composer.getLockFile('composer.json', [], '{}') + ).not.toBeNull(); + }); + it('catches errors', async () => { + platform.getFile.mockReturnValueOnce('Current composer.lock'); + fs.outputFile = jest.fn(() => { + throw new Error('not found'); + }); + expect(await composer.getLockFile('composer.json', [], '{}')).toBeNull(); + }); +}); diff --git a/test/workers/branch/__snapshots__/get-updated.spec.js.snap b/test/workers/branch/__snapshots__/get-updated.spec.js.snap index 6cb392e1064cffd86f9ff85e1e145d4212bf910c..8ac0c1c993f472ce5e6caccccbebfe61b7f3c3e4 100644 --- a/test/workers/branch/__snapshots__/get-updated.spec.js.snap +++ b/test/workers/branch/__snapshots__/get-updated.spec.js.snap @@ -3,6 +3,7 @@ exports[`workers/branch/get-updated getUpdatedPackageFiles() handles content change 1`] = ` Object { "parentBranch": undefined, + "updatedLockFiles": Array [], "updatedPackageFiles": Array [ Object { "contents": "some new content", @@ -15,6 +16,25 @@ Object { exports[`workers/branch/get-updated getUpdatedPackageFiles() handles empty 1`] = ` Object { "parentBranch": undefined, + "updatedLockFiles": Array [], "updatedPackageFiles": Array [], } `; + +exports[`workers/branch/get-updated getUpdatedPackageFiles() handles lock files 1`] = ` +Object { + "parentBranch": undefined, + "updatedLockFiles": Array [ + Object { + "contents": "some contents", + "name": "composer.json", + }, + ], + "updatedPackageFiles": Array [ + Object { + "contents": "some new content", + "name": "undefined", + }, + ], +} +`; diff --git a/test/workers/branch/get-updated.spec.js b/test/workers/branch/get-updated.spec.js index 744ea0f1d5905e4b4fce335a483b3ae94713ec24..859380b14f8e6a7711aae4f619b1e31b67d89a75 100644 --- a/test/workers/branch/get-updated.spec.js +++ b/test/workers/branch/get-updated.spec.js @@ -1,3 +1,4 @@ +const composer = require('../../../lib/manager/composer'); const npm = require('../../../lib/manager/npm'); const { getUpdatedPackageFiles, @@ -12,6 +13,8 @@ describe('workers/branch/get-updated', () => { ...defaultConfig, upgrades: [], }; + composer.updateDependency = jest.fn(); + composer.getLockFile = jest.fn(); npm.updateDependency = jest.fn(); }); it('handles empty', async () => { @@ -40,5 +43,18 @@ describe('workers/branch/get-updated', () => { const res = await getUpdatedPackageFiles(config); expect(res).toMatchSnapshot(); }); + it('handles lock files', async () => { + config.parentBranch = 'some-branch'; + config.upgrades.push({ + manager: 'composer', + }); + composer.updateDependency.mockReturnValue('some new content'); + composer.getLockFile.mockReturnValue({ + name: 'composer.json', + contents: 'some contents', + }); + const res = await getUpdatedPackageFiles(config); + expect(res).toMatchSnapshot(); + }); }); });