diff --git a/sources/src/caching/cache-cleaner.ts b/sources/src/caching/cache-cleaner.ts index feb8708..e23d5ab 100644 --- a/sources/src/caching/cache-cleaner.ts +++ b/sources/src/caching/cache-cleaner.ts @@ -55,11 +55,12 @@ export class CacheCleaner { ) fs.writeFileSync(path.resolve(cleanupProjectDir, 'build.gradle'), 'task("noop") {}') - const executable = await provisioner.provisionGradle('current') + // Gradle >= 8.9 required for cache cleanup + const executable = await provisioner.provisionGradleAtLeast('8.9') await core.group('Executing Gradle to clean up caches', async () => { core.info(`Cleaning up caches last used before ${cleanTimestamp}`) - await this.executeCleanupBuild(executable!, cleanupProjectDir) + await this.executeCleanupBuild(executable, cleanupProjectDir) }) } diff --git a/sources/src/caching/gradle-home-extry-extractor.ts b/sources/src/caching/gradle-home-extry-extractor.ts index 35a1f48..9a761b2 100644 --- a/sources/src/caching/gradle-home-extry-extractor.ts +++ b/sources/src/caching/gradle-home-extry-extractor.ts @@ -2,7 +2,6 @@ import path from 'path' import fs from 'fs' import * as core from '@actions/core' import * as glob from '@actions/glob' -import * as semver from 'semver' import {CacheEntryListener, CacheListener} from './cache-reporting' import {cacheDebug, hashFileNames, isCacheDebuggingEnabled, restoreCache, saveCache, tryDelete} from './cache-utils' @@ -10,6 +9,7 @@ import {cacheDebug, hashFileNames, isCacheDebuggingEnabled, restoreCache, saveCa import {BuildResult, loadBuildResults} from '../build-results' import {CacheConfig, ACTION_METADATA_DIR} from '../configuration' import {getCacheKeyBase} from './cache-key' +import {versionIsAtLeast} from '../execution/gradle' const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE' const CACHE_PROTOCOL_VERSION = 'v1' @@ -434,8 +434,7 @@ export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor { // If any associated build result used Gradle < 8.6, then mark it as not cacheable if ( pathResults.find(result => { - const gradleVersion = semver.coerce(result.gradleVersion) - return gradleVersion && semver.lt(gradleVersion, '8.6.0') + return !versionIsAtLeast(result.gradleVersion, '8.6.0') }) ) { core.info( diff --git a/sources/src/execution/gradle.ts b/sources/src/execution/gradle.ts index 35f09e4..f8237c6 100644 --- a/sources/src/execution/gradle.ts +++ b/sources/src/execution/gradle.ts @@ -1,6 +1,8 @@ import * as core from '@actions/core' import * as exec from '@actions/exec' +import which from 'which' +import * as semver from 'semver' import * as provisioner from './provision' import * as gradlew from './gradlew' @@ -31,3 +33,42 @@ async function executeGradleBuild(executable: string | undefined, root: string, core.setFailed(`Gradle build failed: see console output for details`) } } + +export function versionIsAtLeast(actualVersion: string, requiredVersion: string): boolean { + const splitVersion = actualVersion.split('-') + const coreVersion = splitVersion[0] + const prerelease = splitVersion.length > 1 + + const actualSemver = semver.coerce(coreVersion)! + const comparisonSemver = semver.coerce(requiredVersion)! + + if (prerelease) { + return semver.gt(actualSemver, comparisonSemver) + } else { + return semver.gte(actualSemver, comparisonSemver) + } +} + +export async function findGradleVersionOnPath(): Promise { + const gradleExecutable = await which('gradle', {nothrow: true}) + if (gradleExecutable) { + const output = await exec.getExecOutput(gradleExecutable, ['-v'], {silent: true}) + const version = parseGradleVersionFromOutput(output.stdout) + return version ? new GradleExecutable(version, gradleExecutable) : undefined + } + + return undefined +} + +export function parseGradleVersionFromOutput(output: string): string | undefined { + const regex = /Gradle (\d+\.\d+(\.\d+)?(-.*)?)/ + const versionString = output.match(regex)?.[1] + return versionString +} + +class GradleExecutable { + constructor( + readonly version: string, + readonly executable: string + ) {} +} diff --git a/sources/src/execution/provision.ts b/sources/src/execution/provision.ts index 2a6d346..8bc5711 100644 --- a/sources/src/execution/provision.ts +++ b/sources/src/execution/provision.ts @@ -1,13 +1,12 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' -import which from 'which' import * as httpm from '@actions/http-client' import * as core from '@actions/core' import * as cache from '@actions/cache' -import * as exec from '@actions/exec' import * as toolCache from '@actions/tool-cache' +import {findGradleVersionOnPath, versionIsAtLeast} from './gradle' import * as gradlew from './gradlew' import {handleCacheFailure} from '../caching/cache-utils' import {CacheConfig} from '../configuration' @@ -26,6 +25,16 @@ export async function provisionGradle(gradleVersion: string): Promise { + const installedVersion = await installGradleVersionAtLeast(await gradleRelease(gradleVersion)) + return addToPath(installedVersion) +} + async function addToPath(executable: string): Promise { core.addPath(path.dirname(executable)) return executable @@ -51,7 +60,7 @@ async function resolveGradleVersion(version: string): Promise case 'release-nightly': return gradleReleaseNightly() default: - return gradle(version) + return gradleRelease(version) } } @@ -76,7 +85,7 @@ async function gradleReleaseNightly(): Promise { return await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/release-nightly`) } -async function gradle(version: string): Promise { +async function gradleRelease(version: string): Promise { const versionInfo = await findGradleVersionDeclaration(version) if (!versionInfo) { throw new Error(`Gradle version ${version} does not exists`) @@ -97,10 +106,24 @@ async function findGradleVersionDeclaration(version: string): Promise { return core.group(`Provision Gradle ${versionInfo.version}`, async () => { - const preInstalledGradle = await findGradleVersionOnPath(versionInfo) - if (preInstalledGradle !== undefined) { + const gradleOnPath = await findGradleVersionOnPath() + if (gradleOnPath?.version === versionInfo.version) { core.info(`Gradle version ${versionInfo.version} is already available on PATH. Not installing.`) - return preInstalledGradle + return gradleOnPath.executable + } + + return locateGradleAndDownloadIfRequired(versionInfo) + }) +} + +async function installGradleVersionAtLeast(versionInfo: GradleVersionInfo): Promise { + return core.group(`Provision Gradle >= ${versionInfo.version}`, async () => { + const gradleOnPath = await findGradleVersionOnPath() + if (gradleOnPath && versionIsAtLeast(gradleOnPath.version, versionInfo.version)) { + core.info( + `Gradle version ${gradleOnPath.version} is available on PATH and >= ${versionInfo.version}. Not installing.` + ) + return gradleOnPath.executable } return locateGradleAndDownloadIfRequired(versionInfo) @@ -192,15 +215,3 @@ interface GradleVersionInfo { version: string downloadUrl: string } - -async function findGradleVersionOnPath(versionInfo: GradleVersionInfo): Promise { - const gradleExecutable = await which('gradle', {nothrow: true}) - if (gradleExecutable) { - const output = await exec.getExecOutput(gradleExecutable, ['-v'], {silent: true}) - if (output.stdout.includes(`\nGradle ${versionInfo.version}\n`)) { - return gradleExecutable - } - } - - return undefined -} diff --git a/sources/test/jest/gradle-version.test.ts b/sources/test/jest/gradle-version.test.ts new file mode 100644 index 0000000..81b761d --- /dev/null +++ b/sources/test/jest/gradle-version.test.ts @@ -0,0 +1,104 @@ +import { describe } from 'node:test' +import { versionIsAtLeast, parseGradleVersionFromOutput } from '../../src/execution/gradle' + +describe('gradle', () => { + describe('can compare version with', () => { + it('same version', async () => { + expect(versionIsAtLeast('6.7.1', '6.7.1')).toBe(true) + expect(versionIsAtLeast('7.0', '7.0')).toBe(true) + expect(versionIsAtLeast('7.0', '7.0.0')).toBe(true) + }) + it('newer version', async () => { + expect(versionIsAtLeast('6.7.1', '6.7.2')).toBe(false) + expect(versionIsAtLeast('7.0', '8.0')).toBe(false) + expect(versionIsAtLeast('7.0', '7.0.1')).toBe(false) + }) + it('older version', async () => { + expect(versionIsAtLeast('6.7.2', '6.7.1')).toBe(true) + expect(versionIsAtLeast('8.0', '7.0')).toBe(true) + expect(versionIsAtLeast('7.0.1', '7.0')).toBe(true) + }) + it('rc version', async () => { + expect(versionIsAtLeast('8.0.2-rc-1', '8.0.1')).toBe(true) + expect(versionIsAtLeast('8.0.2-rc-1', '8.0.2')).toBe(false) + expect(versionIsAtLeast('8.1-rc-1', '8.0')).toBe(true) + expect(versionIsAtLeast('8.0-rc-1', '8.0')).toBe(false) + }) + it('snapshot version', async () => { + expect(versionIsAtLeast('8.11-20240829002031+0000', '8.10')).toBe(true) + expect(versionIsAtLeast('8.11-20240829002031+0000', '8.10.1')).toBe(true) + expect(versionIsAtLeast('8.11-20240829002031+0000', '8.11')).toBe(false) + + expect(versionIsAtLeast('8.10.2-20240828012138+0000', '8.10')).toBe(true) + expect(versionIsAtLeast('8.10.2-20240828012138+0000', '8.10.1')).toBe(true) + expect(versionIsAtLeast('8.10.2-20240828012138+0000', '8.10.2')).toBe(false) + expect(versionIsAtLeast('8.10.2-20240828012138+0000', '8.11')).toBe(false) + + expect(versionIsAtLeast('9.1-branch-provider_api_migration_public_api_changes-20240826121451+0000', '9.0')).toBe(true) + expect(versionIsAtLeast('9.1-branch-provider_api_migration_public_api_changes-20240826121451+0000', '9.0.1')).toBe(true) + expect(versionIsAtLeast('9.1-branch-provider_api_migration_public_api_changes-20240826121451+0000', '9.1')).toBe(false) + }) + }) + describe('can parse version from output', () => { + it('major version', async () => { + const output = ` + ------------------------------------------------------------ + Gradle 8.9 + ------------------------------------------------------------ + ` + const version = await parseGradleVersionFromOutput(output)! + expect(version).toBe('8.9') + }) + + it('patch version', async () => { + const output = ` + ------------------------------------------------------------ + Gradle 8.9.1 + ------------------------------------------------------------ + ` + const version = await parseGradleVersionFromOutput(output)! + expect(version).toBe('8.9.1') + }) + + it('rc version', async () => { + const output = ` + ------------------------------------------------------------ + Gradle 8.9-rc-1 + ------------------------------------------------------------ + ` + const version = await parseGradleVersionFromOutput(output)! + expect(version).toBe('8.9-rc-1') + }) + + it('milestone version', async () => { + const output = ` + ------------------------------------------------------------ + Gradle 8.0-milestone-6 + ------------------------------------------------------------ + ` + const version = await parseGradleVersionFromOutput(output)! + expect(version).toBe('8.0-milestone-6') + }) + + it('snapshot version', async () => { + const output = ` + ------------------------------------------------------------ + Gradle 8.10.2-20240828012138+0000 + ------------------------------------------------------------ + ` + const version = await parseGradleVersionFromOutput(output)! + expect(version).toBe('8.10.2-20240828012138+0000') + }) + + it('branch version', async () => { + const output = ` + ------------------------------------------------------------ + Gradle 9.0-branch-provider_api_migration_public_api_changes-20240830060514+0000 + ------------------------------------------------------------ + ` + const version = await parseGradleVersionFromOutput(output)! + expect(version).toBe('9.0-branch-provider_api_migration_public_api_changes-20240830060514+0000') + }) + }) +}) +