From fbbbce608e5df637ac5a6447774d38e03b2e79fc Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Mon, 17 May 2021 17:21:28 +0400
Subject: [PATCH] chore: Configure fs local dirs via admin config (#9990)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 lib/config/admin.ts                           | 12 ++-
 lib/config/types.ts                           |  2 +
 lib/datasource/crate/index.spec.ts            | 29 ++++---
 lib/manager/batect/extract.spec.ts            | 30 ++++++--
 lib/manager/bundler/artifacts.spec.ts         | 33 ++++----
 lib/manager/bundler/utils.ts                  |  3 +-
 lib/manager/cargo/artifacts.spec.ts           | 15 +++-
 lib/manager/cargo/extract.spec.ts             | 55 +++++---------
 lib/manager/cocoapods/artifacts.spec.ts       | 39 +++++-----
 lib/manager/cocoapods/extract.spec.ts         |  7 ++
 lib/manager/cocoapods/utils.ts                |  4 +-
 lib/manager/composer/artifacts.spec.ts        | 28 ++++---
 lib/manager/composer/artifacts.ts             |  5 +-
 lib/manager/git-submodules/extract.spec.ts    | 16 ++--
 lib/manager/git-submodules/extract.ts         |  6 +-
 lib/manager/git-submodules/update.spec.ts     | 12 ++-
 lib/manager/git-submodules/update.ts          |  6 +-
 lib/manager/gitlabci/extract.spec.ts          | 28 +++++--
 lib/manager/gomod/artifacts.spec.ts           | 27 ++++---
 .../gradle-wrapper/artifacts-real.spec.ts     | 35 +++++----
 lib/manager/gradle-wrapper/artifacts.spec.ts  | 30 +++++---
 lib/manager/gradle-wrapper/artifacts.ts       |  3 +-
 lib/manager/gradle/index-real.spec.ts         | 11 ++-
 lib/manager/gradle/index.spec.ts              | 37 +++++----
 lib/manager/gradle/index.ts                   |  8 +-
 lib/manager/helmv3/artifacts.spec.ts          | 18 +++--
 lib/manager/mix/artifacts.spec.ts             | 19 +++--
 lib/manager/mix/extract.spec.ts               |  5 ++
 lib/manager/npm/post-update/index.ts          | 28 ++++---
 lib/manager/nuget/artifacts.spec.ts           | 20 +++--
 lib/manager/nuget/artifacts.ts                |  4 +-
 lib/manager/nuget/extract.spec.ts             | 75 ++++++-------------
 lib/manager/nuget/extract.ts                  |  7 +-
 .../pip_requirements/artifacts.spec.ts        |  5 +-
 .../__snapshots__/extract.spec.ts.snap        |  6 +-
 lib/manager/pip_setup/extract.spec.ts         |  2 +
 lib/manager/pip_setup/index.spec.ts           | 16 +++-
 lib/manager/pipenv/artifacts.spec.ts          | 33 ++++----
 lib/manager/poetry/artifacts.spec.ts          | 20 ++---
 lib/manager/types.ts                          |  5 --
 lib/platform/azure/index.ts                   |  2 -
 lib/platform/bitbucket-server/index.spec.ts   |  6 --
 lib/platform/bitbucket-server/index.ts        |  6 +-
 lib/platform/bitbucket/index.spec.ts          |  2 -
 lib/platform/bitbucket/index.ts               |  2 -
 lib/platform/gitea/index.spec.ts              |  2 -
 lib/platform/gitea/index.ts                   |  3 -
 lib/platform/github/index.ts                  |  3 +-
 lib/platform/github/types.ts                  |  1 -
 lib/platform/gitlab/index.spec.ts             | 12 ---
 lib/platform/gitlab/index.ts                  |  3 -
 lib/platform/types.ts                         |  1 -
 lib/util/exec/common.ts                       |  2 -
 lib/util/exec/docker/index.ts                 | 14 +++-
 lib/util/exec/exec.spec.ts                    | 10 +--
 lib/util/exec/index.ts                        |  7 +-
 lib/util/fs/index.spec.ts                     |  8 +-
 lib/util/fs/index.ts                          | 18 ++---
 lib/util/git/index.spec.ts                    |  9 +--
 lib/util/git/index.ts                         | 19 +++--
 lib/util/index.ts                             | 11 ---
 lib/workers/branch/index.spec.ts              | 45 +++++------
 lib/workers/branch/lock-files/index.spec.ts   | 15 ++--
 lib/workers/global/index.ts                   |  4 +-
 lib/workers/repository/index.spec.ts          |  2 +
 lib/workers/repository/index.ts               | 12 +--
 lib/workers/repository/init/cache.spec.ts     |  2 +
 lib/workers/repository/init/index.spec.ts     |  8 ++
 lib/workers/repository/stats.ts               |  1 +
 69 files changed, 532 insertions(+), 442 deletions(-)

diff --git a/lib/config/admin.ts b/lib/config/admin.ts
index c25361eac9..8596085786 100644
--- a/lib/config/admin.ts
+++ b/lib/config/admin.ts
@@ -3,7 +3,7 @@ import type { RenovateConfig, RepoAdminConfig } from './types';
 let adminConfig: RepoAdminConfig = {};
 
 // TODO: once admin config work is complete, add a test to make sure this list includes all options with admin=true (#9603)
-export const repoAdminOptions = [
+const repoAdminOptions = [
   'allowCustomCrateRegistries',
   'allowPostUpgradeCommandTemplating',
   'allowScripts',
@@ -15,14 +15,20 @@ export const repoAdminOptions = [
   'dryRun',
   'exposeAllEnv',
   'privateKey',
+  'localDir',
+  'cacheDir',
 ];
 
-export function setAdminConfig(config: RenovateConfig = {}): void {
+export function setAdminConfig(
+  config: RenovateConfig | RepoAdminConfig = {}
+): RenovateConfig {
   adminConfig = {};
+  const result = { ...config };
   for (const option of repoAdminOptions) {
     adminConfig[option] = config[option];
-    delete config[option]; // eslint-disable-line no-param-reassign
+    delete result[option]; // eslint-disable-line no-param-reassign
   }
+  return result;
 }
 
 export function getAdminConfig(): RepoAdminConfig {
diff --git a/lib/config/types.ts b/lib/config/types.ts
index bb9684796c..e0cfa91a98 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -94,6 +94,8 @@ export interface RepoAdminConfig {
   dryRun?: boolean;
   exposeAllEnv?: boolean;
   privateKey?: string | Buffer;
+  localDir?: string;
+  cacheDir?: string;
 }
 
 export interface LegacyAdminConfig {
diff --git a/lib/datasource/crate/index.spec.ts b/lib/datasource/crate/index.spec.ts
index 4e00641136..00ae5986f2 100644
--- a/lib/datasource/crate/index.spec.ts
+++ b/lib/datasource/crate/index.spec.ts
@@ -7,8 +7,8 @@ import { getPkgReleases } from '..';
 import * as httpMock from '../../../test/http-mock';
 import { getName, loadFixture } from '../../../test/util';
 import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import * as memCache from '../../util/cache/memory';
-import { setFsConfig } from '../../util/fs';
 import { RegistryFlavor, RegistryInfo } from './types';
 import { id as datasource, fetchCrateRecordsPayload, getIndexSuffix } from '.';
 
@@ -75,22 +75,21 @@ describe(getName(), () => {
 
   describe('getReleases', () => {
     let tmpDir: DirectoryResult | null;
-    let localDir: string | null;
-    let cacheDir: string | null;
+    let adminConfig: RepoAdminConfig;
 
     beforeEach(async () => {
       httpMock.setup();
 
       tmpDir = await dir();
-      localDir = join(tmpDir.path, 'local');
-      cacheDir = join(tmpDir.path, 'cache');
-      setFsConfig({
-        localDir,
-        cacheDir,
-      });
+
+      adminConfig = {
+        localDir: join(tmpDir.path, 'local'),
+        cacheDir: join(tmpDir.path, 'cache'),
+      };
+      setAdminConfig(adminConfig);
+
       simpleGit.mockReset();
       memCache.init();
-      setAdminConfig();
     });
 
     afterEach(() => {
@@ -232,7 +231,7 @@ describe(getName(), () => {
     });
     it('clones cloudsmith private registry', async () => {
       const { mockClone } = setupGitMocks();
-      setAdminConfig({ allowCustomCrateRegistries: true });
+      setAdminConfig({ ...adminConfig, allowCustomCrateRegistries: true });
       const url = 'https://dl.cloudsmith.io/basic/myorg/myrepo/cargo/index.git';
       const res = await getPkgReleases({
         datasource,
@@ -246,7 +245,7 @@ describe(getName(), () => {
     });
     it('clones other private registry', async () => {
       const { mockClone } = setupGitMocks();
-      setAdminConfig({ allowCustomCrateRegistries: true });
+      setAdminConfig({ ...adminConfig, allowCustomCrateRegistries: true });
       const url = 'https://github.com/mcorbin/testregistry';
       const res = await getPkgReleases({
         datasource,
@@ -260,7 +259,7 @@ describe(getName(), () => {
     });
     it('clones once then reuses the cache', async () => {
       const { mockClone } = setupGitMocks();
-      setAdminConfig({ allowCustomCrateRegistries: true });
+      setAdminConfig({ ...adminConfig, allowCustomCrateRegistries: true });
       const url = 'https://github.com/mcorbin/othertestregistry';
       await getPkgReleases({
         datasource,
@@ -276,7 +275,7 @@ describe(getName(), () => {
     });
     it('guards against race conditions while cloning', async () => {
       const { mockClone } = setupGitMocks(250);
-      setAdminConfig({ allowCustomCrateRegistries: true });
+      setAdminConfig({ ...adminConfig, allowCustomCrateRegistries: true });
       const url = 'https://github.com/mcorbin/othertestregistry';
 
       await Promise.all([
@@ -302,7 +301,7 @@ describe(getName(), () => {
     });
     it('returns null when git clone fails', async () => {
       setupErrorGitMock();
-      setAdminConfig({ allowCustomCrateRegistries: true });
+      setAdminConfig({ ...adminConfig, allowCustomCrateRegistries: true });
       const url = 'https://github.com/mcorbin/othertestregistry';
 
       const result = await getPkgReleases({
diff --git a/lib/manager/batect/extract.spec.ts b/lib/manager/batect/extract.spec.ts
index 590e80b652..9596f4b7bb 100644
--- a/lib/manager/batect/extract.spec.ts
+++ b/lib/manager/batect/extract.spec.ts
@@ -1,9 +1,11 @@
 import { getName } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { id as gitTagDatasource } from '../../datasource/git-tags';
 import { id as dockerVersioning } from '../../versioning/docker';
 import { id as semverVersioning } from '../../versioning/semver';
 import { getDep } from '../dockerfile/extract';
-import type { PackageDependency } from '../types';
+import type { ExtractConfig, PackageDependency } from '../types';
 import { extractAllPackageFiles } from './extract';
 
 const fixturesDir = 'lib/manager/batect/__fixtures__';
@@ -25,22 +27,40 @@ function createGitDependency(repo: string, version: string): PackageDependency {
   };
 }
 
+const adminConfig: RepoAdminConfig = {
+  localDir: '',
+};
+
+const config: ExtractConfig = {};
+
 describe(getName(), () => {
   describe('extractPackageFile()', () => {
+    beforeEach(() => {
+      setAdminConfig(adminConfig);
+    });
+
+    afterEach(() => {
+      setAdminConfig();
+    });
+
     it('returns empty array for empty configuration file', async () => {
       expect(
-        await extractAllPackageFiles({}, [`${fixturesDir}/empty/batect.yml`])
+        await extractAllPackageFiles(config, [
+          `${fixturesDir}/empty/batect.yml`,
+        ])
       ).toEqual([]);
     });
 
     it('returns empty array for non-object configuration file', async () => {
       expect(
-        await extractAllPackageFiles({}, [`${fixturesDir}/invalid/batect.yml`])
+        await extractAllPackageFiles(config, [
+          `${fixturesDir}/invalid/batect.yml`,
+        ])
       ).toEqual([]);
     });
 
     it('returns an a package file with no dependencies for configuration file without containers or includes', async () => {
-      const result = await extractAllPackageFiles({}, [
+      const result = await extractAllPackageFiles(config, [
         `${fixturesDir}/no-containers-or-includes/batect.yml`,
       ]);
 
@@ -53,7 +73,7 @@ describe(getName(), () => {
     });
 
     it('extracts all available images and bundles from a valid Batect configuration file, including dependencies in included files', async () => {
-      const result = await extractAllPackageFiles({}, [
+      const result = await extractAllPackageFiles(config, [
         `${fixturesDir}/valid/batect.yml`,
       ]);
 
diff --git a/lib/manager/bundler/artifacts.spec.ts b/lib/manager/bundler/artifacts.spec.ts
index 7b7ea59220..05cf2d6ca7 100644
--- a/lib/manager/bundler/artifacts.spec.ts
+++ b/lib/manager/bundler/artifacts.spec.ts
@@ -2,13 +2,15 @@ import { exec as _exec } from 'child_process';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { fs, git, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import * as _datasource from '../../datasource';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
-import { setFsConfig } from '../../util/fs';
-import { StatusResult } from '../../util/git';
+import type { StatusResult } from '../../util/git';
+import type { UpdateArtifactsConfig } from '../types';
 import * as _bundlerHostRules from './host-rules';
 import { updateArtifacts } from '.';
 
@@ -26,7 +28,13 @@ jest.mock('../../../lib/util/git');
 jest.mock('../../../lib/util/host-rules');
 jest.mock('./host-rules');
 
-let config;
+const adminConfig: RepoAdminConfig = {
+  // `join` fixes Windows CI
+  localDir: join('/tmp/github/some/repo'),
+  cacheDir: join('/tmp/cache'),
+};
+
+const config: UpdateArtifactsConfig = {};
 
 describe('bundler.updateArtifacts()', () => {
   beforeEach(async () => {
@@ -35,18 +43,15 @@ describe('bundler.updateArtifacts()', () => {
 
     delete process.env.GEM_HOME;
 
-    config = {
-      // `join` fixes Windows CI
-      localDir: join('/tmp/github/some/repo'),
-      cacheDir: join('/tmp/cache'),
-    };
-
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
     bundlerHostRules.findAllAuthenticatable.mockReturnValue([]);
     docker.resetPrefetchedImages();
 
-    await setExecConfig(config);
-    setFsConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
+  });
+  afterEach(() => {
+    setAdminConfig();
   });
   it('returns null by default', async () => {
     expect(
@@ -120,8 +125,10 @@ describe('bundler.updateArtifacts()', () => {
   describe('Docker', () => {
     beforeEach(async () => {
       jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-      await setExecConfig({ ...config, binarySource: BinarySource.Docker });
-      setFsConfig({ ...config, binarySource: BinarySource.Docker });
+      await setExecConfig({
+        ...adminConfig,
+        binarySource: BinarySource.Docker,
+      });
     });
     it('.ruby-version', async () => {
       fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
diff --git a/lib/manager/bundler/utils.ts b/lib/manager/bundler/utils.ts
index c63546de5a..4c40aecceb 100644
--- a/lib/manager/bundler/utils.ts
+++ b/lib/manager/bundler/utils.ts
@@ -1,4 +1,5 @@
 import { join } from 'upath';
+import { getAdminConfig } from '../../config/admin';
 import { logger } from '../../logger';
 import { ensureDir } from '../../util/fs';
 import type { UpdateArtifactsConfig } from '../types';
@@ -7,7 +8,7 @@ export async function getGemHome(
   config: UpdateArtifactsConfig
 ): Promise<string> {
   const cacheDir =
-    process.env.GEM_HOME || join(config.cacheDir, './others/gem');
+    process.env.GEM_HOME || join(getAdminConfig().cacheDir, './others/gem');
   await ensureDir(cacheDir);
   logger.debug(`Using gem home ${cacheDir}`);
   return cacheDir;
diff --git a/lib/manager/cargo/artifacts.spec.ts b/lib/manager/cargo/artifacts.spec.ts
index 021a9477bf..a2c8bf0fc3 100644
--- a/lib/manager/cargo/artifacts.spec.ts
+++ b/lib/manager/cargo/artifacts.spec.ts
@@ -3,10 +3,13 @@ import _fs from 'fs-extra';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { git, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
+import type { UpdateArtifactsConfig } from '../types';
 import * as cargo from './artifacts';
 
 jest.mock('fs-extra');
@@ -19,7 +22,9 @@ const fs: jest.Mocked<typeof _fs> = _fs as any;
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const env = mocked(_env);
 
-const config = {
+const config: UpdateArtifactsConfig = {};
+
+const adminConfig: RepoAdminConfig = {
   // `join` fixes Windows CI
   localDir: join('/tmp/github/some/repo'),
 };
@@ -30,9 +35,13 @@ describe('.updateArtifacts()', () => {
     jest.resetModules();
 
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    await setExecConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
     docker.resetPrefetchedImages();
   });
+  afterEach(() => {
+    setAdminConfig();
+  });
   it('returns null if no Cargo.lock found', async () => {
     fs.stat.mockRejectedValue(new Error('not found!'));
     const updatedDeps = ['dep1'];
@@ -128,7 +137,7 @@ describe('.updateArtifacts()', () => {
   it('returns updated Cargo.lock with docker', async () => {
     fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     git.getFile.mockResolvedValueOnce('Old Cargo.lock');
     const execSnapshots = mockExecAll(exec);
     fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any);
diff --git a/lib/manager/cargo/extract.spec.ts b/lib/manager/cargo/extract.spec.ts
index 7c4db8abeb..25cd4087e2 100644
--- a/lib/manager/cargo/extract.spec.ts
+++ b/lib/manager/cargo/extract.spec.ts
@@ -1,7 +1,10 @@
 import { dir } from 'tmp-promise';
 import { join } from 'upath';
 import { getName, loadFixture } from '../../../test/util';
-import { setFsConfig, writeLocalFile } from '../../util/fs';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
+import { writeLocalFile } from '../../util/fs';
+import type { ExtractConfig } from '../types';
 import { extractPackageFile } from './extract';
 
 const cargo1toml = loadFixture('Cargo.1.toml');
@@ -14,9 +17,21 @@ const cargo6toml = loadFixture('Cargo.6.toml');
 
 describe(getName(), () => {
   describe('extractPackageFile()', () => {
-    let config;
-    beforeEach(() => {
+    let config: ExtractConfig;
+    let adminConfig: RepoAdminConfig;
+
+    beforeEach(async () => {
       config = {};
+      const tmpDir = await dir();
+      adminConfig = {
+        localDir: join(tmpDir.path, 'local'),
+        cacheDir: join(tmpDir.path, 'cache'),
+      };
+
+      setAdminConfig(adminConfig);
+    });
+    afterEach(() => {
+      setAdminConfig();
     });
     it('returns null for invalid toml', async () => {
       expect(
@@ -67,35 +82,17 @@ describe(getName(), () => {
       expect(res.deps).toHaveLength(4);
     });
     it('extracts registry urls from .cargo/config.toml', async () => {
-      const tmpDir = await dir();
-      const localDir = join(tmpDir.path, 'local');
-      const cacheDir = join(tmpDir.path, 'cache');
-      setFsConfig({
-        localDir,
-        cacheDir,
-      });
       await writeLocalFile('.cargo/config.toml', cargo6configtoml);
-
       const res = await extractPackageFile(cargo6toml, 'Cargo.toml', {
         ...config,
-        localDir,
       });
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(3);
     });
     it('extracts registry urls from .cargo/config (legacy path)', async () => {
-      const tmpDir = await dir();
-      const localDir = join(tmpDir.path, 'local');
-      const cacheDir = join(tmpDir.path, 'cache');
-      setFsConfig({
-        localDir,
-        cacheDir,
-      });
       await writeLocalFile('.cargo/config', cargo6configtoml);
-
       const res = await extractPackageFile(cargo6toml, 'Cargo.toml', {
         ...config,
-        localDir,
       });
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(3);
@@ -108,35 +105,19 @@ describe(getName(), () => {
       expect(res.deps).toHaveLength(1);
     });
     it('fails to parse cargo config with invalid TOML', async () => {
-      const tmpDir = await dir();
-      const localDir = join(tmpDir.path, 'local');
-      const cacheDir = join(tmpDir.path, 'cache');
-      setFsConfig({
-        localDir,
-        cacheDir,
-      });
       await writeLocalFile('.cargo/config', '[registries');
 
       const res = await extractPackageFile(cargo6toml, 'Cargo.toml', {
         ...config,
-        localDir,
       });
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(3);
     });
     it('ignore cargo config registries with missing index', async () => {
-      const tmpDir = await dir();
-      const localDir = join(tmpDir.path, 'local');
-      const cacheDir = join(tmpDir.path, 'cache');
-      setFsConfig({
-        localDir,
-        cacheDir,
-      });
       await writeLocalFile('.cargo/config', '[registries.mine]\nfoo = "bar"');
 
       const res = await extractPackageFile(cargo6toml, 'Cargo.toml', {
         ...config,
-        localDir,
       });
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(3);
diff --git a/lib/manager/cocoapods/artifacts.spec.ts b/lib/manager/cocoapods/artifacts.spec.ts
index 42013554b3..2944f67e22 100644
--- a/lib/manager/cocoapods/artifacts.spec.ts
+++ b/lib/manager/cocoapods/artifacts.spec.ts
@@ -3,11 +3,14 @@ import _fs from 'fs-extra';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { git, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import * as _datasource from '../../datasource';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as _env from '../../util/exec/env';
-import { StatusResult } from '../../util/git';
+import type { StatusResult } from '../../util/git';
+import type { UpdateArtifactsConfig } from '../types';
 import { updateArtifacts } from '.';
 
 jest.mock('fs-extra');
@@ -24,7 +27,9 @@ const datasource = mocked(_datasource);
 
 delete process.env.CP_HOME_DIR;
 
-const config = {
+const config: UpdateArtifactsConfig = {};
+
+const adminConfig: RepoAdminConfig = {
   localDir: join('/tmp/github/some/repo'),
   cacheDir: join('/tmp/cache'),
 };
@@ -33,7 +38,9 @@ describe('.updateArtifacts()', () => {
   beforeEach(async () => {
     jest.resetAllMocks();
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    await setExecConfig(config);
+
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
 
     datasource.getPkgReleases.mockResolvedValue({
       releases: [
@@ -46,6 +53,9 @@ describe('.updateArtifacts()', () => {
       ],
     });
   });
+  afterEach(() => {
+    setAdminConfig();
+  });
   it('returns null if no Podfile.lock found', async () => {
     const execSnapshots = mockExecAll(exec);
     expect(
@@ -72,15 +82,16 @@ describe('.updateArtifacts()', () => {
   });
   it('returns null for invalid local directory', async () => {
     const execSnapshots = mockExecAll(exec);
-    const noLocalDirConfig = {
-      localDir: undefined,
-    };
+    setAdminConfig({
+      localDir: '',
+    });
+
     expect(
       await updateArtifacts({
         packageFileName: 'Podfile',
         updatedDeps: ['foo'],
         newPackageFileContent: '',
-        config: noLocalDirConfig,
+        config: {},
       })
     ).toBeNull();
     expect(execSnapshots).toMatchSnapshot();
@@ -116,7 +127,7 @@ describe('.updateArtifacts()', () => {
   });
   it('returns updated Podfile', async () => {
     const execSnapshots = mockExecAll(exec);
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     fs.readFile.mockResolvedValueOnce('Old Podfile' as any);
     git.getRepoStatus.mockResolvedValueOnce({
       modified: ['Podfile.lock'],
@@ -134,7 +145,7 @@ describe('.updateArtifacts()', () => {
   });
   it('returns updated Podfile and Pods files', async () => {
     const execSnapshots = mockExecAll(exec);
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     fs.readFile.mockResolvedValueOnce('Old Manifest.lock' as any);
     fs.readFile.mockResolvedValueOnce('New Podfile' as any);
     fs.readFile.mockResolvedValueOnce('Pods manifest' as any);
@@ -187,10 +198,7 @@ describe('.updateArtifacts()', () => {
   it('dynamically selects Docker image tag', async () => {
     const execSnapshots = mockExecAll(exec);
 
-    await setExecConfig({
-      ...config,
-      binarySource: 'docker',
-    });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
 
     fs.readFile.mockResolvedValueOnce('COCOAPODS: 1.2.4' as any);
 
@@ -211,10 +219,7 @@ describe('.updateArtifacts()', () => {
   it('falls back to the `latest` Docker image tag', async () => {
     const execSnapshots = mockExecAll(exec);
 
-    await setExecConfig({
-      ...config,
-      binarySource: 'docker',
-    });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
 
     fs.readFile.mockResolvedValueOnce('COCOAPODS: 1.2.4' as any);
     datasource.getPkgReleases.mockResolvedValueOnce({
diff --git a/lib/manager/cocoapods/extract.spec.ts b/lib/manager/cocoapods/extract.spec.ts
index fc8e923e14..6ae2c5c649 100644
--- a/lib/manager/cocoapods/extract.spec.ts
+++ b/lib/manager/cocoapods/extract.spec.ts
@@ -1,12 +1,19 @@
 import { getName, loadFixture } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { extractPackageFile } from '.';
 
 const simplePodfile = loadFixture('Podfile.simple');
 const complexPodfile = loadFixture('Podfile.complex');
 
+const adminConfig: RepoAdminConfig = {
+  localDir: '',
+};
+
 describe(getName(), () => {
   describe('extractPackageFile()', () => {
     it('extracts all dependencies', async () => {
+      setAdminConfig(adminConfig);
       const simpleResult = (await extractPackageFile(simplePodfile, 'Podfile'))
         .deps;
       expect(simpleResult).toMatchSnapshot();
diff --git a/lib/manager/cocoapods/utils.ts b/lib/manager/cocoapods/utils.ts
index c393af56d8..e3a9af3b59 100644
--- a/lib/manager/cocoapods/utils.ts
+++ b/lib/manager/cocoapods/utils.ts
@@ -1,4 +1,5 @@
 import { join } from 'upath';
+import { getAdminConfig } from '../../config/admin';
 import { logger } from '../../logger';
 import { ensureDir } from '../../util/fs';
 import type { UpdateArtifactsConfig } from '../types';
@@ -6,8 +7,9 @@ import type { UpdateArtifactsConfig } from '../types';
 export async function getCocoaPodsHome(
   config: UpdateArtifactsConfig
 ): Promise<string> {
+  const adminCacheDir = getAdminConfig().cacheDir;
   const cacheDir =
-    process.env.CP_HOME_DIR || join(config.cacheDir, './others/cocoapods');
+    process.env.CP_HOME_DIR || join(adminCacheDir, './others/cocoapods');
   await ensureDir(cacheDir);
   logger.debug(`Using cocoapods home ${cacheDir}`);
   return cacheDir;
diff --git a/lib/manager/composer/artifacts.spec.ts b/lib/manager/composer/artifacts.spec.ts
index a9082620ad..23218407a9 100644
--- a/lib/manager/composer/artifacts.spec.ts
+++ b/lib/manager/composer/artifacts.spec.ts
@@ -3,6 +3,7 @@ import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { env, fs, git, mocked, partial } from '../../../test/util';
 import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import {
   PLATFORM_TYPE_GITHUB,
   PLATFORM_TYPE_GITLAB,
@@ -12,9 +13,9 @@ import * as datasourcePackagist from '../../datasource/packagist';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
-import { setFsConfig } from '../../util/fs';
-import { StatusResult } from '../../util/git';
+import type { StatusResult } from '../../util/git';
 import * as hostRules from '../../util/host-rules';
+import type { UpdateArtifactsConfig } from '../types';
 import * as composer from './artifacts';
 
 jest.mock('child_process');
@@ -26,12 +27,16 @@ jest.mock('../../util/git');
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const datasource = mocked(_datasource);
 
-const config = {
+const config: UpdateArtifactsConfig = {
+  composerIgnorePlatformReqs: true,
+  ignoreScripts: false,
+};
+
+const adminConfig: RepoAdminConfig = {
+  allowScripts: false,
   // `join` fixes Windows CI
   localDir: join('/tmp/github/some/repo'),
   cacheDir: join('/tmp/renovate/cache'),
-  composerIgnorePlatformReqs: true,
-  ignoreScripts: false,
 };
 
 const repoStatus = partial<StatusResult>({
@@ -45,11 +50,13 @@ describe('.updateArtifacts()', () => {
     jest.resetAllMocks();
     jest.resetModules();
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    await setExecConfig(config);
-    setFsConfig(config);
+    await setExecConfig(adminConfig as never);
     docker.resetPrefetchedImages();
     hostRules.clear();
-    setAdminConfig({ allowScripts: false });
+    setAdminConfig(adminConfig);
+  });
+  afterEach(() => {
+    setAdminConfig();
   });
   it('returns if no composer.lock found', async () => {
     expect(
@@ -66,7 +73,7 @@ describe('.updateArtifacts()', () => {
     const execSnapshots = mockExecAll(exec);
     fs.readLocalFile.mockReturnValueOnce('Current composer.lock' as any);
     git.getRepoStatus.mockResolvedValue(repoStatus);
-    setAdminConfig({ allowScripts: true });
+    setAdminConfig({ ...adminConfig, allowScripts: true });
     expect(
       await composer.updateArtifacts({
         packageFileName: 'composer.json',
@@ -196,8 +203,7 @@ describe('.updateArtifacts()', () => {
   });
   it('supports docker mode', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
-    setFsConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
 
     const execSnapshots = mockExecAll(exec);
diff --git a/lib/manager/composer/artifacts.ts b/lib/manager/composer/artifacts.ts
index ed430b707b..f67f3e2b63 100644
--- a/lib/manager/composer/artifacts.ts
+++ b/lib/manager/composer/artifacts.ts
@@ -77,9 +77,10 @@ export async function updateArtifacts({
 }: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
   logger.debug(`composer.updateArtifacts(${packageFileName})`);
 
+  const { allowScripts, cacheDir: adminCacheDir } = getAdminConfig();
   const cacheDir =
     process.env.COMPOSER_CACHE_DIR ||
-    upath.join(config.cacheDir, './others/composer');
+    upath.join(adminCacheDir, './others/composer');
   await ensureDir(cacheDir);
   logger.debug(`Using composer cache ${cacheDir}`);
 
@@ -124,7 +125,7 @@ export async function updateArtifacts({
       args += ' --ignore-platform-reqs';
     }
     args += ' --no-ansi --no-interaction';
-    if (!getAdminConfig().allowScripts || config.ignoreScripts) {
+    if (!allowScripts || config.ignoreScripts) {
       args += ' --no-scripts --no-autoloader';
     }
     logger.debug({ cmd, args }, 'composer command');
diff --git a/lib/manager/git-submodules/extract.spec.ts b/lib/manager/git-submodules/extract.spec.ts
index c277c84667..89d2826460 100644
--- a/lib/manager/git-submodules/extract.spec.ts
+++ b/lib/manager/git-submodules/extract.spec.ts
@@ -1,6 +1,7 @@
 import { mock } from 'jest-mock-extended';
 import _simpleGit, { Response, SimpleGit } from 'simple-git';
 import { getName, partial } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
 import * as hostRules from '../../util/host-rules';
 import type { PackageFile } from '../types';
 import extractPackageFile from './extract';
@@ -9,8 +10,6 @@ jest.mock('simple-git');
 const simpleGit: jest.Mock<Partial<SimpleGit>> = _simpleGit as never;
 const Git: typeof _simpleGit = jest.requireActual('simple-git');
 
-const localDir = `${__dirname}/__fixtures__`;
-
 describe(getName(), () => {
   // flaky ci tests
   jest.setTimeout(10 * 1000);
@@ -45,19 +44,18 @@ describe(getName(), () => {
   });
   describe('extractPackageFile()', () => {
     it('extracts submodules', async () => {
+      setAdminConfig({ localDir: `${__dirname}/__fixtures__` });
       hostRules.add({ matchHost: 'github.com', token: 'abc123' });
       let res: PackageFile;
-      expect(
-        await extractPackageFile('', '.gitmodules.1', { localDir })
-      ).toBeNull();
-      res = await extractPackageFile('', '.gitmodules.2', { localDir });
+      expect(await extractPackageFile('', '.gitmodules.1', {})).toBeNull();
+      res = await extractPackageFile('', '.gitmodules.2', {});
       expect(res.deps).toHaveLength(1);
       expect(res.deps[0].currentValue).toEqual('main');
-      res = await extractPackageFile('', '.gitmodules.3', { localDir });
+      res = await extractPackageFile('', '.gitmodules.3', {});
       expect(res.deps).toHaveLength(1);
-      res = await extractPackageFile('', '.gitmodules.4', { localDir });
+      res = await extractPackageFile('', '.gitmodules.4', {});
       expect(res.deps).toHaveLength(1);
-      res = await extractPackageFile('', '.gitmodules.5', { localDir });
+      res = await extractPackageFile('', '.gitmodules.5', {});
       expect(res.deps).toHaveLength(3);
       expect(res.deps[2].lookupName).toEqual(
         'https://github.com/renovatebot/renovate-config.git'
diff --git a/lib/manager/git-submodules/extract.ts b/lib/manager/git-submodules/extract.ts
index ac41b7f6b7..ce01daf9c7 100644
--- a/lib/manager/git-submodules/extract.ts
+++ b/lib/manager/git-submodules/extract.ts
@@ -1,6 +1,7 @@
 import URL from 'url';
 import Git, { SimpleGit } from 'simple-git';
 import upath from 'upath';
+import { getAdminConfig } from '../../config/admin';
 import * as datasourceGitRefs from '../../datasource/git-refs';
 import { logger } from '../../logger';
 import { getHttpUrl, getRemoteUrlWithToken } from '../../util/git/url';
@@ -87,8 +88,9 @@ export default async function extractPackageFile(
   fileName: string,
   config: ManagerConfig
 ): Promise<PackageFile | null> {
-  const git = Git(config.localDir);
-  const gitModulesPath = upath.join(config.localDir, fileName);
+  const { localDir } = getAdminConfig();
+  const git = Git(localDir);
+  const gitModulesPath = upath.join(localDir, fileName);
 
   const depNames = await getModules(git, gitModulesPath);
 
diff --git a/lib/manager/git-submodules/update.spec.ts b/lib/manager/git-submodules/update.spec.ts
index 4b239cead6..9887e7ceb8 100644
--- a/lib/manager/git-submodules/update.spec.ts
+++ b/lib/manager/git-submodules/update.spec.ts
@@ -1,6 +1,9 @@
 import _simpleGit from 'simple-git';
 import { dir } from 'tmp-promise';
+import { join } from 'upath';
 import { getName } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import type { Upgrade } from '../types';
 import updateDependency from './update';
 
@@ -10,9 +13,16 @@ const simpleGit: any = _simpleGit;
 describe(getName(), () => {
   describe('updateDependency', () => {
     let upgrade: Upgrade;
+    let adminConfig: RepoAdminConfig;
     beforeAll(async () => {
+      upgrade = { depName: 'renovate' };
+
       const tmpDir = await dir();
-      upgrade = { localDir: tmpDir.path, depName: 'renovate' };
+      adminConfig = { localDir: join(tmpDir.path) };
+      setAdminConfig(adminConfig);
+    });
+    afterAll(() => {
+      setAdminConfig();
     });
     it('returns null on error', async () => {
       simpleGit.mockReturnValue({
diff --git a/lib/manager/git-submodules/update.ts b/lib/manager/git-submodules/update.ts
index a62933fd64..da05a5ae46 100644
--- a/lib/manager/git-submodules/update.ts
+++ b/lib/manager/git-submodules/update.ts
@@ -1,5 +1,6 @@
 import Git from 'simple-git';
 import upath from 'upath';
+import { getAdminConfig } from '../../config/admin';
 import { logger } from '../../logger';
 import type { UpdateDependencyConfig } from '../types';
 
@@ -7,8 +8,9 @@ export default async function updateDependency({
   fileContent,
   upgrade,
 }: UpdateDependencyConfig): Promise<string | null> {
-  const git = Git(upgrade.localDir);
-  const submoduleGit = Git(upath.join(upgrade.localDir, upgrade.depName));
+  const { localDir } = getAdminConfig();
+  const git = Git(localDir);
+  const submoduleGit = Git(upath.join(localDir, upgrade.depName));
 
   try {
     await git.submoduleUpdate(['--init', upgrade.depName]);
diff --git a/lib/manager/gitlabci/extract.spec.ts b/lib/manager/gitlabci/extract.spec.ts
index d739dcaae0..ae3f1f3076 100644
--- a/lib/manager/gitlabci/extract.spec.ts
+++ b/lib/manager/gitlabci/extract.spec.ts
@@ -1,19 +1,33 @@
 import { getName, logger } from '../../../test/util';
-import type { PackageDependency } from '../types';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
+import type { ExtractConfig, PackageDependency } from '../types';
 import { extractAllPackageFiles } from './extract';
 
+const config: ExtractConfig = {};
+
+const adminConfig: RepoAdminConfig = { localDir: '' };
+
 describe(getName(), () => {
+  beforeEach(() => {
+    setAdminConfig(adminConfig);
+  });
+
+  afterEach(() => {
+    setAdminConfig();
+  });
+
   describe('extractAllPackageFiles()', () => {
     it('returns null for empty', async () => {
       expect(
-        await extractAllPackageFiles({}, [
+        await extractAllPackageFiles(config, [
           'lib/manager/gitlabci/__fixtures__/gitlab-ci.2.yaml',
         ])
       ).toBeNull();
     });
 
     it('extracts multiple included image lines', async () => {
-      const res = await extractAllPackageFiles({}, [
+      const res = await extractAllPackageFiles(config, [
         'lib/manager/gitlabci/__fixtures__/gitlab-ci.3.yaml',
       ]);
       expect(res).toMatchSnapshot();
@@ -29,7 +43,7 @@ describe(getName(), () => {
     });
 
     it('extracts named services', async () => {
-      const res = await extractAllPackageFiles({}, [
+      const res = await extractAllPackageFiles(config, [
         'lib/manager/gitlabci/__fixtures__/gitlab-ci.5.yaml',
       ]);
       expect(res).toMatchSnapshot();
@@ -38,7 +52,7 @@ describe(getName(), () => {
     });
 
     it('extracts multiple image lines', async () => {
-      const res = await extractAllPackageFiles({}, [
+      const res = await extractAllPackageFiles(config, [
         'lib/manager/gitlabci/__fixtures__/gitlab-ci.yaml',
       ]);
       expect(res).toMatchSnapshot();
@@ -56,7 +70,7 @@ describe(getName(), () => {
     });
 
     it('extracts multiple image lines with comments', async () => {
-      const res = await extractAllPackageFiles({}, [
+      const res = await extractAllPackageFiles(config, [
         'lib/manager/gitlabci/__fixtures__/gitlab-ci.1.yaml',
       ]);
       expect(res).toMatchSnapshot();
@@ -72,7 +86,7 @@ describe(getName(), () => {
     });
 
     it('catches errors', async () => {
-      const res = await extractAllPackageFiles({}, [
+      const res = await extractAllPackageFiles(config, [
         'lib/manager/gitlabci/__fixtures__/gitlab-ci.4.yaml',
       ]);
       expect(res).toBeNull();
diff --git a/lib/manager/gomod/artifacts.spec.ts b/lib/manager/gomod/artifacts.spec.ts
index 50e4b014d5..0d5f5db948 100644
--- a/lib/manager/gomod/artifacts.spec.ts
+++ b/lib/manager/gomod/artifacts.spec.ts
@@ -3,13 +3,15 @@ import _fs from 'fs-extra';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { git, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
-import { setFsConfig } from '../../util/fs';
-import { StatusResult } from '../../util/git';
+import type { StatusResult } from '../../util/git';
 import * as _hostRules from '../../util/host-rules';
+import type { UpdateArtifactsConfig } from '../types';
 import * as gomod from './artifacts';
 
 jest.mock('fs-extra');
@@ -36,10 +38,13 @@ require gopkg.in/russross/blackfriday.v1 v1.0.0
 replace github.com/pkg/errors => ../errors
 `;
 
-const config = {
+const adminConfig: RepoAdminConfig = {
   // `join` fixes Windows CI
   localDir: join('/tmp/github/some/repo'),
   cacheDir: join('/tmp/renovate/cache'),
+};
+
+const config: UpdateArtifactsConfig = {
   constraints: { go: '1.14' },
 };
 
@@ -58,10 +63,13 @@ describe('.updateArtifacts()', () => {
 
     delete process.env.GOPATH;
     env.getChildProcessEnv.mockReturnValue({ ...envMock.basic, ...goEnv });
-    await setExecConfig(config);
-    setFsConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
     docker.resetPrefetchedImages();
   });
+  afterEach(() => {
+    setAdminConfig();
+  });
   it('returns if no go.sum found', async () => {
     const execSnapshots = mockExecAll(exec);
     expect(
@@ -147,8 +155,7 @@ describe('.updateArtifacts()', () => {
   });
   it('supports docker mode without credentials', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
-    setFsConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     fs.readFile.mockResolvedValueOnce('Current go.sum' as any);
     fs.readFile.mockResolvedValueOnce(null as any); // vendor modules filename
     const execSnapshots = mockExecAll(exec);
@@ -192,8 +199,7 @@ describe('.updateArtifacts()', () => {
   });
   it('supports docker mode with credentials', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
-    setFsConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     hostRules.find.mockReturnValueOnce({
       token: 'some-token',
     });
@@ -219,8 +225,7 @@ describe('.updateArtifacts()', () => {
   });
   it('supports docker mode with goModTidy', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
-    setFsConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     hostRules.find.mockReturnValueOnce({});
     fs.readFile.mockResolvedValueOnce('Current go.sum' as any);
     fs.readFile.mockResolvedValueOnce(null as any); // vendor modules filename
diff --git a/lib/manager/gradle-wrapper/artifacts-real.spec.ts b/lib/manager/gradle-wrapper/artifacts-real.spec.ts
index befff2cee4..3df9468491 100644
--- a/lib/manager/gradle-wrapper/artifacts-real.spec.ts
+++ b/lib/manager/gradle-wrapper/artifacts-real.spec.ts
@@ -3,17 +3,23 @@ import Git from 'simple-git';
 import { resolve } from 'upath';
 import * as httpMock from '../../../test/http-mock';
 import { getName, git, partial } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
-import { setFsConfig } from '../../util/fs';
-import { StatusResult } from '../../util/git';
+import type { StatusResult } from '../../util/git';
 import { ifSystemSupportsGradle } from '../gradle/__testutil__/gradle';
+import type { UpdateArtifactsConfig } from '../types';
 import * as dcUpdate from '.';
 
 jest.mock('../../util/git');
 
 const fixtures = resolve(__dirname, './__fixtures__');
-const config = {
+
+const adminConfig: RepoAdminConfig = {
   localDir: resolve(fixtures, './testFiles'),
+};
+
+const config: UpdateArtifactsConfig = {
   newValue: '5.6.4',
 };
 
@@ -37,14 +43,15 @@ describe(getName(), () => {
 
     beforeEach(async () => {
       jest.resetAllMocks();
-      await setExecConfig(config);
-      setFsConfig(config);
+      await setExecConfig(adminConfig as never);
+      setAdminConfig(adminConfig);
       httpMock.setup();
     });
 
     afterEach(async () => {
       await Git(fixtures).checkout(['HEAD', '--', '.']);
       httpMock.reset();
+      setAdminConfig();
     });
 
     it('replaces existing value', async () => {
@@ -160,17 +167,20 @@ describe(getName(), () => {
     });
 
     it('gradlew failed', async () => {
-      const cfg = { ...config, localDir: resolve(fixtures, './wrongCmd') };
+      const wrongCmdConfig = {
+        ...adminConfig,
+        localDir: resolve(fixtures, './wrongCmd'),
+      };
 
-      await setExecConfig(cfg);
-      setFsConfig(cfg);
+      await setExecConfig(wrongCmdConfig);
+      setAdminConfig(wrongCmdConfig);
       const res = await dcUpdate.updateArtifacts({
         packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
         updatedDeps: [],
         newPackageFileContent: await readString(
           `./testFiles/gradle/wrapper/gradle-wrapper.properties`
         ),
-        config: cfg,
+        config,
       });
 
       expect(res[0].artifactError.lockFile).toEqual(
@@ -186,13 +196,12 @@ describe(getName(), () => {
     });
 
     it('gradlew not found', async () => {
+      setAdminConfig({ localDir: 'some-dir' });
       const res = await dcUpdate.updateArtifacts({
         packageFileName: 'gradle-wrapper.properties',
         updatedDeps: [],
         newPackageFileContent: undefined,
-        config: {
-          localDir: 'some-dir',
-        },
+        config: {},
       });
 
       expect(res).toBeNull();
@@ -234,7 +243,7 @@ describe(getName(), () => {
 
       expect(
         await readString(
-          config.localDir,
+          adminConfig.localDir,
           `./gradle/wrapper/gradle-wrapper.properties`
         )
       ).toEqual(newContent);
diff --git a/lib/manager/gradle-wrapper/artifacts.spec.ts b/lib/manager/gradle-wrapper/artifacts.spec.ts
index 178960701e..16f92f1dd0 100644
--- a/lib/manager/gradle-wrapper/artifacts.spec.ts
+++ b/lib/manager/gradle-wrapper/artifacts.spec.ts
@@ -12,11 +12,13 @@ import {
   git,
   partial,
 } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import { resetPrefetchedImages } from '../../util/exec/docker';
-import { setFsConfig } from '../../util/fs';
-import { StatusResult } from '../../util/git';
+import type { StatusResult } from '../../util/git';
+import type { UpdateArtifactsConfig } from '../types';
 import * as dcUpdate from '.';
 
 jest.mock('child_process');
@@ -26,11 +28,16 @@ jest.mock('../../util/exec/env');
 
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const fixtures = resolve(__dirname, './__fixtures__');
-const config = {
+
+const adminConfig: RepoAdminConfig = {
   localDir: resolve(fixtures, './testFiles'),
+};
+
+const dockerAdminConfig = { ...adminConfig, binarySource: BinarySource.Docker };
+
+const config: UpdateArtifactsConfig = {
   newValue: '5.6.4',
 };
-const dockerConfig = { ...config, binarySource: BinarySource.Docker };
 
 addReplacingSerializer('gradlew.bat', '<gradlew>');
 addReplacingSerializer('./gradlew', '<gradlew>');
@@ -50,8 +57,8 @@ describe(getName(), () => {
       LC_ALL: 'en_US',
     });
 
-    await setExecConfig(config);
-    setFsConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
     resetPrefetchedImages();
 
     fs.readLocalFile.mockResolvedValue('test');
@@ -59,6 +66,7 @@ describe(getName(), () => {
 
   afterEach(() => {
     httpMock.reset();
+    setAdminConfig();
   });
 
   it('replaces existing value', async () => {
@@ -97,13 +105,12 @@ describe(getName(), () => {
   });
 
   it('gradlew not found', async () => {
+    setAdminConfig({ ...adminConfig, localDir: 'some-dir' });
     const res = await dcUpdate.updateArtifacts({
       packageFileName: 'gradle-wrapper.properties',
       updatedDeps: [],
       newPackageFileContent: undefined,
-      config: {
-        localDir: 'some-dir',
-      },
+      config: {},
     });
 
     expect(res).toBeNull();
@@ -148,7 +155,10 @@ describe(getName(), () => {
       packageFileName: 'gradle-wrapper.properties',
       updatedDeps: [],
       newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`,
-      config: dockerConfig,
+      config: {
+        ...config,
+        ...dockerAdminConfig,
+      },
     });
 
     expect(result).toHaveLength(1);
diff --git a/lib/manager/gradle-wrapper/artifacts.ts b/lib/manager/gradle-wrapper/artifacts.ts
index e6f4ddcf5f..e377604b26 100644
--- a/lib/manager/gradle-wrapper/artifacts.ts
+++ b/lib/manager/gradle-wrapper/artifacts.ts
@@ -1,5 +1,6 @@
 import { stat } from 'fs-extra';
 import { resolve } from 'upath';
+import { getAdminConfig } from '../../config/admin';
 import { TEMPORARY_ERROR } from '../../constants/error-messages';
 import { logger } from '../../logger';
 import { ExecOptions, exec } from '../../util/exec';
@@ -54,7 +55,7 @@ export async function updateArtifacts({
   config,
 }: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
   try {
-    const projectDir = config.localDir;
+    const { localDir: projectDir } = getAdminConfig();
     logger.debug({ updatedDeps }, 'gradle-wrapper.updateArtifacts()');
     const gradlew = gradleWrapperFileName(config);
     const gradlewPath = resolve(projectDir, `./${gradlew}`);
diff --git a/lib/manager/gradle/index-real.spec.ts b/lib/manager/gradle/index-real.spec.ts
index 5643b0c051..0b126bff9b 100644
--- a/lib/manager/gradle/index-real.spec.ts
+++ b/lib/manager/gradle/index-real.spec.ts
@@ -1,6 +1,8 @@
 import fsExtra from 'fs-extra';
 import tmp, { DirectoryResult } from 'tmp-promise';
 import { getName } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import type { ExtractConfig } from '../types';
 import { ifSystemSupportsGradle } from './__testutil__/gradle';
 import * as manager from '.';
@@ -19,11 +21,14 @@ describe(getName(), () => {
     let workingDir: DirectoryResult;
     let testRunConfig: ExtractConfig;
     let successFile: string;
+    let adminConfig: RepoAdminConfig;
 
     beforeEach(async () => {
       workingDir = await tmp.dir({ unsafeCleanup: true });
       successFile = '';
-      testRunConfig = { ...baseConfig, localDir: workingDir.path };
+      adminConfig = { localDir: workingDir.path };
+      setAdminConfig(adminConfig);
+      testRunConfig = { ...baseConfig };
       await fsExtra.copy(`${fixtures}/minimal-project`, workingDir.path);
       await fsExtra.copy(`${fixtures}/gradle-wrappers/6`, workingDir.path);
 
@@ -42,6 +47,10 @@ allprojects {
       successFile = `${workingDir.path}/${SUCCESS_FILE_NAME}`;
     });
 
+    afterEach(() => {
+      setAdminConfig();
+    });
+
     it('executes an executable gradle wrapper', async () => {
       const gradlew = await fsExtra.stat(`${workingDir.path}/gradlew`);
       await manager.executeGradle(testRunConfig, workingDir.path, gradlew);
diff --git a/lib/manager/gradle/index.spec.ts b/lib/manager/gradle/index.spec.ts
index c2c1712047..7171b2ac81 100644
--- a/lib/manager/gradle/index.spec.ts
+++ b/lib/manager/gradle/index.spec.ts
@@ -10,11 +10,13 @@ import {
   loadFixture,
   mocked,
 } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
-import { setFsConfig } from '../../util/fs';
+import type { ExtractConfig } from '../types';
 import { extractAllPackageFiles, updateDependency } from '.';
 
 jest.mock('child_process');
@@ -26,17 +28,21 @@ const fs = mocked(_fs);
 jest.mock('../../util/exec/env');
 const env = mocked(_env);
 
+const adminConfig: RepoAdminConfig = {
+  localDir: join('/foo/bar'),
+};
+
+const dockerAdminConfig = {
+  ...adminConfig,
+  binarySource: BinarySource.Docker,
+};
+
 const gradleOutput = {
   stdout: 'gradle output',
   stderr: '',
 };
 
-const utilConfig = {
-  localDir: join('/foo/bar'),
-};
-
-const config = {
-  ...utilConfig,
+const config: ExtractConfig = {
   gradle: {
     timeout: 60,
   },
@@ -83,8 +89,12 @@ describe(getName(), () => {
   }
 
   beforeAll(async () => {
-    await setExecConfig(utilConfig);
-    setFsConfig(utilConfig);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
+  });
+
+  afterAll(() => {
+    setAdminConfig();
   });
 
   beforeEach(() => {
@@ -205,9 +215,8 @@ describe(getName(), () => {
     });
 
     it('should use docker if required', async () => {
-      const dockerConfig = { ...config, binarySource: BinarySource.Docker };
       jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-      await setExecConfig(dockerConfig);
+      await setExecConfig(dockerAdminConfig);
       const execSnapshots = setupMocks({ wrapperFilename: null });
       const dependencies = await extractAllPackageFiles(config, [
         'build.gradle',
@@ -217,9 +226,8 @@ describe(getName(), () => {
     });
 
     it('should use docker even if gradlew is available', async () => {
-      const dockerConfig = { ...config, binarySource: BinarySource.Docker };
       jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-      await setExecConfig(dockerConfig);
+      await setExecConfig(dockerAdminConfig);
       const execSnapshots = setupMocks();
       const dependencies = await extractAllPackageFiles(config, [
         'build.gradle',
@@ -229,9 +237,8 @@ describe(getName(), () => {
     });
 
     it('should use docker even if gradlew.bat is available on Windows', async () => {
-      const dockerConfig = { ...config, binarySource: BinarySource.Docker };
       jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-      await setExecConfig(dockerConfig);
+      await setExecConfig(dockerAdminConfig);
       jest.spyOn(os, 'platform').mockReturnValueOnce('win32');
       const execSnapshots = setupMocks({ wrapperFilename: 'gradlew.bat' });
       const dependencies = await extractAllPackageFiles(config, [
diff --git a/lib/manager/gradle/index.ts b/lib/manager/gradle/index.ts
index c83bf3d783..a463bb45e5 100644
--- a/lib/manager/gradle/index.ts
+++ b/lib/manager/gradle/index.ts
@@ -1,6 +1,7 @@
 import { Stats } from 'fs';
 import { stat } from 'fs-extra';
 import upath from 'upath';
+import { getAdminConfig } from '../../config/admin';
 import { TEMPORARY_ERROR } from '../../constants/error-messages';
 import { LANGUAGE_JAVA } from '../../constants/languages';
 import * as datasourceMaven from '../../datasource/maven';
@@ -92,12 +93,11 @@ export async function extractAllPackageFiles(
 ): Promise<PackageFile[] | null> {
   let rootBuildGradle: string | undefined;
   let gradlew: Stats | null;
+  const { localDir } = getAdminConfig();
   for (const packageFile of packageFiles) {
     const dirname = upath.dirname(packageFile);
     const gradlewPath = upath.join(dirname, gradleWrapperFileName(config));
-    gradlew = await stat(upath.join(config.localDir, gradlewPath)).catch(
-      () => null
-    );
+    gradlew = await stat(upath.join(localDir, gradlewPath)).catch(() => null);
 
     if (['build.gradle', 'build.gradle.kts'].includes(packageFile)) {
       rootBuildGradle = packageFile;
@@ -116,7 +116,7 @@ export async function extractAllPackageFiles(
   }
   logger.debug('Extracting dependencies from all gradle files');
 
-  const cwd = upath.join(config.localDir, upath.dirname(rootBuildGradle));
+  const cwd = upath.join(localDir, upath.dirname(rootBuildGradle));
 
   await createRenovateGradlePlugin(cwd);
   await executeGradle(config, cwd, gradlew);
diff --git a/lib/manager/helmv3/artifacts.spec.ts b/lib/manager/helmv3/artifacts.spec.ts
index 506d17a6cd..0fcb69da2b 100644
--- a/lib/manager/helmv3/artifacts.spec.ts
+++ b/lib/manager/helmv3/artifacts.spec.ts
@@ -3,10 +3,13 @@ import _fs from 'fs-extra';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { git, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
+import type { UpdateArtifactsConfig } from '../types';
 import * as helmv3 from './artifacts';
 
 jest.mock('fs-extra');
@@ -19,20 +22,25 @@ const fs: jest.Mocked<typeof _fs> = _fs as any;
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const env = mocked(_env);
 
-const config = {
-  // `join` fixes Windows CI
-  localDir: join('/tmp/github/some/repo'),
+const adminConfig: RepoAdminConfig = {
+  localDir: join('/tmp/github/some/repo'), // `join` fixes Windows CI
 };
 
+const config: UpdateArtifactsConfig = {};
+
 describe('.updateArtifacts()', () => {
   beforeEach(async () => {
     jest.resetAllMocks();
     jest.resetModules();
 
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    await setExecConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
     docker.resetPrefetchedImages();
   });
+  afterEach(() => {
+    setAdminConfig();
+  });
   it('returns null if no Chart.lock found', async () => {
     const updatedDeps = ['dep1'];
     expect(
@@ -102,7 +110,7 @@ describe('.updateArtifacts()', () => {
 
   it('returns updated Chart.lock with docker', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     git.getFile.mockResolvedValueOnce('Old Chart.lock');
     const execSnapshots = mockExecAll(exec);
     fs.readFile.mockResolvedValueOnce('New Chart.lock' as any);
diff --git a/lib/manager/mix/artifacts.spec.ts b/lib/manager/mix/artifacts.spec.ts
index 84f1fa93f8..16824a564b 100644
--- a/lib/manager/mix/artifacts.spec.ts
+++ b/lib/manager/mix/artifacts.spec.ts
@@ -1,27 +1,37 @@
 import { join } from 'upath';
 import { envMock, exec, mockExecAll } from '../../../test/exec-util';
 import { env, fs, getName } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
+import type { UpdateArtifactsConfig } from '../types';
 import { updateArtifacts } from '.';
 
 jest.mock('child_process');
 jest.mock('../../util/exec/env');
 jest.mock('../../util/fs');
 
-const config = {
+const adminConfig: RepoAdminConfig = {
   // `join` fixes Windows CI
   localDir: join('/tmp/github/some/repo'),
 };
 
+const config: UpdateArtifactsConfig = {};
+
 describe(getName(), () => {
   beforeEach(async () => {
     jest.resetAllMocks();
     jest.resetModules();
 
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    await setExecConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
+  });
+
+  afterEach(() => {
+    setAdminConfig();
   });
 
   it('returns null if no mix.lock found', async () => {
@@ -74,10 +84,7 @@ describe(getName(), () => {
 
   it('returns updated mix.lock', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({
-      ...config,
-      binarySource: BinarySource.Docker,
-    });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     fs.readLocalFile.mockResolvedValueOnce('Old mix.lock');
     const execSnapshots = mockExecAll(exec);
     fs.readLocalFile.mockResolvedValueOnce('New mix.lock');
diff --git a/lib/manager/mix/extract.spec.ts b/lib/manager/mix/extract.spec.ts
index 4c3050b92e..0a452b6d39 100644
--- a/lib/manager/mix/extract.spec.ts
+++ b/lib/manager/mix/extract.spec.ts
@@ -1,9 +1,14 @@
 import { getName, loadFixture } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
 import { extractPackageFile } from '.';
 
 const sample = loadFixture('mix.exs');
 
 describe(getName(), () => {
+  beforeEach(() => {
+    setAdminConfig({ localDir: '' });
+  });
+
   describe('extractPackageFile()', () => {
     it('returns empty for invalid dependency file', async () => {
       expect(
diff --git a/lib/manager/npm/post-update/index.ts b/lib/manager/npm/post-update/index.ts
index 0be43d8b51..ec10665961 100644
--- a/lib/manager/npm/post-update/index.ts
+++ b/lib/manager/npm/post-update/index.ts
@@ -1,6 +1,7 @@
 import is from '@sindresorhus/is';
 import { parseSyml } from '@yarnpkg/parsers';
 import upath from 'upath';
+import { getAdminConfig } from '../../../config/admin';
 import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../../constants/error-messages';
 import { id as npmId } from '../../../datasource/npm';
 import { logger } from '../../../logger';
@@ -133,9 +134,10 @@ export async function writeExistingFiles(
     { packageFiles: npmFiles.map((n) => n.packageFile) },
     'Writing package.json files'
   );
+  const { localDir } = getAdminConfig();
   for (const packageFile of npmFiles) {
     const basedir = upath.join(
-      config.localDir,
+      localDir,
       upath.dirname(packageFile.packageFile)
     );
     const npmrc: string = packageFile.npmrc || config.npmrc;
@@ -163,7 +165,7 @@ export async function writeExistingFiles(
     }
     const { npmLock } = packageFile;
     if (npmLock) {
-      const npmLockPath = upath.join(config.localDir, npmLock);
+      const npmLockPath = upath.join(localDir, npmLock);
       if (
         process.env.RENOVATE_REUSE_PACKAGE_LOCK === 'false' ||
         config.reuseLockFiles === false
@@ -225,11 +227,12 @@ export async function writeUpdatedPackageFiles(
     logger.debug('No files found');
     return;
   }
+  const { localDir } = getAdminConfig();
   for (const packageFile of config.updatedPackageFiles) {
     if (packageFile.name.endsWith('package-lock.json')) {
       logger.debug(`Writing package-lock file: ${packageFile.name}`);
       await outputFile(
-        upath.join(config.localDir, packageFile.name),
+        upath.join(localDir, packageFile.name),
         packageFile.contents
       );
       continue; // eslint-disable-line
@@ -258,7 +261,7 @@ export async function writeUpdatedPackageFiles(
       logger.warn({ err }, 'Error adding token to package files');
     }
     await outputFile(
-      upath.join(config.localDir, packageFile.name),
+      upath.join(localDir, packageFile.name),
       JSON.stringify(massagedFile)
     );
   }
@@ -470,9 +473,10 @@ export async function getAdditionalFiles(
   } catch (err) {
     logger.warn({ err }, 'Error getting token for packageFile');
   }
+  const { localDir } = getAdminConfig();
   for (const npmLock of dirs.npmLockDirs) {
     const lockFileDir = upath.dirname(npmLock);
-    const fullLockFileDir = upath.join(config.localDir, lockFileDir);
+    const fullLockFileDir = upath.join(localDir, lockFileDir);
     const npmrcContent = await getNpmrcContent(fullLockFileDir);
     await updateNpmrcContent(
       fullLockFileDir,
@@ -535,7 +539,7 @@ export async function getAdditionalFiles(
 
   for (const yarnLock of dirs.yarnLockDirs) {
     const lockFileDir = upath.dirname(yarnLock);
-    const fullLockFileDir = upath.join(config.localDir, lockFileDir);
+    const fullLockFileDir = upath.join(localDir, lockFileDir);
     const npmrcContent = await getNpmrcContent(fullLockFileDir);
     await updateNpmrcContent(
       fullLockFileDir,
@@ -548,7 +552,7 @@ export async function getAdditionalFiles(
       (upgrade) => upgrade.yarnLock === yarnLock
     );
     const res = await yarn.generateLockFile(
-      upath.join(config.localDir, lockFileDir),
+      upath.join(localDir, lockFileDir),
       env,
       config,
       upgrades
@@ -594,7 +598,7 @@ export async function getAdditionalFiles(
           name: lockFileName,
           contents: res.lockFile,
         });
-        await updateYarnOffline(lockFileDir, config.localDir, updatedArtifacts);
+        await updateYarnOffline(lockFileDir, localDir, updatedArtifacts);
       }
     }
     await resetNpmrcContent(fullLockFileDir, npmrcContent);
@@ -602,7 +606,7 @@ export async function getAdditionalFiles(
 
   for (const pnpmShrinkwrap of dirs.pnpmShrinkwrapDirs) {
     const lockFileDir = upath.dirname(pnpmShrinkwrap);
-    const fullLockFileDir = upath.join(config.localDir, lockFileDir);
+    const fullLockFileDir = upath.join(localDir, lockFileDir);
     const npmrcContent = await getNpmrcContent(fullLockFileDir);
     await updateNpmrcContent(
       fullLockFileDir,
@@ -614,7 +618,7 @@ export async function getAdditionalFiles(
       (upgrade) => upgrade.pnpmShrinkwrap === pnpmShrinkwrap
     );
     const res = await pnpm.generateLockFile(
-      upath.join(config.localDir, lockFileDir),
+      upath.join(localDir, lockFileDir),
       env,
       config,
       upgrades
@@ -681,7 +685,7 @@ export async function getAdditionalFiles(
     const skipInstalls =
       lockFile === 'npm-shrinkwrap.json' ? false : config.skipInstalls;
     const fullLearnaFileDir = upath.join(
-      config.localDir,
+      localDir,
       getSubDirectory(lernaJsonFile)
     );
     const npmrcContent = await getNpmrcContent(fullLearnaFileDir);
@@ -753,7 +757,7 @@ export async function getAdditionalFiles(
         );
         if (existingContent) {
           logger.trace('Found lock file');
-          const lockFilePath = upath.join(config.localDir, filename);
+          const lockFilePath = upath.join(localDir, filename);
           logger.trace('Checking against ' + lockFilePath);
           try {
             let newContent: string;
diff --git a/lib/manager/nuget/artifacts.spec.ts b/lib/manager/nuget/artifacts.spec.ts
index e98ff5a29a..877d8de308 100644
--- a/lib/manager/nuget/artifacts.spec.ts
+++ b/lib/manager/nuget/artifacts.spec.ts
@@ -2,12 +2,14 @@ import { exec as _exec } from 'child_process';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { fs, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
-import { setFsConfig } from '../../util/fs';
 import * as _hostRules from '../../util/host-rules';
+import type { UpdateArtifactsConfig } from '../types';
 import * as nuget from './artifacts';
 import {
   getConfiguredRegistries as _getConfiguredRegistries,
@@ -31,12 +33,14 @@ const getRandomString: jest.Mock<typeof _getRandomString> =
   _getRandomString as any;
 const hostRules = mocked(_hostRules);
 
-const config = {
+const adminConfig: RepoAdminConfig = {
   // `join` fixes Windows CI
   localDir: join('/tmp/github/some/repo'),
   cacheDir: join('/tmp/renovate/cache'),
 };
 
+const config: UpdateArtifactsConfig = {};
+
 describe('updateArtifacts', () => {
   beforeEach(async () => {
     jest.resetAllMocks();
@@ -47,10 +51,15 @@ describe('updateArtifacts', () => {
       Promise.resolve(dirName)
     );
     getRandomString.mockReturnValue('not-so-random' as any);
-    await setExecConfig(config);
-    setFsConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
     docker.resetPrefetchedImages();
   });
+
+  afterEach(() => {
+    setAdminConfig();
+  });
+
   it('aborts if no lock file found', async () => {
     const execSnapshots = mockExecAll(exec);
     fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
@@ -145,8 +154,7 @@ describe('updateArtifacts', () => {
 
   it('supports docker mode', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({ ...config, binarySource: BinarySource.Docker });
-    setFsConfig({ ...config, binarySource: BinarySource.Docker });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     const execSnapshots = mockExecAll(exec);
     fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
     fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any);
diff --git a/lib/manager/nuget/artifacts.ts b/lib/manager/nuget/artifacts.ts
index 79c724dce6..f36891f0d9 100644
--- a/lib/manager/nuget/artifacts.ts
+++ b/lib/manager/nuget/artifacts.ts
@@ -1,4 +1,5 @@
 import { join } from 'path';
+import { getAdminConfig } from '../../config/admin';
 import { TEMPORARY_ERROR } from '../../constants/error-messages';
 import { id, parseRegistryUrl } from '../../datasource/nuget';
 import { logger } from '../../logger';
@@ -28,8 +29,9 @@ async function addSourceCmds(
   config: UpdateArtifactsConfig,
   nugetConfigFile: string
 ): Promise<string[]> {
+  const { localDir } = getAdminConfig();
   const registries =
-    (await getConfiguredRegistries(packageFileName, config.localDir)) ||
+    (await getConfiguredRegistries(packageFileName, localDir)) ||
     getDefaultRegistries();
   const result = [];
   for (const registry of registries) {
diff --git a/lib/manager/nuget/extract.spec.ts b/lib/manager/nuget/extract.spec.ts
index 532b9e61de..95ac00b060 100644
--- a/lib/manager/nuget/extract.spec.ts
+++ b/lib/manager/nuget/extract.spec.ts
@@ -1,16 +1,23 @@
-import { readFileSync } from 'fs';
 import * as upath from 'upath';
-import { getName } from '../../../test/util';
+import { getName, loadFixture } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import type { ExtractConfig } from '../types';
 import { extractPackageFile } from './extract';
 
+const config: ExtractConfig = {};
+
+const adminConfig: RepoAdminConfig = {
+  localDir: upath.resolve('lib/manager/nuget/__fixtures__'),
+};
+
 describe(getName(), () => {
   describe('extractPackageFile()', () => {
-    let config: ExtractConfig;
     beforeEach(() => {
-      config = {
-        localDir: upath.resolve('lib/manager/nuget/__fixtures__'),
-      };
+      setAdminConfig(adminConfig);
+    });
+    afterEach(() => {
+      setAdminConfig();
     });
     it('returns empty for invalid csproj', async () => {
       expect(
@@ -20,41 +27,28 @@ describe(getName(), () => {
     it('extracts package version dependency', async () => {
       const packageFile =
         'with-centralized-package-versions/Directory.Packages.props';
-      const sample = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const sample = loadFixture(packageFile);
       const res = await extractPackageFile(sample, packageFile, config);
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(1);
     });
     it('extracts all dependencies', async () => {
       const packageFile = 'sample.csproj';
-      const sample = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const sample = loadFixture(packageFile);
       const res = await extractPackageFile(sample, packageFile, config);
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(17);
     });
     it('extracts all dependencies from global packages file', async () => {
       const packageFile = 'packages.props';
-      const sample = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const sample = loadFixture(packageFile);
       const res = await extractPackageFile(sample, packageFile, config);
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(17);
     });
     it('considers NuGet.config', async () => {
       const packageFile = 'with-config-file/with-config-file.csproj';
-      const contents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
-
+      const contents = loadFixture(packageFile);
       expect(
         await extractPackageFile(contents, packageFile, config)
       ).toMatchSnapshot();
@@ -62,10 +56,7 @@ describe(getName(), () => {
     it('considers lower-case nuget.config', async () => {
       const packageFile =
         'with-lower-case-config-file/with-lower-case-config-file.csproj';
-      const contents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const contents = loadFixture(packageFile);
 
       expect(
         await extractPackageFile(contents, packageFile, config)
@@ -74,10 +65,7 @@ describe(getName(), () => {
     it('considers pascal-case NuGet.Config', async () => {
       const packageFile =
         'with-pascal-case-config-file/with-pascal-case-config-file.csproj';
-      const contents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const contents = loadFixture(packageFile);
 
       expect(
         await extractPackageFile(contents, packageFile, config)
@@ -86,10 +74,7 @@ describe(getName(), () => {
     it('handles malformed NuGet.config', async () => {
       const packageFile =
         'with-malformed-config-file/with-malformed-config-file.csproj';
-      const contents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const contents = loadFixture(packageFile);
 
       expect(
         await extractPackageFile(contents, packageFile, config)
@@ -98,10 +83,7 @@ describe(getName(), () => {
     it('handles NuGet.config without package sources', async () => {
       const packageFile =
         'without-package-sources/without-package-sources.csproj';
-      const contents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const contents = loadFixture(packageFile);
 
       expect(
         await extractPackageFile(contents, packageFile, config)
@@ -110,10 +92,7 @@ describe(getName(), () => {
     it('ignores local feed in NuGet.config', async () => {
       const packageFile =
         'with-local-feed-in-config-file/with-local-feed-in-config-file.csproj';
-      const contents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const contents = loadFixture(packageFile);
 
       expect(
         await extractPackageFile(contents, packageFile, config)
@@ -121,15 +100,9 @@ describe(getName(), () => {
     });
     it('extracts registry URLs independently', async () => {
       const packageFile = 'multiple-package-files/one/one.csproj';
-      const contents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const contents = loadFixture(packageFile);
       const otherPackageFile = 'multiple-package-files/two/two.csproj';
-      const otherContents = readFileSync(
-        upath.join(config.localDir, packageFile),
-        'utf8'
-      );
+      const otherContents = loadFixture(otherPackageFile);
       expect(
         await extractPackageFile(contents, packageFile, config)
       ).toMatchSnapshot();
diff --git a/lib/manager/nuget/extract.ts b/lib/manager/nuget/extract.ts
index 0cebc39edf..27a6ebecaf 100644
--- a/lib/manager/nuget/extract.ts
+++ b/lib/manager/nuget/extract.ts
@@ -1,4 +1,5 @@
 import { XmlDocument, XmlElement, XmlNode } from 'xmldoc';
+import { getAdminConfig } from '../../config/admin';
 import * as datasourceNuget from '../../datasource/nuget';
 import { logger } from '../../logger';
 import { getSiblingFileName, localPathExists } from '../../util/fs';
@@ -70,10 +71,8 @@ export async function extractPackageFile(
 ): Promise<PackageFile | null> {
   logger.trace({ packageFile }, 'nuget.extractPackageFile()');
 
-  const registries = await getConfiguredRegistries(
-    packageFile,
-    config.localDir
-  );
+  const { localDir } = getAdminConfig();
+  const registries = await getConfiguredRegistries(packageFile, localDir);
   const registryUrls = registries
     ? registries.map((registry) => registry.url)
     : undefined;
diff --git a/lib/manager/pip_requirements/artifacts.spec.ts b/lib/manager/pip_requirements/artifacts.spec.ts
index 456744ce6a..4cad75929c 100644
--- a/lib/manager/pip_requirements/artifacts.spec.ts
+++ b/lib/manager/pip_requirements/artifacts.spec.ts
@@ -1,4 +1,6 @@
 import _fs from 'fs-extra';
+import { setAdminConfig } from '../../config/admin';
+import type { UpdateArtifactsConfig } from '../types';
 import { updateArtifacts } from './artifacts';
 
 const fs: jest.Mocked<typeof _fs> = _fs as any;
@@ -7,7 +9,7 @@ jest.mock('fs-extra');
 jest.mock('child_process');
 jest.mock('../../util/exec');
 
-const config = {};
+const config: UpdateArtifactsConfig = {};
 
 const newPackageFileContent = `atomicwrites==1.4.0 \
 --hash=sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4 \
@@ -17,6 +19,7 @@ describe('.updateArtifacts()', () => {
   beforeEach(() => {
     jest.resetAllMocks();
     jest.resetModules();
+    setAdminConfig({ localDir: '' });
   });
   it('returns null if no updatedDeps were provided', async () => {
     expect(
diff --git a/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap b/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap
index 4316f62fbe..cfeee48ee6 100644
--- a/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap
+++ b/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap
@@ -7,7 +7,7 @@ Array [
   Object {
     "cmd": "python --version",
     "options": Object {
-      "cwd": null,
+      "cwd": "/tmp/foo/bar",
       "encoding": "utf-8",
       "env": Object {
         "HOME": "/home/user",
@@ -25,7 +25,7 @@ Array [
   Object {
     "cmd": "python3 --version",
     "options": Object {
-      "cwd": null,
+      "cwd": "/tmp/foo/bar",
       "encoding": "utf-8",
       "env": Object {
         "HOME": "/home/user",
@@ -43,7 +43,7 @@ Array [
   Object {
     "cmd": "python3.8 --version",
     "options": Object {
-      "cwd": null,
+      "cwd": "/tmp/foo/bar",
       "encoding": "utf-8",
       "env": Object {
         "HOME": "/home/user",
diff --git a/lib/manager/pip_setup/extract.spec.ts b/lib/manager/pip_setup/extract.spec.ts
index cf8d187a70..37c254d32e 100644
--- a/lib/manager/pip_setup/extract.spec.ts
+++ b/lib/manager/pip_setup/extract.spec.ts
@@ -1,5 +1,6 @@
 import { envMock, exec, mockExecSequence } from '../../../test/exec-util';
 import { env, getName } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
 import {
   getPythonAlias,
   parsePythonVersion,
@@ -17,6 +18,7 @@ describe(getName(), () => {
     resetModule();
 
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
+    setAdminConfig({ localDir: '/tmp/foo/bar' });
   });
   describe('parsePythonVersion', () => {
     it('returns major and minor version numbers', () => {
diff --git a/lib/manager/pip_setup/index.spec.ts b/lib/manager/pip_setup/index.spec.ts
index 28acf04ee1..fffa8ae536 100644
--- a/lib/manager/pip_setup/index.spec.ts
+++ b/lib/manager/pip_setup/index.spec.ts
@@ -6,10 +6,12 @@ import {
   mockExecSequence,
 } from '../../../test/exec-util';
 import { env, getName, loadFixture } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as fs from '../../util/fs';
-import { setFsConfig } from '../../util/fs';
+import type { ExtractConfig } from '../types';
 import * as extract from './extract';
 import { extractPackageFile } from '.';
 
@@ -17,11 +19,13 @@ const packageFile = 'setup.py';
 const content = loadFixture(packageFile);
 const jsonContent = loadFixture('setup.py.json');
 
-const config = {
+const adminConfig: RepoAdminConfig = {
   localDir: '/tmp/github/some/repo',
   cacheDir: '/tmp/renovate/cache',
 };
 
+const config: ExtractConfig = {};
+
 jest.mock('child_process');
 jest.mock('../../util/exec/env');
 
@@ -44,14 +48,18 @@ describe(getName(), () => {
       jest.resetModules();
       extract.resetModule();
 
-      await setExecConfig(config);
-      setFsConfig(config);
+      await setExecConfig(adminConfig as never);
+      setAdminConfig(adminConfig);
       env.getChildProcessEnv.mockReturnValue(envMock.basic);
 
       // do not copy extract.py
       jest.spyOn(fs, 'writeLocalFile').mockResolvedValue();
     });
 
+    afterEach(() => {
+      setAdminConfig();
+    });
+
     it('returns found deps', async () => {
       const execSnapshots = mockExecSequence(exec, [
         ...pythonVersionCallResults,
diff --git a/lib/manager/pipenv/artifacts.spec.ts b/lib/manager/pipenv/artifacts.spec.ts
index e7830f9eb0..64951dc94e 100644
--- a/lib/manager/pipenv/artifacts.spec.ts
+++ b/lib/manager/pipenv/artifacts.spec.ts
@@ -3,12 +3,14 @@ import _fs from 'fs-extra';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { git, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
-import { setFsConfig } from '../../util/fs';
 import type { StatusResult } from '../../util/git';
+import type { UpdateArtifactsConfig } from '../types';
 import * as pipenv from './artifacts';
 
 jest.mock('fs-extra');
@@ -22,13 +24,14 @@ const fs: jest.Mocked<typeof _fs> = _fs as any;
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const env = mocked(_env);
 
-const config = {
+const adminConfig: RepoAdminConfig = {
   // `join` fixes Windows CI
   localDir: join('/tmp/github/some/repo'),
   cacheDir: join('/tmp/renovate/cache'),
 };
+const dockerAdminConfig = { ...adminConfig, binarySource: BinarySource.Docker };
 
-const dockerConfig = { ...config, binarySource: BinarySource.Docker };
+const config: UpdateArtifactsConfig = {};
 const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true };
 
 describe('.updateArtifacts()', () => {
@@ -41,8 +44,8 @@ describe('.updateArtifacts()', () => {
       LC_ALL: 'en_US',
     });
 
-    await setExecConfig(config);
-    setFsConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
     docker.resetPrefetchedImages();
     pipFileLock = {
       _meta: { requires: {} },
@@ -109,8 +112,7 @@ describe('.updateArtifacts()', () => {
   });
   it('supports docker mode', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig(dockerConfig);
-    setFsConfig(dockerConfig);
+    await setExecConfig(dockerAdminConfig);
     pipFileLock._meta.requires.python_version = '3.7';
     fs.readFile.mockResolvedValueOnce(JSON.stringify(pipFileLock) as any);
     const execSnapshots = mockExecAll(exec);
@@ -123,7 +125,7 @@ describe('.updateArtifacts()', () => {
         packageFileName: 'Pipfile',
         updatedDeps: [],
         newPackageFileContent: 'some new content',
-        config: dockerConfig,
+        config: { ...config, ...dockerAdminConfig },
       })
     ).not.toBeNull();
     expect(execSnapshots).toMatchSnapshot();
@@ -161,8 +163,7 @@ describe('.updateArtifacts()', () => {
   });
   it('uses pipenv version from Pipfile', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig(dockerConfig);
-    setFsConfig(dockerConfig);
+    await setExecConfig(dockerAdminConfig);
     pipFileLock.default.pipenv.version = '==2020.8.13';
     fs.readFile.mockResolvedValueOnce(JSON.stringify(pipFileLock) as any);
     const execSnapshots = mockExecAll(exec);
@@ -175,15 +176,14 @@ describe('.updateArtifacts()', () => {
         packageFileName: 'Pipfile',
         updatedDeps: [],
         newPackageFileContent: 'some new content',
-        config: dockerConfig,
+        config: { ...config, ...dockerAdminConfig },
       })
     ).not.toBeNull();
     expect(execSnapshots).toMatchSnapshot();
   });
   it('uses pipenv version from Pipfile dev packages', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig(dockerConfig);
-    setFsConfig(dockerConfig);
+    await setExecConfig(dockerAdminConfig);
     pipFileLock.develop.pipenv.version = '==2020.8.13';
     fs.readFile.mockResolvedValueOnce(JSON.stringify(pipFileLock) as any);
     const execSnapshots = mockExecAll(exec);
@@ -196,15 +196,14 @@ describe('.updateArtifacts()', () => {
         packageFileName: 'Pipfile',
         updatedDeps: [],
         newPackageFileContent: 'some new content',
-        config: dockerConfig,
+        config: { ...config, ...dockerAdminConfig },
       })
     ).not.toBeNull();
     expect(execSnapshots).toMatchSnapshot();
   });
   it('uses pipenv version from config', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig(dockerConfig);
-    setFsConfig(dockerConfig);
+    await setExecConfig(dockerAdminConfig);
     pipFileLock.default.pipenv.version = '==2020.8.13';
     fs.readFile.mockResolvedValueOnce(JSON.stringify(pipFileLock) as any);
     const execSnapshots = mockExecAll(exec);
@@ -217,7 +216,7 @@ describe('.updateArtifacts()', () => {
         packageFileName: 'Pipfile',
         updatedDeps: [],
         newPackageFileContent: 'some new content',
-        config: { ...dockerConfig, constraints: { pipenv: '==2020.1.1' } },
+        config: { ...dockerAdminConfig, constraints: { pipenv: '==2020.1.1' } },
       })
     ).not.toBeNull();
     expect(execSnapshots).toMatchSnapshot();
diff --git a/lib/manager/poetry/artifacts.spec.ts b/lib/manager/poetry/artifacts.spec.ts
index 52289eb293..85a77bc6cf 100644
--- a/lib/manager/poetry/artifacts.spec.ts
+++ b/lib/manager/poetry/artifacts.spec.ts
@@ -3,12 +3,15 @@ import _fs from 'fs-extra';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/exec-util';
 import { loadFixture, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import * as _datasource from '../../datasource';
 import { setExecConfig } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import * as docker from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
 import * as _hostRules from '../../util/host-rules';
+import type { UpdateArtifactsConfig } from '../types';
 import { updateArtifacts } from './artifacts';
 
 const pyproject10toml = loadFixture('pyproject.10.toml');
@@ -25,15 +28,18 @@ const env = mocked(_env);
 const datasource = mocked(_datasource);
 const hostRules = mocked(_hostRules);
 
-const config = {
+const adminConfig: RepoAdminConfig = {
   localDir: join('/tmp/github/some/repo'),
 };
 
+const config: UpdateArtifactsConfig = {};
+
 describe('.updateArtifacts()', () => {
   beforeEach(async () => {
     jest.resetAllMocks();
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    await setExecConfig(config);
+    await setExecConfig(adminConfig as never);
+    setAdminConfig(adminConfig);
     docker.resetPrefetchedImages();
   });
   it('returns null if no poetry.lock found', async () => {
@@ -128,10 +134,7 @@ describe('.updateArtifacts()', () => {
   });
   it('returns updated poetry.lock using docker', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({
-      ...config,
-      binarySource: BinarySource.Docker,
-    });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     fs.readFile.mockResolvedValueOnce('[metadata]\n' as any);
     const execSnapshots = mockExecAll(exec);
     fs.readFile.mockReturnValueOnce('New poetry.lock' as any);
@@ -157,10 +160,7 @@ describe('.updateArtifacts()', () => {
   });
   it('returns updated poetry.lock using docker (constraints)', async () => {
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
-    await setExecConfig({
-      ...config,
-      binarySource: BinarySource.Docker,
-    });
+    await setExecConfig({ ...adminConfig, binarySource: BinarySource.Docker });
     fs.readFile.mockResolvedValueOnce(
       '[metadata]\npython-versions = "~2.7 || ^3.4"' as any
     );
diff --git a/lib/manager/types.ts b/lib/manager/types.ts
index d1af311bcc..40b7f21c2b 100644
--- a/lib/manager/types.ts
+++ b/lib/manager/types.ts
@@ -11,7 +11,6 @@ export type Result<T> = T | Promise<T>;
 
 export interface ManagerConfig {
   binarySource?: string;
-  localDir?: string;
   registryUrls?: string[];
 }
 
@@ -21,7 +20,6 @@ export interface ManagerData<T> {
 
 export interface ExtractConfig {
   binarySource?: string;
-  localDir?: string;
   registryUrls?: string[];
   endpoint?: string;
   gradle?: { timeout?: number };
@@ -44,7 +42,6 @@ export interface CustomExtractConfig extends ExtractConfig {
 export interface UpdateArtifactsConfig extends ManagerConfig {
   isLockFileMaintenance?: boolean;
   constraints?: Record<string, string>;
-  cacheDir?: string;
   composerIgnorePlatformReqs?: boolean;
   currentValue?: string;
   postUpdateOptions?: string[];
@@ -185,7 +182,6 @@ export interface Upgrade<T = Record<string, any>>
   isLockfileUpdate?: boolean;
   currentRawValue?: any;
   depGroup?: string;
-  localDir?: string;
   name?: string;
   newDigest?: string;
   newFrom?: string;
@@ -278,7 +274,6 @@ export interface ManagerApi {
 
 // TODO: name and properties used by npm manager
 export interface PostUpdateConfig extends ManagerConfig, Record<string, any> {
-  cacheDir?: string;
   updatedPackageFiles?: File[];
   postUpdateOptions?: string[];
   skipInstalls?: boolean;
diff --git a/lib/platform/azure/index.ts b/lib/platform/azure/index.ts
index 0e8f4bf753..6abee9274b 100644
--- a/lib/platform/azure/index.ts
+++ b/lib/platform/azure/index.ts
@@ -138,7 +138,6 @@ export async function getJsonFile(
 
 export async function initRepo({
   repository,
-  localDir,
   cloneSubmodules,
 }: RepoParams): Promise<RepoResult> {
   logger.debug(`initRepo("${repository}")`);
@@ -178,7 +177,6 @@ export async function initRepo({
   const url = repo.remoteUrl || manualUrl;
   await git.initRepo({
     ...config,
-    localDir,
     url,
     extraCloneOpts: getStorageExtraCloneOpts(opts),
     gitAuthorName: global.gitAuthor?.name,
diff --git a/lib/platform/bitbucket-server/index.spec.ts b/lib/platform/bitbucket-server/index.spec.ts
index 69c3d2a5c4..45d6d558ff 100644
--- a/lib/platform/bitbucket-server/index.spec.ts
+++ b/lib/platform/bitbucket-server/index.spec.ts
@@ -195,7 +195,6 @@ describe(getName(), () => {
         await bitbucket.initRepo({
           endpoint: 'https://stash.renovatebot.com/vcs/',
           repository: 'SOME/repo',
-          localDir: '',
           ...config,
         });
         return scope;
@@ -294,7 +293,6 @@ describe(getName(), () => {
             await bitbucket.initRepo({
               endpoint: 'https://stash.renovatebot.com/vcs/',
               repository: 'SOME/repo',
-              localDir: '',
             })
           ).toMatchSnapshot();
           expect(httpMock.getTrace()).toMatchSnapshot();
@@ -318,7 +316,6 @@ describe(getName(), () => {
           const res = await bitbucket.initRepo({
             endpoint: 'https://stash.renovatebot.com/vcs/',
             repository: 'SOME/repo',
-            localDir: '',
           });
           expect(git.initRepo).toHaveBeenCalledWith(
             expect.objectContaining({ url: sshLink('SOME', 'repo') })
@@ -345,7 +342,6 @@ describe(getName(), () => {
           const res = await bitbucket.initRepo({
             endpoint: 'https://stash.renovatebot.com/vcs/',
             repository: 'SOME/repo',
-            localDir: '',
           });
           expect(git.initRepo).toHaveBeenCalledWith(
             expect.objectContaining({
@@ -379,7 +375,6 @@ describe(getName(), () => {
           const res = await bitbucket.initRepo({
             endpoint: 'https://stash.renovatebot.com/vcs/',
             repository: 'SOME/repo',
-            localDir: '',
           });
           expect(git.initRepo).toHaveBeenCalledWith(
             expect.objectContaining({
@@ -404,7 +399,6 @@ describe(getName(), () => {
             bitbucket.initRepo({
               endpoint: 'https://stash.renovatebot.com/vcs/',
               repository: 'SOME/repo',
-              localDir: '',
             })
           ).rejects.toThrow(REPOSITORY_EMPTY);
           expect(httpMock.getTrace()).toMatchSnapshot();
diff --git a/lib/platform/bitbucket-server/index.ts b/lib/platform/bitbucket-server/index.ts
index c6b9ec421e..4f0a81e3d5 100644
--- a/lib/platform/bitbucket-server/index.ts
+++ b/lib/platform/bitbucket-server/index.ts
@@ -145,13 +145,10 @@ export async function getJsonFile(
 // Initialize BitBucket Server by getting base branch
 export async function initRepo({
   repository,
-  localDir,
   cloneSubmodules,
   ignorePrAuthor,
 }: RepoParams): Promise<RepoResult> {
-  logger.debug(
-    `initRepo("${JSON.stringify({ repository, localDir }, null, 2)}")`
-  );
+  logger.debug(`initRepo("${JSON.stringify({ repository }, null, 2)}")`);
   const opts = hostRules.find({
     hostType: defaults.hostType,
     url: defaults.endpoint,
@@ -215,7 +212,6 @@ export async function initRepo({
 
     await git.initRepo({
       ...config,
-      localDir,
       url: gitUrl,
       gitAuthorName: global.gitAuthor?.name,
       gitAuthorEmail: global.gitAuthor?.email,
diff --git a/lib/platform/bitbucket/index.spec.ts b/lib/platform/bitbucket/index.spec.ts
index 6a0e40e004..d024762275 100644
--- a/lib/platform/bitbucket/index.spec.ts
+++ b/lib/platform/bitbucket/index.spec.ts
@@ -84,7 +84,6 @@ describe(getName(), () => {
 
     await bitbucket.initRepo({
       repository: 'some/repo',
-      localDir: '',
       ...config,
     });
 
@@ -149,7 +148,6 @@ describe(getName(), () => {
       expect(
         await bitbucket.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).toMatchSnapshot();
       expect(httpMock.getTrace()).toMatchSnapshot();
diff --git a/lib/platform/bitbucket/index.ts b/lib/platform/bitbucket/index.ts
index e1c4eae8ab..342e3c4659 100644
--- a/lib/platform/bitbucket/index.ts
+++ b/lib/platform/bitbucket/index.ts
@@ -121,7 +121,6 @@ export async function getJsonFile(
 // Initialize bitbucket by getting base branch and SHA
 export async function initRepo({
   repository,
-  localDir,
   cloneSubmodules,
   ignorePrAuthor,
 }: RepoParams): Promise<RepoResult> {
@@ -178,7 +177,6 @@ export async function initRepo({
 
   await git.initRepo({
     ...config,
-    localDir,
     url,
     gitAuthorName: global.gitAuthor?.name,
     gitAuthorEmail: global.gitAuthor?.email,
diff --git a/lib/platform/gitea/index.spec.ts b/lib/platform/gitea/index.spec.ts
index 7fca3de799..8d42f712ec 100644
--- a/lib/platform/gitea/index.spec.ts
+++ b/lib/platform/gitea/index.spec.ts
@@ -184,7 +184,6 @@ describe(getName(), () => {
 
     return gitea.initRepo({
       repository: mockRepo.full_name,
-      localDir: '',
       ...config,
     });
   }
@@ -251,7 +250,6 @@ describe(getName(), () => {
   describe('initRepo', () => {
     const initRepoCfg: RepoParams = {
       repository: mockRepo.full_name,
-      localDir: '',
     };
 
     it('should propagate API errors', async () => {
diff --git a/lib/platform/gitea/index.ts b/lib/platform/gitea/index.ts
index e7f97546a4..a09a65b261 100644
--- a/lib/platform/gitea/index.ts
+++ b/lib/platform/gitea/index.ts
@@ -39,7 +39,6 @@ import { smartLinks } from './utils';
 
 interface GiteaRepoConfig {
   repository: string;
-  localDir: string;
   mergeMethod: helper.PRMergeMethod;
 
   prList: Promise<Pr[]> | null;
@@ -226,14 +225,12 @@ const platform: Platform = {
 
   async initRepo({
     repository,
-    localDir,
     cloneSubmodules,
   }: RepoParams): Promise<RepoResult> {
     let repo: helper.Repo;
 
     config = {} as any;
     config.repository = repository;
-    config.localDir = localDir;
     config.cloneSubmodules = cloneSubmodules;
 
     // Attempt to fetch information about repository
diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts
index 749cb7d13f..18a7ca7838 100644
--- a/lib/platform/github/index.ts
+++ b/lib/platform/github/index.ts
@@ -168,14 +168,13 @@ export async function initRepo({
   repository,
   forkMode,
   forkToken,
-  localDir,
   renovateUsername,
   cloneSubmodules,
   ignorePrAuthor,
 }: RepoParams): Promise<RepoResult> {
   logger.debug(`initRepo("${repository}")`);
   // config is used by the platform api itself, not necessary for the app layer to know
-  config = { localDir, repository, cloneSubmodules, ignorePrAuthor } as any;
+  config = { repository, cloneSubmodules, ignorePrAuthor } as any;
   // istanbul ignore if
   if (endpoint) {
     // Necessary for Renovate Pro - do not remove
diff --git a/lib/platform/github/types.ts b/lib/platform/github/types.ts
index b765520e03..8b6736a10c 100644
--- a/lib/platform/github/types.ts
+++ b/lib/platform/github/types.ts
@@ -69,7 +69,6 @@ export interface LocalRepoConfig {
   defaultBranch: string;
   repositoryOwner: string;
   repository: string | null;
-  localDir: string;
   isGhe: boolean;
   renovateUsername: string;
   productLinks: any;
diff --git a/lib/platform/gitlab/index.spec.ts b/lib/platform/gitlab/index.spec.ts
index 713f02c3ef..a436586ac9 100644
--- a/lib/platform/gitlab/index.spec.ts
+++ b/lib/platform/gitlab/index.spec.ts
@@ -164,7 +164,6 @@ describe(getName(), () => {
   async function initRepo(
     repoParams: RepoParams = {
       repository: 'some/repo',
-      localDir: '',
     },
     repoResp = null,
     scope = httpMock.scope(gitlabApiHost)
@@ -191,7 +190,6 @@ describe(getName(), () => {
         .reply(200, okReturn);
       await gitlab.initRepo({
         repository: 'some/repo/project',
-        localDir: '',
       });
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
@@ -203,7 +201,6 @@ describe(getName(), () => {
       await expect(
         gitlab.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).rejects.toThrow('always error');
       expect(httpMock.getTrace()).toMatchSnapshot();
@@ -216,7 +213,6 @@ describe(getName(), () => {
       await expect(
         gitlab.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).rejects.toThrow(REPOSITORY_ARCHIVED);
       expect(httpMock.getTrace()).toMatchSnapshot();
@@ -229,7 +225,6 @@ describe(getName(), () => {
       await expect(
         gitlab.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).rejects.toThrow(REPOSITORY_MIRRORED);
       expect(httpMock.getTrace()).toMatchSnapshot();
@@ -242,7 +237,6 @@ describe(getName(), () => {
       await expect(
         gitlab.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).rejects.toThrow(REPOSITORY_DISABLED);
       expect(httpMock.getTrace()).toMatchSnapshot();
@@ -255,7 +249,6 @@ describe(getName(), () => {
       await expect(
         gitlab.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).rejects.toThrow(REPOSITORY_DISABLED);
       expect(httpMock.getTrace()).toMatchSnapshot();
@@ -268,7 +261,6 @@ describe(getName(), () => {
       await expect(
         gitlab.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).rejects.toThrow(REPOSITORY_EMPTY);
       expect(httpMock.getTrace()).toMatchSnapshot();
@@ -281,7 +273,6 @@ describe(getName(), () => {
       await expect(
         gitlab.initRepo({
           repository: 'some/repo',
-          localDir: '',
         })
       ).rejects.toThrow(REPOSITORY_EMPTY);
       expect(httpMock.getTrace()).toMatchSnapshot();
@@ -296,7 +287,6 @@ describe(getName(), () => {
         });
       await gitlab.initRepo({
         repository: 'some/repo/project',
-        localDir: '',
       });
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
@@ -306,7 +296,6 @@ describe(getName(), () => {
       await initRepo(
         {
           repository: 'some/repo/project',
-          localDir: '',
         },
         {
           default_branch: 'master',
@@ -322,7 +311,6 @@ describe(getName(), () => {
       await initRepo(
         {
           repository: 'some/repo/project',
-          localDir: '',
         },
         {
           default_branch: 'master',
diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts
index 6faaf766cc..87f86415f4 100755
--- a/lib/platform/gitlab/index.ts
+++ b/lib/platform/gitlab/index.ts
@@ -52,7 +52,6 @@ import type {
 
 let config: {
   repository: string;
-  localDir: string;
   email: string;
   prList: any[];
   issueList: GitlabIssue[];
@@ -166,13 +165,11 @@ export async function getJsonFile(
 // Initialize GitLab by getting base branch
 export async function initRepo({
   repository,
-  localDir,
   cloneSubmodules,
   ignorePrAuthor,
 }: RepoParams): Promise<RepoResult> {
   config = {} as any;
   config.repository = urlEscape(repository);
-  config.localDir = localDir;
   config.cloneSubmodules = cloneSubmodules;
   config.ignorePrAuthor = ignorePrAuthor;
 
diff --git a/lib/platform/types.ts b/lib/platform/types.ts
index d52a7e6a90..240425ace7 100644
--- a/lib/platform/types.ts
+++ b/lib/platform/types.ts
@@ -27,7 +27,6 @@ export interface RepoResult {
 }
 
 export interface RepoParams {
-  localDir: string;
   repository: string;
   endpoint?: string;
   forkMode?: string;
diff --git a/lib/util/exec/common.ts b/lib/util/exec/common.ts
index 3be896bbfc..3c09ad1947 100644
--- a/lib/util/exec/common.ts
+++ b/lib/util/exec/common.ts
@@ -13,8 +13,6 @@ export enum BinarySource {
 
 export interface ExecConfig {
   binarySource: Opt<BinarySource>;
-  localDir: Opt<string>;
-  cacheDir: Opt<string>;
 }
 
 export type VolumesPair = [string, string];
diff --git a/lib/util/exec/docker/index.ts b/lib/util/exec/docker/index.ts
index 2ae548575a..b58a9d82d3 100644
--- a/lib/util/exec/docker/index.ts
+++ b/lib/util/exec/docker/index.ts
@@ -1,3 +1,4 @@
+import is from '@sindresorhus/is';
 import { getAdminConfig } from '../../../config/admin';
 import { SYSTEM_INSUFFICIENT_MEMORY } from '../../../constants/error-messages';
 import { getPkgReleases } from '../../../datasource';
@@ -31,12 +32,12 @@ export function resetPrefetchedImages(): void {
 }
 
 function expandVolumeOption(x: VolumeOption): VolumesPair | null {
-  if (typeof x === 'string') {
+  if (is.nonEmptyString(x)) {
     return [x, x];
   }
   if (Array.isArray(x) && x.length === 2) {
     const [from, to] = x;
-    if (typeof from === 'string' && typeof to === 'string') {
+    if (is.nonEmptyString(from) && is.nonEmptyString(to)) {
       return [from, to];
     }
   }
@@ -195,8 +196,13 @@ export async function generateDockerCommand(
   const volumes = options.volumes || [];
   const preCommands = options.preCommands || [];
   const postCommands = options.postCommands || [];
-  const { localDir, cacheDir } = config;
-  const { dockerUser, dockerChildPrefix, dockerImagePrefix } = getAdminConfig();
+  const {
+    localDir,
+    cacheDir,
+    dockerUser,
+    dockerChildPrefix,
+    dockerImagePrefix,
+  } = getAdminConfig();
   const result = ['docker run --rm'];
   const containerName = getContainerName(image, dockerChildPrefix);
   const containerLabel = getContainerLabel(dockerChildPrefix);
diff --git a/lib/util/exec/exec.spec.ts b/lib/util/exec/exec.spec.ts
index 387c15d46c..b0b1c3cae3 100644
--- a/lib/util/exec/exec.spec.ts
+++ b/lib/util/exec/exec.spec.ts
@@ -28,7 +28,7 @@ interface TestInput {
   inOpts: ExecOptions;
   outCmd: string[];
   outOpts: RawExecOptions[];
-  adminConfig?: RepoAdminConfig;
+  adminConfig?: Partial<RepoAdminConfig>;
 }
 
 describe(getName(), () => {
@@ -40,10 +40,7 @@ describe(getName(), () => {
   const defaultCwd = `-w "${cwd}"`;
   const defaultVolumes = `-v "${cwd}":"${cwd}" -v "${cacheDir}":"${cacheDir}"`;
 
-  const execConfig = {
-    cacheDir,
-    localDir: cwd,
-  };
+  const execConfig = {};
 
   beforeEach(() => {
     dockerModule.resetPrefetchedImages();
@@ -56,7 +53,6 @@ describe(getName(), () => {
 
   afterEach(() => {
     process.env = processEnvOrig;
-    setAdminConfig();
   });
 
   const image = 'image';
@@ -721,7 +717,7 @@ describe(getName(), () => {
       callback(null, { stdout: '', stderr: '' });
       return undefined;
     });
-    setAdminConfig(adminConfig);
+    setAdminConfig({ cacheDir, localDir: cwd, ...adminConfig });
     await exec(cmd as string, inOpts);
 
     expect(actualCmd).toEqual(outCommand);
diff --git a/lib/util/exec/index.ts b/lib/util/exec/index.ts
index c2205e02c4..a01b886e5e 100644
--- a/lib/util/exec/index.ts
+++ b/lib/util/exec/index.ts
@@ -22,8 +22,6 @@ import { getChildProcessEnv } from './env';
 
 const execConfig: ExecConfig = {
   binarySource: null,
-  localDir: null,
-  cacheDir: null,
 };
 
 export async function setExecConfig(
@@ -99,10 +97,11 @@ export async function exec(
   const extraEnv = { ...opts.extraEnv, ...customEnvVariables };
   let cwd;
   // istanbul ignore if
+  const { localDir } = getAdminConfig();
   if (cwdFile) {
-    cwd = join(execConfig.localDir, dirname(cwdFile));
+    cwd = join(localDir, dirname(cwdFile));
   }
-  cwd = cwd || opts.cwd || execConfig.localDir;
+  cwd = cwd || opts.cwd || localDir;
   const childEnv = createChildEnv(env, extraEnv);
 
   const execOptions: ExecOptions = { ...opts };
diff --git a/lib/util/fs/index.spec.ts b/lib/util/fs/index.spec.ts
index c79946db3f..b8a05915e2 100644
--- a/lib/util/fs/index.spec.ts
+++ b/lib/util/fs/index.spec.ts
@@ -1,16 +1,20 @@
 import { withDir } from 'tmp-promise';
 import { getName } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
 import {
   findLocalSiblingOrParent,
   getSubDirectory,
   localPathExists,
   readLocalFile,
-  setFsConfig,
   writeLocalFile,
 } from '.';
 
 describe(getName(), () => {
   describe('readLocalFile', () => {
+    beforeEach(() => {
+      setAdminConfig({ localDir: '' });
+    });
+
     it('reads buffer', async () => {
       expect(await readLocalFile(__filename)).toBeInstanceOf(Buffer);
     });
@@ -46,7 +50,7 @@ describe(getName(), () => {
     it('returns path for file', async () => {
       await withDir(
         async (localDir) => {
-          setFsConfig({
+          setAdminConfig({
             localDir: localDir.path,
           });
 
diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts
index 0e978cd4a6..71f4c42bf3 100644
--- a/lib/util/fs/index.ts
+++ b/lib/util/fs/index.ts
@@ -1,18 +1,10 @@
 import * as fs from 'fs-extra';
 import { isAbsolute, join, parse } from 'upath';
-import type { RenovateConfig } from '../../config/types';
+import { getAdminConfig } from '../../config/admin';
 import { logger } from '../../logger';
 
 export * from './proxies';
 
-let localDir = '';
-let cacheDir = '';
-
-export function setFsConfig(config: Partial<RenovateConfig>): void {
-  localDir = config.localDir;
-  cacheDir = config.cacheDir;
-}
-
 export function getSubDirectory(fileName: string): string {
   return parse(fileName).dir;
 }
@@ -34,6 +26,7 @@ export async function readLocalFile(
   fileName: string,
   encoding?: string
 ): Promise<string | Buffer> {
+  const { localDir } = getAdminConfig();
   const localFileName = join(localDir, fileName);
   try {
     const fileContent = await fs.readFile(localFileName, encoding);
@@ -48,11 +41,13 @@ export async function writeLocalFile(
   fileName: string,
   fileContent: string
 ): Promise<void> {
+  const { localDir } = getAdminConfig();
   const localFileName = join(localDir, fileName);
   await fs.outputFile(localFileName, fileContent);
 }
 
 export async function deleteLocalFile(fileName: string): Promise<void> {
+  const { localDir } = getAdminConfig();
   if (localDir) {
     const localFileName = join(localDir, fileName);
     await fs.remove(localFileName);
@@ -64,6 +59,7 @@ export async function renameLocalFile(
   fromFile: string,
   toFile: string
 ): Promise<void> {
+  const { localDir } = getAdminConfig();
   await fs.move(join(localDir, fromFile), join(localDir, toFile));
 }
 
@@ -74,6 +70,7 @@ export async function ensureDir(dirName: string): Promise<void> {
 
 // istanbul ignore next
 export async function ensureLocalDir(dirName: string): Promise<void> {
+  const { localDir } = getAdminConfig();
   const localDirName = join(localDir, dirName);
   await fs.ensureDir(localDirName);
 }
@@ -82,6 +79,7 @@ export async function ensureCacheDir(
   dirName: string,
   envPathVar?: string
 ): Promise<string> {
+  const { cacheDir } = getAdminConfig();
   const envCacheDirName = envPathVar ? process.env[envPathVar] : null;
   const cacheDirName = envCacheDirName || join(cacheDir, dirName);
   await fs.ensureDir(cacheDirName);
@@ -94,10 +92,12 @@ export async function ensureCacheDir(
  * without risk of that information leaking to other repositories/users.
  */
 export function privateCacheDir(): string {
+  const { cacheDir } = getAdminConfig();
   return join(cacheDir, '__renovate-private-cache');
 }
 
 export function localPathExists(pathName: string): Promise<boolean> {
+  const { localDir } = getAdminConfig();
   // Works for both files as well as directories
   return fs
     .stat(join(localDir, pathName))
diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts
index b0ae4b96ce..bf9445b65f 100644
--- a/lib/util/git/index.spec.ts
+++ b/lib/util/git/index.spec.ts
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
 import Git from 'simple-git';
 import tmp from 'tmp-promise';
 import { getName } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
 import * as git from '.';
 
 describe(getName(), () => {
@@ -68,8 +69,8 @@ describe(getName(), () => {
     await repo.clone(base.path, '.', ['--bare']);
     await repo.addConfig('commit.gpgsign', 'false');
     tmpDir = await tmp.dir({ unsafeCleanup: true });
+    setAdminConfig({ localDir: tmpDir.path });
     await git.initRepo({
-      localDir: tmpDir.path,
       url: origin.path,
       gitAuthorName: 'Jest',
       gitAuthorEmail: 'Jest@example.com',
@@ -105,7 +106,6 @@ describe(getName(), () => {
       await repo.commit('Add submodule');
       await git.initRepo({
         cloneSubmodules: true,
-        localDir: tmpDir.path,
         url: base.path,
       });
       await git.syncGit();
@@ -370,7 +370,6 @@ describe(getName(), () => {
       await git.checkoutBranch('develop');
 
       await git.initRepo({
-        localDir: tmpDir.path,
         url: base.path,
       });
 
@@ -392,7 +391,6 @@ describe(getName(), () => {
       await repo.checkout(defaultBranch);
 
       await git.initRepo({
-        localDir: tmpDir.path,
         url: base.path,
       });
 
@@ -400,7 +398,6 @@ describe(getName(), () => {
       expect(git.branchExists('renovate/test')).toBe(true);
 
       await git.initRepo({
-        localDir: tmpDir.path,
         url: base.path,
       });
 
@@ -429,7 +426,6 @@ describe(getName(), () => {
       await repo.commit('Add submodule');
       await git.initRepo({
         cloneSubmodules: true,
-        localDir: tmpDir.path,
         url: base.path,
       });
       await git.syncGit();
@@ -440,7 +436,6 @@ describe(getName(), () => {
     it('should use extra clone configuration', async () => {
       await fs.emptyDir(tmpDir.path);
       await git.initRepo({
-        localDir: tmpDir.path,
         url: origin.path,
         extraCloneOpts: {
           '-c': 'extra.clone.config=test-extra-config-value',
diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts
index db1a25825e..01629bd800 100644
--- a/lib/util/git/index.ts
+++ b/lib/util/git/index.ts
@@ -7,6 +7,7 @@ import Git, {
   StatusResult as StatusResult_,
 } from 'simple-git';
 import { join } from 'upath';
+import { getAdminConfig } from '../../config/admin';
 import { configFileNames } from '../../config/app-strings';
 import { RenovateConfig } from '../../config/types';
 import {
@@ -36,7 +37,6 @@ export type DiffResult = DiffResult_;
 export type CommitSha = string;
 
 interface StorageConfig {
-  localDir: string;
   currentBranch?: string;
   url: string;
   extraCloneOpts?: GitOptions;
@@ -171,7 +171,8 @@ export async function initRepo(args: StorageConfig): Promise<void> {
   config.ignoredAuthors = [];
   config.additionalBranches = [];
   config.branchIsModified = {};
-  git = Git(config.localDir);
+  const { localDir } = getAdminConfig();
+  git = Git(localDir);
   gitInitialized = false;
   await fetchBranchCommits();
 }
@@ -252,8 +253,9 @@ export async function syncGit(): Promise<void> {
     return;
   }
   gitInitialized = true;
-  logger.debug('Initializing git repository into ' + config.localDir);
-  const gitHead = join(config.localDir, '.git/HEAD');
+  const { localDir } = getAdminConfig();
+  logger.debug('Initializing git repository into ' + localDir);
+  const gitHead = join(localDir, '.git/HEAD');
   let clone = true;
 
   if (await fs.exists(gitHead)) {
@@ -277,7 +279,7 @@ export async function syncGit(): Promise<void> {
     }
   }
   if (clone) {
-    await fs.emptyDir(config.localDir);
+    await fs.emptyDir(localDir);
     const cloneStart = Date.now();
     try {
       // clone only the default branch
@@ -690,7 +692,8 @@ export async function commitFiles({
     await writePrivateKey();
     privateKeySet = true;
   }
-  await configSigningKey(config.localDir);
+  const { localDir } = getAdminConfig();
+  await configSigningKey(localDir);
   try {
     await git.reset(ResetMode.HARD);
     await git.raw(['clean', '-fd']);
@@ -701,7 +704,7 @@ export async function commitFiles({
       // istanbul ignore if
       if (file.name === '|delete|') {
         deleted.push(file.contents as string);
-      } else if (await isDirectory(join(config.localDir, file.name))) {
+      } else if (await isDirectory(join(localDir, file.name))) {
         fileNames.push(file.name);
         await gitAdd(file.name);
       } else {
@@ -713,7 +716,7 @@ export async function commitFiles({
         } else {
           contents = file.contents;
         }
-        await fs.outputFile(join(config.localDir, file.name), contents);
+        await fs.outputFile(join(localDir, file.name), contents);
       }
     }
     // istanbul ignore if
diff --git a/lib/util/index.ts b/lib/util/index.ts
index 67239ec5cd..0d7c8fbf0f 100644
--- a/lib/util/index.ts
+++ b/lib/util/index.ts
@@ -1,14 +1,3 @@
-import type { RenovateConfig } from '../config/types';
-import { setExecConfig } from './exec';
-import { setFsConfig } from './fs';
-
-export async function setUtilConfig(
-  config: Partial<RenovateConfig>
-): Promise<void> {
-  await setExecConfig(config);
-  setFsConfig(config);
-}
-
 export function sampleSize(array: string[], n: number): string[] {
   const length = array == null ? 0 : array.length;
   if (!length || n < 1) {
diff --git a/lib/workers/branch/index.spec.ts b/lib/workers/branch/index.spec.ts
index e092459f7e..f3157d3158 100644
--- a/lib/workers/branch/index.spec.ts
+++ b/lib/workers/branch/index.spec.ts
@@ -7,6 +7,7 @@ import {
   platform,
 } from '../../../test/util';
 import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
 import {
   MANAGER_LOCKFILE_ERROR,
   REPOSITORY_CHANGED,
@@ -62,6 +63,8 @@ const sanitize = mocked(_sanitize);
 const fs = mocked(_fs);
 const limits = mocked(_limits);
 
+const adminConfig: RepoAdminConfig = { localDir: '', cacheDir: '' };
+
 describe(getName(), () => {
   describe('processBranch', () => {
     const updatedPackageFiles: PackageFilesResult = {
@@ -94,7 +97,7 @@ describe(getName(), () => {
           body: '',
         },
       });
-      setAdminConfig();
+      setAdminConfig(adminConfig);
       sanitize.sanitize.mockImplementation((input) => input);
     });
     afterEach(() => {
@@ -382,7 +385,7 @@ describe(getName(), () => {
       git.branchExists.mockReturnValue(true);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
       automerge.tryBranchAutomerge.mockResolvedValueOnce('automerged');
-      setAdminConfig({ dryRun: true });
+      setAdminConfig({ ...adminConfig, dryRun: true });
       await branchWorker.processBranch(config);
       expect(automerge.tryBranchAutomerge).toHaveBeenCalledTimes(1);
       expect(prWorker.ensurePr).toHaveBeenCalledTimes(0);
@@ -634,7 +637,7 @@ describe(getName(), () => {
       checkExisting.prAlreadyExisted.mockResolvedValueOnce({
         state: PrState.Closed,
       } as Pr);
-      setAdminConfig({ dryRun: true });
+      setAdminConfig({ ...adminConfig, dryRun: true });
       expect(await branchWorker.processBranch(config)).toMatchSnapshot();
     });
 
@@ -644,7 +647,7 @@ describe(getName(), () => {
         state: PrState.Open,
       } as Pr);
       git.isBranchModified.mockResolvedValueOnce(true);
-      setAdminConfig({ dryRun: true });
+      setAdminConfig({ ...adminConfig, dryRun: true });
       expect(await branchWorker.processBranch(config)).toMatchSnapshot();
     });
 
@@ -666,7 +669,7 @@ describe(getName(), () => {
       git.isBranchModified.mockResolvedValueOnce(true);
       schedule.isScheduledNow.mockReturnValueOnce(false);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
-      setAdminConfig({ dryRun: true });
+      setAdminConfig({ ...adminConfig, dryRun: true });
       expect(
         await branchWorker.processBranch({
           ...config,
@@ -699,7 +702,7 @@ describe(getName(), () => {
         pr: {},
       } as EnsurePrResult);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
-      setAdminConfig({ dryRun: true });
+      setAdminConfig({ ...adminConfig, dryRun: true });
       expect(
         await branchWorker.processBranch({
           ...config,
@@ -773,12 +776,12 @@ describe(getName(), () => {
       schedule.isScheduledNow.mockReturnValueOnce(false);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
 
-      const adminConfig = {
+      setAdminConfig({
+        ...adminConfig,
         allowedPostUpgradeCommands: ['^echo {{{versioning}}}$'],
         allowPostUpgradeCommandTemplating: true,
         exposeAllEnv: true,
-      };
-      setAdminConfig(adminConfig);
+      });
 
       const result = await branchWorker.processBranch({
         ...config,
@@ -853,12 +856,12 @@ describe(getName(), () => {
       schedule.isScheduledNow.mockReturnValueOnce(false);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
 
-      const adminConfig = {
+      setAdminConfig({
+        ...adminConfig,
         allowedPostUpgradeCommands: ['^exit 1$'],
         allowPostUpgradeCommandTemplating: true,
         exposeAllEnv: true,
-      };
-      setAdminConfig(adminConfig);
+      });
 
       exec.exec.mockRejectedValue(new Error('Meh, this went wrong!'));
 
@@ -922,12 +925,12 @@ describe(getName(), () => {
 
       schedule.isScheduledNow.mockReturnValueOnce(false);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
-      const adminConfig = {
+      setAdminConfig({
+        ...adminConfig,
         allowedPostUpgradeCommands: ['^echo {{{versioning}}}$'],
         allowPostUpgradeCommandTemplating: false,
         exposeAllEnv: true,
-      };
-      setAdminConfig(adminConfig);
+      });
       const result = await branchWorker.processBranch({
         ...config,
         postUpgradeTasks: {
@@ -1002,12 +1005,12 @@ describe(getName(), () => {
       schedule.isScheduledNow.mockReturnValueOnce(false);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
 
-      const adminConfig = {
+      setAdminConfig({
+        ...adminConfig,
         allowedPostUpgradeCommands: ['^echo {{{depName}}}$'],
         allowPostUpgradeCommandTemplating: true,
         exposeAllEnv: true,
-      };
-      setAdminConfig(adminConfig);
+      });
 
       const inconfig: BranchConfig = {
         ...config,
@@ -1136,12 +1139,12 @@ describe(getName(), () => {
       schedule.isScheduledNow.mockReturnValueOnce(false);
       commit.commitFilesToBranch.mockResolvedValueOnce(null);
 
-      const adminConfig = {
+      setAdminConfig({
+        ...adminConfig,
         allowedPostUpgradeCommands: ['^echo hardcoded-string$'],
         allowPostUpgradeCommandTemplating: true,
         trustLevel: 'high',
-      };
-      setAdminConfig(adminConfig);
+      });
 
       const inconfig: BranchConfig = {
         ...config,
diff --git a/lib/workers/branch/lock-files/index.spec.ts b/lib/workers/branch/lock-files/index.spec.ts
index dc73ebb3cb..947caf80d6 100644
--- a/lib/workers/branch/lock-files/index.spec.ts
+++ b/lib/workers/branch/lock-files/index.spec.ts
@@ -1,4 +1,5 @@
 import { getName, git, mocked } from '../../../../test/util';
+import { setAdminConfig } from '../../../config/admin';
 import { getConfig } from '../../../config/defaults';
 import * as _lockFiles from '../../../manager/npm/post-update';
 import * as _lerna from '../../../manager/npm/post-update/lerna';
@@ -9,7 +10,7 @@ import type { PostUpdateConfig } from '../../../manager/types';
 import * as _fs from '../../../util/fs/proxies';
 import * as _hostRules from '../../../util/host-rules';
 
-const defaultConfig = getConfig();
+const config: PostUpdateConfig = getConfig();
 
 const fs = mocked(_fs);
 const lockFiles = mocked(_lockFiles);
@@ -29,12 +30,10 @@ const { writeUpdatedPackageFiles, getAdditionalFiles } = lockFiles;
 
 describe(getName(), () => {
   describe('writeUpdatedPackageFiles', () => {
-    let config: PostUpdateConfig;
     beforeEach(() => {
-      config = {
-        ...defaultConfig,
+      setAdminConfig({
         localDir: 'some-tmp-dir',
-      };
+      });
       fs.outputFile = jest.fn();
     });
     it('returns if no updated packageFiles', async () => {
@@ -70,12 +69,10 @@ describe(getName(), () => {
     });
   });
   describe('getAdditionalFiles', () => {
-    let config: PostUpdateConfig;
     beforeEach(() => {
-      config = {
-        ...defaultConfig,
+      setAdminConfig({
         localDir: 'some-tmp-dir',
-      };
+      });
       git.getFile.mockResolvedValueOnce('some lock file contents');
       npm.generateLockFile = jest.fn();
       npm.generateLockFile.mockResolvedValueOnce({
diff --git a/lib/workers/global/index.ts b/lib/workers/global/index.ts
index d7715983fe..6d096d4f8c 100644
--- a/lib/workers/global/index.ts
+++ b/lib/workers/global/index.ts
@@ -14,7 +14,7 @@ import type {
 } from '../../config/types';
 import { CONFIG_PRESETS_INVALID } from '../../constants/error-messages';
 import { getProblems, logger, setMeta } from '../../logger';
-import { setUtilConfig } from '../../util';
+import { setExecConfig } from '../../util/exec';
 import * as hostRules from '../../util/host-rules';
 import * as repositoryWorker from '../repository';
 import { autodiscoverRepositories } from './autodiscover';
@@ -103,7 +103,7 @@ export async function start(): Promise<number> {
         break;
       }
       const repoConfig = await getRepositoryConfig(config, repository);
-      await setUtilConfig(repoConfig);
+      await setExecConfig(repoConfig);
       if (repoConfig.hostRules) {
         hostRules.clear();
         repoConfig.hostRules.forEach((rule) => hostRules.add(rule));
diff --git a/lib/workers/repository/index.spec.ts b/lib/workers/repository/index.spec.ts
index c6c6f5e5c1..cbc9cd3d54 100644
--- a/lib/workers/repository/index.spec.ts
+++ b/lib/workers/repository/index.spec.ts
@@ -1,6 +1,7 @@
 import { mock } from 'jest-mock-extended';
 
 import { RenovateConfig, getConfig, getName, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
 import * as _process from './process';
 import { ExtractResult } from './process/extract-update';
 import { renovateRepository } from '.';
@@ -17,6 +18,7 @@ describe(getName(), () => {
     let config: RenovateConfig;
     beforeEach(() => {
       config = getConfig();
+      setAdminConfig({ localDir: '' });
     });
     it('runs', async () => {
       process.extractDependencies.mockResolvedValue(mock<ExtractResult>());
diff --git a/lib/workers/repository/index.ts b/lib/workers/repository/index.ts
index 162ac84aeb..ecce455412 100644
--- a/lib/workers/repository/index.ts
+++ b/lib/workers/repository/index.ts
@@ -1,5 +1,5 @@
 import fs from 'fs-extra';
-import { setAdminConfig } from '../../config/admin';
+import { getAdminConfig, setAdminConfig } from '../../config/admin';
 import type { RenovateConfig } from '../../config/types';
 import { logger, setMeta } from '../../logger';
 import { deleteLocalFile, privateCacheDir } from '../../util/fs';
@@ -28,16 +28,16 @@ export async function renovateRepository(
   repoConfig: RenovateConfig
 ): Promise<ProcessResult> {
   splitInit();
-  let config = { ...repoConfig };
-  setAdminConfig(config);
+  let config = setAdminConfig(repoConfig);
   setMeta({ repository: config.repository });
   logger.info({ renovateVersion }, 'Repository started');
   logger.trace({ config });
   let repoResult: ProcessResult;
   queue.clear();
+  const { localDir } = getAdminConfig();
   try {
-    await fs.ensureDir(config.localDir);
-    logger.debug('Using localDir: ' + config.localDir);
+    await fs.ensureDir(localDir);
+    logger.debug('Using localDir: ' + localDir);
     config = await initRepo(config);
     addSplit('init');
     const { branches, branchList, packageFiles } = await extractDependencies(
@@ -57,7 +57,7 @@ export async function renovateRepository(
     const errorRes = await handleError(config, err);
     repoResult = processResult(config, errorRes);
   }
-  if (config.localDir && !config.persistRepoData) {
+  if (localDir && !config.persistRepoData) {
     try {
       await deleteLocalFile('.');
     } catch (err) /* istanbul ignore if */ {
diff --git a/lib/workers/repository/init/cache.spec.ts b/lib/workers/repository/init/cache.spec.ts
index 6dbb39a126..52af4a3f35 100644
--- a/lib/workers/repository/init/cache.spec.ts
+++ b/lib/workers/repository/init/cache.spec.ts
@@ -1,4 +1,5 @@
 import { RenovateConfig, getConfig, getName } from '../../../../test/util';
+import { setAdminConfig } from '../../../config/admin';
 import { initializeCaches } from './cache';
 
 describe(getName(), () => {
@@ -6,6 +7,7 @@ describe(getName(), () => {
     let config: RenovateConfig;
     beforeEach(() => {
       config = { ...getConfig() };
+      setAdminConfig({ cacheDir: '' });
     });
     it('initializes', async () => {
       expect(await initializeCaches(config)).toBeUndefined();
diff --git a/lib/workers/repository/init/index.spec.ts b/lib/workers/repository/init/index.spec.ts
index 54e9a8e7f1..b453a079a3 100644
--- a/lib/workers/repository/init/index.spec.ts
+++ b/lib/workers/repository/init/index.spec.ts
@@ -1,4 +1,5 @@
 import { getName, logger, mocked } from '../../../../test/util';
+import { setAdminConfig } from '../../../config/admin';
 import * as _secrets from '../../../config/secrets';
 import * as _onboarding from '../onboarding/branch';
 import * as _apis from './apis';
@@ -22,6 +23,13 @@ const onboarding = mocked(_onboarding);
 const secrets = mocked(_secrets);
 
 describe(getName(), () => {
+  beforeEach(() => {
+    setAdminConfig({ localDir: '', cacheDir: '' });
+  });
+  afterEach(() => {
+    setAdminConfig();
+  });
+
   describe('initRepo', () => {
     it('runs', async () => {
       apis.initApis.mockResolvedValue({} as never);
diff --git a/lib/workers/repository/stats.ts b/lib/workers/repository/stats.ts
index 7384796e3e..3d15ec92fd 100644
--- a/lib/workers/repository/stats.ts
+++ b/lib/workers/repository/stats.ts
@@ -5,6 +5,7 @@ import type { RequestStats } from '../../util/http/types';
 
 export function printRequestStats(): void {
   const httpRequests = memCache.get<RequestStats[]>('http-requests');
+  // istanbul ignore next
   if (!httpRequests) {
     return;
   }
-- 
GitLab