From 7e0f05064581aeb42331f3c4b06109525c87cb58 Mon Sep 17 00:00:00 2001 From: Daz DeBoer Date: Sat, 31 Aug 2024 11:11:24 -0600 Subject: [PATCH] Ensure minimum Gradle version for cache-cleanup (#364) Instead of always installing and using the latest Gradle version for cache cleanup, we now require at least Gradle 8.9. This avoids downloading and installing Gradle if the version on PATH is sufficient to perform cache cleanup. --- sources/src/caching/cache-cleaner.ts | 5 +- .../caching/gradle-home-extry-extractor.ts | 5 +- sources/src/execution/gradle.ts | 41 +++++++ sources/src/execution/provision.ts | 49 +++++---- sources/test/jest/gradle-version.test.ts | 104 ++++++++++++++++++ 5 files changed, 180 insertions(+), 24 deletions(-) create mode 100644 sources/test/jest/gradle-version.test.ts 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') + }) + }) +}) +