diff --git a/src/cache-gradle-user-home.ts b/src/cache-gradle-user-home.ts index 4dc6f51..90529eb 100644 --- a/src/cache-gradle-user-home.ts +++ b/src/cache-gradle-user-home.ts @@ -5,7 +5,14 @@ import * as core from '@actions/core' import * as glob from '@actions/glob' import * as exec from '@actions/exec' -import {AbstractCache, CachingReport, getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils' +import { + AbstractCache, + CacheEntryReport, + CachingReport, + getCacheKeyPrefix, + hashFileNames, + tryDelete +} from './cache-utils' const META_FILE_DIR = '.gradle-build-action' @@ -23,17 +30,33 @@ export class GradleUserHomeCache extends AbstractCache { async afterRestore(report: CachingReport): Promise { await this.reportGradleUserHomeSize('as restored from cache') - const result = await this.restoreArtifactBundles() + await this.restoreArtifactBundles(report) await this.reportGradleUserHomeSize('after restoring common artifacts') - if (!result) { - report.fullyRestored = false - } } - private async restoreArtifactBundles(): Promise { - const processes: Promise[] = [] + private async restoreArtifactBundles(report: CachingReport): Promise { + const processes: Promise[] = [] + + // This is special logic that allows the tests to simulate a "not restored" state by configuring an empty set of bundles + // This is similar to how the primary implementation should work: + // - Iterate over bundle meta-files as the basis for restoring content. + // - Leave bundle meta-files for successful restore. + // - Remove bundle meta-files that are not restored. + if (this.getArtifactBundles().size === 0) { + const bundleMetaFiles = await this.getBundleMetaFiles() + + for (const bundleMetaFile of bundleMetaFiles) { + const bundle = path.basename(bundleMetaFile, '.cache') + + core.info(`Found bundle metafile for ${bundle} but no such bundle configured`) + report.addEntryReport(bundleMetaFile).markRequested('BUNDLE_NOT_CONFIGURED') + tryDelete(bundleMetaFile) + } + } + for (const [bundle, pattern] of this.getArtifactBundles()) { - const p = this.restoreArtifactBundle(bundle, pattern) + const bundleEntryReport = report.addEntryReport(bundle) + const p = this.restoreArtifactBundle(bundle, pattern, bundleEntryReport) // Run sequentially when debugging enabled if (this.cacheDebuggingEnabled) { await p @@ -41,32 +64,39 @@ export class GradleUserHomeCache extends AbstractCache { processes.push(p) } - const results = await Promise.all(processes) - // Assume that no-bundles means not-fully-restored - return results.length > 0 && results.every(Boolean) + await Promise.all(processes) } - private async restoreArtifactBundle(bundle: string, artifactPath: string): Promise { + private async restoreArtifactBundle(bundle: string, artifactPath: string, report: CacheEntryReport): Promise { const bundleMetaFile = this.getBundleMetaFile(bundle) if (fs.existsSync(bundleMetaFile)) { const cacheKey = fs.readFileSync(bundleMetaFile, 'utf-8').trim() - const restoreKey = await this.restoreCache([artifactPath], cacheKey) - if (restoreKey) { + report.markRequested(cacheKey) + + const restoredKey = await this.restoreCache([artifactPath], cacheKey) + if (restoredKey) { core.info(`Restored ${bundle} with key ${cacheKey} to ${artifactPath}`) + report.markRestored(restoredKey) } else { - this.debug(`Did not restore ${bundle} with key ${cacheKey} to ${artifactPath}`) - return false + core.info(`Did not restore ${bundle} with key ${cacheKey} to ${artifactPath}`) + // TODO Remove the .cache file here? } } else { this.debug(`No metafile found to restore ${bundle}: ${bundleMetaFile}`) } - return true } private getBundleMetaFile(name: string): string { return path.resolve(this.gradleUserHome, META_FILE_DIR, `${name}.cache`) } + private async getBundleMetaFiles(): Promise { + const metaFiles = path.resolve(this.gradleUserHome, META_FILE_DIR, '*.cache') + const globber = await glob.create(metaFiles) + const bundleFiles = await globber.glob() + return bundleFiles + } + async beforeSave(): Promise { await this.reportGradleUserHomeSize('before saving common artifacts') this.removeExcludedPaths() diff --git a/src/cache-utils.ts b/src/cache-utils.ts index dc2d8a4..bc01836 100644 --- a/src/cache-utils.ts +++ b/src/cache-utils.ts @@ -106,10 +106,44 @@ class CacheKey { } export class CachingReport { - fullyRestored: boolean + cacheEntryReports: CacheEntryReport[] = [] - constructor(fullyRestored: boolean) { - this.fullyRestored = fullyRestored + get fullyRestored(): boolean { + return this.cacheEntryReports.every(x => !x.wasRequestedButNotRestored()) + } + + addEntryReport(name: string): CacheEntryReport { + const report = new CacheEntryReport(name) + this.cacheEntryReports.push(report) + return report + } +} + +export class CacheEntryReport { + entryName: string + requestedKey: string | undefined + requestedRestoreKeys: string[] | undefined + restoredKey: string | undefined + restoredSize: number | undefined + + savedKey: string | undefined + savedSize: number | undefined + + constructor(entryName: string) { + this.entryName = entryName + } + + wasRequestedButNotRestored(): boolean { + return this.requestedKey !== undefined && this.restoredKey === undefined + } + + markRequested(key: string, restoreKeys: string[] = []): void { + this.requestedKey = key + this.requestedRestoreKeys = restoreKeys + } + + markRestored(key: string): void { + this.restoredKey = key } } @@ -129,13 +163,15 @@ export abstract class AbstractCache { this.cacheDebuggingEnabled = isCacheDebuggingEnabled() } - async restore(): Promise { + async restore(report: CachingReport): Promise { if (this.cacheOutputExists()) { core.info(`${this.cacheDescription} already exists. Not restoring from cache.`) - return new CachingReport(false) + return } const cacheKey = this.prepareCacheKey() + const entryReport = report.addEntryReport(this.cacheName) + entryReport.markRequested(cacheKey.key, cacheKey.restoreKeys) this.debug( `Requesting ${this.cacheDescription} with @@ -147,21 +183,18 @@ export abstract class AbstractCache { if (!cacheResult) { core.info(`${this.cacheDescription} cache not found. Will start with empty.`) - return new CachingReport(false) + return } core.saveState(this.cacheResultStateKey, cacheResult) - + entryReport.markRestored(cacheResult) core.info(`Restored ${this.cacheDescription} from cache key: ${cacheResult}`) - const report = new CachingReport(true) try { await this.afterRestore(report) } catch (error) { core.warning(`Restore ${this.cacheDescription} failed in 'afterRestore': ${error}`) } - - return report } prepareCacheKey(): CacheKey { diff --git a/src/caches.ts b/src/caches.ts index fe43c0f..bcebc24 100644 --- a/src/caches.ts +++ b/src/caches.ts @@ -1,7 +1,7 @@ import {GradleUserHomeCache} from './cache-gradle-user-home' import {ProjectDotGradleCache} from './cache-project-dot-gradle' import * as core from '@actions/core' -import {isCacheDisabled, isCacheReadOnly} from './cache-utils' +import {CachingReport, isCacheDisabled, isCacheReadOnly} from './cache-utils' const BUILD_ROOT_DIR = 'BUILD_ROOT_DIR' @@ -13,12 +13,15 @@ export async function restore(buildRootDirectory: string): Promise { await core.group('Restore Gradle state from cache', async () => { core.saveState(BUILD_ROOT_DIR, buildRootDirectory) - const gradleHomeRestore = await new GradleUserHomeCache(buildRootDirectory).restore() + + const cachingReport = new CachingReport() + + await new GradleUserHomeCache(buildRootDirectory).restore(cachingReport) const projectDotGradleCache = new ProjectDotGradleCache(buildRootDirectory) - if (gradleHomeRestore.fullyRestored) { + if (cachingReport.fullyRestored) { // Only restore the configuration-cache if the Gradle Home is fully restored - await projectDotGradleCache.restore() + await projectDotGradleCache.restore(cachingReport) } else { // Otherwise, prepare the cache key for later save() projectDotGradleCache.prepareCacheKey()