diff --git a/docs/usage/ruby.md b/docs/usage/ruby.md
index 1848b217af112edf40f9fd07c49407c45e9f4195..aa35baf1497da8de60485a75b044838204d3c53b 100644
--- a/docs/usage/ruby.md
+++ b/docs/usage/ruby.md
@@ -17,7 +17,9 @@ Renovate supports upgrading dependencies in Bundler's Gemfiles and their accompa
 
 ## Warnings
 
-Renovate doesn't update dependencies without a version constraint.
+When using `"rangeStrategy": "update-lockfile"`, all gems listed in the `Gemfile` will be updated, even if they do not have a version specified.
+
+When using other `rangeStrategy` options, Renovate doesn't update dependencies without a version constraint.
 Example: `gem 'some-gem', '~> 1.2.3'` will update `some-gem` if a new version matching the constraint is available, but `gem 'some-gem'` won't.
 If you always want to have the latest available version, consider specifying `gem 'some-gem', '> 0'`.
 
diff --git a/lib/modules/manager/bundler/artifacts.spec.ts b/lib/modules/manager/bundler/artifacts.spec.ts
index b0e47017b197c57f9b8736c9346601af87162e00..19c640a7b20ec563348a6dec996a0a790ef65df0 100644
--- a/lib/modules/manager/bundler/artifacts.spec.ts
+++ b/lib/modules/manager/bundler/artifacts.spec.ts
@@ -16,6 +16,7 @@ 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 '.';
 
@@ -50,230 +51,74 @@ const updatedGemfileLock = {
 };
 
 describe('modules/manager/bundler/artifacts', () => {
-  beforeEach(() => {
-    jest.resetAllMocks();
-    jest.resetModules();
-
-    delete process.env.GEM_HOME;
-
-    env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    bundlerHostRules.findAllAuthenticatable.mockReturnValue([]);
-    docker.resetPrefetchedImages();
-
-    GlobalConfig.set(adminConfig);
-    fs.ensureCacheDir.mockResolvedValue('/tmp/cache/others/gem');
-  });
+  describe('buildArgs', () => {
+    it('returns only --update arg when no config is specified', () => {
+      const config: UpdateArtifactsConfig = {};
+      expect(buildArgs(config)).toStrictEqual(['--update']);
+    });
 
-  afterEach(() => {
-    GlobalConfig.reset();
-  });
+    it('adds --conservative when bundlerConservative is set as postUpdateOption', () => {
+      const config: UpdateArtifactsConfig = {
+        postUpdateOptions: ['bundlerConservative'],
+      };
+      expect(buildArgs(config)).toStrictEqual(['--conservative', '--update']);
+    });
 
-  it('returns null by default', async () => {
-    expect(
-      await updateArtifacts({
-        packageFileName: '',
-        updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-        newPackageFileContent: '',
-        config,
-      })
-    ).toBeNull();
-  });
+    it('adds --patch and --strict when update type is patch', () => {
+      const config: UpdateArtifactsConfig = { updateType: 'patch' };
+      expect(buildArgs(config)).toStrictEqual([
+        '--patch',
+        '--strict',
+        '--update',
+      ]);
+    });
 
-  it('returns null if Gemfile.lock was not changed', async () => {
-    fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-    fs.writeLocalFile.mockResolvedValueOnce();
-    const execSnapshots = mockExecAll();
-    git.getRepoStatus.mockResolvedValueOnce({
-      modified: [] as string[],
-    } as StatusResult);
-    fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
-    expect(
-      await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-        newPackageFileContent: 'Updated Gemfile content',
-        config,
-      })
-    ).toBeNull();
-    expect(execSnapshots).toMatchObject([
-      { cmd: 'bundler lock --update foo bar' },
-    ]);
+    it('adds --minor and --strict when update type is minor', () => {
+      const config: UpdateArtifactsConfig = { updateType: 'minor' };
+      expect(buildArgs(config)).toStrictEqual([
+        '--minor',
+        '--strict',
+        '--update',
+      ]);
+    });
   });
 
-  it('works for default binarySource', async () => {
-    fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-    fs.readLocalFile.mockResolvedValueOnce(null);
-    const execSnapshots = mockExecAll();
-    git.getRepoStatus.mockResolvedValueOnce({
-      modified: ['Gemfile.lock'],
-    } as StatusResult);
-    fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
-    expect(
-      await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-        newPackageFileContent: 'Updated Gemfile content',
-        config,
-      })
-    ).toEqual([updatedGemfileLock]);
-    expect(execSnapshots).toMatchObject([
-      { cmd: 'bundler lock --update foo bar' },
-    ]);
-  });
+  describe('updateArtifacts', () => {
+    beforeEach(() => {
+      jest.resetAllMocks();
+      jest.resetModules();
 
-  it('works explicit global binarySource', async () => {
-    GlobalConfig.set({ ...adminConfig, binarySource: 'global' });
-    fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-    fs.readLocalFile.mockResolvedValueOnce(null);
-    const execSnapshots = mockExecAll();
-    git.getRepoStatus.mockResolvedValueOnce({
-      modified: ['Gemfile.lock'],
-    } as StatusResult);
-    fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
-    expect(
-      await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-        newPackageFileContent: 'Updated Gemfile content',
-        config,
-      })
-    ).toEqual([updatedGemfileLock]);
-    expect(execSnapshots).toMatchObject([
-      { cmd: 'bundler lock --update foo bar' },
-    ]);
-  });
+      delete process.env.GEM_HOME;
 
-  it('supports conservative mode', async () => {
-    fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-    fs.readLocalFile.mockResolvedValueOnce(null);
-    const execSnapshots = mockExecAll();
-    git.getRepoStatus.mockResolvedValueOnce({
-      modified: ['Gemfile.lock'],
-    } as StatusResult);
-    fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
-    expect(
-      await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-        newPackageFileContent: 'Updated Gemfile content',
-        config: {
-          ...config,
-          postUpdateOptions: [
-            ...(config.postUpdateOptions ?? []),
-            'bundlerConservative',
-          ],
-        },
-      })
-    ).toEqual([updatedGemfileLock]);
-    expect(execSnapshots).toMatchObject([
-      expect.objectContaining({
-        cmd: 'bundler lock --conservative --update foo bar',
-      }),
-    ]);
-  });
+      env.getChildProcessEnv.mockReturnValue(envMock.basic);
+      bundlerHostRules.findAllAuthenticatable.mockReturnValue([]);
+      docker.resetPrefetchedImages();
 
