From 5f59a0b5a83c0e270d434e3e2b61b6b573dda91d Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Tue, 11 Jun 2019 15:06:29 +0200
Subject: [PATCH] feat(composer): support lock file maintenance (#3912)

---
 lib/manager/composer/artifacts.js             | 17 +++++++---
 lib/manager/composer/index.js                 |  3 +-
 lib/workers/branch/get-updated.js             | 34 +++++++++++++++++--
 lib/workers/pr/pr-body.js                     |  2 +-
 test/manager/composer/artifacts.spec.js       | 15 ++++++++
 .../__snapshots__/get-updated.spec.js.snap    | 28 +++++++++++++++
 test/workers/branch/get-updated.spec.js       | 34 +++++++++++++++++++
 .../pr/__snapshots__/index.spec.js.snap       |  2 +-
 8 files changed, 125 insertions(+), 10 deletions(-)

diff --git a/lib/manager/composer/artifacts.js b/lib/manager/composer/artifacts.js
index 583e710a6a..0c9642afbd 100644
--- a/lib/manager/composer/artifacts.js
+++ b/lib/manager/composer/artifacts.js
@@ -35,6 +35,9 @@ async function updateArtifacts(
     const localPackageFileName = upath.join(config.localDir, packageFileName);
     await fs.outputFile(localPackageFileName, newPackageFileContent);
     const localLockFileName = upath.join(config.localDir, lockFileName);
+    if (config.isLockFileMaintenance) {
+      await fs.remove(localLockFileName);
+    }
     const authJson = {};
     let credentials = hostRules.find({
       hostType: 'github',
@@ -115,10 +118,16 @@ async function updateArtifacts(
       logger.info('Running composer via global composer');
       cmd = 'composer';
     }
-    const args =
-      ('update ' + updatedDeps.join(' ')).trim() +
-      ' --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --with-dependencies';
-    logger.debug({ cmd, args }, 'composer update command');
+    let args;
+    if (config.isLockFileMaintenance) {
+      args = 'install';
+    } else {
+      args =
+        ('update ' + updatedDeps.join(' ')).trim() + ' --with-dependencies';
+    }
+    args +=
+      ' --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader';
+    logger.debug({ cmd, args }, 'composer command');
     ({ stdout, stderr } = await exec(`${cmd} ${args}`, {
       cwd,
       shell: true,
diff --git a/lib/manager/composer/index.js b/lib/manager/composer/index.js
index 568ada54ab..e31490e30d 100644
--- a/lib/manager/composer/index.js
+++ b/lib/manager/composer/index.js
@@ -11,6 +11,5 @@ module.exports = {
   language,
   updateDependency,
   getRangeStrategy,
-  // TODO: support this
-  // supportsLockFileMaintenance: true,
+  supportsLockFileMaintenance: true,
 };
diff --git a/lib/workers/branch/get-updated.js b/lib/workers/branch/get-updated.js
index f8b0c54253..fe95591229 100644
--- a/lib/workers/branch/get-updated.js
+++ b/lib/workers/branch/get-updated.js
@@ -11,14 +11,16 @@ async function getUpdatedPackageFiles(config) {
   const updatedFileContents = {};
   const packageFileManagers = {};
   const packageFileUpdatedDeps = {};
-
+  const lockFileMaintenanceFiles = [];
   for (const upgrade of config.upgrades) {
     const { manager, packageFile, depName } = upgrade;
     packageFileManagers[packageFile] = manager;
     packageFileUpdatedDeps[packageFile] =
       packageFileUpdatedDeps[packageFile] || [];
     packageFileUpdatedDeps[packageFile].push(depName);
-    if (upgrade.updateType !== 'lockFileMaintenance') {
+    if (upgrade.updateType === 'lockFileMaintenance') {
+      lockFileMaintenanceFiles.push(packageFile);
+    } else {
       const existingContent =
         updatedFileContents[packageFile] ||
         (await platform.getFile(packageFile, config.parentBranch));
@@ -90,6 +92,34 @@ async function getUpdatedPackageFiles(config) {
       }
     }
   }
+  if (!config.parentBranch) {
+    // Only perform lock file maintenance if it's a fresh commit
+    for (const packageFile of lockFileMaintenanceFiles) {
+      const manager = packageFileManagers[packageFile];
+      const updateArtifacts = get(manager, 'updateArtifacts');
+      if (updateArtifacts) {
+        const packageFileContents =
+          updatedFileContents[packageFile] ||
+          (await platform.getFile(packageFile, config.parentBranch));
+        const results = await updateArtifacts(
+          packageFile,
+          [],
+          packageFileContents,
+          config
+        );
+        if (is.nonEmptyArray(results)) {
+          for (const res of results) {
+            const { file, artifactError } = res;
+            if (file) {
+              updatedArtifacts.push(file);
+            } else if (artifactError) {
+              artifactErrors.push(artifactError);
+            }
+          }
+        }
+      }
+    }
+  }
   return {
     parentBranch: config.parentBranch, // Need to overwrite original config
     updatedPackageFiles,
diff --git a/lib/workers/pr/pr-body.js b/lib/workers/pr/pr-body.js
index 179b0e904f..fdd3b7211c 100644
--- a/lib/workers/pr/pr-body.js
+++ b/lib/workers/pr/pr-body.js
@@ -159,7 +159,7 @@ async function getPrBody(config) {
 
   if (config.updateType === 'lockFileMaintenance') {
     prBody +=
-      ':wrench: This Pull Request updates `package.json` lock files to use the latest dependency versions.\n\n';
+      ':wrench: This Pull Request updates lock files to use the latest dependency versions.\n\n';
   }
 
   if (config.isPin) {
diff --git a/test/manager/composer/artifacts.spec.js b/test/manager/composer/artifacts.spec.js
index 5bf5e76a76..5fbc84598f 100644
--- a/test/manager/composer/artifacts.spec.js
+++ b/test/manager/composer/artifacts.spec.js
@@ -73,6 +73,21 @@ describe('.updateArtifacts()', () => {
       await composer.updateArtifacts('composer.json', [], '{}', config)
     ).not.toBeNull();
   });
+  it('performs lockFileMaintenance', async () => {
+    platform.getFile.mockReturnValueOnce('Current composer.lock');
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
+    fs.readFile = jest.fn(() => 'New composer.lock');
+    platform.getRepoStatus.mockResolvedValue({ modified: ['composer.lock'] });
+    expect(
+      await composer.updateArtifacts('composer.json', [], '{}', {
+        ...config,
+        isLockFileMaintenance: true,
+      })
+    ).not.toBeNull();
+  });
   it('supports docker mode', async () => {
     platform.getFile.mockReturnValueOnce('Current composer.lock');
     exec.mockReturnValueOnce({
diff --git a/test/workers/branch/__snapshots__/get-updated.spec.js.snap b/test/workers/branch/__snapshots__/get-updated.spec.js.snap
index ef9ad00fea..0b5271543d 100644
--- a/test/workers/branch/__snapshots__/get-updated.spec.js.snap
+++ b/test/workers/branch/__snapshots__/get-updated.spec.js.snap
@@ -60,3 +60,31 @@ Object {
   ],
 }
 `;
+
+exports[`workers/branch/get-updated getUpdatedPackageFiles() handles lockFileMaintenance 1`] = `
+Object {
+  "artifactErrors": Array [],
+  "parentBranch": undefined,
+  "updatedArtifacts": Array [
+    Object {
+      "contents": "some contents",
+      "name": "composer.json",
+    },
+  ],
+  "updatedPackageFiles": Array [],
+}
+`;
+
+exports[`workers/branch/get-updated getUpdatedPackageFiles() handles lockFileMaintenance error 1`] = `
+Object {
+  "artifactErrors": Array [
+    Object {
+      "name": "composer.lock",
+      "stderr": "some error",
+    },
+  ],
+  "parentBranch": undefined,
+  "updatedArtifacts": Array [],
+  "updatedPackageFiles": Array [],
+}
+`;
diff --git a/test/workers/branch/get-updated.spec.js b/test/workers/branch/get-updated.spec.js
index 4bacff0a4a..a00e4e9880 100644
--- a/test/workers/branch/get-updated.spec.js
+++ b/test/workers/branch/get-updated.spec.js
@@ -55,6 +55,40 @@ describe('workers/branch/get-updated', () => {
       const res = await getUpdatedPackageFiles(config);
       expect(res).toMatchSnapshot();
     });
+    it('handles lockFileMaintenance', async () => {
+      // config.parentBranch = 'some-branch';
+      config.upgrades.push({
+        manager: 'composer',
+        updateType: 'lockFileMaintenance',
+      });
+      composer.updateArtifacts.mockReturnValue([
+        {
+          file: {
+            name: 'composer.json',
+            contents: 'some contents',
+          },
+        },
+      ]);
+      const res = await getUpdatedPackageFiles(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('handles lockFileMaintenance error', async () => {
+      // config.parentBranch = 'some-branch';
+      config.upgrades.push({
+        manager: 'composer',
+        updateType: 'lockFileMaintenance',
+      });
+      composer.updateArtifacts.mockReturnValue([
+        {
+          artifactError: {
+            name: 'composer.lock',
+            stderr: 'some error',
+          },
+        },
+      ]);
+      const res = await getUpdatedPackageFiles(config);
+      expect(res).toMatchSnapshot();
+    });
     it('handles lock file errors', async () => {
       config.parentBranch = 'some-branch';
       config.upgrades.push({
diff --git a/test/workers/pr/__snapshots__/index.spec.js.snap b/test/workers/pr/__snapshots__/index.spec.js.snap
index 05de881dd5..8885b22f7b 100644
--- a/test/workers/pr/__snapshots__/index.spec.js.snap
+++ b/test/workers/pr/__snapshots__/index.spec.js.snap
@@ -140,7 +140,7 @@ note 2
 
 :abcd: If you wish to disable git hash updates, add \`\\":disableDigestUpdates\\"\` to the extends array in your config.
 
-:wrench: This Pull Request updates \`package.json\` lock files to use the latest dependency versions.
+:wrench: This Pull Request updates lock files to use the latest dependency versions.
 
 ---
 
-- 
GitLab