Treat configuration-cache as an extracted entry

Instead of using a fallback strategy to locate a configuration-cache entry
based on the current job and git SHA, these entries are now keyed based on their
file content with the keys persisted in the primary Gradle User Home entry.

This removes the chance of having a configuration-cache entry restored that is
incompatible with the restored Gradle User Home state, and makes the logic easier
to understand.

This change involved a fairly major refactor, with the CacheEntryExtractor being
split out from the primary cache implementation, and adding a separate extractor
implementation for configuration-cache.
This commit is contained in:
Daz DeBoer 2021-12-29 16:07:33 -07:00
parent 12fc52a49a
commit 76ea8a76b2
No known key found for this signature in database
GPG key ID: DD6B9F0B06683D5D
9 changed files with 661 additions and 586 deletions

View file

@ -7,13 +7,14 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
jobs: jobs:
# Run initial Gradle builds to push initial cache entries # Run initial Gradle builds to push initial cache entries
# These builds should start fresh without cache hits, due to the seed injected into the cache key above. # These builds should start fresh without cache hits, due to the seed injected into the cache key above.
seed-build: seed-build-groovy:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy-
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
@ -26,12 +27,11 @@ jobs:
- name: Groovy build with configuration-cache enabled - name: Groovy build with configuration-cache enabled
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test --configuration-cache run: ./gradlew test --configuration-cache
- name: Kotlin build with configuration-cache enabled
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache
configuration-cache-groovy: configuration-cache-groovy:
needs: seed-build env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy-
needs: seed-build-groovy
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
@ -49,29 +49,11 @@ jobs:
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test --configuration-cache run: ./gradlew test --configuration-cache
# Test restore configuration-cache from the second build invocation
configuration-cache-kotlin:
needs: seed-build
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
with:
cache-read-only: true
- name: Execute Gradle build and verify cached configuration
env:
VERIFY_CACHED_CONFIGURATION: true
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache
# Check that the build can run when no extracted cache entries are restored # Check that the build can run when no extracted cache entries are restored
no-extracted-cache-entries-restored: no-extracted-cache-entries-restored:
needs: seed-build env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy-
needs: seed-build-groovy
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
@ -79,12 +61,67 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Gradle with no cache extracted cache entries restored - name: Setup Gradle with no extracted cache entries restored
uses: ./ uses: ./
env: env:
GRADLE_BUILD_ACTION_NO_EXTRACTED_ENTRIES: true GRADLE_BUILD_ACTION_SKIP_RESTORE: "generated-gradle-jars|wrapper-zips|java-toolchains|instrumented-jars|dependencies|kotlin-dsl"
with: with:
cache-read-only: true cache-read-only: true
- name: Check execute Gradle build with configuration cache enabled (but not restored) - name: Check execute Gradle build with configuration cache enabled (but not restored)
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test --configuration-cache run: ./gradlew test --configuration-cache
seed-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-kotlin-
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
- name: Kotlin build with configuration-cache enabled
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew help --configuration-cache
modify-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-kotlin-
needs: seed-build-kotlin
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
- name: Kotlin build with configuration-cache enabled
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache
# Test restore configuration-cache from the third build invocation
configuration-cache-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-kotlin-
needs: modify-build-kotlin
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
with:
cache-read-only: true
- name: Execute Gradle build and verify cached configuration
env:
VERIFY_CACHED_CONFIGURATION: true
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache

View file

@ -73,7 +73,7 @@ jobs:
- name: Setup Gradle with no extracted cache entries restored - name: Setup Gradle with no extracted cache entries restored
uses: ./ uses: ./
env: env:
GRADLE_BUILD_ACTION_NO_EXTRACTED_ENTRIES: true GRADLE_BUILD_ACTION_SKIP_RESTORE: "generated-gradle-jars|wrapper-zips|java-toolchains|instrumented-jars|dependencies|kotlin-dsl"
with: with:
cache-read-only: true cache-read-only: true
- name: Check executee Gradle build - name: Check executee Gradle build

View file

@ -64,7 +64,6 @@ runs:
using: 'node12' using: 'node12'
main: 'dist/main/index.js' main: 'dist/main/index.js'
post: 'dist/post/index.js' post: 'dist/post/index.js'
post-if: success()
branding: branding:
icon: 'box' icon: 'box'

View file

