From 77e106841cd4490353ca23205227ee57fdefef28 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Wed, 9 Aug 2023 09:08:22 +0200
Subject: [PATCH] fix(bundler): update patch, minor, major separately (#23770)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/modules/manager/bundler/artifacts.spec.ts | 43 +++----------
 lib/modules/manager/bundler/artifacts.ts      | 60 ++++++++-----------
 2 files changed, 34 insertions(+), 69 deletions(-)

diff --git a/lib/modules/manager/bundler/artifacts.spec.ts b/lib/modules/manager/bundler/artifacts.spec.ts
index 225205793e..f90b772d99 100644
--- a/lib/modules/manager/bundler/artifacts.spec.ts
+++ b/lib/modules/manager/bundler/artifacts.spec.ts
@@ -16,7 +16,6 @@ import { ExecError } from '../../../util/exec/exec-error';
 import type { StatusResult } from '../../../util/git/types';
 import * as _datasource from '../../datasource';
 import type { UpdateArtifactsConfig } from '../types';
-import { buildArgs } from './artifacts';
 import * as _bundlerHostRules from './host-rules';
 import { updateArtifacts } from '.';
 
@@ -51,38 +50,6 @@ const updatedGemfileLock = {
 };
 
 describe('modules/manager/bundler/artifacts', () => {
-  describe('buildArgs', () => {
-    it('returns only --update arg when no config is specified', () => {
-      const config: UpdateArtifactsConfig = {};
-      expect(buildArgs(config)).toStrictEqual(['--update']);
-    });
-
-    it('adds --conservative when bundlerConservative is set as postUpdateOption', () => {
-      const config: UpdateArtifactsConfig = {
-        postUpdateOptions: ['bundlerConservative'],
-      };
-      expect(buildArgs(config)).toStrictEqual(['--conservative', '--update']);
-    });
-
-    it('adds --patch and --strict when update type is patch', () => {
-      const config: UpdateArtifactsConfig = { updateType: 'patch' };
-      expect(buildArgs(config)).toStrictEqual([
-        '--patch',
-        '--strict',
-        '--update',
-      ]);
-    });
-
-    it('adds --minor and --strict when update type is minor', () => {
-      const config: UpdateArtifactsConfig = { updateType: 'minor' };
-      expect(buildArgs(config)).toStrictEqual([
-        '--minor',
-        '--strict',
-        '--update',
-      ]);
-    });
-  });
-
   describe('updateArtifacts', () => {
     beforeEach(() => {
       jest.resetAllMocks();
@@ -196,7 +163,10 @@ describe('modules/manager/bundler/artifacts', () => {
       expect(
         await updateArtifacts({
           packageFileName: 'Gemfile',
-          updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+          updatedDeps: [
+            { depName: 'foo', updateType: 'minor' },
+            { depName: 'bar', updateType: 'patch' },
+          ],
           newPackageFileContent: 'Updated Gemfile content',
           config: {
             ...config,
@@ -210,7 +180,10 @@ describe('modules/manager/bundler/artifacts', () => {
       ).toEqual([updatedGemfileLock]);
       expect(execSnapshots).toMatchObject([
         expect.objectContaining({
-          cmd: 'bundler lock --patch --strict --conservative --update foo bar',
+          cmd: 'bundler lock --patch --strict --conservative --update bar',
+        }),
+        expect.objectContaining({
+          cmd: 'bundler lock --minor --strict --conservative --update foo',
         }),
       ]);
     });
diff --git a/lib/modules/manager/bundler/artifacts.ts b/lib/modules/manager/bundler/artifacts.ts
index 9f25465218..7e363484ce 100644
--- a/lib/modules/manager/bundler/artifacts.ts
+++ b/lib/modules/manager/bundler/artifacts.ts
@@ -18,11 +18,7 @@ import {
 import { getRepoStatus } from '../../../util/git';
 import { newlineRegex, regEx } from '../../../util/regex';
 import { isValid } from '../../versioning/ruby';
-import type {
-  UpdateArtifact,
-  UpdateArtifactsConfig,
-  UpdateArtifactsResult,
-} from '../types';
+import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
 import {
   getBundlerConstraint,
   getLockFilePath,
@@ -35,26 +31,6 @@ import {
 
 const hostConfigVariablePrefix = 'BUNDLE_';
 
-export function buildArgs(config: UpdateArtifactsConfig): string[] {
-  const args: string[] = [];
-  // --major is the default and does not need to be handled separately.
-  switch (config.updateType) {
-    case 'patch':
-      args.push('--patch', '--strict');
-      break;
-    case 'minor':
-      args.push('--minor', '--strict');
-      break;
-  }
-
-  if (config.postUpdateOptions?.includes('bundlerConservative')) {
-    args.push('--conservative');
-  }
-
-  args.push('--update');
-  return args;
-}
-
 function buildBundleHostVariable(hostRule: HostRule): Record<string, string> {
   if (!hostRule.resolvedHost || hostRule.resolvedHost.includes('-')) {
     return {};
@@ -108,8 +84,6 @@ export async function updateArtifacts(
     return null;
   }
 
-  const args = buildArgs(config);
-
   const updatedDepNames = updatedDeps
     .map(({ depName }) => depName)
     .filter(is.nonEmptyStringAndNotWhitespace);
@@ -117,15 +91,33 @@ export async function updateArtifacts(
   try {
     await writeLocalFile(packageFileName, newPackageFileContent);
 
-    let cmd: string;
+    const commands: string[] = [];
 
     if (config.isLockFileMaintenance) {
-      cmd = 'bundler lock --update';
+      commands.push('bundler lock --update');
     } else {
-      cmd = `bundler lock ${args.join(' ')} ${updatedDepNames
-        .filter((dep) => dep !== 'ruby')
-        .map(quote)
-        .join(' ')}`;
+      const updateTypes = {
+        patch: '--patch --strict ',
+        minor: '--minor --strict ',
+        major: '',
+      };
+      for (const [updateType, updateArg] of Object.entries(updateTypes)) {
+        const deps = updatedDeps
+          .filter((dep) => (dep.updateType ?? 'major') === updateType)
+          .map((dep) => dep.depName)
+          .filter(is.string)
+          .filter((dep) => dep !== 'ruby');
+        let additionalArgs = '';
+        if (config.postUpdateOptions?.includes('bundlerConservative')) {
+          additionalArgs = '--conservative ';
+        }
+        if (deps.length) {
+          const cmd = `bundler lock ${updateArg}${additionalArgs}--update ${deps
+            .map(quote)
+            .join(' ')}`;
+          commands.push(cmd);
+        }
+      }
     }
 
     const bundlerHostRules = findAllAuthenticatable({
@@ -200,7 +192,7 @@ export async function updateArtifacts(
       ],
       preCommands,
     };
-    await exec(cmd, execOptions);
+    await exec(commands, execOptions);
 
     const status = await getRepoStatus();
     if (!status.modified.includes(lockFileName)) {
-- 
GitLab