-  it('supports install mode', async () => {
-    GlobalConfig.set({
-      ...adminConfig,
-      binarySource: 'install',
+      GlobalConfig.set(adminConfig);
+      fs.ensureCacheDir.mockResolvedValue('/tmp/cache/others/gem');
     });
-    fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-    fs.readLocalFile.mockResolvedValueOnce('1.2.0');
-    // bundler
-    datasource.getPkgReleases.mockResolvedValueOnce({
-      releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
-    });
-    const execSnapshots = mockExecAll();
-    git.getRepoStatus.mockResolvedValueOnce({
-      modified: ['Gemfile.lock'],
-    } as StatusResult);
-    fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
-    expect(
-      await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-        newPackageFileContent: 'Updated Gemfile content',
-        config,
-      })
-    ).toEqual([updatedGemfileLock]);
-    expect(execSnapshots).toMatchObject([
-      { cmd: 'install-tool ruby 1.2.0' },
-      { cmd: 'install-tool bundler 2.3.5' },
-      { cmd: 'ruby --version' },
-      { cmd: 'bundler lock --update foo bar' },
-    ]);
-  });
 
-  describe('Docker', () => {
-    beforeEach(() => {
-      GlobalConfig.set({
-        ...adminConfig,
-        binarySource: 'docker',
-      });
+    afterEach(() => {
+      GlobalConfig.reset();
     });
 
-    it('.ruby-version', async () => {
-      fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      fs.readLocalFile.mockResolvedValueOnce('1.2.0');
-      // bundler
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
-      });
-      const execSnapshots = mockExecAll();
-      git.getRepoStatus.mockResolvedValueOnce({
-        modified: ['Gemfile.lock'],
-      } as StatusResult);
-      fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+    it('returns null by default', async () => {
       expect(
         await updateArtifacts({
-          packageFileName: 'Gemfile',
+          packageFileName: '',
           updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-          newPackageFileContent: 'Updated Gemfile content',
+          newPackageFileContent: '',
           config,
         })
-      ).toEqual([updatedGemfileLock]);
-      expect(execSnapshots).toMatchObject([
-        { cmd: 'docker pull renovate/sidecar' },
-        { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
-        {
-          cmd:
-            'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
-            '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
-            '-v "/tmp/cache":"/tmp/cache" ' +
-            '-e GEM_HOME ' +
-            '-e BUILDPACK_CACHE_DIR ' +
-            '-e CONTAINERBASE_CACHE_DIR ' +
-            '-w "/tmp/github/some/repo" ' +
-            'renovate/sidecar' +
-            ' bash -l -c "' +
-            'install-tool ruby 1.2.0' +
-            ' && ' +
-            'install-tool bundler 2.3.5' +
-            ' && ' +
-            'ruby --version' +
-            ' && ' +
-            'bundler lock --update foo bar' +
-            '"',
-        },
-      ]);
+      ).toBeNull();
     });
 
-    it('constraints options', async () => {
-      GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+    it('returns null if Gemfile.lock was not changed', async () => {
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
-      });
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [
-          { version: '1.0.0' },
-          { version: '1.2.0' },
-          { version: '1.3.0' },
-        ],
-      });
+      fs.writeLocalFile.mockResolvedValueOnce();
       const execSnapshots = mockExecAll();
       git.getRepoStatus.mockResolvedValueOnce({
-        modified: ['Gemfile.lock'],
+        modified: [] as string[],
       } as StatusResult);
       fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
       expect(
@@ -281,56 +126,17 @@ describe('modules/manager/bundler/artifacts', () => {
           packageFileName: 'Gemfile',
           updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
           newPackageFileContent: 'Updated Gemfile content',
-          config: {
-            ...config,
-            constraints: {
-              ruby: '1.2.5',
-              bundler: '3.2.1',
-            },
-          },
+          config,
         })
-      ).toEqual([updatedGemfileLock]);
+      ).toBeNull();
       expect(execSnapshots).toMatchObject([
-        { cmd: 'docker pull renovate/sidecar' },
-        { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
-        {
-          cmd:
-            'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
-            '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
-            '-v "/tmp/cache":"/tmp/cache" ' +
-            '-e GEM_HOME ' +
-            '-e BUILDPACK_CACHE_DIR ' +
-            '-e CONTAINERBASE_CACHE_DIR ' +
-            '-w "/tmp/github/some/repo" ' +
-            'renovate/sidecar' +
-            ' bash -l -c "' +
-            'install-tool ruby 1.2.5' +
-            ' && ' +
-            'install-tool bundler 3.2.1' +
-            ' && ' +
-            'ruby --version' +
-            ' && ' +
-            'bundler lock --update foo bar' +
-            '"',
-        },
+        { cmd: 'bundler lock --update foo bar' },
       ]);
     });
 
-    it('invalid constraints options', async () => {
-      GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+    it('works for default binarySource', async () => {
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      // ruby
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [
-          { version: '1.0.0' },
-          { version: '1.2.0' },
-          { version: '1.3.0' },
-        ],
-      });
-      // bundler
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
-      });
+      fs.readLocalFile.mockResolvedValueOnce(null);
       const execSnapshots = mockExecAll();
       git.getRepoStatus.mockResolvedValueOnce({
         modified: ['Gemfile.lock'],
@@ -341,61 +147,18 @@ describe('modules/manager/bundler/artifacts', () => {
           packageFileName: 'Gemfile',
           updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
           newPackageFileContent: 'Updated Gemfile content',
-          config: {
-            ...config,
-            constraints: {
-              ruby: 'foo',
-              bundler: 'bar',
-            },
-          },
+          config,
         })
       ).toEqual([updatedGemfileLock]);
       expect(execSnapshots).toMatchObject([
-        { cmd: 'docker pull renovate/sidecar' },
-        { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
-        {
-          cmd:
-            'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
-            '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
-            '-v "/tmp/cache":"/tmp/cache" ' +
-            '-e GEM_HOME ' +
-            '-e BUILDPACK_CACHE_DIR ' +
-            '-e CONTAINERBASE_CACHE_DIR ' +
-            '-w "/tmp/github/some/repo" ' +
-            'renovate/sidecar' +
-            ' bash -l -c "' +
-            'install-tool ruby 1.3.0' +
-            ' && ' +
-            'install-tool bundler 2.3.5' +
-            ' && ' +
-            'ruby --version' +
-            ' && ' +
-            'bundler lock --update foo bar' +
-            '"',
-        },
+        { cmd: 'bundler lock --update foo bar' },
       ]);
     });
 
-    it('injects bundler host configuration environment variables', async () => {
-      GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+    it('works explicit global binarySource', async () => {
+      GlobalConfig.set({ ...adminConfig, binarySource: 'global' });
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      fs.readLocalFile.mockResolvedValueOnce('1.2.0');
-      // bundler
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
-      });
-      bundlerHostRules.findAllAuthenticatable.mockReturnValue([
-        {
-          hostType: 'bundler',
-          matchHost: 'gems.private.com',
-          resolvedHost: 'gems.private.com',
-          username: 'some-user',
-          password: 'some-password',
-        },
-      ]);
-      bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
-        'some-user:some-password'
-      );
+      fs.readLocalFile.mockResolvedValueOnce(null);
       const execSnapshots = mockExecAll();
       git.getRepoStatus.mockResolvedValueOnce({
         modified: ['Gemfile.lock'],
@@ -410,56 +173,13 @@ describe('modules/manager/bundler/artifacts', () => {
         })
       ).toEqual([updatedGemfileLock]);
       expect(execSnapshots).toMatchObject([
-        { cmd: 'docker pull renovate/sidecar' },
-        { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
-        {
-          cmd:
-            'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
-            '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
-            '-v "/tmp/cache":"/tmp/cache" ' +
-            '-e BUNDLE_GEMS__PRIVATE__COM ' +
-            '-e GEM_HOME ' +
-            '-e BUILDPACK_CACHE_DIR ' +
-            '-e CONTAINERBASE_CACHE_DIR ' +
-            '-w "/tmp/github/some/repo" ' +
-            'renovate/sidecar' +
-            ' bash -l -c "' +
-            'install-tool ruby 1.2.0' +
-            ' && ' +
-            'install-tool bundler 2.3.5' +
-            ' && ' +
-            'ruby --version' +
-            ' && ' +
-            'bundler lock --update foo bar' +
-            '"',
-        },
+        { cmd: 'bundler lock --update foo bar' },
       ]);
     });
 
-    it('injects bundler host configuration as command with bundler < 2', async () => {
-      GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+    it('supports conservative mode and updateType option', async () => {
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      fs.readLocalFile.mockResolvedValueOnce('1.2.0');
-      // ruby
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [
-          { version: '1.0.0' },
-          { version: '1.2.0' },
-          { version: '1.3.0' },
-        ],
-      });
-      bundlerHostRules.findAllAuthenticatable.mockReturnValue([
-        {
-          hostType: 'bundler',
-          matchHost: 'gems-private.com',
-          resolvedHost: 'gems-private.com',
-          username: 'some-user',
-          password: 'some-password',
-        },
-      ]);
-      bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
-        'some-user:some-password'
-      );
+      fs.readLocalFile.mockResolvedValueOnce(null);
       const execSnapshots = mockExecAll();
       git.getRepoStatus.mockResolvedValueOnce({
         modified: ['Gemfile.lock'],
@@ -472,138 +192,32 @@ describe('modules/manager/bundler/artifacts', () => {
           newPackageFileContent: 'Updated Gemfile content',
           config: {
             ...config,
-            constraints: {
-              bundler: '1.2',
-            },
+            updateType: 'patch',
+            postUpdateOptions: [
+              ...(config.postUpdateOptions ?? []),
+              'bundlerConservative',
+            ],
           },
         })
       ).toEqual([updatedGemfileLock]);
       expect(execSnapshots).toMatchObject([
-        { cmd: 'docker pull renovate/sidecar' },
-        { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
-        {
-          cmd:
-            'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
-            '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
-            '-v "/tmp/cache":"/tmp/cache" ' +
-            '-e GEM_HOME ' +
-            '-e BUILDPACK_CACHE_DIR ' +
-            '-e CONTAINERBASE_CACHE_DIR ' +
-            '-w "/tmp/github/some/repo" ' +
-            'renovate/sidecar' +
-            ' bash -l -c "' +
-            'install-tool ruby 1.2.0' +
-            ' && ' +
-            'install-tool bundler 1.2' +
-            ' && ' +
-            'ruby --version' +
-            ' && ' +
-            'bundler config --local gems-private.com some-user:some-password' +
-            ' && ' +
-            'bundler lock --update foo bar' +
-            '"',
-        },
+        expect.objectContaining({
+          cmd: 'bundler lock --patch --strict --conservative --update foo bar',
+        }),
       ]);
     });
 
-    it('injects bundler host configuration as command with bundler >= 2', async () => {
-      GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
-      fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      fs.readLocalFile.mockResolvedValueOnce('1.2.0');
-      // ruby
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [
-          { version: '1.0.0' },
-          { version: '1.2.0' },
-          { version: '1.3.0' },
-        ],
+    it('supports install mode', async () => {
+      GlobalConfig.set({
+        ...adminConfig,
+        binarySource: 'install',
       });
-      bundlerHostRules.findAllAuthenticatable.mockReturnValue([
-        {
-          hostType: 'bundler',
-          matchHost: 'gems-private.com',
-          resolvedHost: 'gems-private.com',
-          username: 'some-user',
-          password: 'some-password',
-        },
-      ]);
-      bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
-        'some-user:some-password'
-      );
-      const execSnapshots = mockExecAll();
-      git.getRepoStatus.mockResolvedValueOnce({
-        modified: ['Gemfile.lock'],
-      } as StatusResult);
-      fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
-      expect(
-        await updateArtifacts({
-          packageFileName: 'Gemfile',
-          updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
-          newPackageFileContent: 'Updated Gemfile content',
-          config: {
-            ...config,
-            constraints: {
-              bundler: '2.1',
-            },
-          },
-        })
-      ).toEqual([updatedGemfileLock]);
-      expect(execSnapshots).toMatchObject([
-        { cmd: 'docker pull renovate/sidecar' },
-        { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
-        {
-          cmd:
-            'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
-            '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
-            '-v "/tmp/cache":"/tmp/cache" ' +
-            '-e GEM_HOME ' +
-            '-e BUILDPACK_CACHE_DIR ' +
-            '-e CONTAINERBASE_CACHE_DIR ' +
-            '-w "/tmp/github/some/repo" ' +
-            'renovate/sidecar' +
-            ' bash -l -c "' +
-            'install-tool ruby 1.2.0' +
-            ' && ' +
-            'install-tool bundler 2.1' +
-            ' && ' +
-            'ruby --version' +
-            ' && ' +
-            'bundler config set --local gems-private.com some-user:some-password' +
-            ' && ' +
-            'bundler lock --update foo bar' +
-            '"',
-        },
-      ]);
-    });
-
-    it('injects bundler host configuration as command with bundler == latest', async () => {
-      GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
       fs.readLocalFile.mockResolvedValueOnce('1.2.0');
-      // ruby
-      datasource.getPkgReleases.mockResolvedValueOnce({
-        releases: [
-          { version: '1.0.0' },
-          { version: '1.2.0' },
-          { version: '1.3.0' },
-        ],
-      });
       // bundler
       datasource.getPkgReleases.mockResolvedValueOnce({
         releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
       });
-      bundlerHostRules.findAllAuthenticatable.mockReturnValue([
-        {
-          hostType: 'bundler',
-          matchHost: 'gems-private.com',
-          resolvedHost: 'gems-private.com',
-          username: 'some-user',
-          password: 'some-password',
-        },
-      ]);
-      bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
-        'some-user:some-password'
-      );
       const execSnapshots = mockExecAll();
       git.getRepoStatus.mockResolvedValueOnce({
         modified: ['Gemfile.lock'],
@@ -618,84 +232,455 @@ describe('modules/manager/bundler/artifacts', () => {
         })
       ).toEqual([updatedGemfileLock]);
       expect(execSnapshots).toMatchObject([
-        { cmd: 'docker pull renovate/sidecar' },
-        { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
-        {
-          cmd:
-            'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
-            '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
-            '-v "/tmp/cache":"/tmp/cache" ' +
-            '-e GEM_HOME ' +
-            '-e BUILDPACK_CACHE_DIR ' +
-            '-e CONTAINERBASE_CACHE_DIR ' +
-            '-w "/tmp/github/some/repo" ' +
-            'renovate/sidecar' +
-            ' bash -l -c "' +
-            'install-tool ruby 1.2.0' +
-            ' && ' +
-            'install-tool bundler 1.3.0' +
-            ' && ' +
-            'ruby --version' +
-            ' && ' +
-            'bundler config set --local gems-private.com some-user:some-password' +
-            ' && ' +
-            'bundler lock --update foo bar' +
-            '"',
-        },
+        { cmd: 'install-tool ruby 1.2.0' },
+        { cmd: 'install-tool bundler 2.3.5' },
+        { cmd: 'ruby --version' },
+        { cmd: 'bundler lock --update foo bar' },
       ]);
     });
-  });
 
-  it('returns error when failing in lockFileMaintenance true', async () => {
-    const execError = new ExecError('Exec error', {
-      cmd: '',
-      stdout: ' foo was resolved to',
-      stderr: '',
-      options: { encoding: 'utf8' },
-    });
-    fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-    fs.writeLocalFile.mockResolvedValueOnce(null as never);
-    const execSnapshots = mockExecAll(execError);
-    git.getRepoStatus.mockResolvedValueOnce({
-      modified: ['Gemfile.lock'],
-    } as StatusResult);
-    expect(
-      await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [],
-        newPackageFileContent: '{}',
-        config: {
-          ...config,
-          isLockFileMaintenance: true,
-        },
-      })
-    ).toMatchObject([{ artifactError: { lockFile: 'Gemfile.lock' } }]);
-    expect(execSnapshots).toMatchObject([{ cmd: 'bundler lock --update' }]);
-  });
+    describe('Docker', () => {
+      beforeEach(() => {
+        GlobalConfig.set({
+          ...adminConfig,
+          binarySource: 'docker',
+        });
+      });
 
-  it('performs lockFileMaintenance', async () => {
-    fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-    fs.writeLocalFile.mockResolvedValueOnce();
-    const execSnapshots = mockExecAll();
-    git.getRepoStatus.mockResolvedValueOnce({
-      modified: ['Gemfile.lock'],
-    } as StatusResult);
-    fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
-    expect(
-      await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [],
-        newPackageFileContent: '{}',
-        config: {
-          ...config,
-          isLockFileMaintenance: true,
-        },
-      })
-    ).not.toBeNull();
-    expect(execSnapshots).toMatchObject([{ cmd: 'bundler lock --update' }]);
-  });
+      it('.ruby-version', async () => {
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        fs.readLocalFile.mockResolvedValueOnce('1.2.0');
+        // bundler
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
+        });
+        const execSnapshots = mockExecAll();
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+            newPackageFileContent: 'Updated Gemfile content',
+            config,
+          })
+        ).toEqual([updatedGemfileLock]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'docker pull renovate/sidecar' },
+          { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
+          {
+            cmd:
+              'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+              '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+              '-v "/tmp/cache":"/tmp/cache" ' +
+              '-e GEM_HOME ' +
+              '-e BUILDPACK_CACHE_DIR ' +
+              '-e CONTAINERBASE_CACHE_DIR ' +
+              '-w "/tmp/github/some/repo" ' +
+              'renovate/sidecar' +
+              ' bash -l -c "' +
+              'install-tool ruby 1.2.0' +
+              ' && ' +
+              'install-tool bundler 2.3.5' +
+              ' && ' +
+              'ruby --version' +
+              ' && ' +
+              'bundler lock --update foo bar' +
+              '"',
+          },
+        ]);
+      });
+
+      it('constraints options', async () => {
+        GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
+        });
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [
+            { version: '1.0.0' },
+            { version: '1.2.0' },
+            { version: '1.3.0' },
+          ],
+        });
+        const execSnapshots = mockExecAll();
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+            newPackageFileContent: 'Updated Gemfile content',
+            config: {
+              ...config,
+              constraints: {
+                ruby: '1.2.5',
+                bundler: '3.2.1',
+              },
+            },
+          })
+        ).toEqual([updatedGemfileLock]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'docker pull renovate/sidecar' },
+          { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
+          {
+            cmd:
+              'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+              '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+              '-v "/tmp/cache":"/tmp/cache" ' +
+              '-e GEM_HOME ' +
+              '-e BUILDPACK_CACHE_DIR ' +
+              '-e CONTAINERBASE_CACHE_DIR ' +
+              '-w "/tmp/github/some/repo" ' +
+              'renovate/sidecar' +
+              ' bash -l -c "' +
+              'install-tool ruby 1.2.5' +
+              ' && ' +
+              'install-tool bundler 3.2.1' +
+              ' && ' +
+              'ruby --version' +
+              ' && ' +
+              'bundler lock --update foo bar' +
+              '"',
+          },
+        ]);
+      });
+
+      it('invalid constraints options', async () => {
+        GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        // ruby
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [
+            { version: '1.0.0' },
+            { version: '1.2.0' },
+            { version: '1.3.0' },
+          ],
+        });
+        // bundler
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
+        });
+        const execSnapshots = mockExecAll();
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+            newPackageFileContent: 'Updated Gemfile content',
+            config: {
+              ...config,
+              constraints: {
+                ruby: 'foo',
+                bundler: 'bar',
+              },
+            },
+          })
+        ).toEqual([updatedGemfileLock]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'docker pull renovate/sidecar' },
+          { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
+          {
+            cmd:
+              'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+              '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+              '-v "/tmp/cache":"/tmp/cache" ' +
+              '-e GEM_HOME ' +
+              '-e BUILDPACK_CACHE_DIR ' +
+              '-e CONTAINERBASE_CACHE_DIR ' +
+              '-w "/tmp/github/some/repo" ' +
+              'renovate/sidecar' +
+              ' bash -l -c "' +
+              'install-tool ruby 1.3.0' +
+              ' && ' +
+              'install-tool bundler 2.3.5' +
+              ' && ' +
+              'ruby --version' +
+              ' && ' +
+              'bundler lock --update foo bar' +
+              '"',
+          },
+        ]);
+      });
+
+      it('injects bundler host configuration environment variables', async () => {
+        GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        fs.readLocalFile.mockResolvedValueOnce('1.2.0');
+        // bundler
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
+        });
+        bundlerHostRules.findAllAuthenticatable.mockReturnValue([
+          {
+            hostType: 'bundler',
+            matchHost: 'gems.private.com',
+            resolvedHost: 'gems.private.com',
+            username: 'some-user',
+            password: 'some-password',
+          },
+        ]);
+        bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
+          'some-user:some-password'
+        );
+        const execSnapshots = mockExecAll();
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+            newPackageFileContent: 'Updated Gemfile content',
+            config,
+          })
+        ).toEqual([updatedGemfileLock]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'docker pull renovate/sidecar' },
+          { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
+          {
+            cmd:
+              'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+              '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+              '-v "/tmp/cache":"/tmp/cache" ' +
+              '-e BUNDLE_GEMS__PRIVATE__COM ' +
+              '-e GEM_HOME ' +
+              '-e BUILDPACK_CACHE_DIR ' +
+              '-e CONTAINERBASE_CACHE_DIR ' +
+              '-w "/tmp/github/some/repo" ' +
+              'renovate/sidecar' +
+              ' bash -l -c "' +
+              'install-tool ruby 1.2.0' +
+              ' && ' +
+              'install-tool bundler 2.3.5' +
+              ' && ' +
+              'ruby --version' +
+              ' && ' +
+              'bundler lock --update foo bar' +
+              '"',
+          },
+        ]);
+      });
+
+      it('injects bundler host configuration as command with bundler < 2', async () => {
+        GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        fs.readLocalFile.mockResolvedValueOnce('1.2.0');
+        // ruby
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [
+            { version: '1.0.0' },
+            { version: '1.2.0' },
+            { version: '1.3.0' },
+          ],
+        });
+        bundlerHostRules.findAllAuthenticatable.mockReturnValue([
+          {
+            hostType: 'bundler',
+            matchHost: 'gems-private.com',
+            resolvedHost: 'gems-private.com',
+            username: 'some-user',
+            password: 'some-password',
+          },
+        ]);
+        bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
+          'some-user:some-password'
+        );
+        const execSnapshots = mockExecAll();
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+            newPackageFileContent: 'Updated Gemfile content',
+            config: {
+              ...config,
+              constraints: {
+                bundler: '1.2',
+              },
+            },
+          })
+        ).toEqual([updatedGemfileLock]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'docker pull renovate/sidecar' },
+          { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
+          {
+            cmd:
+              'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+              '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+              '-v "/tmp/cache":"/tmp/cache" ' +
+              '-e GEM_HOME ' +
+              '-e BUILDPACK_CACHE_DIR ' +
+              '-e CONTAINERBASE_CACHE_DIR ' +
+              '-w "/tmp/github/some/repo" ' +
+              'renovate/sidecar' +
+              ' bash -l -c "' +
+              'install-tool ruby 1.2.0' +
+              ' && ' +
+              'install-tool bundler 1.2' +
+              ' && ' +
+              'ruby --version' +
+              ' && ' +
+              'bundler config --local gems-private.com some-user:some-password' +
+              ' && ' +
+              'bundler lock --update foo bar' +
+              '"',
+          },
+        ]);
+      });
+
+      it('injects bundler host configuration as command with bundler >= 2', async () => {
+        GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        fs.readLocalFile.mockResolvedValueOnce('1.2.0');
+        // ruby
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [
+            { version: '1.0.0' },
+            { version: '1.2.0' },
+            { version: '1.3.0' },
+          ],
+        });
+        bundlerHostRules.findAllAuthenticatable.mockReturnValue([
+          {
+            hostType: 'bundler',
+            matchHost: 'gems-private.com',
+            resolvedHost: 'gems-private.com',
+            username: 'some-user',
+            password: 'some-password',
+          },
+        ]);
+        bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
+          'some-user:some-password'
+        );
+        const execSnapshots = mockExecAll();
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+            newPackageFileContent: 'Updated Gemfile content',
+            config: {
+              ...config,
+              constraints: {
+                bundler: '2.1',
+              },
+            },
+          })
+        ).toEqual([updatedGemfileLock]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'docker pull renovate/sidecar' },
+          { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
+          {
+            cmd:
+              'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+              '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+              '-v "/tmp/cache":"/tmp/cache" ' +
+              '-e GEM_HOME ' +
+              '-e BUILDPACK_CACHE_DIR ' +
+              '-e CONTAINERBASE_CACHE_DIR ' +
+              '-w "/tmp/github/some/repo" ' +
+              'renovate/sidecar' +
+              ' bash -l -c "' +
+              'install-tool ruby 1.2.0' +
+              ' && ' +
+              'install-tool bundler 2.1' +
+              ' && ' +
+              'ruby --version' +
+              ' && ' +
+              'bundler config set --local gems-private.com some-user:some-password' +
+              ' && ' +
+              'bundler lock --update foo bar' +
+              '"',
+          },
+        ]);
+      });
+
+      it('injects bundler host configuration as command with bundler == latest', async () => {
+        GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        fs.readLocalFile.mockResolvedValueOnce('1.2.0');
+        // ruby
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [
+            { version: '1.0.0' },
+            { version: '1.2.0' },
+            { version: '1.3.0' },
+          ],
+        });
+        // bundler
+        datasource.getPkgReleases.mockResolvedValueOnce({
+          releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
+        });
+        bundlerHostRules.findAllAuthenticatable.mockReturnValue([
+          {
+            hostType: 'bundler',
+            matchHost: 'gems-private.com',
+            resolvedHost: 'gems-private.com',
+            username: 'some-user',
+            password: 'some-password',
+          },
+        ]);
+        bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
+          'some-user:some-password'
+        );
+        const execSnapshots = mockExecAll();
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
+            newPackageFileContent: 'Updated Gemfile content',
+            config,
+          })
+        ).toEqual([updatedGemfileLock]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'docker pull renovate/sidecar' },
+          { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
+          {
+            cmd:
+              'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+              '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+              '-v "/tmp/cache":"/tmp/cache" ' +
+              '-e GEM_HOME ' +
+              '-e BUILDPACK_CACHE_DIR ' +
+              '-e CONTAINERBASE_CACHE_DIR ' +
+              '-w "/tmp/github/some/repo" ' +
+              'renovate/sidecar' +
+              ' bash -l -c "' +
+              'install-tool ruby 1.2.0' +
+              ' && ' +
+              'install-tool bundler 1.3.0' +
+              ' && ' +
+              'ruby --version' +
+              ' && ' +
+              'bundler config set --local gems-private.com some-user:some-password' +
+              ' && ' +
+              'bundler lock --update foo bar' +
+              '"',
+          },
+        ]);
+      });
+    });
 