@ -1,15 +1,26 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as cache from '@actions/cache' import * as exec from '@actions/exec'
import * as github from '@actions/github' import * as github from '@actions/github'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import {CacheListener} from './cache-reporting' import {CacheListener} from './cache-reporting'
import {isCacheDebuggingEnabled, getCacheKeyPrefix, determineJobContext, handleCacheFailure} from './cache-utils' import {
getCacheKeyPrefix,
determineJobContext,
saveCache,
restoreCache,
cacheDebug,
isCacheDebuggingEnabled,
tryDelete
} from './cache-utils'
import {ConfigurationCacheEntryExtractor, GradleHomeEntryExtractor} from './cache-extract-entries'
const CACHE_PROTOCOL_VERSION = 'v5-' const CACHE_PROTOCOL_VERSION = 'v5-'
export const META_FILE_DIR = '.gradle-build-action' export const META_FILE_DIR = '.gradle-build-action'
export const PROJECT_ROOTS_FILE = 'project-roots.txt' export const PROJECT_ROOTS_FILE = 'project-roots.txt'
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
const EXCLUDE_PATHS_PARAMETER = 'gradle-home-cache-excludes'
/** /**
* Represents a key used to restore a cache entry. * Represents a key used to restore a cache entry.
@ -62,22 +73,20 @@ function generateCacheKey(cacheName: string): CacheKey {
return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs]) return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs])
} }
export abstract class AbstractCache { export class GradleStateCache {
private cacheName: string private cacheName: string
private cacheDescription: string private cacheDescription: string
private cacheKeyStateKey: string private cacheKeyStateKey: string
private cacheResultStateKey: string private cacheResultStateKey: string
protected readonly gradleUserHome: string protected readonly gradleUserHome: string
protected readonly cacheDebuggingEnabled: boolean
constructor(gradleUserHome: string, cacheName: string, cacheDescription: string) { constructor(gradleUserHome: string) {
this.gradleUserHome = gradleUserHome this.gradleUserHome = gradleUserHome
this.cacheName = cacheName this.cacheName = 'gradle'
this.cacheDescription = cacheDescription this.cacheDescription = 'Gradle User Home'
this.cacheKeyStateKey = `CACHE_KEY_${cacheName}` this.cacheKeyStateKey = `CACHE_KEY_gradle`
this.cacheResultStateKey = `CACHE_RESULT_${cacheName}` this.cacheResultStateKey = `CACHE_RESULT_gradle`
this.cacheDebuggingEnabled = isCacheDebuggingEnabled()
} }
init(): void { init(): void {
@ -96,15 +105,16 @@ export abstract class AbstractCache {
async restore(listener: CacheListener): Promise<void> { async restore(listener: CacheListener): Promise<void> {
const entryListener = listener.entry(this.cacheDescription) const entryListener = listener.entry(this.cacheDescription)
const cacheKey = this.prepareCacheKey() const cacheKey = generateCacheKey(this.cacheName)
core.saveState(this.cacheKeyStateKey, cacheKey.key)
this.debug( cacheDebug(
`Requesting ${this.cacheDescription} with `Requesting ${this.cacheDescription} with
key:${cacheKey.key} key:${cacheKey.key}
restoreKeys:[${cacheKey.restoreKeys}]` restoreKeys:[${cacheKey.restoreKeys}]`
) )
const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys) const cacheResult = await restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys)
entryListener.markRequested(cacheKey.key, cacheKey.restoreKeys) entryListener.markRequested(cacheKey.key, cacheKey.restoreKeys)
if (!cacheResult) { if (!cacheResult) {
@ -124,27 +134,16 @@ export abstract class AbstractCache {
} }
} }
prepareCacheKey(): CacheKey { /**
const cacheKey = generateCacheKey(this.cacheName) * Restore any extracted cache entries after the main Gradle User Home entry is restored.
core.saveState(this.cacheKeyStateKey, cacheKey.key) */
return cacheKey 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')
} }
protected async restoreCache(
cachePath: string[],
cacheKey: string,
cacheRestoreKeys: string[] = []
): Promise<cache.CacheEntry | undefined> {
try {
return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys)
} catch (error) {
handleCacheFailure(error, `Failed to restore ${cacheKey}`)
return undefined
}
}
protected async afterRestore(_listener: CacheListener): Promise<void> {}
/** /**
* Saves the cache entry based on the current cache key unless the cache was restored with the exact key, * 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. * in which case we cannot overwrite it.
@ -171,7 +170,7 @@ export abstract class AbstractCache {
core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKeyFromRestore}`) core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKeyFromRestore}`)
const cachePath = this.getCachePath() const cachePath = this.getCachePath()
const savedEntry = await this.saveCache(cachePath, cacheKeyFromRestore) const savedEntry = await saveCache(cachePath, cacheKeyFromRestore)
if (savedEntry) { if (savedEntry) {
listener.entry(this.cacheDescription).markSaved(savedEntry.key, savedEntry.size) listener.entry(this.cacheDescription).markSaved(savedEntry.key, savedEntry.size)
@ -180,25 +179,155 @@ export abstract class AbstractCache {
return return
} }
protected async beforeSave(_listener: CacheListener): Promise<void> {} /**
* Extract and save any defined extracted cache entries prior to the main Gradle User Home entry being saved.
protected async saveCache(cachePath: string[], cacheKey: string): Promise<cache.CacheEntry | undefined> { */
try { async beforeSave(listener: CacheListener): Promise<void> {
return await cache.saveCache(cachePath, cacheKey) await this.debugReportGradleUserHomeSize('before saving common artifacts')
} catch (error) { this.deleteExcludedPaths()
handleCacheFailure(error, `Failed to save cache entry ${cacheKey}`) await Promise.all([
} new GradleHomeEntryExtractor(this.gradleUserHome).extract(listener),
return undefined new ConfigurationCacheEntryExtractor(this.gradleUserHome).extract(listener)
])
await this.debugReportGradleUserHomeSize(
"after extracting common artifacts (only 'caches' and 'notifications' will be stored)"
)
} }
protected debug(message: string): void { /**
if (this.cacheDebuggingEnabled) { * Delete any file paths that are excluded by the `gradle-home-cache-excludes` parameter.
core.info(message) */
} else { private deleteExcludedPaths(): void {
core.debug(message) const rawPaths: string[] = core.getMultilineInput(EXCLUDE_PATHS_PARAMETER)
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
for (const p of resolvedPaths) {
cacheDebug(`Deleting excluded path: ${p}`)
tryDelete(p)
} }
} }
protected abstract getCachePath(): string[] /**
protected abstract initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void * 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
// Only run again root build. Do not run against included builds.
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.
def message = "Build '\${rootProjectName}'\${buildOutcome} - \${buildScan.buildScanUri}"
println("::notice ::\${message}")
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
}
}
}`
)
const projectRootCapture = path.resolve(initScriptsDir, 'project-root-capture.init.gradle')
fs.writeFileSync(
projectRootCapture,
`
// Only run again root build. Do not run against included builds.
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
}
}
}`
)
}
/**
* 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('-----------------------')
}
} }

View file

@ -0,0 +1,376 @@
import path from 'path'
import fs from 'fs'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import {META_FILE_DIR, PROJECT_ROOTS_FILE} from './cache-base'
import {CacheEntryListener, CacheListener} from './cache-reporting'
import {
cacheDebug,
getCacheKeyPrefix,
hashFileNames,
isCacheDebuggingEnabled,
restoreCache,
saveCache,
tryDelete
} from './cache-utils'
const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE'
/**
* Represents the result of attempting to load or store an extracted cache entry.
* An undefined cacheKey indicates that the operation did not succeed.
* The collected results are then used to populate the `cache-metadata.json` file for later use.
*/
class ExtractedCacheEntry {
artifactType: string
pattern: string
cacheKey: string | undefined
constructor(artifactType: string, pattern: string, cacheKey: string | undefined) {
this.artifactType = artifactType
this.pattern = pattern
this.cacheKey = cacheKey
}
}
/**
* Representation of all of the extracted cache entries for this Gradle User Home.
* This object is persisted to JSON file in the Gradle User Home directory for storing,
* and subsequently used to restore the Gradle User Home.
*/
class ExtractedCacheEntryMetadata {
entries: ExtractedCacheEntry[] = []
}
/**
* The specification for a type of extracted cache entry.
*/
class ExtractedCacheEntryDefinition {
artifactType: string
pattern: string
bundle: boolean
uniqueFileNames = true
constructor(artifactType: string, pattern: string, bundle: boolean) {
this.artifactType = artifactType
this.pattern = pattern
this.bundle = bundle
}
withNonUniqueFileNames(): ExtractedCacheEntryDefinition {
this.uniqueFileNames = false
return this
}
}
/**
* Caches and restores the entire Gradle User Home directory, extracting entries containing common artifacts
* for more efficient storage.
*/
abstract class AbstractEntryExtractor {
protected readonly gradleUserHome: string
private extractorName: string
constructor(gradleUserHome: string, extractorName: string) {
this.gradleUserHome = gradleUserHome
this.extractorName = extractorName
}
/**
* Restores any artifacts that were cached separately, based on the information in the `cache-metadata.json` file.
* Each extracted cache entry is restored in parallel, except when debugging is enabled.
*/
async restore(listener: CacheListener): Promise<void> {
const previouslyExtractedCacheEntries = this.loadExtractedCacheEntries()
const processes: Promise<ExtractedCacheEntry>[] = []
for (const cacheEntry of previouslyExtractedCacheEntries) {
const artifactType = cacheEntry.artifactType
const entryListener = listener.entry(cacheEntry.pattern)
// Handle case where the extracted-cache-entry definitions have been changed
const skipRestore = process.env[SKIP_RESTORE_VAR] || ''
if (skipRestore.includes(artifactType)) {
core.info(`Not restoring extracted cache entry for ${artifactType}`)
entryListener.markRequested('SKIP_RESTORE')
} else {
processes.push(
this.awaitForDebugging(
this.restoreExtractedCacheEntry(
artifactType,
cacheEntry.cacheKey!,
cacheEntry.pattern,
entryListener
)
)
)
}
}
this.saveMetadataForCacheResults(await Promise.all(processes))
}
private async restoreExtractedCacheEntry(
artifactType: string,
cacheKey: string,
pattern: string,
listener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
listener.markRequested(cacheKey)
const restoredEntry = await restoreCache([pattern], cacheKey)
if (restoredEntry) {
core.info(`Restored ${artifactType} with key ${cacheKey} to ${pattern}`)
listener.markRestored(restoredEntry.key, restoredEntry.size)
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
} else {
core.info(`Did not restore ${artifactType} with key ${cacheKey} to ${pattern}`)
return new ExtractedCacheEntry(artifactType, pattern, undefined)
}
}
/**
* Saves any artifacts that are configured to be cached separately, based on the extracted cache entry definitions.
* Each entry is extracted and saved in parallel, except when debugging is enabled.
*/
async extract(listener: CacheListener): Promise<void> {
// Load the cache entry definitions (from config) and the previously restored entries (from persisted metadata file)
const cacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
cacheDebug(
`Extracting cache entries for ${this.extractorName}: ${JSON.stringify(cacheEntryDefinitions, null, 2)}`
)
const previouslyRestoredEntries = this.loadExtractedCacheEntries()
const cacheActions: Promise<ExtractedCacheEntry>[] = []
// For each cache entry definition, determine if it has already been restored, and if not, extract it
for (const cacheEntryDefinition of cacheEntryDefinitions) {
const artifactType = cacheEntryDefinition.artifactType
const pattern = cacheEntryDefinition.pattern
// Find all matching files for this cache entry definition
const globber = await glob.create(pattern, {
implicitDescendants: false,
followSymbolicLinks: false
})
const matchingFiles = await globber.glob()
if (matchingFiles.length === 0) {
cacheDebug(`No files found to cache for ${artifactType}`)
continue
}
if (cacheEntryDefinition.bundle) {
// For an extracted "bundle", use the defined pattern and cache all matching files in a single entry.
cacheActions.push(
this.awaitForDebugging(
this.saveExtractedCacheEntry(
matchingFiles,
artifactType,
pattern,
cacheEntryDefinition.uniqueFileNames,
previouslyRestoredEntries,
listener.entry(pattern)
)
)
)
} else {
// Otherwise cache each matching file in a separate entry, using the complete file path as the cache pattern.
for (const cacheFile of matchingFiles) {
cacheActions.push(
this.awaitForDebugging(
this.saveExtractedCacheEntry(
[cacheFile],
artifactType,
cacheFile,
cacheEntryDefinition.uniqueFileNames,
previouslyRestoredEntries,
listener.entry(cacheFile)
)
)
)
}
}
}
this.saveMetadataForCacheResults(await Promise.all(cacheActions))
}
private async saveExtractedCacheEntry(
matchingFiles: string[],
artifactType: string,
pattern: string,
uniqueFileNames: boolean,
previouslyRestoredEntries: ExtractedCacheEntry[],
entryListener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
const cacheKey = uniqueFileNames
? this.createCacheKeyFromFileNames(artifactType, matchingFiles)
: await this.createCacheKeyFromFileContents(artifactType, pattern)
const previouslyRestoredKey = previouslyRestoredEntries.find(
x => x.artifactType === artifactType && x.pattern === pattern
)?.cacheKey
if (previouslyRestoredKey === cacheKey) {
cacheDebug(`No change to previously restored ${artifactType}. Not saving.`)
} else {
core.info(`Caching ${artifactType} with path '${pattern}' and cache key: ${cacheKey}`)
const savedEntry = await saveCache([pattern], cacheKey)
if (savedEntry !== undefined) {
entryListener.markSaved(savedEntry.key, savedEntry.size)
}
}
for (const file of matchingFiles) {
tryDelete(file)
}
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
}
protected createCacheKeyFromFileNames(artifactType: string, files: string[]): string {
const cacheKeyPrefix = getCacheKeyPrefix()
const relativeFiles = files.map(x => path.relative(this.gradleUserHome, x))
const key = hashFileNames(relativeFiles)
cacheDebug(`Generating cache key for ${artifactType} from file names: ${relativeFiles}`)
return `${cacheKeyPrefix}${artifactType}-${key}`
}
protected async createCacheKeyFromFileContents(artifactType: string, pattern: string): Promise<string> {
const cacheKeyPrefix = getCacheKeyPrefix()
const key = await glob.hashFiles(pattern)
cacheDebug(`Generating cache key for ${artifactType} from files matching: ${pattern}`)
return `${cacheKeyPrefix}${artifactType}-${key}`
}
// Run actions sequentially if debugging is enabled
private async awaitForDebugging(p: Promise<ExtractedCacheEntry>): Promise<ExtractedCacheEntry> {
if (isCacheDebuggingEnabled()) {
await p
}
return p
}
/**
* Load information about the extracted cache entries previously restored/saved. This is loaded from the 'cache-metadata.json' file.
*/
protected loadExtractedCacheEntries(): ExtractedCacheEntry[] {
const cacheMetadataFile = this.getCacheMetadataFile()
if (!fs.existsSync(cacheMetadataFile)) {
return []
}
const filedata = fs.readFileSync(cacheMetadataFile, 'utf-8')
cacheDebug(`Loaded cache metadata: ${filedata}`)
const extractedCacheEntryMetadata = JSON.parse(filedata) as ExtractedCacheEntryMetadata
return extractedCacheEntryMetadata.entries
}
/**
* Saves information about the extracted cache entries into the 'cache-metadata.json' file.
*/
private saveMetadataForCacheResults(results: ExtractedCacheEntry[]): void {
const extractedCacheEntryMetadata = new ExtractedCacheEntryMetadata()
extractedCacheEntryMetadata.entries = results.filter(x => x.cacheKey !== undefined)
const filedata = JSON.stringify(extractedCacheEntryMetadata)
cacheDebug(`Saving cache metadata: ${filedata}`)
fs.writeFileSync(this.getCacheMetadataFile(), filedata, 'utf-8')
}
private getCacheMetadataFile(): string {
const actionMetadataDirectory = path.resolve(this.gradleUserHome, META_FILE_DIR)
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
return path.resolve(actionMetadataDirectory, `${this.extractorName}-entry-metadata.json`)
}
protected abstract getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[]
}
export class GradleHomeEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'gradle-home')
}
/**
* Return the extracted cache entry definitions, which determine which artifacts will be cached
* separately from the rest of the Gradle User Home cache entry.
*/
protected getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[] {
const entryDefinition = (
artifactType: string,
patterns: string[],
bundle: boolean
): ExtractedCacheEntryDefinition => {
const resolvedPattern = patterns.map(x => path.resolve(this.gradleUserHome, x)).join('\n')
return new ExtractedCacheEntryDefinition(artifactType, resolvedPattern, bundle)
}
return [
entryDefinition('generated-gradle-jars', ['caches/*/generated-gradle-jars/*.jar'], false),
entryDefinition('wrapper-zips', ['wrapper/dists/*/*/*.zip'], false),
entryDefinition('java-toolchains', ['jdks/*.zip', 'jdks/*.tar.gz'], false),
entryDefinition('dependencies', ['caches/modules-*/files-*/*/*/*/*'], true),
entryDefinition('instrumented-jars', ['caches/jars-*/*'], true),
entryDefinition('kotlin-dsl', ['caches/*/kotlin-dsl/*/*'], true)
]
}
}
export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'configuration-cache')
}
/**
* Handle the case where Gradle User Home has not been fully restored, so that the configuration-cache
* entry is not reusable.
*/
async restore(listener: CacheListener): Promise<void> {
if (listener.fullyRestored) {
return super.restore(listener)
}
core.info('Not restoring configuration-cache state, as Gradle User Home was not fully restored')
for (const cacheEntry of this.loadExtractedCacheEntries()) {
listener.entry(cacheEntry.pattern).markRequested('NOT_RESTORED')
}
}
/**
* Extract cache entries for the configuration cache in each project.
*/
protected getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[] {
return this.getProjectRoots().map(projectRoot => {
const configCachePath = path.resolve(projectRoot, '.gradle/configuration-cache')
return new ExtractedCacheEntryDefinition(
'configuration-cache',
configCachePath,
true
).withNonUniqueFileNames()
})
}
/**
* For every Gradle invocation, we record the project root directory. This method returns the entire
* set of project roots, to allow saving of configuration-cache entries for each.
*/
private getProjectRoots(): string[] {
const projectList = path.resolve(this.gradleUserHome, PROJECT_ROOTS_FILE)
if (!fs.existsSync(projectList)) {
core.info(`Missing project list file ${projectList}`)
return []
}
const projectRoots = fs.readFileSync(projectList, 'utf-8')
core.info(`Found project roots '${projectRoots}' in ${projectList}`)
return projectRoots.trim().split('\n')
}
}

