import type { Stats } from 'node:fs'; import os from 'node:os'; import upath from 'upath'; import { mockDeep } from 'vitest-mock-extended'; import { envMock, mockExecAll } from '~test/exec-util.ts'; import { Fixtures } from '~test/fixtures.ts'; import * as httpMock from '~test/http-mock.ts'; import { env, fs, git, logger, partial } from '~test/util.ts'; import { GlobalConfig } from '../../../config/global.ts'; import type { InternalGlobalConfigOptions, RepoGlobalConfig, } from '../../../config/types.ts'; import { resetPrefetchedImages } from '../../../util/exec/docker/index.ts'; import type { StatusResult } from '../../../util/git/types.ts'; import { getPkgReleases } from '../../datasource/index.ts'; import { updateArtifacts as gradleUpdateArtifacts } from '../gradle/index.ts'; import type { UpdateArtifactsConfig, UpdateArtifactsResult } from '../types.ts'; import { updateBuildFile, updateLockFiles } from './artifacts.ts'; import { updateArtifacts } from './index.ts'; vi.mock('../../../util/fs/index.ts'); vi.mock('../../../util/exec/env.ts'); vi.mock('../../datasource/index.ts', () => mockDeep()); vi.mock('../gradle/index.ts'); process.env.CONTAINERBASE = 'true'; const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = { // `join` fixes Windows CI localDir: upath.join('/tmp/github/some/repo'), cacheDir: upath.join('/tmp/cache'), containerbaseDir: upath.join('/tmp/cache/containerbase'), // although not enabled by default, let's assume it is allowedUnsafeExecutions: ['gradleWrapper'], binarySource: 'global', }; const config: UpdateArtifactsConfig = { newValue: '5.6.4', }; const osPlatformSpy = vi.spyOn(os, 'platform'); describe('modules/manager/gradle-wrapper/artifacts', () => { beforeEach(() => { osPlatformSpy.mockReturnValue('linux'); env.getChildProcessEnv.mockReturnValue({ ...envMock.basic, LANG: 'en_US.UTF-8', LC_ALL: 'en_US', }); GlobalConfig.set(adminConfig); resetPrefetchedImages(); // remove any test-specific overrides delete config.toolSettings; fs.readLocalFile.mockResolvedValue('test'); fs.statLocalFile.mockResolvedValue( partial({ isFile: () => true, mode: 0o555, }), ); // java vi.mocked(getPkgReleases).mockResolvedValueOnce({ releases: [ { version: '8.0.1' }, { version: '11.0.1' }, { version: '16.0.1' }, { version: '17.0.0' }, ], }); }); describe('updateArtifacts()', () => { it('Custom Gradle Wrapper heap settings are populated', async () => { const execSnapshots = mockExecAll(); httpMock .scope('https://services.gradle.org') .get('/distributions/gradle-6.3-bin.zip.sha256') .reply( 200, '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768', ); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: ['gradle/wrapper/gradle-wrapper.properties'], }), ); GlobalConfig.set({ ...adminConfig, toolSettings: { jvmMaxMemory: 600 }, }); const result = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`, config, }); expect(result).toEqual([ { file: { contents: 'test', path: 'gradle/wrapper/gradle-wrapper.properties', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: './gradlew -Dorg.gradle.jvmargs="-Xms600m -Xmx600m" :wrapper --gradle-distribution-url https://services.gradle.org/distributions/gradle-6.3-bin.zip --gradle-distribution-sha256-sum 038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768', }, ]); }); it('replaces existing value', async () => { const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: [ 'gradle/wrapper/gradle-wrapper.properties', 'gradlew', 'gradlew.bat', ], }), ); const res = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: Fixtures.get( 'expectedFiles/gradle/wrapper/gradle-wrapper.properties', ), config: { ...config, newValue: '6.3' }, }); expect(res).toEqual( [ 'gradle/wrapper/gradle-wrapper.properties', 'gradlew', 'gradlew.bat', ].map((fileProjectPath) => ({ file: { type: 'addition', path: fileProjectPath, contents: 'test', }, })), ); expect(execSnapshots).toMatchObject([ { cmd: './gradlew -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" :wrapper --gradle-distribution-url https://services.gradle.org/distributions/gradle-6.3-bin.zip', options: { cwd: '/tmp/github/some/repo', env: { GRADLE_OPTS: '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.caching=false', }, }, }, ]); }); it('quotes a distributionUrl containing shell metacharacters', async () => { const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['gradle/wrapper/gradle-wrapper.properties'], }), ); await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], // distributionUrl as it would be parsed from an attacker-controlled // gradle-wrapper.properties newPackageFileContent: 'distributionUrl=https\\://example.com/gradle.zip;touch pwned', config: { ...config, newValue: '6.3' }, }); expect(execSnapshots).toMatchObject([ { cmd: './gradlew -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" :wrapper --gradle-distribution-url \'https://example.com/gradle.zip;touch pwned\'', }, ]); }); it('aborts if allowedUnsafeExecutions does not include `toolSettings`', async () => { GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: [ 'gradle/wrapper/gradle-wrapper.properties', 'gradlew', 'gradlew.bat', ], }), ); const res = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: Fixtures.get( 'expectedFiles/gradle/wrapper/gradle-wrapper.properties', ), config: { ...config, newValue: '6.3' }, }); expect(res).toBeNull(); expect(execSnapshots).toBeEmptyArray(); expect(logger.logger.trace).toHaveBeenCalledWith( 'Not allowed to execute gradle due to allowedUnsafeExecutions - aborting update', ); }); it('gradlew not found', async () => { const execSnapshots = mockExecAll(); fs.statLocalFile.mockResolvedValue( partial({ isFile: () => false, mode: 0o555, }), ); const result = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: '', config: {}, }); expect(result).toBeNull(); expect(execSnapshots).toBeEmptyArray(); }); it('gradlew failed', async () => { const execSnapshots = mockExecAll(new Error('failed')); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: [], }), ); const result = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: '', config, }); expect(result).toBeEmptyArray(); expect(execSnapshots).toMatchObject([ { cmd: './gradlew -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" :wrapper --gradle-version 5.6.4', options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('updates distributionSha256Sum (docker)', async () => { const execSnapshots = mockExecAll(); httpMock .scope('https://services.gradle.org') .get('/distributions/gradle-6.3-bin.zip.sha256') .reply( 200, '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768', ); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: ['gradle/wrapper/gradle-wrapper.properties'], }), ); GlobalConfig.set({ ...adminConfig, binarySource: 'docker', dockerSidecarImage: 'ghcr.io/renovatebot/base-image', }); const result = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`, config, }); expect(result).toEqual([ { file: { contents: 'test', path: 'gradle/wrapper/gradle-wrapper.properties', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'docker pull ghcr.io/renovatebot/base-image' }, { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, { cmd: 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' + '-v "/tmp/cache":"/tmp/cache" ' + '-e CI ' + '-e GRADLE_OPTS ' + '-e CONTAINERBASE_CACHE_DIR ' + '-w "/tmp/github/some/repo" ' + 'ghcr.io/renovatebot/base-image' + " bash -l -c '" + 'install-tool java 11.0.1' + ' && ' + './gradlew -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" :wrapper --gradle-distribution-url https://services.gradle.org/distributions/gradle-6.3-bin.zip --gradle-distribution-sha256-sum 038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768' + "'", options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('updates distributionSha256Sum (install)', async () => { const execSnapshots = mockExecAll(); httpMock .scope('https://services.gradle.org') .get('/distributions/gradle-6.3-bin.zip.sha256') .reply( 200, '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768', ); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: ['gradle/wrapper/gradle-wrapper.properties'], }), ); GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); const result = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`, config, }); expect(result).toEqual([ { file: { contents: 'test', path: 'gradle/wrapper/gradle-wrapper.properties', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool java 11.0.1' }, { cmd: './gradlew -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" :wrapper --gradle-distribution-url https://services.gradle.org/distributions/gradle-6.3-bin.zip --gradle-distribution-sha256-sum 038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768', options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('distributionSha256Sum 404', async () => { const execSnapshots = mockExecAll(); httpMock .scope('https://services.gradle.org') .get('/distributions/gradle-6.3-bin.zip.sha256') .reply(404); const result = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`, config, }); expect(result).toEqual([ { artifactError: { fileName: 'gradle/wrapper/gradle-wrapper.properties', stderr: 'Request failed with status code 404 (Not Found): GET https://services.gradle.org/distributions/gradle-6.3-bin.zip.sha256', }, }, ]); expect(execSnapshots).toBeEmptyArray(); }); it('handles gradle-wrapper in subdirectory', async () => { const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: [ 'sub/gradle/wrapper/gradle-wrapper.properties', 'sub/gradlew', 'sub/gradlew.bat', ], }), ); const res = await updateArtifacts({ packageFileName: 'sub/gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: Fixtures.get( 'expectedFiles/gradle/wrapper/gradle-wrapper.properties', ), config: { ...config, newValue: '6.3' }, }); expect(res).toEqual( [ 'sub/gradle/wrapper/gradle-wrapper.properties', 'sub/gradlew', 'sub/gradlew.bat', ].map((fileProjectPath) => ({ file: { type: 'addition', path: fileProjectPath, contents: 'test', }, })), ); expect(execSnapshots).toMatchObject([ { cmd: './gradlew -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" :wrapper --gradle-distribution-url https://services.gradle.org/distributions/gradle-6.3-bin.zip', options: { cwd: '/tmp/github/some/repo/sub', env: { GRADLE_OPTS: '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.caching=false', }, }, }, ]); }); }); describe('updateBuildFile()', () => { it('updates wrapper configuration in gradle build file', async () => { fs.readLocalFile.mockResolvedValueOnce(` tasks.named("wrapper", Wrapper::class) { gradleVersion = '5.6.2' distributionSha256Sum="027fdd265d277bae65a0d349b6b8da02135b0b8e14ba891e26281fa877fe37a2" distributionUrl = "https://services.gradle.org/distributions/gradle-5.6.2-bin.zip" }`); fs.writeLocalFile.mockImplementationOnce( (fileName: string, fileContent: string | Buffer): Promise => { expect(fileContent).toBe(` tasks.named("wrapper", Wrapper::class) { gradleVersion = '6.3' distributionSha256Sum="038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768" distributionUrl = "https://services.gradle.org/distributions/gradle-6.3-bin.zip" }`); return Promise.resolve(); }, ); const res = await updateBuildFile('', { gradleVersion: '6.3', distributionSha256Sum: '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768', distributionUrl: 'https://services.gradle.org/distributions/gradle-6.3-bin.zip', }); expect(res).toBe('build.gradle.kts'); }); it('gradle build file update skips missing distributionSha256Sum property', async () => { fs.localPathExists.mockResolvedValueOnce(true); fs.readLocalFile.mockResolvedValueOnce(` wrapper { gradleVersion = '5.6.2' distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" }`); fs.writeLocalFile.mockImplementationOnce( (fileName: string, fileContent: string | Buffer): Promise => { expect(fileContent).toBe(` wrapper { gradleVersion = '6.3' distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" }`); return Promise.resolve(); }, ); const res = await updateBuildFile('', { gradleVersion: '6.3', distributionSha256Sum: null, distributionUrl: 'https://services.gradle.org/distributions/gradle-6.3-bin.zip', }); expect(res).toBe('build.gradle'); }); it('gradle build file update returns early if file not found', async () => { fs.readLocalFile.mockResolvedValueOnce(null); const res = await updateBuildFile('', { gradleVersion: '6.3', distributionSha256Sum: '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768', distributionUrl: 'https://services.gradle.org/distributions/gradle-6.3-bin.zip', }); expect(logger.logger.debug).toHaveBeenCalledWith( 'build.gradle or build.gradle.kts not found', ); expect(res).toBe('build.gradle.kts'); }); }); describe('updateLockFiles()', () => { it('returns early if build script file not found', async () => { fs.readLocalFile.mockResolvedValueOnce(null); const res = await updateLockFiles('', {}); expect(logger.logger.debug).toHaveBeenCalledWith( 'build.gradle or build.gradle.kts not found', ); expect(res).toBeNull(); }); it('includes gradle lockfile in result', async () => { const execSnapshots = mockExecAll(); const updatedArtifacts: UpdateArtifactsResult[] = [ { file: { type: 'addition', path: 'gradle.lockfile', contents: 'test', }, }, ]; vi.mocked(gradleUpdateArtifacts).mockResolvedValue(updatedArtifacts); git.getRepoStatus.mockResolvedValue( partial({ modified: ['gradle.lockfile'], }), ); const res = await updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: '', config: { ...config, newValue: '8.2' }, }); expect(res).toStrictEqual(updatedArtifacts); expect(execSnapshots).toMatchObject([ { cmd: './gradlew -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" :wrapper --gradle-version 8.2', }, ]); }); }); });