diff --git a/jest.config.js b/jest.config.js
index 81fd8f5c8739420aa76d82a476210c7c1ecbab85..ff50dede20cdc9fc9bd802569ba8d8918ae89251 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -7,7 +7,7 @@ module.exports = {
   collectCoverageFrom: [
     'lib/**/*.{js,ts}',
     '!lib/**/*.{d,spec}.ts',
-    '!lib/**/{__fixtures__,__mocks__}/**/*.{js,ts}',
+    '!lib/**/{__fixtures__,__mocks__,__testutil__}/**/*.{js,ts}',
   ],
   coverageReporters: ci
     ? ['html', 'json', 'text-summary']
diff --git a/lib/manager/gradle/__snapshots__/index.spec.ts.snap b/lib/manager/gradle/__snapshots__/index.spec.ts.snap
index 906a110e139826404f0725aabe2c89ded8133cbb..68432b5d63d7ada7de63459ce2d7ddb397d8faeb 100644
--- a/lib/manager/gradle/__snapshots__/index.spec.ts.snap
+++ b/lib/manager/gradle/__snapshots__/index.spec.ts.snap
@@ -16,7 +16,29 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
+    },
+  },
+]
+`;
+
+exports[`manager/gradle extractPackageFile should execute gradle if gradlew is not available 1`] = `
+Array [
+  Object {
+    "cmd": "gradle --init-script renovate-plugin.gradle renovate",
+    "options": Object {
+      "cwd": "localDir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 60000,
     },
   },
 ]
@@ -38,7 +60,29 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
+    },
+  },
+]
+`;
+
+exports[`manager/gradle extractPackageFile should execute gradlew.bat when available on Windows 1`] = `
+Array [
+  Object {
+    "cmd": "gradlew.bat --init-script renovate-plugin.gradle renovate",
+    "options": Object {
+      "cwd": "localDir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 60000,
     },
   },
 ]