View file

@ -1,428 +0,0 @@
import path from 'path'
import fs from 'fs'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import * as exec from '@actions/exec'
import {AbstractCache, META_FILE_DIR} from './cache-base'
import {CacheEntryListener, CacheListener} from './cache-reporting'
import {getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils'
const META_FILE = 'cache-metadata.json'
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
const EXCLUDE_PATHS_PARAMETER = 'gradle-home-cache-excludes'
const NO_EXTRACTED_ENTRIES_VAR = 'GRADLE_BUILD_ACTION_NO_EXTRACTED_ENTRIES'
/**
* Represents the result of attempting to load or store an extracted cache entry.
* An undefined cacheKey indicates that the operation did not succeed.
* The collected results are then used to populate the `cache-metadata.json` file for later use.
*/
class ExtractedCacheEntry {
artifactType: string
pattern: string
cacheKey: string | undefined
constructor(artifactType: string, pattern: string, cacheKey: string | undefined) {
this.artifactType = artifactType
this.pattern = pattern
this.cacheKey = cacheKey
}
}
/**
* Representation of all of the extracted cache entries for this Gradle User Home.
* This object is persisted to JSON file in the Gradle User Home directory for storing,
* and subsequently used to restore the Gradle User Home.
*/
class ExtractedCacheEntryMetadata {
entries: ExtractedCacheEntry[] = []
}
/**
* The specification for a type of extracted cache entry.
*/
class ExtractedCacheEntryDefinition {
artifactType: string
pattern: string
bundle: boolean
constructor(artifactType: string, pattern: string, bundle: boolean) {
this.artifactType = artifactType
this.pattern = pattern
this.bundle = bundle
}
}
/**
* Caches and restores the entire Gradle User Home directory, extracting entries containing common artifacts
* for more efficient storage.
*/
export class GradleUserHomeCache extends AbstractCache {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'gradle', 'Gradle User Home')
}
/**
* 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 this.restoreExtractedCacheEntries(listener)
await this.debugReportGradleUserHomeSize('after restoring common artifacts')
}
/**
* Restores any artifacts that were cached separately, based on the information in the `cache-metadata.json` file.
* Each extracted cache entry is restored in parallel, except when debugging is enabled.
*/
private async restoreExtractedCacheEntries(listener: CacheListener): Promise<void> {
const previouslyExtractedCacheEntries = this.loadExtractedCacheEntries()
const processes: Promise<ExtractedCacheEntry>[] = []
for (const cacheEntry of previouslyExtractedCacheEntries) {
const artifactType = cacheEntry.artifactType
const entryListener = listener.entry(cacheEntry.pattern)
// Handle case where the extracted-cache-entry definitions have been changed
if (process.env[NO_EXTRACTED_ENTRIES_VAR] === 'true') {
core.info(`Not restoring any extracted cache entries for ${artifactType}`)
entryListener.markRequested('EXTRACTED_ENTRY_NOT_DEFINED')
} else {
processes.push(
this.restoreExtractedCacheEntry(
artifactType,
cacheEntry.cacheKey!,
cacheEntry.pattern,
entryListener
)
)
}
}
this.saveMetadataForCacheResults(await this.collectCacheResults(processes))
}
private async restoreExtractedCacheEntry(
artifactType: string,
cacheKey: string,
pattern: string,
listener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
listener.markRequested(cacheKey)
const restoredEntry = await this.restoreCache([pattern], cacheKey)
if (restoredEntry) {
core.info(`Restored ${artifactType} with key ${cacheKey} to ${pattern}`)
listener.markRestored(restoredEntry.key, restoredEntry.size)
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
} else {
core.info(`Did not restore ${artifactType} with key ${cacheKey} to ${pattern}`)
return new ExtractedCacheEntry(artifactType, pattern, undefined)
}
}
/**
* 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.removeExcludedPaths()
await this.saveExtractedCacheEntries(listener)
await this.debugReportGradleUserHomeSize(
"after saving common artifacts (only 'caches' and 'notifications' will be stored)"
)
}
/**
* Delete any file paths that are excluded by the `gradle-home-cache-excludes` parameter.
*/
private removeExcludedPaths(): void {
const rawPaths: string[] = core.getMultilineInput(EXCLUDE_PATHS_PARAMETER)
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
for (const p of resolvedPaths) {
this.debug(`Deleting excluded path: ${p}`)
tryDelete(p)
}
}
/**
* Saves any artifacts that are configured to be cached separately, based on the extracted cache entry definitions.
* Each entry is extracted and saved in parallel, except when debugging is enabled.
*/
private async saveExtractedCacheEntries(listener: CacheListener): Promise<void> {
// Load the cache entry definitions (from config) and the previously restored entries (from filesystem)
const cacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
const previouslyRestoredEntries = this.loadExtractedCacheEntries()
const cacheActions: Promise<ExtractedCacheEntry>[] = []
// For each cache entry definition, determine if it has already been restored, and if not, extract it
for (const cacheEntryDefinition of cacheEntryDefinitions) {
const artifactType = cacheEntryDefinition.artifactType
const pattern = cacheEntryDefinition.pattern
// Find all matching files for this cache entry definition
const globber = await glob.create(pattern, {
implicitDescendants: false,
followSymbolicLinks: false
})
const matchingFiles = await globber.glob()
if (matchingFiles.length === 0) {
this.debug(`No files found to cache for ${artifactType}`)
continue
}
if (cacheEntryDefinition.bundle) {
// For an extracted "bundle", use the defined pattern and cache all matching files in a single entry.
cacheActions.push(
this.saveExtractedCacheEntry(
matchingFiles,
artifactType,
pattern,
previouslyRestoredEntries,
listener.entry(pattern)
)
)
} else {
// Otherwise cache each matching file in a separate entry, using the complete file path as the cache pattern.
for (const cacheFile of matchingFiles) {
cacheActions.push(
this.saveExtractedCacheEntry(
[cacheFile],
artifactType,
cacheFile,
previouslyRestoredEntries,
listener.entry(cacheFile)
)
)
}
}
}
this.saveMetadataForCacheResults(await this.collectCacheResults(cacheActions))
}
private async saveExtractedCacheEntry(
matchingFiles: string[],
artifactType: string,
pattern: string,
previouslyRestoredEntries: ExtractedCacheEntry[],
entryListener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
const cacheKey = this.createCacheKeyForArtifacts(artifactType, matchingFiles)
const previouslyRestoredKey = previouslyRestoredEntries.find(
x => x.artifactType === artifactType && x.pattern === pattern
)?.cacheKey
if (previouslyRestoredKey === cacheKey) {
this.debug(`No change to previously restored ${artifactType}. Not saving.`)
} else {
core.info(`Caching ${artifactType} with path '${pattern}' and cache key: ${cacheKey}`)
const savedEntry = await this.saveCache([pattern], cacheKey)
if (savedEntry !== undefined) {
entryListener.markSaved(savedEntry.key, savedEntry.size)
}
}
for (const file of matchingFiles) {
tryDelete(file)
}
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
}
protected createCacheKeyForArtifacts(artifactType: string, files: string[]): string {
const cacheKeyPrefix = getCacheKeyPrefix()
const relativeFiles = files.map(x => path.relative(this.gradleUserHome, x))
const key = hashFileNames(relativeFiles)
this.debug(`Generating cache key for ${artifactType} from files: ${relativeFiles}`)
return `${cacheKeyPrefix}${artifactType}-${key}`
}
private isBundlePattern(pattern: string): boolean {
// If pattern ends with `/*`, then we cache as a "bundle": all of the matching files in a single cache entry
return pattern.endsWith(`${path.sep}*`)
}
private async collectCacheResults(processes: Promise<ExtractedCacheEntry>[]): Promise<ExtractedCacheEntry[]> {
// Run cache actions sequentially when debugging enabled
if (this.cacheDebuggingEnabled) {
for (const p of processes) {
await p
}
}
return await Promise.all(processes)
}
/**
* Load information about the extracted cache entries previously restored/saved. This is loaded from the 'cache-metadata.json' file.
*/
private loadExtractedCacheEntries(): ExtractedCacheEntry[] {
const cacheMetadataFile = path.resolve(this.gradleUserHome, META_FILE_DIR, META_FILE)
if (!fs.existsSync(cacheMetadataFile)) {
return []
}
const filedata = fs.readFileSync(cacheMetadataFile, 'utf-8')
core.debug(`Loaded cache metadata: ${filedata}`)
const extractedCacheEntryMetadata = JSON.parse(filedata) as ExtractedCacheEntryMetadata
return extractedCacheEntryMetadata.entries
}
/**
* Saves information about the extracted cache entries into the 'cache-metadata.json' file.
*/
private saveMetadataForCacheResults(results: ExtractedCacheEntry[]): void {
const extractedCacheEntryMetadata = new ExtractedCacheEntryMetadata()
extractedCacheEntryMetadata.entries = results.filter(x => x.cacheKey !== undefined)
const filedata = JSON.stringify(extractedCacheEntryMetadata)
core.debug(`Saving cache metadata: ${filedata}`)
const actionMetadataDirectory = path.resolve(this.gradleUserHome, META_FILE_DIR)
const cacheMetadataFile = path.resolve(actionMetadataDirectory, META_FILE)
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
fs.writeFileSync(cacheMetadataFile, filedata, 'utf-8')
}
/**
* 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))
this.debug(`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)
}
/**
* Return the extracted cache entry definitions, which determine which artifacts will be cached
* separately from the rest of the Gradle User Home cache entry.
*/
private getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[] {
const entryDefinition = (
artifactType: string,
patterns: string[],
bundle: boolean
): ExtractedCacheEntryDefinition => {
const resolvedPattern = patterns.map(x => this.resolveCachePath(x)).join('\n')
return new ExtractedCacheEntryDefinition(artifactType, resolvedPattern, bundle)
}
const definitions = [
entryDefinition('generated-gradle-jars', ['caches/*/generated-gradle-jars/*.jar'], false),
entryDefinition('wrapper-zips', ['wrapper/dists/*/*/*.zip'], false),
entryDefinition('java-toolchains', ['jdks/*.zip', 'jdks/*.tar.gz'], false),
entryDefinition('dependencies', ['caches/modules-*/files-*/*/*/*/*'], true),
entryDefinition('instrumented-jars', ['caches/jars-*/*'], true),
entryDefinition('kotlin-dsl', ['caches/*/kotlin-dsl/*/*'], true)
]
this.debug(`Using extracted cache entry definitions: ${JSON.stringify(definitions, null, 2)}`)
return definitions
}
/**
* 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 (!this.cacheDebuggingEnabled) {
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('-----------------------')
}
protected 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
// Only run again root build. Do not run against included builds.
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.
def message = "Build '\${rootProjectName}'\${buildOutcome} - \${buildScan.buildScanUri}"
println("::notice ::\${message}")
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
}
}
}`
)
}
}

View file

@ -1,52 +0,0 @@
import * as core from '@actions/core'
import path from 'path'
import fs from 'fs'
import {AbstractCache, META_FILE_DIR, PROJECT_ROOTS_FILE} from './cache-base'
/**
* A simple cache that saves and restores the '.gradle/configuration-cache' directory in the project root.
*/
export class ProjectDotGradleCache extends AbstractCache {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'project', 'Project configuration cache')
}
protected getCachePath(): string[] {
return this.getProjectRoots().map(x => path.resolve(x, '.gradle/configuration-cache'))
}
protected initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
const projectRootCapture = path.resolve(initScriptsDir, 'project-root-capture.init.gradle')
fs.writeFileSync(
projectRootCapture,
`
// Only run again root build. Do not run against included builds.
def isTopLevelBuild = gradle.getParent() == null
if (isTopLevelBuild) {
settingsEvaluated { settings ->
def projectRootEntry = settings.rootDir.absolutePath + "\\n"
def projectRootList = new File(settings.gradle.gradleUserHomeDir, "${META_FILE_DIR}/${PROJECT_ROOTS_FILE}")
println "Adding " + projectRootEntry + " to " + projectRootList
if (!projectRootList.exists() || !projectRootList.text.contains(projectRootEntry)) {
projectRootList << projectRootEntry
}
}
}`
)
}
/**
* For every Gradle invocation, we record the project root directory. This method returns the entire
* set of project roots, to allow saving of configuration-cache entries for each.
*/
private getProjectRoots(): string[] {
const projectList = path.resolve(this.gradleUserHome, META_FILE_DIR, PROJECT_ROOTS_FILE)
if (!fs.existsSync(projectList)) {
core.info(`Missing project list file ${projectList}`)
return []
}
const projectRoots = fs.readFileSync(projectList, 'utf-8')
core.info(`Found project roots '${projectRoots}' in ${projectList}`)
return projectRoots.trim().split('\n')
}
}

