2021-10-30 13:15:20 +00:00
|
|
|
import * as core from '@actions/core'
|
2021-12-29 23:07:33 +00:00
|
|
|
import * as exec from '@actions/exec'
|
2021-10-30 13:15:20 +00:00
|
|
|
import * as github from '@actions/github'
|
2021-12-07 23:52:53 +00:00
|
|
|
import path from 'path'
|
|
|
|
import fs from 'fs'
|
2021-12-07 19:29:37 +00:00
|
|
|
import {CacheListener} from './cache-reporting'
|
2021-12-29 23:07:33 +00:00
|
|
|
import {
|
|
|
|
getCacheKeyPrefix,
|
|
|
|
determineJobContext,
|
|
|
|
saveCache,
|
|
|
|
restoreCache,
|
|
|
|
cacheDebug,
|
|
|
|
isCacheDebuggingEnabled,
|
|
|
|
tryDelete
|
|
|
|
} from './cache-utils'
|
|
|
|
import {ConfigurationCacheEntryExtractor, GradleHomeEntryExtractor} from './cache-extract-entries'
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-11-28 17:19:56 +00:00
|
|
|
const CACHE_PROTOCOL_VERSION = 'v5-'
|
2021-12-07 23:52:53 +00:00
|
|
|
|
|
|
|
export const META_FILE_DIR = '.gradle-build-action'
|
|
|
|
export const PROJECT_ROOTS_FILE = 'project-roots.txt'
|
2021-12-29 23:07:33 +00:00
|
|
|
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
|
|
|
|
const EXCLUDE_PATHS_PARAMETER = 'gradle-home-cache-excludes'
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-11-28 17:19:56 +00:00
|
|
|
/**
|
|
|
|
* Represents a key used to restore a cache entry.
|
|
|
|
* The Github Actions cache will first try for an exact match on the key.
|
|
|
|
* If that fails, it will try for a prefix match on any of the restoreKeys.
|
|
|
|
*/
|
|
|
|
class CacheKey {
|
|
|
|
key: string
|
|
|
|
restoreKeys: string[]
|
|
|
|
|
|
|
|
constructor(key: string, restoreKeys: string[]) {
|
|
|
|
this.key = key
|
|
|
|
this.restoreKeys = restoreKeys
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a cache key specific to the current job execution.
|
|
|
|
* The key is constructed from the following inputs:
|
|
|
|
* - A user-defined prefix (optional)
|
|
|
|
* - The cache protocol version
|
|
|
|
* - The name of the cache
|
|
|
|
* - The runner operating system
|
|
|
|
* - The name of the Job being executed
|
|
|
|
* - The matrix values for the Job being executed (job context)
|
|
|
|
* - The SHA of the commit being executed
|
|
|
|
*
|
|
|
|
* Caches are restored by trying to match the these key prefixes in order:
|
|
|
|
* - The full key with SHA
|
|
|
|
* - A previous key for this Job + matrix
|
|
|
|
* - Any previous key for this Job (any matrix)
|
|
|
|
* - Any previous key for this cache on the current OS
|
|
|
|
*/
|
2021-10-30 13:15:20 +00:00
|
|
|
function generateCacheKey(cacheName: string): CacheKey {
|
2021-11-28 17:19:56 +00:00
|
|
|
const cacheKeyBase = `${getCacheKeyPrefix()}${CACHE_PROTOCOL_VERSION}${cacheName}`
|
2021-10-30 13:15:20 +00:00
|
|
|
|
|
|
|
// At the most general level, share caches for all executions on the same OS
|
|
|
|
const runnerOs = process.env['RUNNER_OS'] || ''
|
2021-11-28 17:19:56 +00:00
|
|
|
const cacheKeyForOs = `${cacheKeyBase}|${runnerOs}`
|
2021-10-30 13:15:20 +00:00
|
|
|
|
|
|
|
// Prefer caches that run this job
|
|
|
|
const cacheKeyForJob = `${cacheKeyForOs}|${github.context.job}`
|
|
|
|
|
|
|
|
// Prefer (even more) jobs that run this job with the same context (matrix)
|
|
|
|
const cacheKeyForJobContext = `${cacheKeyForJob}[${determineJobContext()}]`
|
|
|
|
|
|
|
|
// Exact match on Git SHA
|
|
|
|
const cacheKey = `${cacheKeyForJobContext}-${github.context.sha}`
|
|
|
|
|
|
|
|
return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs])
|
|
|
|
}
|
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
export class GradleStateCache {
|
2021-10-30 13:15:20 +00:00
|
|
|
private cacheName: string
|
|
|
|
private cacheDescription: string
|
|
|
|
private cacheKeyStateKey: string
|
|
|
|
private cacheResultStateKey: string
|
|
|
|
|
2021-12-07 23:52:53 +00:00
|
|
|
protected readonly gradleUserHome: string
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
constructor(gradleUserHome: string) {
|
2021-12-07 23:52:53 +00:00
|
|
|
this.gradleUserHome = gradleUserHome
|
2021-12-29 23:07:33 +00:00
|
|
|
this.cacheName = 'gradle'
|
|
|
|
this.cacheDescription = 'Gradle User Home'
|
|
|
|
this.cacheKeyStateKey = `CACHE_KEY_gradle`
|
|
|
|
this.cacheResultStateKey = `CACHE_RESULT_gradle`
|
2021-10-30 13:15:20 +00:00
|
|
|
}
|
|
|
|
|
2021-12-07 23:52:53 +00:00
|
|
|
init(): void {
|
|
|
|
const actionCacheDir = path.resolve(this.gradleUserHome, '.gradle-build-action')
|
|
|
|
fs.mkdirSync(actionCacheDir, {recursive: true})
|
|
|
|
|
|
|
|
const initScriptsDir = path.resolve(this.gradleUserHome, 'init.d')
|
|
|
|
fs.mkdirSync(initScriptsDir, {recursive: true})
|
|
|
|
|
|
|
|
this.initializeGradleUserHome(this.gradleUserHome, initScriptsDir)
|
|
|
|
}
|
|
|
|
|
2022-01-17 19:38:14 +00:00
|
|
|
cacheOutputExists(): boolean {
|
|
|
|
// Need to check for 'caches' directory to avoid incorrect detection on MacOS agents
|
|
|
|
const paths = this.getCachePath()
|
|
|
|
return paths.some(x => fs.existsSync(x))
|
|
|
|
}
|
|
|
|
|
2021-11-28 17:19:56 +00:00
|
|
|
/**
|
|
|
|
* Restores the cache entry, finding the closest match to the currently running job.
|
|
|
|
*/
|
2021-10-30 13:21:27 +00:00
|
|
|
async restore(listener: CacheListener): Promise<void> {
|
2021-11-28 17:19:56 +00:00
|
|
|
const entryListener = listener.entry(this.cacheDescription)
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
const cacheKey = generateCacheKey(this.cacheName)
|
|
|
|
core.saveState(this.cacheKeyStateKey, cacheKey.key)
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
cacheDebug(
|
2021-10-30 13:15:20 +00:00
|
|
|
`Requesting ${this.cacheDescription} with
|
2021-12-07 19:56:36 +00:00
|
|
|
key:${cacheKey.key}
|
|
|
|
restoreKeys:[${cacheKey.restoreKeys}]`
|
2021-10-30 13:15:20 +00:00
|
|
|
)
|
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
const cacheResult = await restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys)
|
2021-11-28 17:19:56 +00:00
|
|
|
entryListener.markRequested(cacheKey.key, cacheKey.restoreKeys)
|
2021-10-30 13:15:20 +00:00
|
|
|
|
|
|
|
if (!cacheResult) {
|
2021-11-27 23:07:07 +00:00
|
|
|
core.info(`${this.cacheDescription} cache not found. Will initialize empty.`)
|
2021-10-30 13:15:20 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-11-01 02:35:28 +00:00
|
|
|
core.saveState(this.cacheResultStateKey, cacheResult.key)
|
2021-11-28 17:19:56 +00:00
|
|
|
entryListener.markRestored(cacheResult.key, cacheResult.size)
|
|
|
|
|
2021-11-01 02:35:28 +00:00
|
|
|
core.info(`Restored ${this.cacheDescription} from cache key: ${cacheResult.key}`)
|
2021-10-30 13:15:20 +00:00
|
|
|
|
|
|
|
try {
|
2021-10-30 13:21:27 +00:00
|
|
|
await this.afterRestore(listener)
|
2021-10-30 13:15:20 +00:00
|
|
|
} catch (error) {
|
|
|
|
core.warning(`Restore ${this.cacheDescription} failed in 'afterRestore': ${error}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
/**
|
|
|
|
* Restore any extracted cache entries after the main Gradle User Home entry is restored.
|
|
|
|
*/
|
|
|
|
async afterRestore(listener: CacheListener): Promise<void> {
|
|
|
|
await this.debugReportGradleUserHomeSize('as restored from cache')
|
|
|
|
await new GradleHomeEntryExtractor(this.gradleUserHome).restore(listener)
|
|
|
|
await new ConfigurationCacheEntryExtractor(this.gradleUserHome).restore(listener)
|
|
|
|
await this.debugReportGradleUserHomeSize('after restoring common artifacts')
|
2021-10-30 13:15:20 +00:00
|
|
|
}
|
|
|
|
|
2021-11-28 17:19:56 +00:00
|
|
|
/**
|
2021-12-07 19:56:36 +00:00
|
|
|
* Saves the cache entry based on the current cache key unless the cache was restored with the exact key,
|
|
|
|
* in which case we cannot overwrite it.
|
2021-11-28 17:19:56 +00:00
|
|
|
*
|
|
|
|
* If the cache entry was restored with a partial match on a restore key, then
|
|
|
|
* it is saved with the exact key.
|
|
|
|
*/
|
2021-10-30 13:21:27 +00:00
|
|
|
async save(listener: CacheListener): Promise<void> {
|
2021-11-28 17:19:56 +00:00
|
|
|
// Retrieve the state set in the previous 'restore' step.
|
|
|
|
const cacheKeyFromRestore = core.getState(this.cacheKeyStateKey)
|
|
|
|
const cacheResultFromRestore = core.getState(this.cacheResultStateKey)
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-11-28 17:19:56 +00:00
|
|
|
if (cacheResultFromRestore && cacheKeyFromRestore === cacheResultFromRestore) {
|
|
|
|
core.info(`Cache hit occurred on the cache key ${cacheKeyFromRestore}, not saving cache.`)
|
2021-10-30 13:15:20 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2021-10-30 13:21:27 +00:00
|
|
|
await this.beforeSave(listener)
|
2021-10-30 13:15:20 +00:00
|
|
|
} catch (error) {
|
|
|
|
core.warning(`Save ${this.cacheDescription} failed in 'beforeSave': ${error}`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-11-28 17:19:56 +00:00
|
|
|
core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKeyFromRestore}`)
|
2021-10-30 13:15:20 +00:00
|
|
|
const cachePath = this.getCachePath()
|
2021-12-29 23:07:33 +00:00
|
|
|
const savedEntry = await saveCache(cachePath, cacheKeyFromRestore)
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-10-30 18:17:41 +00:00
|
|
|
if (savedEntry) {
|
|
|
|
listener.entry(this.cacheDescription).markSaved(savedEntry.key, savedEntry.size)
|
|
|
|
}
|
2021-10-30 13:15:20 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
/**
|
|
|
|
* Extract and save any defined extracted cache entries prior to the main Gradle User Home entry being saved.
|
|
|
|
*/
|
|
|
|
async beforeSave(listener: CacheListener): Promise<void> {
|
|
|
|
await this.debugReportGradleUserHomeSize('before saving common artifacts')
|
|
|
|
this.deleteExcludedPaths()
|
|
|
|
await Promise.all([
|
|
|
|
new GradleHomeEntryExtractor(this.gradleUserHome).extract(listener),
|
|
|
|
new ConfigurationCacheEntryExtractor(this.gradleUserHome).extract(listener)
|
|
|
|
])
|
|
|
|
await this.debugReportGradleUserHomeSize(
|
|
|
|
"after extracting common artifacts (only 'caches' and 'notifications' will be stored)"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete any file paths that are excluded by the `gradle-home-cache-excludes` parameter.
|
|
|
|
*/
|
|
|
|
private deleteExcludedPaths(): void {
|
|
|
|
const rawPaths: string[] = core.getMultilineInput(EXCLUDE_PATHS_PARAMETER)
|
|
|
|
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
for (const p of resolvedPaths) {
|
|
|
|
cacheDebug(`Deleting excluded path: ${p}`)
|
|
|
|
tryDelete(p)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines the paths within Gradle User Home to cache.
|
|
|
|
* By default, this is the 'caches' and 'notifications' directories,
|
|
|
|
* but this can be overridden by the `gradle-home-cache-includes` parameter.
|
|
|
|
*/
|
|
|
|
protected getCachePath(): string[] {
|
|
|
|
const rawPaths: string[] = core.getMultilineInput(INCLUDE_PATHS_PARAMETER)
|
|
|
|
rawPaths.push(META_FILE_DIR)
|
|
|
|
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
|
|
|
|
cacheDebug(`Using cache paths: ${resolvedPaths}`)
|
|
|
|
return resolvedPaths
|
|
|
|
}
|
|
|
|
|
|
|
|
private resolveCachePath(rawPath: string): string {
|
|
|
|
if (rawPath.startsWith('!')) {
|
|
|
|
const resolved = this.resolveCachePath(rawPath.substring(1))
|
|
|
|
return `!${resolved}`
|
|
|
|
}
|
|
|
|
return path.resolve(this.gradleUserHome, rawPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
private initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
|
|
|
|
const propertiesFile = path.resolve(gradleUserHome, 'gradle.properties')
|
|
|
|
fs.writeFileSync(propertiesFile, 'org.gradle.daemon=false')
|
|
|
|
|
|
|
|
const buildScanCapture = path.resolve(initScriptsDir, 'build-scan-capture.init.gradle')
|
|
|
|
fs.writeFileSync(
|
|
|
|
buildScanCapture,
|
|
|
|
`import org.gradle.util.GradleVersion
|
|
|
|
|
2021-12-31 16:35:21 +00:00
|
|
|
// Only run against root build. Do not run against included builds.
|
2021-12-29 23:07:33 +00:00
|
|
|
def isTopLevelBuild = gradle.getParent() == null
|
|
|
|
if (isTopLevelBuild) {
|
|
|
|
def version = GradleVersion.current().baseVersion
|
|
|
|
def atLeastGradle4 = version >= GradleVersion.version("4.0")
|
|
|
|
def atLeastGradle6 = version >= GradleVersion.version("6.0")
|
|
|
|
|
|
|
|
if (atLeastGradle6) {
|
|
|
|
settingsEvaluated { settings ->
|
|
|
|
if (settings.pluginManager.hasPlugin("com.gradle.enterprise")) {
|
|
|
|
registerCallbacks(settings.extensions["gradleEnterprise"].buildScan, settings.rootProject.name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (atLeastGradle4) {
|
|
|
|
projectsEvaluated { gradle ->
|
|
|
|
if (gradle.rootProject.pluginManager.hasPlugin("com.gradle.build-scan")) {
|
|
|
|
registerCallbacks(gradle.rootProject.extensions["buildScan"], gradle.rootProject.name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
def registerCallbacks(buildScanExtension, rootProjectName) {
|
|
|
|
buildScanExtension.with {
|
|
|
|
def buildOutcome = ""
|
|
|
|
def scanFile = new File("gradle-build-scan.txt")
|
|
|
|
|
|
|
|
buildFinished { result ->
|
|
|
|
buildOutcome = result.failure == null ? " succeeded" : " failed"
|
|
|
|
}
|
|
|
|
|
|
|
|
buildScanPublished { buildScan ->
|
|
|
|
scanFile.text = buildScan.buildScanUri
|
|
|
|
|
|
|
|
// Send commands directly to GitHub Actions via STDOUT.
|
2021-12-31 16:35:21 +00:00
|
|
|
def gradleCommand = rootProjectName + " " + gradle.startParameter.taskNames.join(" ")
|
|
|
|
def message = "Gradle build '\${gradleCommand}'\${buildOutcome} - \${buildScan.buildScanUri}"
|
2021-12-29 23:07:33 +00:00
|
|
|
println("::notice ::\${message}")
|
|
|
|
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
|
2021-10-30 13:15:20 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-29 23:07:33 +00:00
|
|
|
}`
|
|
|
|
)
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
const projectRootCapture = path.resolve(initScriptsDir, 'project-root-capture.init.gradle')
|
|
|
|
fs.writeFileSync(
|
|
|
|
projectRootCapture,
|
|
|
|
`
|
2021-12-31 16:35:21 +00:00
|
|
|
// Only run against root build. Do not run against included builds.
|
2021-12-29 23:07:33 +00:00
|
|
|
def isTopLevelBuild = gradle.getParent() == null
|
|
|
|
if (isTopLevelBuild) {
|
|
|
|
settingsEvaluated { settings ->
|
|
|
|
def projectRootEntry = settings.rootDir.absolutePath + "\\n"
|
|
|
|
def projectRootList = new File(settings.gradle.gradleUserHomeDir, "${PROJECT_ROOTS_FILE}")
|
|
|
|
if (!projectRootList.exists() || !projectRootList.text.contains(projectRootEntry)) {
|
|
|
|
projectRootList << projectRootEntry
|
2021-10-30 13:15:20 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-29 23:07:33 +00:00
|
|
|
}`
|
|
|
|
)
|
|
|
|
}
|
2021-10-30 13:15:20 +00:00
|
|
|
|
2021-12-29 23:07:33 +00:00
|
|
|
/**
|
|
|
|
* When cache debugging is enabled, this method will give a detailed report
|
|
|
|
* of the Gradle User Home contents.
|
|
|
|
*/
|
|
|
|
private async debugReportGradleUserHomeSize(label: string): Promise<void> {
|
|
|
|
if (!isCacheDebuggingEnabled()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (!fs.existsSync(this.gradleUserHome)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const result = await exec.getExecOutput('du', ['-h', '-c', '-t', '5M'], {
|
|
|
|
cwd: this.gradleUserHome,
|
|
|
|
silent: true,
|
|
|
|
ignoreReturnCode: true
|
|
|
|
})
|
|
|
|
|
|
|
|
core.info(`Gradle User Home (directories >5M): ${label}`)
|
|
|
|
|
|
|
|
core.info(
|
|
|
|
result.stdout
|
|
|
|
.trimEnd()
|
|
|
|
.replace(/\t/g, ' ')
|
|
|
|
.split('\n')
|
|
|
|
.map(it => {
|
|
|
|
return ` ${it}`
|
|
|
|
})
|
|
|
|
.join('\n')
|
|
|
|
)
|
|
|
|
|
|
|
|
core.info('-----------------------')
|
|
|
|
}
|
2021-10-30 13:15:20 +00:00
|
|
|
}
|