@@ -60,7 +104,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
@@ -82,7 +126,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
@@ -91,7 +135,7 @@ Array [
 exports[`manager/gradle extractPackageFile should return empty if there is no dependency report 1`] = `
 Array [
   Object {
-    "cmd": "gradle --init-script renovate-plugin.gradle renovate",
+    "cmd": "./gradlew --init-script renovate-plugin.gradle renovate",
     "options": Object {
       "cwd": "localDir",
       "encoding": "utf-8",
@@ -104,7 +148,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
@@ -247,7 +291,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
@@ -332,7 +376,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
@@ -475,7 +519,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
@@ -483,10 +527,22 @@ Array [
 
 exports[`manager/gradle extractPackageFile should return null and gradle should not be executed if no root build.gradle 1`] = `Array []`;
 
-exports[`manager/gradle extractPackageFile should run gradlew through \`sh\` when available but not executable 1`] = `
+exports[`manager/gradle extractPackageFile should use docker even if gradlew is available 1`] = `
 Array [
   Object {
-    "cmd": "sh gradlew --init-script renovate-plugin.gradle renovate",
+    "cmd": "docker pull renovate/gradle",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker ps --filter name=renovate_gradle -aq | xargs --no-run-if-empty docker rm -f",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker run --rm --name=renovate_gradle --label=renovate_child -v \\"localDir\\":\\"localDir\\" -w \\"localDir\\" renovate/gradle bash -l -c \\"./gradlew --init-script renovate-plugin.gradle renovate\\"",
     "options": Object {
       "cwd": "localDir",
       "encoding": "utf-8",
@@ -499,14 +555,20 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 900000,
     },
   },
 ]
 `;
 
-exports[`manager/gradle extractPackageFile should use docker even if gradlew is available 1`] = `
+exports[`manager/gradle extractPackageFile should use docker even if gradlew.bat is available on Windows 1`] = `
 Array [
+  Object {
+    "cmd": "docker pull renovate/gradle",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
   Object {
     "cmd": "docker ps --filter name=renovate_gradle -aq | xargs --no-run-if-empty docker rm -f",
     "options": Object {
@@ -561,7 +623,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
@@ -623,7 +685,7 @@ Array [
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
       },
-      "timeout": 20000,
+      "timeout": 60000,
     },
   },
 ]
diff --git a/lib/manager/gradle/__testutil__/gradle.ts b/lib/manager/gradle/__testutil__/gradle.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6fc104c33102eb981896a71a25ce0d4923f5146b
--- /dev/null
+++ b/lib/manager/gradle/__testutil__/gradle.ts
@@ -0,0 +1,87 @@
+import { spawnSync, SpawnSyncReturns } from 'child_process';
+
+const failIfNoJavaEnv = 'CI';
+
+const gradleJavaVersionSupport = {
+  5: { min: 8, max: 12 },
+  6: { min: 8, max: 13 },
+};
+
+const skipJava = process.env.SKIP_JAVA_TESTS === 'true';
+const enforceJava = process.env[failIfNoJavaEnv] === 'true' && !skipJava;
+
+function parseJavaVersion(javaVersionOutput: string): number {
+  const versionMatch = /version "(?:1\.)?(\d+)[\d._-]*"/.exec(
+    javaVersionOutput
+  );
+  if (versionMatch !== null && versionMatch.length === 2) {
+    return parseInt(versionMatch[1], 10);
+  }
+  if (enforceJava) {
+    throw Error(`This test suite needs Java and ${failIfNoJavaEnv} is set. However, we cannot parse the Java version.
+The output of java -version was:
+${javaVersionOutput}`);
+  }
+  return 0;
+}
+
+let cachedJavaVersion: number | null = null;
+
+function determineJavaVersion(): number {
+  if (cachedJavaVersion == null) {
+    let javaVersionCommand: SpawnSyncReturns<string>;
+    let error: Error;
+    try {
+      javaVersionCommand = spawnSync('java', ['-version'], {
+        encoding: 'utf8',
+        windowsHide: true,
+      });
+    } catch (e) {
+      error = e;
+    }
+    if (javaVersionCommand.error) {
+      error = javaVersionCommand.error;
+    }
+    if (error) {
+      if (!enforceJava) {
+        return 0;
+      }
+      throw Error(
+        `This test suite needs Java and ${failIfNoJavaEnv} is set.
+Result of java -version:
+${error}`
+      );
+    }
+    cachedJavaVersion = parseJavaVersion(javaVersionCommand.stderr);
+  }
+  return cachedJavaVersion;
+}
+
+class WithGradle {
+  private gradleSupportsThisJavaVersion: boolean;
+
+  constructor(private gradleVersion: number) {
+    const javaVersion = determineJavaVersion();
+    if (gradleJavaVersionSupport[gradleVersion] === undefined) {
+      throw Error(`Unknown gradle version '${gradleVersion}'!`);
+    }
+
+    const supportedJavaVersions = gradleJavaVersionSupport[gradleVersion];
+    this.gradleSupportsThisJavaVersion =
+      javaVersion >= supportedJavaVersions.min &&
+      javaVersion <= supportedJavaVersions.max;
+    if (!this.gradleSupportsThisJavaVersion && enforceJava) {
+      throw Error(
+        `This test needs a Java version between ${supportedJavaVersions.min} and ${supportedJavaVersions.max}. The current Java version is ${javaVersion} and ${failIfNoJavaEnv} is set!`
+      );
+    }
+  }
+
+  get it(): jest.It {
+    return !this.gradleSupportsThisJavaVersion || skipJava ? it.skip : it;
+  }
+}
+
+export function ifSystemSupportsGradle(gradleVersion: number): WithGradle {
+  return new WithGradle(gradleVersion);
+}
diff --git a/lib/manager/gradle/gradle-updates-report.spec.ts b/lib/manager/gradle/gradle-updates-report.spec.ts
index cb2ae6471edac2861687bd76c8613b81b5322447..0c46c1399cf90300b0133ab8474fa257113f7fa7 100644
--- a/lib/manager/gradle/gradle-updates-report.spec.ts
+++ b/lib/manager/gradle/gradle-updates-report.spec.ts
@@ -1,70 +1,18 @@
 import tmp, { DirectoryResult } from 'tmp-promise';
 import * as fs from 'fs-extra';
 import * as path from 'path';
-import { spawnSync, SpawnSyncReturns } from 'child_process';
 import { exec } from '../../util/exec';
 import { GRADLE_DEPENDENCY_REPORT_OPTIONS } from './index';
 import {
   createRenovateGradlePlugin,
   GRADLE_DEPENDENCY_REPORT_FILENAME,
 } from './gradle-updates-report';
+import { ifSystemSupportsGradle } from './__testutil__/gradle';
 
 const fixtures = 'lib/manager/gradle/__fixtures__';
-const failIfNoJavaEnv = 'CI';
-
-const gradleJavaVersionSupport = {
-  5: { min: 8, max: 12 },
-  6: { min: 8, max: 13 },
-};
-
-const skipJava = process.env.SKIP_JAVA_TESTS === 'true';
-const enforceJava = process.env[failIfNoJavaEnv] === 'true' && !skipJava;
-
-function parseJavaVersion(javaVersionOutput: string) {
-  const versionMatch = /version "(?:1\.)?(\d+)[\d._-]*"/.exec(
-    javaVersionOutput
-  );
-  if (versionMatch !== null && versionMatch.length === 2) {
-    return parseInt(versionMatch[1], 10);
-  }
-  if (enforceJava) {
-    throw Error(`This test suite needs Java and ${failIfNoJavaEnv} is set. However, we cannot parse the Java version.
-The output of java -version was:
-${javaVersionOutput}`);
-  }
-  return 0;
-}
-
-function determineJavaVersion(): number {
-  let javaVersionCommand: SpawnSyncReturns<string>;
-  let error: Error;
-  try {
-    javaVersionCommand = spawnSync('java', ['-version'], {
-      encoding: 'utf8',
-      windowsHide: true,
-    });
-  } catch (e) {
-    error = e;
-  }
-  if (javaVersionCommand.error) {
-    error = javaVersionCommand.error;
-  }
-  if (error) {
-    if (!enforceJava) {
-      return 0;
-    }
-    throw Error(
-      `This test suite needs Java and ${failIfNoJavaEnv} is set.
-Result of java -version:
-${error}`
-    );
-  }
-  return parseJavaVersion(javaVersionCommand.stderr);
-}
 
 describe('lib/manager/gradle/gradle-updates-report', () => {
   let workingDir: DirectoryResult;
-  const javaVersion = determineJavaVersion();
 
   beforeEach(async () => {
     workingDir = await tmp.dir({ unsafeCleanup: true });
@@ -72,16 +20,7 @@ describe('lib/manager/gradle/gradle-updates-report', () => {
 
   describe('createRenovateGradlePlugin', () => {
     for (const gradleVersion of [5, 6]) {
-      const supportedJavaVersions = gradleJavaVersionSupport[gradleVersion];
-      const gradleSupportsThisJavaVersion =
-        javaVersion >= supportedJavaVersions.min &&
-        javaVersion <= supportedJavaVersions.max;
-      if (!gradleSupportsThisJavaVersion && enforceJava) {
-        throw Error(
-          `This test needs a Java version between ${supportedJavaVersions.min} and ${supportedJavaVersions.max}. The current Java version is ${javaVersion} and ${failIfNoJavaEnv} is set!`
-        );
-      }
-      (!gradleSupportsThisJavaVersion || skipJava ? it.skip : it)(
+      ifSystemSupportsGradle(gradleVersion).it(
         `generates a report for Gradle version ${gradleVersion}`,
         // the function creation is correct and intended
         // eslint-disable-next-line no-loop-func
diff --git a/lib/manager/gradle/index.spec.ts b/lib/manager/gradle/index.spec.ts
index 79a39ef379d9c9b51c298e99e7a411e6d1edb8c7..917f9b8f84647d2b5a17cc5803825d54674a69fc 100644
--- a/lib/manager/gradle/index.spec.ts
+++ b/lib/manager/gradle/index.spec.ts
@@ -1,28 +1,26 @@
 import { toUnix } from 'upath';
 import _fs from 'fs-extra';
-import fsReal from 'fs';
+import fsReal, { Stats } from 'fs';
 import { exec as _exec } from 'child_process';
-import * as manager from '.';
-import { platform as _platform, Platform } from '../../platform';
+import * as _os from 'os';
+import tmp, { DirectoryResult } from 'tmp-promise';
+import * as path from 'path';
+import { platform as _platform } from '../../platform';
 import { envMock, mockExecAll } from '../../../test/execUtil';
 import * as _env from '../../util/exec/env';
-import { mocked } from '../../../test/util';
 import { BinarySource } from '../../util/exec/common';
-import { setUtilConfig } from '../../util';
+import * as _docker from '../../util/exec/docker';
+import * as _util from '../../util';
+import { ifSystemSupportsGradle } from './__testutil__/gradle';
+import * as _manager from '.';
+import { ExtractConfig } from '../common';
 
-jest.mock('fs-extra');
-jest.mock('child_process');
-jest.mock('../../util/exec/env');
-
-const platform: jest.Mocked<Platform> = _platform as any;
-const fs: jest.Mocked<typeof _fs> = _fs as any;
-const exec: jest.Mock<typeof _exec> = _exec as any;
-const env = mocked(_env);
+const fixtures = 'lib/manager/gradle/__fixtures__';
 
 const config = {
   localDir: 'localDir',
   gradle: {
-    timeout: 20,
+    timeout: 60,
   },
 };
 
@@ -36,22 +34,64 @@ const gradleOutput = {
   stderr: '',
 };
 
-describe('manager/gradle', () => {
-  beforeEach(() => {
-    jest.resetAllMocks();
-    jest.resetModules();
-
-    fs.readFile.mockResolvedValue(updatesDependenciesReport as any);
-    fs.mkdir.mockResolvedValue();
-    fs.exists.mockResolvedValue(true);
-    fs.access.mockResolvedValue(undefined);
-    platform.getFile.mockResolvedValue('some content');
-
-    env.getChildProcessEnv.mockReturnValue(envMock.basic);
-    setUtilConfig(config);
-  });
+function resetMocks() {
+  jest.resetAllMocks();
+  jest.resetModules();
+}
+
+async function setupMocks() {
+  resetMocks();
+
+  jest.mock('fs-extra');
+  jest.mock('child_process');
+  jest.mock('../../util/exec/env');
+  jest.mock('../../platform');
+  jest.mock('os');
 
+  const fs: jest.Mocked<typeof _fs> = require('fs-extra');
+  const os: jest.Mocked<typeof _os> = require('os');
+  const platform: jest.Mocked<typeof _platform> = require('../../platform')
+    .platform;
+  const env: jest.Mocked<typeof _env> = require('../../util/exec/env');
+  const exec: jest.Mock<typeof _exec> = require('child_process').exec;
+  const util: jest.Mocked<typeof _util> = require('../../util');
+
+  platform.getFile.mockResolvedValue('some content');
+  env.getChildProcessEnv.mockReturnValue(envMock.basic);
+  await util.setUtilConfig(config);
+
+  return [require('.'), exec, fs, util, os];
+}
+
+describe('manager/gradle', () => {
   describe('extractPackageFile', () => {
+    let manager: typeof _manager;
+    let exec: jest.Mock<typeof _exec>;
+    let fs: jest.Mocked<typeof _fs>;
+    let util: jest.Mocked<typeof _util>;
+    let os: jest.Mocked<typeof _os>;
+    let docker: typeof _docker;
+
+    beforeAll(async () => {
+      [manager, exec, fs, util, os] = await setupMocks();
+      docker = require('../../util/exec/docker');
+    });
+
+    afterAll(resetMocks);
+
+    beforeEach(() => {
+      fs.readFile.mockResolvedValue(updatesDependenciesReport as any);
+      fs.mkdir.mockResolvedValue();
+      fs.exists.mockResolvedValue(true);
+      fs.stat.mockResolvedValue({
+        mode: 0o755,
+        isFile: () => true,
+      } as Stats);
+      exec.mockReset();
+      docker.resetPrefetchedImages();
+      os.platform.mockReturnValue('linux');
+    });
+
     it('should return gradle dependencies', async () => {
       const execSnapshots = mockExecAll(exec, gradleOutput);
 
@@ -141,10 +181,18 @@ describe('manager/gradle', () => {
       expect(execSnapshots).toMatchSnapshot();
     });
 
-    it('should run gradlew through `sh` when available but not executable', async () => {
+    it('should execute gradlew.bat when available on Windows', async () => {
+      const execSnapshots = mockExecAll(exec, gradleOutput);
+      os.platform.mockReturnValue('win32');
+
+      await manager.extractAllPackageFiles(config, ['build.gradle']);
+      expect(execSnapshots).toMatchSnapshot();
+    });
+
+    it('should execute gradle if gradlew is not available', async () => {
       const execSnapshots = mockExecAll(exec, gradleOutput);
 
-      fs.access.mockRejectedValue(undefined);
+      fs.stat.mockResolvedValue({ isFile: () => false } as Stats);
       await manager.extractAllPackageFiles(config, ['build.gradle']);
       expect(execSnapshots).toMatchSnapshot();
     });
@@ -152,7 +200,7 @@ describe('manager/gradle', () => {
     it('should return null and gradle should not be executed if no root build.gradle', async () => {
       const execSnapshots = mockExecAll(exec, gradleOutput);
 
-      fs.exists.mockResolvedValue(false);
+      fs.stat.mockResolvedValue({ isFile: () => false } as Stats);
 
       const packageFiles = ['foo/build.gradle'];
       expect(
@@ -185,7 +233,7 @@ describe('manager/gradle', () => {
     });
 
     it('should use docker if required', async () => {
-      setUtilConfig({ ...config, binarySource: BinarySource.Docker });
+      util.setUtilConfig({ ...config, binarySource: BinarySource.Docker });
       const execSnapshots = mockExecAll(exec, gradleOutput);
 
       const configWithDocker = {
@@ -198,7 +246,21 @@ describe('manager/gradle', () => {
     });
 
     it('should use docker even if gradlew is available', async () => {
-      setUtilConfig({ ...config, binarySource: BinarySource.Docker });
+      util.setUtilConfig({ ...config, binarySource: BinarySource.Docker });
+      const execSnapshots = mockExecAll(exec, gradleOutput);
+
+      const configWithDocker = {
+        binarySource: BinarySource.Docker,
+        ...config,
+        gradle: {},
+      };
+      await manager.extractAllPackageFiles(configWithDocker, ['build.gradle']);
+
+      expect(execSnapshots).toMatchSnapshot();
+    });
+
+    it('should use docker even if gradlew.bat is available on Windows', async () => {
+      os.platform.mockReturnValue('win32');
       const execSnapshots = mockExecAll(exec, gradleOutput);
 
       const configWithDocker = {
@@ -213,6 +275,14 @@ describe('manager/gradle', () => {
   });
 
   describe('updateDependency', () => {
+    let manager: typeof _manager;
+    let exec: jest.Mock<typeof _exec>;
+
+    beforeAll(async () => {
+      [manager, exec] = await setupMocks();
+    });
+    afterAll(resetMocks);
+
     it('should update an existing module dependency', () => {
       const execSnapshots = mockExecAll(exec, gradleOutput);
 
@@ -303,4 +373,70 @@ describe('manager/gradle', () => {
       expect(execSnapshots).toMatchSnapshot();
     });
   });
+
+  describe('executeGradle integration', () => {
+    const SUCCESS_FILE = 'success.indicator';
+    let workingDir: DirectoryResult;
+    let testRunConfig: ExtractConfig;
+
+    const manager = require('.');
+
+    beforeEach(async () => {
+      workingDir = await tmp.dir({ unsafeCleanup: true });
+      testRunConfig = { ...config, localDir: workingDir.path };
+      await _fs.copy(`${fixtures}/minimal-project`, workingDir.path);
+      await _fs.copy(`${fixtures}/gradle-wrappers/6`, workingDir.path);
+
+      const mockPluginContent = `
+allprojects {
+  tasks.register("renovate") {
+    doLast {
+      new File('${SUCCESS_FILE}').write 'success'
+    }
+  }
+}`;
+      await _fs.writeFile(
+        path.join(workingDir.path, 'renovate-plugin.gradle'),
+        mockPluginContent
+      );
+    });
+
+    ifSystemSupportsGradle(6).it(
+      'executes an executable gradle wrapper',
+      async () => {
+        const gradlew = await fsReal.promises.stat(
+          path.join(workingDir.path, 'gradlew')
+        );
+        await manager.executeGradle(testRunConfig, workingDir.path, gradlew);
+        await expect(
+          fsReal.promises.readFile(
+            path.join(workingDir.path, SUCCESS_FILE),
+            'utf8'
+          )
+        ).resolves.toBe('success');
+      },
+      120000
+    );
+
+    ifSystemSupportsGradle(6).it(
+      'executes a not-executable gradle wrapper',
+      async () => {
+        await fsReal.promises.chmod(
+          path.join(workingDir.path, 'gradlew'),
+          '444'
+        );
+        const gradlew = await fsReal.promises.stat(
+          path.join(workingDir.path, 'gradlew')
+        );
+        await manager.executeGradle(testRunConfig, workingDir.path, gradlew);
+        await expect(
+          fsReal.promises.readFile(
+            path.join(workingDir.path, SUCCESS_FILE),
+            'utf8'
+          )
+        ).resolves.toBe('success');
+      },
+      120000
+    );
+  });
 });
diff --git a/lib/manager/gradle/index.ts b/lib/manager/gradle/index.ts
index 393d71744999ba2d479ab4949c99a8a905649b4f..7123933b766cd156153e34aec229bf48538d6625 100644
--- a/lib/manager/gradle/index.ts
+++ b/lib/manager/gradle/index.ts
@@ -1,20 +1,10 @@
-import { access, constants, exists } from 'fs-extra';
+import * as os from 'os';
+import * as fs from 'fs-extra';
+import { Stats } from 'fs';
 import upath from 'upath';
-
 import { exec, ExecOptions } from '../../util/exec';
 import { logger } from '../../logger';
 import * as mavenVersioning from '../../versioning/maven';
-
-import {
-  init,
-  collectVersionVariables,
-  updateGradleVersion,
-  GradleDependency,
-} from './build-gradle';
-import {
-  createRenovateGradlePlugin,
-  extractDependenciesFromUpdatesReport,
-} from './gradle-updates-report';
 import {
   PackageFile,
   ExtractConfig,
@@ -25,42 +15,59 @@ import { platform } from '../../platform';
 import { LANGUAGE_JAVA } from '../../constants/languages';
 import * as datasourceMaven from '../../datasource/maven';
 import { DatasourceError } from '../../datasource';
+import { BinarySource } from '../../util/exec/common';
+import {
+  init,
+  collectVersionVariables,
+  updateGradleVersion,
+  GradleDependency,
+} from './build-gradle';
+import {
+  createRenovateGradlePlugin,
+  extractDependenciesFromUpdatesReport,
+} from './gradle-updates-report';
 
 export const GRADLE_DEPENDENCY_REPORT_OPTIONS =
   '--init-script renovate-plugin.gradle renovate';
 const TIMEOUT_CODE = 143;
 
-async function canExecute(path: string): Promise<boolean> {
-  try {
-    await access(path, constants.X_OK);
-    return true;
-  } catch {
-    return false;
+function gradleWrapperFileName(config: ExtractConfig): string {
+  if (
+    os.platform() === 'win32' &&
+    config.binarySource !== BinarySource.Docker
+  ) {
+    return 'gradlew.bat';
   }
+  return './gradlew';
 }
 
-async function getGradleCommandLine(
+async function prepareGradleCommandLine(
   config: ExtractConfig,
-  cwd: string
+  cwd: string,
+  gradlew: Stats
 ): Promise<string> {
   const args = GRADLE_DEPENDENCY_REPORT_OPTIONS;
+  const gradlewName = gradleWrapperFileName(config);
+
+  if (gradlew.isFile()) {
+    // if the file is not executable by others
+    // eslint-disable-next-line no-bitwise
+    if ((gradlew.mode & 0o1) === 0) {
+      // add the execution permission to the owner, group and others
+      // eslint-disable-next-line no-bitwise
+      await fs.chmod(upath.join(cwd, gradlewName), gradlew.mode | 0o111);
+    }
 
-  const gradlewPath = upath.join(cwd, 'gradlew');
-  const gradlewExists = await exists(gradlewPath);
-  const gradlewExecutable = gradlewExists && (await canExecute(gradlewPath));
-  if (gradlewExecutable) {
-    return `./gradlew ${args}`;
-  }
-  if (gradlewExists) {
-    return `sh gradlew ${args}`;
+    return `${gradlewName} ${args}`;
   }
 
   return `gradle ${args}`;
 }
 
-async function executeGradle(
+export async function executeGradle(
   config: ExtractConfig,
-  cwd: string
+  cwd: string,
+  gradlew: Stats
 ): Promise<void> {
   let stdout: string;
   let stderr: string;
@@ -68,7 +75,7 @@ async function executeGradle(
     config.gradle && config.gradle.timeout
       ? config.gradle.timeout * 1000
       : undefined;
-  const cmd = await getGradleCommandLine(config, cwd);
+  const cmd = await prepareGradleCommandLine(config, cwd, gradlew);
   const execOptions: ExecOptions = {
     timeout,
     cwd,
@@ -97,19 +104,19 @@ export async function extractAllPackageFiles(
   packageFiles: string[]
 ): Promise<PackageFile[] | null> {
   let rootBuildGradle: string | undefined;
+  let gradlew: Stats;
   for (const packageFile of packageFiles) {
+    const dirname = upath.dirname(packageFile);
+    const gradlewPath = upath.join(dirname, gradleWrapperFileName(config));
+    gradlew = await fs.stat(upath.join(config.localDir, gradlewPath));
+
     if (['build.gradle', 'build.gradle.kts'].includes(packageFile)) {
       rootBuildGradle = packageFile;
       break;
     }
 
     // If there is gradlew in the same directory, the directory should be a Gradle project root
-    const dirname = upath.dirname(packageFile);
-    const gradlewPath = upath.join(dirname, 'gradlew');
-    const gradlewExists = await exists(
-      upath.join(config.localDir, gradlewPath)
-    );
-    if (gradlewExists) {
+    if (gradlew.isFile()) {
       rootBuildGradle = packageFile;
       break;
     }
@@ -123,7 +130,7 @@ export async function extractAllPackageFiles(
   const cwd = upath.join(config.localDir, upath.dirname(rootBuildGradle));
 
   await createRenovateGradlePlugin(cwd);
-  await executeGradle(config, cwd);
+  await executeGradle(config, cwd, gradlew);
 
   init();
 
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 9802bf51a91005edd903da718fac2254ce398159..5fe1b487cf013b4298af84341a51bae7bbcbce80 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -14,6 +14,7 @@
     "./dist",
     "**/__mocks__/**",
     "**/__fixtures__/**",
+    "**/__testutil__/**",
     "**/*.spec.ts",
     "./test"
   ]