mirror of
https://github.com/gradle/actions.git
synced 2026-05-07 02:22:05 +08:00
Add Gradle provisioning for cache cleanup (self-contained)
Restore full Gradle provisioning in the legacy module with all functions copied locally. No cross-package imports from sources/src/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,10 @@ import * as exec from '@actions/exec'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import {BuildResults} from './build-results-adapter'
|
||||
import {findGradleExecutableForCleanup} from './gradle-utils'
|
||||
import {versionIsAtLeast, provisionGradleWithVersionAtLeast} from './gradle-utils'
|
||||
|
||||
const MINIMUM_CLEANUP_GRADLE_VERSION = '8.11'
|
||||
const DEFAULT_CLEANUP_GRADLE_VERSION = '9.4.1'
|
||||
|
||||
export class CacheCleaner {
|
||||
private readonly gradleUserHome: string
|
||||
@@ -23,15 +26,32 @@ export class CacheCleaner {
|
||||
}
|
||||
|
||||
async forceCleanup(buildResults: BuildResults): Promise<void> {
|
||||
const executable = findGradleExecutableForCleanup(buildResults)
|
||||
if (!executable) {
|
||||
core.warning('Cache cleanup skipped: no suitable Gradle >= 8.11 found in build results.')
|
||||
return
|
||||
}
|
||||
const executable = await this.gradleExecutableForCleanup(buildResults)
|
||||
const cleanTimestamp = core.getState('clean-timestamp')
|
||||
await this.forceCleanupFilesOlderThan(cleanTimestamp, executable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to use the newest Gradle version that was used to run a build, at least 8.11.
|
||||
*
|
||||
* This will avoid the need to provision a Gradle version for the cleanup when not necessary.
|
||||
*/
|
||||
private async gradleExecutableForCleanup(buildResults: BuildResults): Promise<string> {
|
||||
const preferredVersion = buildResults.highestGradleVersion()
|
||||
if (preferredVersion && versionIsAtLeast(preferredVersion, MINIMUM_CLEANUP_GRADLE_VERSION)) {
|
||||
try {
|
||||
return await provisionGradleWithVersionAtLeast(preferredVersion)
|
||||
} catch (e) {
|
||||
core.info(
|
||||
`Failed to provision Gradle ${preferredVersion} for cache cleanup. Falling back to default version.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the default version for cache-cleanup
|
||||
return await provisionGradleWithVersionAtLeast(DEFAULT_CLEANUP_GRADLE_VERSION)
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
async forceCleanupFilesOlderThan(cleanTimestamp: string, executable: string): Promise<void> {
|
||||
// Run a dummy Gradle build to trigger cache cleanup
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as semver from 'semver'
|
||||
import {BuildResults} from './build-results-adapter'
|
||||
import * as httpm from '@actions/http-client'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
|
||||
import which from 'which'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
const gradleVersionsBaseUrl = 'https://services.gradle.org/versions'
|
||||
|
||||
class GradleVersion {
|
||||
static PATTERN = /((\d+)(\.\d+)+)(-([a-z]+)-(\w+))?(-(SNAPSHOT|\d{14}([-+]\d{4})?))?/
|
||||
@@ -63,33 +68,96 @@ export function versionIsAtLeast(actualVersion: string, requiredVersion: string)
|
||||
return true
|
||||
}
|
||||
|
||||
function wrapperScriptFilename(): string {
|
||||
return IS_WINDOWS ? 'gradlew.bat' : 'gradlew'
|
||||
function installScriptFilename(): string {
|
||||
return IS_WINDOWS ? 'gradle.bat' : 'gradle'
|
||||
}
|
||||
|
||||
async function findGradleExecutableOnPath(): Promise<string | null> {
|
||||
return await which('gradle', {nothrow: true})
|
||||
}
|
||||
|
||||
async function determineGradleVersion(gradleExecutable: string): Promise<string | undefined> {
|
||||
const {exec} = await import('@actions/exec')
|
||||
const output = await (await import('@actions/exec')).getExecOutput(gradleExecutable, ['-v'], {silent: true})
|
||||
const regex = /Gradle (\d+\.\d+(\.\d+)?(-.*)?)/
|
||||
return output.stdout.match(regex)?.[1]
|
||||
}
|
||||
|
||||
interface GradleVersionInfo {
|
||||
version: string
|
||||
downloadUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find a Gradle wrapper script from build results that has Gradle >= 8.11.
|
||||
* Returns the full path to the wrapper script, or null if none found.
|
||||
* Find (or install) a Gradle executable that meets the specified version requirement.
|
||||
* Checks Gradle on PATH and any candidates first, then downloads if needed.
|
||||
*/
|
||||
export function findGradleExecutableForCleanup(buildResults: BuildResults): string | null {
|
||||
const preferredVersion = buildResults.highestGradleVersion()
|
||||
if (!preferredVersion || !versionIsAtLeast(preferredVersion, '8.11')) {
|
||||
core.info(
|
||||
`No Gradle version >= 8.11 found in build results (highest: ${preferredVersion ?? 'none'}). Cache cleanup will be skipped.`
|
||||
)
|
||||
return null
|
||||
}
|
||||
export async function provisionGradleWithVersionAtLeast(
|
||||
minimumVersion: string,
|
||||
candidates: string[] = []
|
||||
): Promise<string> {
|
||||
const gradleOnPath = await findGradleExecutableOnPath()
|
||||
const allCandidates = gradleOnPath ? [gradleOnPath, ...candidates] : candidates
|
||||
|
||||
// Find a build result with the highest version that has a wrapper script
|
||||
for (const result of buildResults.results) {
|
||||
if (versionIsAtLeast(result.gradleVersion, '8.11')) {
|
||||
const wrapperScript = path.resolve(result.rootProjectDir, wrapperScriptFilename())
|
||||
if (fs.existsSync(wrapperScript)) {
|
||||
return wrapperScript
|
||||
return core.group(`Provision Gradle >= ${minimumVersion}`, async () => {
|
||||
for (const candidate of allCandidates) {
|
||||
const candidateVersion = await determineGradleVersion(candidate)
|
||||
if (candidateVersion && versionIsAtLeast(candidateVersion, minimumVersion)) {
|
||||
core.info(
|
||||
`Gradle version ${candidateVersion} is available at ${candidate} and >= ${minimumVersion}. Not installing.`
|
||||
)
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return locateGradleAndDownloadIfRequired(await gradleRelease(minimumVersion))
|
||||
})
|
||||
}
|
||||
|
||||
async function gradleRelease(version: string): Promise<GradleVersionInfo> {
|
||||
const allVersions: GradleVersionInfo[] = JSON.parse(
|
||||
await httpGetString(`${gradleVersionsBaseUrl}/all`)
|
||||
)
|
||||
const versionInfo = allVersions.find(entry => entry.version === version)
|
||||
if (!versionInfo) {
|
||||
throw new Error(`Gradle version ${version} does not exist`)
|
||||
}
|
||||
return versionInfo
|
||||
}
|
||||
|
||||
async function locateGradleAndDownloadIfRequired(versionInfo: GradleVersionInfo): Promise<string> {
|
||||
const installsDir = path.join(getProvisionDir(), 'installs')
|
||||
const installDir = path.join(installsDir, `gradle-${versionInfo.version}`)
|
||||
if (fs.existsSync(installDir)) {
|
||||
core.info(`Gradle installation already exists at ${installDir}`)
|
||||
return executableFrom(installDir)
|
||||
}
|
||||
|
||||
core.info('Could not locate a Gradle >= 8.11 executable for cache cleanup.')
|
||||
return null
|
||||
const downloadPath = path.join(getProvisionDir(), `downloads/gradle-${versionInfo.version}-bin.zip`)
|
||||
await toolCache.downloadTool(versionInfo.downloadUrl, downloadPath)
|
||||
core.info(`Downloaded ${versionInfo.downloadUrl} to ${downloadPath} (size ${fs.statSync(downloadPath).size})`)
|
||||
|
||||
await toolCache.extractZip(downloadPath, installsDir)
|
||||
core.info(`Extracted Gradle ${versionInfo.version} to ${installDir}`)
|
||||
|
||||
const executable = executableFrom(installDir)
|
||||
fs.chmodSync(executable, '755')
|
||||
core.info(`Provisioned Gradle executable ${executable}`)
|
||||
|
||||
return executable
|
||||
}
|
||||
|
||||
function getProvisionDir(): string {
|
||||
const tmpDir = process.env['RUNNER_TEMP'] ?? os.tmpdir()
|
||||
return path.join(tmpDir, '.gradle-actions/gradle-installations')
|
||||
}
|
||||
|
||||
function executableFrom(installDir: string): string {
|
||||
return path.join(installDir, 'bin', installScriptFilename())
|
||||
}
|
||||
|
||||
async function httpGetString(url: string): Promise<string> {
|
||||
const httpClient = new httpm.HttpClient('gradle/actions')
|
||||
const response = await httpClient.get(url)
|
||||
return response.readBody()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user