View file

@ -46,6 +46,36 @@ export function hashStrings(values: string[]): string {
return hash.digest('hex') return hash.digest('hex')
} }
export async function restoreCache(
cachePath: string[],
cacheKey: string,
cacheRestoreKeys: string[] = []
): Promise<cache.CacheEntry | undefined> {
try {
return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys)
} catch (error) {
handleCacheFailure(error, `Failed to restore ${cacheKey}`)
return undefined
}
}
export async function saveCache(cachePath: string[], cacheKey: string): Promise<cache.CacheEntry | undefined> {
try {
return await cache.saveCache(cachePath, cacheKey)
} catch (error) {
handleCacheFailure(error, `Failed to save cache entry ${cacheKey}`)
}
return undefined
}
export function cacheDebug(message: string): void {
if (isCacheDebuggingEnabled()) {
core.info(message)
} else {
core.debug(message)
}
}
export function handleCacheFailure(error: unknown, message: string): void { export function handleCacheFailure(error: unknown, message: string): void {
if (error instanceof cache.ValidationError) { if (error instanceof cache.ValidationError) {
// Fail on cache validation errors // Fail on cache validation errors

View file

@ -1,8 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {GradleUserHomeCache} from './cache-gradle-user-home'
import {ProjectDotGradleCache} from './cache-project-dot-gradle'
import {isCacheDisabled, isCacheReadOnly} from './cache-utils' import {isCacheDisabled, isCacheReadOnly} from './cache-utils'
import {logCachingReport, CacheListener} from './cache-reporting' import {logCachingReport, CacheListener} from './cache-reporting'
import {GradleStateCache} from './cache-base'
const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED' const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED'
const GRADLE_USER_HOME = 'GRADLE_USER_HOME' const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
@ -13,29 +12,22 @@ export async function restore(gradleUserHome: string): Promise<void> {
return return
} }
const gradleUserHomeCache = new GradleUserHomeCache(gradleUserHome) const gradleStateCache = new GradleStateCache(gradleUserHome)
gradleUserHomeCache.init() gradleStateCache.init()
const projectDotGradleCache = new ProjectDotGradleCache(gradleUserHome)
projectDotGradleCache.init()
await core.group('Restore Gradle state from cache', async () => { await core.group('Restore Gradle state from cache', async () => {
core.saveState(GRADLE_USER_HOME, gradleUserHome) core.saveState(GRADLE_USER_HOME, gradleUserHome)
const cacheListener = new CacheListener() const cacheListener = new CacheListener()
await gradleUserHomeCache.restore(cacheListener) await gradleStateCache.restore(cacheListener)
if (cacheListener.fullyRestored) {
// Only restore the configuration-cache if the Gradle Home is fully restored
await projectDotGradleCache.restore(cacheListener)
} else {
// Otherwise, prepare the cache key for later save()
core.info('Gradle Home cache not fully restored: not restoring configuration-cache state')
projectDotGradleCache.prepareCacheKey()
}
core.saveState(CACHE_LISTENER, cacheListener.stringify()) core.saveState(CACHE_LISTENER, cacheListener.stringify())
}) })
// Export var that is detected in all later restore steps
core.exportVariable(CACHE_RESTORED_VAR, true)
// Export state that is detected in corresponding post-action step
core.saveState(CACHE_RESTORED_VAR, true)
} }
export async function save(): Promise<void> { export async function save(): Promise<void> {
@ -53,10 +45,7 @@ export async function save(): Promise<void> {
await core.group('Caching Gradle state', async () => { await core.group('Caching Gradle state', async () => {
const gradleUserHome = core.getState(GRADLE_USER_HOME) const gradleUserHome = core.getState(GRADLE_USER_HOME)
return Promise.all([ return new GradleStateCache(gradleUserHome).save(cacheListener)
new GradleUserHomeCache(gradleUserHome).save(cacheListener),
new ProjectDotGradleCache(gradleUserHome).save(cacheListener)
])
}) })
logCachingReport(cacheListener) logCachingReport(cacheListener)
@ -72,11 +61,6 @@ function shouldRestoreCaches(): boolean {
core.info('Cache only restored on first action step.') core.info('Cache only restored on first action step.')
return false return false
} }
// Export var that is detected in all later restore steps
core.exportVariable(CACHE_RESTORED_VAR, true)
// Export state that is detected in corresponding post-action step
core.saveState(CACHE_RESTORED_VAR, true)
return true return true
} }