diff --git a/.github/workflows/integTest-caching-configuration-cache.yml b/.github/workflows/integTest-caching-configuration-cache.yml index fd4ac6d..e63b6d4 100644 --- a/.github/workflows/integTest-caching-configuration-cache.yml +++ b/.github/workflows/integTest-caching-configuration-cache.yml @@ -7,13 +7,14 @@ on: workflow_dispatch: env: - GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}- GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true jobs: # 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. - seed-build: + seed-build-groovy: + env: + GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy- strategy: matrix: os: [ubuntu-latest, windows-latest] @@ -26,12 +27,11 @@ jobs: - name: Groovy build with configuration-cache enabled working-directory: __tests__/samples/groovy-dsl 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: - needs: seed-build + env: + GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy- + needs: seed-build-groovy strategy: matrix: os: [ubuntu-latest, windows-latest] @@ -49,29 +49,11 @@ jobs: working-directory: __tests__/samples/groovy-dsl 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 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: matrix: os: [ubuntu-latest, windows-latest] @@ -79,12 +61,67 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v2 - - name: Setup Gradle with no cache extracted cache entries restored + - name: Setup Gradle with no extracted cache entries restored uses: ./ 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: cache-read-only: true - name: Check execute Gradle build with configuration cache enabled (but not restored) working-directory: __tests__/samples/groovy-dsl 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 diff --git a/.github/workflows/integTest-caching-gradle-home.yml b/.github/workflows/integTest-caching-gradle-home.yml index f606afd..0e0656e 100644 --- a/.github/workflows/integTest-caching-gradle-home.yml +++ b/.github/workflows/integTest-caching-gradle-home.yml @@ -73,7 +73,7 @@ jobs: - name: Setup Gradle with no extracted cache entries restored uses: ./ 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: cache-read-only: true - name: Check executee Gradle build diff --git a/action.yml b/action.yml index e7e0537..9cd1f7c 100644 --- a/action.yml +++ b/action.yml @@ -64,7 +64,6 @@ runs: using: 'node12' main: 'dist/main/index.js' post: 'dist/post/index.js' - post-if: success() branding: icon: 'box' diff --git a/src/cache-base.ts b/src/cache-base.ts index 47f58dd..64e674d 100644 --- a/src/cache-base.ts +++ b/src/cache-base.ts @@ -1,15 +1,26 @@ 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 path from 'path' import fs from 'fs' 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-' export const META_FILE_DIR = '.gradle-build-action' 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. @@ -62,22 +73,20 @@ function generateCacheKey(cacheName: string): CacheKey { return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs]) } -export abstract class AbstractCache { +export class GradleStateCache { private cacheName: string private cacheDescription: string private cacheKeyStateKey: string private cacheResultStateKey: string protected readonly gradleUserHome: string - protected readonly cacheDebuggingEnabled: boolean - constructor(gradleUserHome: string, cacheName: string, cacheDescription: string) { + constructor(gradleUserHome: string) { this.gradleUserHome = gradleUserHome - this.cacheName = cacheName - this.cacheDescription = cacheDescription - this.cacheKeyStateKey = `CACHE_KEY_${cacheName}` - this.cacheResultStateKey = `CACHE_RESULT_${cacheName}` - this.cacheDebuggingEnabled = isCacheDebuggingEnabled() + this.cacheName = 'gradle' + this.cacheDescription = 'Gradle User Home' + this.cacheKeyStateKey = `CACHE_KEY_gradle` + this.cacheResultStateKey = `CACHE_RESULT_gradle` } init(): void { @@ -96,15 +105,16 @@ export abstract class AbstractCache { async restore(listener: CacheListener): Promise { 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 key:${cacheKey.key} 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) if (!cacheResult) { @@ -124,27 +134,16 @@ export abstract class AbstractCache { } } - prepareCacheKey(): CacheKey { - const cacheKey = generateCacheKey(this.cacheName) - core.saveState(this.cacheKeyStateKey, cacheKey.key) - return cacheKey + /** + * Restore any extracted cache entries after the main Gradle User Home entry is restored. + */ + async afterRestore(listener: CacheListener): Promise { + 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 { - try { - return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys) - } catch (error) { - handleCacheFailure(error, `Failed to restore ${cacheKey}`) - return undefined - } - } - - protected async afterRestore(_listener: CacheListener): Promise {} - /** * 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. @@ -171,7 +170,7 @@ export abstract class AbstractCache { core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKeyFromRestore}`) const cachePath = this.getCachePath() - const savedEntry = await this.saveCache(cachePath, cacheKeyFromRestore) + const savedEntry = await saveCache(cachePath, cacheKeyFromRestore) if (savedEntry) { listener.entry(this.cacheDescription).markSaved(savedEntry.key, savedEntry.size) @@ -180,25 +179,155 @@ export abstract class AbstractCache { return } - protected async beforeSave(_listener: CacheListener): Promise {} - - protected async saveCache(cachePath: string[], cacheKey: string): Promise { - try { - return await cache.saveCache(cachePath, cacheKey) - } catch (error) { - handleCacheFailure(error, `Failed to save cache entry ${cacheKey}`) - } - return undefined + /** + * Extract and save any defined extracted cache entries prior to the main Gradle User Home entry being saved. + */ + async beforeSave(listener: CacheListener): Promise { + 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)" + ) } - protected debug(message: string): void { - if (this.cacheDebuggingEnabled) { - core.info(message) - } else { - core.debug(message) + /** + * 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)) + + 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 { + 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('-----------------------') + } } diff --git a/src/cache-extract-entries.ts b/src/cache-extract-entries.ts new file mode 100644 index 0000000..f35e0b5 --- /dev/null +++ b/src/cache-extract-entries.ts @@ -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 { + const previouslyExtractedCacheEntries = this.loadExtractedCacheEntries() + + const processes: Promise[] = [] + + 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 { + 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 { + // 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[] = [] + + // 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 { + 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 { + 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): Promise { + 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 { + 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') + } +} diff --git a/src/cache-gradle-user-home.ts b/src/cache-gradle-user-home.ts deleted file mode 100644 index b5a63b9..0000000 --- a/src/cache-gradle-user-home.ts +++ /dev/null @@ -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 { - 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 { - const previouslyExtractedCacheEntries = this.loadExtractedCacheEntries() - - const processes: Promise[] = [] - - 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 { - 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 { - 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 { - // 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[] = [] - - // 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 { - 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[]): Promise { - // 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 { - 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}") - } - } -}` - ) - } -} diff --git a/src/cache-project-dot-gradle.ts b/src/cache-project-dot-gradle.ts deleted file mode 100644 index 3bb55fa..0000000 --- a/src/cache-project-dot-gradle.ts +++ /dev/null @@ -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') - } -} diff --git a/src/cache-utils.ts b/src/cache-utils.ts index 8ab749e..7e4d327 100644 --- a/src/cache-utils.ts +++ b/src/cache-utils.ts @@ -46,6 +46,36 @@ export function hashStrings(values: string[]): string { return hash.digest('hex') } +export async function restoreCache( + cachePath: string[], + cacheKey: string, + cacheRestoreKeys: string[] = [] +): Promise { + 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 { + 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 { if (error instanceof cache.ValidationError) { // Fail on cache validation errors diff --git a/src/caches.ts b/src/caches.ts index d78e061..498ba76 100644 --- a/src/caches.ts +++ b/src/caches.ts @@ -1,8 +1,7 @@ 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 {logCachingReport, CacheListener} from './cache-reporting' +import {GradleStateCache} from './cache-base' const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED' const GRADLE_USER_HOME = 'GRADLE_USER_HOME' @@ -13,29 +12,22 @@ export async function restore(gradleUserHome: string): Promise { return } - const gradleUserHomeCache = new GradleUserHomeCache(gradleUserHome) - gradleUserHomeCache.init() - - const projectDotGradleCache = new ProjectDotGradleCache(gradleUserHome) - projectDotGradleCache.init() + const gradleStateCache = new GradleStateCache(gradleUserHome) + gradleStateCache.init() await core.group('Restore Gradle state from cache', async () => { core.saveState(GRADLE_USER_HOME, gradleUserHome) const cacheListener = new CacheListener() - await gradleUserHomeCache.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() - } + await gradleStateCache.restore(cacheListener) 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 { @@ -53,10 +45,7 @@ export async function save(): Promise { await core.group('Caching Gradle state', async () => { const gradleUserHome = core.getState(GRADLE_USER_HOME) - return Promise.all([ - new GradleUserHomeCache(gradleUserHome).save(cacheListener), - new ProjectDotGradleCache(gradleUserHome).save(cacheListener) - ]) + return new GradleStateCache(gradleUserHome).save(cacheListener) }) logCachingReport(cacheListener) @@ -72,11 +61,6 @@ function shouldRestoreCaches(): boolean { core.info('Cache only restored on first action step.') 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 }