-  describe('Error handling', () => {
     it('returns error when failing in lockFileMaintenance true', async () => {
       const execError = new ExecError('Exec error', {
         cmd: '',
@@ -704,6 +689,7 @@ describe('modules/manager/bundler/artifacts', () => {
         options: { encoding: 'utf8' },
       });
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+      fs.writeLocalFile.mockResolvedValueOnce(null as never);
       const execSnapshots = mockExecAll(execError);
       git.getRepoStatus.mockResolvedValueOnce({
         modified: ['Gemfile.lock'],
@@ -718,47 +704,18 @@ describe('modules/manager/bundler/artifacts', () => {
             isLockFileMaintenance: true,
           },
         })
-      ).toMatchObject([
-        {
-          artifactError: {
-            lockFile: 'Gemfile.lock',
-          },
-        },
-      ]);
+      ).toMatchObject([{ artifactError: { lockFile: 'Gemfile.lock' } }]);
       expect(execSnapshots).toMatchObject([{ cmd: 'bundler lock --update' }]);
     });
 
-    it('rethrows for temporary error', async () => {
-      const execError = new ExecError(TEMPORARY_ERROR, {
-        cmd: '',
-        stdout: '',
-        stderr: '',
-        options: { encoding: 'utf8' },
-      });
-      fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      mockExecAll(execError);
-      await expect(
-        updateArtifacts({
-          packageFileName: 'Gemfile',
-          updatedDeps: [],
-          newPackageFileContent: '{}',
-          config: {
-            ...config,
-            isLockFileMaintenance: true,
-          },
-        })
-      ).rejects.toThrow(TEMPORARY_ERROR);
-    });
-
-    it('handles "Could not parse object" error', async () => {
-      const execError = new ExecError('fatal: Could not parse object', {
-        cmd: '',
-        stdout: 'but that version could not be found',
-        stderr: '',
-        options: { encoding: 'utf8' },
-      });
+    it('performs lockFileMaintenance', async () => {
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      mockExecAll(execError);
+      fs.writeLocalFile.mockResolvedValueOnce();
+      const execSnapshots = mockExecAll();
+      git.getRepoStatus.mockResolvedValueOnce({
+        modified: ['Gemfile.lock'],
+      } as StatusResult);
+      fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
       expect(
         await updateArtifacts({
           packageFileName: 'Gemfile',
@@ -767,64 +724,144 @@ describe('modules/manager/bundler/artifacts', () => {
           config: {
             ...config,
             isLockFileMaintenance: true,
+            updateType: 'patch', // This will have no effect together with isLockFileMaintenance
           },
         })
-      ).toMatchObject([{ artifactError: { lockFile: 'Gemfile.lock' } }]);
+      ).not.toBeNull();
+      expect(execSnapshots).toMatchObject([{ cmd: 'bundler lock --update' }]);
     });
 
-    it('throws on authentication errors', async () => {
-      const execError = new ExecError('Exec error', {
-        cmd: '',
-        stdout: 'Please supply credentials for this source',
-        stderr: 'Please make sure you have the correct access rights',
-        options: { encoding: 'utf8' },
+    describe('Error handling', () => {
+      it('returns error when failing in lockFileMaintenance true', async () => {
+        const execError = new ExecError('Exec error', {
+          cmd: '',
+          stdout: ' foo was resolved to',
+          stderr: '',
+          options: { encoding: 'utf8' },
+        });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        const execSnapshots = mockExecAll(execError);
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [],
+            newPackageFileContent: '{}',
+            config: {
+              ...config,
+              isLockFileMaintenance: true,
+            },
+          })
+        ).toMatchObject([
+          {
+            artifactError: {
+              lockFile: 'Gemfile.lock',
+            },
+          },
+        ]);
+        expect(execSnapshots).toMatchObject([{ cmd: 'bundler lock --update' }]);
       });
-      fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
-      mockExecAll(execError);
-      await expect(
-        updateArtifacts({
+
+      it('rethrows for temporary error', async () => {
+        const execError = new ExecError(TEMPORARY_ERROR, {
+          cmd: '',
+          stdout: '',
+          stderr: '',
+          options: { encoding: 'utf8' },
+        });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        mockExecAll(execError);
+        await expect(
+          updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [],
+            newPackageFileContent: '{}',
+            config: {
+              ...config,
+              isLockFileMaintenance: true,
+            },
+          })
+        ).rejects.toThrow(TEMPORARY_ERROR);
+      });
+
+      it('handles "Could not parse object" error', async () => {
+        const execError = new ExecError('fatal: Could not parse object', {
+          cmd: '',
+          stdout: 'but that version could not be found',
+          stderr: '',
+          options: { encoding: 'utf8' },
+        });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        mockExecAll(execError);
+        expect(
+          await updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [],
+            newPackageFileContent: '{}',
+            config: {
+              ...config,
+              isLockFileMaintenance: true,
+            },
+          })
+        ).toMatchObject([{ artifactError: { lockFile: 'Gemfile.lock' } }]);
+      });
+
+      it('throws on authentication errors', async () => {
+        const execError = new ExecError('Exec error', {
+          cmd: '',
+          stdout: 'Please supply credentials for this source',
+          stderr: 'Please make sure you have the correct access rights',
+          options: { encoding: 'utf8' },
+        });
+        fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
+        mockExecAll(execError);
+        await expect(
+          updateArtifacts({
+            packageFileName: 'Gemfile',
+            updatedDeps: [],
+            newPackageFileContent: '{}',
+            config: {
+              ...config,
+              isLockFileMaintenance: true,
+            },
+          })
+        ).rejects.toThrow(BUNDLER_INVALID_CREDENTIALS);
+      });
+
+      it('handles recursive resolved dependencies', async () => {
+        const execError = new ExecError('Exec error', {
+          cmd: '',
+          stdout: 'foo was resolved to foo',
+          stderr: 'bar was resolved to bar',
+          options: { encoding: 'utf8' },
+        });
+        fs.readLocalFile.mockResolvedValue('Current Gemfile.lock');
+        const execSnapshots = mockExecSequence([
+          execError,
+          { stdout: '', stderr: '' },
+        ]);
+        git.getRepoStatus.mockResolvedValueOnce({
+          modified: ['Gemfile.lock'],
+        } as StatusResult);
+
+        const res = await updateArtifacts({
           packageFileName: 'Gemfile',
-          updatedDeps: [],
+          updatedDeps: [{ depName: 'foo' }],
           newPackageFileContent: '{}',
           config: {
             ...config,
-            isLockFileMaintenance: true,
+            isLockFileMaintenance: false,
           },
-        })
-      ).rejects.toThrow(BUNDLER_INVALID_CREDENTIALS);
-    });
+        });
 
-    it('handles recursive resolved dependencies', async () => {
-      const execError = new ExecError('Exec error', {
-        cmd: '',
-        stdout: 'foo was resolved to foo',
-        stderr: 'bar was resolved to bar',
-        options: { encoding: 'utf8' },
+        expect(res).toMatchObject([{ file: { path: 'Gemfile.lock' } }]);
+        expect(execSnapshots).toMatchObject([
+          { cmd: 'bundler lock --update foo' },
+          { cmd: 'bundler lock --update foo bar' },
+        ]);
       });
-      fs.readLocalFile.mockResolvedValue('Current Gemfile.lock');
-      const execSnapshots = mockExecSequence([
-        execError,
-        { stdout: '', stderr: '' },
-      ]);
-      git.getRepoStatus.mockResolvedValueOnce({
-        modified: ['Gemfile.lock'],
-      } as StatusResult);
-
-      const res = await updateArtifacts({
-        packageFileName: 'Gemfile',
-        updatedDeps: [{ depName: 'foo' }],
-        newPackageFileContent: '{}',
-        config: {
-          ...config,
-          isLockFileMaintenance: false,
-        },
-      });
-
-      expect(res).toMatchObject([{ file: { path: 'Gemfile.lock' } }]);
-      expect(execSnapshots).toMatchObject([
-        { cmd: 'bundler lock --update foo' },
-        { cmd: 'bundler lock --update foo bar' },
-      ]);
     });
   });
 });
diff --git a/lib/modules/manager/bundler/artifacts.ts b/lib/modules/manager/bundler/artifacts.ts
index 46aeed9087b742bd4c7e28a98eb8565f7af2610f..317cc4e4b10690e8f34e7aa5c6836606706de441 100644
--- a/lib/modules/manager/bundler/artifacts.ts
+++ b/lib/modules/manager/bundler/artifacts.ts
@@ -19,7 +19,11 @@ import { getRepoStatus } from '../../../util/git';
 import { newlineRegex, regEx } from '../../../util/regex';
 import { addSecretForSanitizing } from '../../../util/sanitize';
 import { isValid } from '../../versioning/ruby';
-import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
+import type {
+  UpdateArtifact,
+  UpdateArtifactsConfig,
+  UpdateArtifactsResult,
+} from '../types';
 import { getBundlerConstraint, getRubyConstraint } from './common';
 import {
   findAllAuthenticatable,
@@ -28,6 +32,26 @@ 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 {};
@@ -81,11 +105,7 @@ export async function updateArtifacts(
     return null;
   }
 
-  const args = [
-    config.postUpdateOptions?.includes('bundlerConservative') &&
-      '--conservative',
-    '--update',
-  ].filter(is.nonEmptyString);
+  const args = buildArgs(config);
 
   const updatedDepNames = updatedDeps
     .map(({ depName }) => depName)