mirror of
				https://github.com/gradle/gradle-build-action.git
				synced 2025-10-31 07:08:56 +08:00 
			
		
		
		
	Refactor: extract cache-base out of cache-utils
This commit is contained in:
		
							
								
								
									
										95
									
								
								__tests__/cache-base.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								__tests__/cache-base.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| import {CacheEntryReport, CachingReport} from '../src/cache-base' | ||||
|  | ||||
| describe('caching report', () => { | ||||
|     describe('reports not fully restored', () => { | ||||
|         it('with one requested entry report', async () => { | ||||
|             const report = new CachingReport() | ||||
|             report.entryReport('foo').markRequested('1', ['2']) | ||||
|             report.entryReport('bar').markRequested('3').markRestored('4') | ||||
|             expect(report.fullyRestored).toBe(false) | ||||
|         }) | ||||
|     }) | ||||
|     describe('reports fully restored', () => { | ||||
|         it('when empty', async () => { | ||||
|             const report = new CachingReport() | ||||
|             expect(report.fullyRestored).toBe(true) | ||||
|         }) | ||||
|         it('with empty entry reports', async () => { | ||||
|             const report = new CachingReport() | ||||
|             report.entryReport('foo') | ||||
|             report.entryReport('bar') | ||||
|             expect(report.fullyRestored).toBe(true) | ||||
|         }) | ||||
|         it('with restored entry report', async () => { | ||||
|             const report = new CachingReport() | ||||
|             report.entryReport('bar').markRequested('3').markRestored('4') | ||||
|             expect(report.fullyRestored).toBe(true) | ||||
|         }) | ||||
|         it('with multiple restored entry reportss', async () => { | ||||
|             const report = new CachingReport() | ||||
|             report.entryReport('foo').markRestored('4') | ||||
|             report.entryReport('bar').markRequested('3').markRestored('4') | ||||
|             expect(report.fullyRestored).toBe(true) | ||||
|         }) | ||||
|     }) | ||||
|     describe('can be stringified and rehydrated', () => { | ||||
|         it('when empty', async () => { | ||||
|             const report = new CachingReport() | ||||
|  | ||||
|             const stringRep = report.stringify() | ||||
|             const reportClone: CachingReport = CachingReport.rehydrate(stringRep) | ||||
|  | ||||
|             expect(reportClone.cacheEntryReports).toEqual([]) | ||||
|  | ||||
|             // Can call methods on rehydrated | ||||
|             expect(reportClone.entryReport('foo')).toBeInstanceOf(CacheEntryReport) | ||||
|         }) | ||||
|         it('with entry reports', async () => { | ||||
|             const report = new CachingReport() | ||||
|             report.entryReport('foo') | ||||
|             report.entryReport('bar') | ||||
|             report.entryReport('baz') | ||||
|  | ||||
|             const stringRep = report.stringify() | ||||
|             const reportClone: CachingReport = CachingReport.rehydrate(stringRep) | ||||
|  | ||||
|             expect(reportClone.cacheEntryReports.length).toBe(3) | ||||
|             expect(reportClone.cacheEntryReports[0].entryName).toBe('foo') | ||||
|             expect(reportClone.cacheEntryReports[1].entryName).toBe('bar') | ||||
|             expect(reportClone.cacheEntryReports[2].entryName).toBe('baz') | ||||
|  | ||||
|             expect(reportClone.entryReport('foo')).toBe(reportClone.cacheEntryReports[0]) | ||||
|         }) | ||||
|         it('with rehydrated entry report', async () => { | ||||
|             const report = new CachingReport() | ||||
|             const entryReport = report.entryReport('foo') | ||||
|             entryReport.markRequested('1', ['2', '3']) | ||||
|             entryReport.markSaved('4') | ||||
|  | ||||
|             const stringRep = report.stringify() | ||||
|             const reportClone: CachingReport = CachingReport.rehydrate(stringRep) | ||||
|             const entryClone = reportClone.entryReport('foo') | ||||
|  | ||||
|             expect(entryClone.requestedKey).toBe('1') | ||||
|             expect(entryClone.requestedRestoreKeys).toEqual(['2', '3']) | ||||
|             expect(entryClone.savedKey).toBe('4') | ||||
|         }) | ||||
|         it('with live entry report', async () => { | ||||
|             const report = new CachingReport() | ||||
|             const entryReport = report.entryReport('foo') | ||||
|             entryReport.markRequested('1', ['2', '3']) | ||||
|  | ||||
|             const stringRep = report.stringify() | ||||
|             const reportClone: CachingReport = CachingReport.rehydrate(stringRep) | ||||
|             const entryClone = reportClone.entryReport('foo') | ||||
|  | ||||
|             // Check type and call method on rehydrated entry report | ||||
|             expect(entryClone).toBeInstanceOf(CacheEntryReport) | ||||
|             entryClone.markSaved('4') | ||||
|  | ||||
|             expect(entryClone.requestedKey).toBe('1') | ||||
|             expect(entryClone.requestedRestoreKeys).toEqual(['2', '3']) | ||||
|             expect(entryClone.savedKey).toBe('4') | ||||
|         }) | ||||
|     }) | ||||
| }) | ||||
| @@ -17,97 +17,4 @@ describe('cacheUtils-utils', () => { | ||||
|             expect(posixHash).toBe(windowsHash) | ||||
|         }) | ||||
|     }) | ||||
|     describe('caching report', () => { | ||||
|         describe('reports not fully restored', () => { | ||||
|             it('with one requested entry report', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 report.entryReport('foo').markRequested('1', ['2']) | ||||
|                 report.entryReport('bar').markRequested('3').markRestored('4') | ||||
|                 expect(report.fullyRestored).toBe(false) | ||||
|             }) | ||||
|         }) | ||||
|         describe('reports fully restored', () => { | ||||
|             it('when empty', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 expect(report.fullyRestored).toBe(true) | ||||
|             }) | ||||
|             it('with empty entry reports', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 report.entryReport('foo') | ||||
|                 report.entryReport('bar') | ||||
|                 expect(report.fullyRestored).toBe(true) | ||||
|             }) | ||||
|             it('with restored entry report', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 report.entryReport('bar').markRequested('3').markRestored('4') | ||||
|                 expect(report.fullyRestored).toBe(true) | ||||
|             }) | ||||
|             it('with multiple restored entry reportss', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 report.entryReport('foo').markRestored('4') | ||||
|                 report.entryReport('bar').markRequested('3').markRestored('4') | ||||
|                 expect(report.fullyRestored).toBe(true) | ||||
|             }) | ||||
|         }) | ||||
|         describe('can be stringified and rehydrated', () => { | ||||
|             it('when empty', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|  | ||||
|                 const stringRep = report.stringify() | ||||
|                 const reportClone: cacheUtils.CachingReport = cacheUtils.CachingReport.rehydrate(stringRep) | ||||
|  | ||||
|                 expect(reportClone.cacheEntryReports).toEqual([]) | ||||
|  | ||||
|                 // Can call methods on rehydrated | ||||
|                 expect(reportClone.entryReport('foo')).toBeInstanceOf(cacheUtils.CacheEntryReport) | ||||
|             }) | ||||
|             it('with entry reports', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 report.entryReport('foo') | ||||
|                 report.entryReport('bar') | ||||
|                 report.entryReport('baz') | ||||
|  | ||||
|                 const stringRep = report.stringify() | ||||
|                 const reportClone: cacheUtils.CachingReport = cacheUtils.CachingReport.rehydrate(stringRep) | ||||
|  | ||||
|                 expect(reportClone.cacheEntryReports.length).toBe(3) | ||||
|                 expect(reportClone.cacheEntryReports[0].entryName).toBe('foo') | ||||
|                 expect(reportClone.cacheEntryReports[1].entryName).toBe('bar') | ||||
|                 expect(reportClone.cacheEntryReports[2].entryName).toBe('baz') | ||||
|  | ||||
|                 expect(reportClone.entryReport('foo')).toBe(reportClone.cacheEntryReports[0]) | ||||
|             }) | ||||
|             it('with rehydrated entry report', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 const entryReport = report.entryReport('foo') | ||||
|                 entryReport.markRequested('1', ['2', '3']) | ||||
|                 entryReport.markSaved('4') | ||||
|  | ||||
|                 const stringRep = report.stringify() | ||||
|                 const reportClone: cacheUtils.CachingReport = cacheUtils.CachingReport.rehydrate(stringRep) | ||||
|                 const entryClone = reportClone.entryReport('foo') | ||||
|  | ||||
|                 expect(entryClone.requestedKey).toBe('1') | ||||
|                 expect(entryClone.requestedRestoreKeys).toEqual(['2', '3']) | ||||
|                 expect(entryClone.savedKey).toBe('4') | ||||
|             }) | ||||
|             it('with live entry report', async () => { | ||||
|                 const report = new cacheUtils.CachingReport() | ||||
|                 const entryReport = report.entryReport('foo') | ||||
|                 entryReport.markRequested('1', ['2', '3']) | ||||
|  | ||||
|                 const stringRep = report.stringify() | ||||
|                 const reportClone: cacheUtils.CachingReport = cacheUtils.CachingReport.rehydrate(stringRep) | ||||
|                 const entryClone = reportClone.entryReport('foo') | ||||
|  | ||||
|                 // Check type and call method on rehydrated entry report | ||||
|                 expect(entryClone).toBeInstanceOf(cacheUtils.CacheEntryReport) | ||||
|                 entryClone.markSaved('4') | ||||
|  | ||||
|                 expect(entryClone.requestedKey).toBe('1') | ||||
|                 expect(entryClone.requestedRestoreKeys).toEqual(['2', '3']) | ||||
|                 expect(entryClone.savedKey).toBe('4') | ||||
|             }) | ||||
|         }) | ||||
|     }) | ||||
| }) | ||||
|   | ||||
							
								
								
									
										253
									
								
								src/cache-base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								src/cache-base.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,253 @@ | ||||
| import * as core from '@actions/core' | ||||
| import * as cache from '@actions/cache' | ||||
| import * as github from '@actions/github' | ||||
| import {isCacheDebuggingEnabled, getCacheKeyPrefix, hashStrings} from './cache-utils' | ||||
|  | ||||
| const JOB_CONTEXT_PARAMETER = 'workflow-job-context' | ||||
|  | ||||
| function generateCacheKey(cacheName: string): CacheKey { | ||||
|     const cacheKeyPrefix = getCacheKeyPrefix() | ||||
|  | ||||
|     // At the most general level, share caches for all executions on the same OS | ||||
|     const runnerOs = process.env['RUNNER_OS'] || '' | ||||
|     const cacheKeyForOs = `${cacheKeyPrefix}${cacheName}|${runnerOs}` | ||||
|  | ||||
|     // Prefer caches that run this job | ||||
|     const cacheKeyForJob = `${cacheKeyForOs}|${github.context.job}` | ||||
|  | ||||
|     // Prefer (even more) jobs that run this job with the same context (matrix) | ||||
|     const cacheKeyForJobContext = `${cacheKeyForJob}[${determineJobContext()}]` | ||||
|  | ||||
|     // Exact match on Git SHA | ||||
|     const cacheKey = `${cacheKeyForJobContext}-${github.context.sha}` | ||||
|  | ||||
|     return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs]) | ||||
| } | ||||
|  | ||||
| function determineJobContext(): string { | ||||
|     // By default, we hash the full `matrix` data for the run, to uniquely identify this job invocation | ||||
|     const workflowJobContext = core.getInput(JOB_CONTEXT_PARAMETER) | ||||
|     return hashStrings([workflowJobContext]) | ||||
| } | ||||
|  | ||||
| class CacheKey { | ||||
|     key: string | ||||
|     restoreKeys: string[] | ||||
|  | ||||
|     constructor(key: string, restoreKeys: string[]) { | ||||
|         this.key = key | ||||
|         this.restoreKeys = restoreKeys | ||||
|     } | ||||
| } | ||||
|  | ||||
| export class CachingReport { | ||||
|     cacheEntryReports: CacheEntryReport[] = [] | ||||
|  | ||||
|     get fullyRestored(): boolean { | ||||
|         return this.cacheEntryReports.every(x => !x.wasRequestedButNotRestored()) | ||||
|     } | ||||
|  | ||||
|     entryReport(name: string): CacheEntryReport { | ||||
|         for (const report of this.cacheEntryReports) { | ||||
|             if (report.entryName === name) { | ||||
|                 return report | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const newReport = new CacheEntryReport(name) | ||||
|         this.cacheEntryReports.push(newReport) | ||||
|         return newReport | ||||
|     } | ||||
|  | ||||
|     stringify(): string { | ||||
|         return JSON.stringify(this) | ||||
|     } | ||||
|  | ||||
|     static rehydrate(stringRep: string): CachingReport { | ||||
|         const rehydrated: CachingReport = Object.assign(new CachingReport(), JSON.parse(stringRep)) | ||||
|         const entryReports = rehydrated.cacheEntryReports | ||||
|         for (let index = 0; index < entryReports.length; index++) { | ||||
|             const rawEntryReport = entryReports[index] | ||||
|             entryReports[index] = Object.assign(new CacheEntryReport(rawEntryReport.entryName), rawEntryReport) | ||||
|         } | ||||
|         return rehydrated | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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[] = []): CacheEntryReport { | ||||
|         this.requestedKey = key | ||||
|         this.requestedRestoreKeys = restoreKeys | ||||
|         return this | ||||
|     } | ||||
|  | ||||
|     markRestored(key: string): CacheEntryReport { | ||||
|         this.restoredKey = key | ||||
|         return this | ||||
|     } | ||||
|  | ||||
|     markSaved(key: string): CacheEntryReport { | ||||
|         this.savedKey = key | ||||
|         return this | ||||
|     } | ||||
| } | ||||
|  | ||||
| export abstract class AbstractCache { | ||||
|     private cacheName: string | ||||
|     private cacheDescription: string | ||||
|     private cacheKeyStateKey: string | ||||
|     private cacheResultStateKey: string | ||||
|  | ||||
|     protected readonly cacheDebuggingEnabled: boolean | ||||
|  | ||||
|     constructor(cacheName: string, cacheDescription: string) { | ||||
|         this.cacheName = cacheName | ||||
|         this.cacheDescription = cacheDescription | ||||
|         this.cacheKeyStateKey = `CACHE_KEY_${cacheName}` | ||||
|         this.cacheResultStateKey = `CACHE_RESULT_${cacheName}` | ||||
|         this.cacheDebuggingEnabled = isCacheDebuggingEnabled() | ||||
|     } | ||||
|  | ||||
|     async restore(report: CachingReport): Promise<void> { | ||||
|         if (this.cacheOutputExists()) { | ||||
|             core.info(`${this.cacheDescription} already exists. Not restoring from cache.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         const cacheKey = this.prepareCacheKey() | ||||
|         const entryReport = report.entryReport(this.cacheName) | ||||
|         entryReport.markRequested(cacheKey.key, cacheKey.restoreKeys) | ||||
|  | ||||
|         this.debug( | ||||
|             `Requesting ${this.cacheDescription} with | ||||
|                 key:${cacheKey.key} | ||||
|                 restoreKeys:[${cacheKey.restoreKeys}]` | ||||
|         ) | ||||
|  | ||||
|         const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys) | ||||
|  | ||||
|         if (!cacheResult) { | ||||
|             core.info(`${this.cacheDescription} cache not found. Will start with empty.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         core.saveState(this.cacheResultStateKey, cacheResult) | ||||
|         entryReport.markRestored(cacheResult) | ||||
|         core.info(`Restored ${this.cacheDescription} from cache key: ${cacheResult}`) | ||||
|  | ||||
|         try { | ||||
|             await this.afterRestore(report) | ||||
|         } catch (error) { | ||||
|             core.warning(`Restore ${this.cacheDescription} failed in 'afterRestore': ${error}`) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     prepareCacheKey(): CacheKey { | ||||
|         const cacheKey = generateCacheKey(this.cacheName) | ||||
|  | ||||
|         core.saveState(this.cacheKeyStateKey, cacheKey.key) | ||||
|         return cacheKey | ||||
|     } | ||||
|  | ||||
|     protected async restoreCache( | ||||
|         cachePath: string[], | ||||
|         cacheKey: string, | ||||
|         cacheRestoreKeys: string[] = [] | ||||
|     ): Promise<string | undefined> { | ||||
|         try { | ||||
|             return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys) | ||||
|         } catch (error) { | ||||
|             if (error instanceof cache.ValidationError) { | ||||
|                 // Validation errors should fail the build action | ||||
|                 throw error | ||||
|             } | ||||
|             // Warn about any other error and continue | ||||
|             core.warning(`Failed to restore ${cacheKey}: ${error}`) | ||||
|             return undefined | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected async afterRestore(_report: CachingReport): Promise<void> {} | ||||
|  | ||||
|     async save(report: CachingReport): Promise<void> { | ||||
|         if (!this.cacheOutputExists()) { | ||||
|             this.debug(`No ${this.cacheDescription} to cache.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         const cacheKey = core.getState(this.cacheKeyStateKey) | ||||
|         const cacheResult = core.getState(this.cacheResultStateKey) | ||||
|  | ||||
|         if (!cacheKey) { | ||||
|             this.debug(`${this.cacheDescription} existed prior to cache restore. Not saving.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (cacheResult && cacheKey === cacheResult) { | ||||
|             core.info(`Cache hit occurred on the cache key ${cacheKey}, not saving cache.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             await this.beforeSave(report) | ||||
|         } catch (error) { | ||||
|             core.warning(`Save ${this.cacheDescription} failed in 'beforeSave': ${error}`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKey}`) | ||||
|         const cachePath = this.getCachePath() | ||||
|         await this.saveCache(cachePath, cacheKey) | ||||
|  | ||||
|         report.entryReport(this.cacheName).markSaved(cacheKey) | ||||
|  | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     protected async beforeSave(_report: CachingReport): Promise<void> {} | ||||
|  | ||||
|     protected async saveCache(cachePath: string[], cacheKey: string): Promise<void> { | ||||
|         try { | ||||
|             await cache.saveCache(cachePath, cacheKey) | ||||
|         } catch (error) { | ||||
|             if (error instanceof cache.ValidationError) { | ||||
|                 // Validation errors should fail the build action | ||||
|                 throw error | ||||
|             } else if (error instanceof cache.ReserveCacheError) { | ||||
|                 // Reserve cache errors are expected if the artifact has been previously cached | ||||
|                 this.debug(error.message) | ||||
|             } else { | ||||
|                 // Warn about any other error and continue | ||||
|                 core.warning(String(error)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected debug(message: string): void { | ||||
|         if (this.cacheDebuggingEnabled) { | ||||
|             core.info(message) | ||||
|         } else { | ||||
|             core.debug(message) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected abstract cacheOutputExists(): boolean | ||||
|     protected abstract getCachePath(): string[] | ||||
| } | ||||
| @@ -5,14 +5,8 @@ import * as core from '@actions/core' | ||||
| import * as glob from '@actions/glob' | ||||
| import * as exec from '@actions/exec' | ||||
|  | ||||
| import { | ||||
|     AbstractCache, | ||||
|     CacheEntryReport, | ||||
|     CachingReport, | ||||
|     getCacheKeyPrefix, | ||||
|     hashFileNames, | ||||
|     tryDelete | ||||
| } from './cache-utils' | ||||
| import {AbstractCache, CacheEntryReport, CachingReport} from './cache-base' | ||||
| import {getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils' | ||||
|  | ||||
| const META_FILE_DIR = '.gradle-build-action' | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import path from 'path' | ||||
| import fs from 'fs' | ||||
| import {AbstractCache} from './cache-utils' | ||||
| import {AbstractCache} from './cache-base' | ||||
|  | ||||
| // TODO: Maybe allow the user to override / tweak this set | ||||
| const PATHS_TO_CACHE = [ | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| import * as core from '@actions/core' | ||||
| import * as cache from '@actions/cache' | ||||
| import * as github from '@actions/github' | ||||
| import * as crypto from 'crypto' | ||||
| import * as path from 'path' | ||||
| import * as fs from 'fs' | ||||
| @@ -9,7 +7,6 @@ const CACHE_PROTOCOL_VERSION = 'v4-' | ||||
|  | ||||
| const CACHE_DISABLED_PARAMETER = 'cache-disabled' | ||||
| const CACHE_READONLY_PARAMETER = 'cache-read-only' | ||||
| const JOB_CONTEXT_PARAMETER = 'workflow-job-context' | ||||
| const CACHE_DEBUG_VAR = 'GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED' | ||||
| const CACHE_PREFIX_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX' | ||||
|  | ||||
| @@ -30,31 +27,6 @@ export function getCacheKeyPrefix(): string { | ||||
|     return process.env[CACHE_PREFIX_VAR] || CACHE_PROTOCOL_VERSION | ||||
| } | ||||
|  | ||||
| function generateCacheKey(cacheName: string): CacheKey { | ||||
|     const cacheKeyPrefix = getCacheKeyPrefix() | ||||
|  | ||||
|     // At the most general level, share caches for all executions on the same OS | ||||
|     const runnerOs = process.env['RUNNER_OS'] || '' | ||||
|     const cacheKeyForOs = `${cacheKeyPrefix}${cacheName}|${runnerOs}` | ||||
|  | ||||
|     // Prefer caches that run this job | ||||
|     const cacheKeyForJob = `${cacheKeyForOs}|${github.context.job}` | ||||
|  | ||||
|     // Prefer (even more) jobs that run this job with the same context (matrix) | ||||
|     const cacheKeyForJobContext = `${cacheKeyForJob}[${determineJobContext()}]` | ||||
|  | ||||
|     // Exact match on Git SHA | ||||
|     const cacheKey = `${cacheKeyForJobContext}-${github.context.sha}` | ||||
|  | ||||
|     return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs]) | ||||
| } | ||||
|  | ||||
| function determineJobContext(): string { | ||||
|     // By default, we hash the full `matrix` data for the run, to uniquely identify this job invocation | ||||
|     const workflowJobContext = core.getInput(JOB_CONTEXT_PARAMETER) | ||||
|     return hashStrings([workflowJobContext]) | ||||
| } | ||||
|  | ||||
| export function hashStrings(values: string[]): string { | ||||
|     const hash = crypto.createHash('md5') | ||||
|     for (const value of values) { | ||||
| @@ -94,225 +66,3 @@ export async function tryDelete(file: string): Promise<void> { | ||||
| async function delay(ms: number): Promise<void> { | ||||
|     return new Promise(resolve => setTimeout(resolve, ms)) | ||||
| } | ||||
|  | ||||
| class CacheKey { | ||||
|     key: string | ||||
|     restoreKeys: string[] | ||||
|  | ||||
|     constructor(key: string, restoreKeys: string[]) { | ||||
|         this.key = key | ||||
|         this.restoreKeys = restoreKeys | ||||
|     } | ||||
| } | ||||
|  | ||||
| export class CachingReport { | ||||
|     cacheEntryReports: CacheEntryReport[] = [] | ||||
|  | ||||
|     get fullyRestored(): boolean { | ||||
|         return this.cacheEntryReports.every(x => !x.wasRequestedButNotRestored()) | ||||
|     } | ||||
|  | ||||
|     entryReport(name: string): CacheEntryReport { | ||||
|         for (const report of this.cacheEntryReports) { | ||||
|             if (report.entryName === name) { | ||||
|                 return report | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const newReport = new CacheEntryReport(name) | ||||
|         this.cacheEntryReports.push(newReport) | ||||
|         return newReport | ||||
|     } | ||||
|  | ||||
|     stringify(): string { | ||||
|         return JSON.stringify(this) | ||||
|     } | ||||
|  | ||||
|     static rehydrate(stringRep: string): CachingReport { | ||||
|         const rehydrated: CachingReport = Object.assign(new CachingReport(), JSON.parse(stringRep)) | ||||
|         const entryReports = rehydrated.cacheEntryReports | ||||
|         for (let index = 0; index < entryReports.length; index++) { | ||||
|             const rawEntryReport = entryReports[index] | ||||
|             entryReports[index] = Object.assign(new CacheEntryReport(rawEntryReport.entryName), rawEntryReport) | ||||
|         } | ||||
|         return rehydrated | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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[] = []): CacheEntryReport { | ||||
|         this.requestedKey = key | ||||
|         this.requestedRestoreKeys = restoreKeys | ||||
|         return this | ||||
|     } | ||||
|  | ||||
|     markRestored(key: string): CacheEntryReport { | ||||
|         this.restoredKey = key | ||||
|         return this | ||||
|     } | ||||
|  | ||||
|     markSaved(key: string): CacheEntryReport { | ||||
|         this.savedKey = key | ||||
|         return this | ||||
|     } | ||||
| } | ||||
|  | ||||
| export abstract class AbstractCache { | ||||
|     private cacheName: string | ||||
|     private cacheDescription: string | ||||
|     private cacheKeyStateKey: string | ||||
|     private cacheResultStateKey: string | ||||
|  | ||||
|     protected readonly cacheDebuggingEnabled: boolean | ||||
|  | ||||
|     constructor(cacheName: string, cacheDescription: string) { | ||||
|         this.cacheName = cacheName | ||||
|         this.cacheDescription = cacheDescription | ||||
|         this.cacheKeyStateKey = `CACHE_KEY_${cacheName}` | ||||
|         this.cacheResultStateKey = `CACHE_RESULT_${cacheName}` | ||||
|         this.cacheDebuggingEnabled = isCacheDebuggingEnabled() | ||||
|     } | ||||
|  | ||||
|     async restore(report: CachingReport): Promise<void> { | ||||
|         if (this.cacheOutputExists()) { | ||||
|             core.info(`${this.cacheDescription} already exists. Not restoring from cache.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         const cacheKey = this.prepareCacheKey() | ||||
|         const entryReport = report.entryReport(this.cacheName) | ||||
|         entryReport.markRequested(cacheKey.key, cacheKey.restoreKeys) | ||||
|  | ||||
|         this.debug( | ||||
|             `Requesting ${this.cacheDescription} with | ||||
|                 key:${cacheKey.key} | ||||
|                 restoreKeys:[${cacheKey.restoreKeys}]` | ||||
|         ) | ||||
|  | ||||
|         const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys) | ||||
|  | ||||
|         if (!cacheResult) { | ||||
|             core.info(`${this.cacheDescription} cache not found. Will start with empty.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         core.saveState(this.cacheResultStateKey, cacheResult) | ||||
|         entryReport.markRestored(cacheResult) | ||||
|         core.info(`Restored ${this.cacheDescription} from cache key: ${cacheResult}`) | ||||
|  | ||||
|         try { | ||||
|             await this.afterRestore(report) | ||||
|         } catch (error) { | ||||
|             core.warning(`Restore ${this.cacheDescription} failed in 'afterRestore': ${error}`) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     prepareCacheKey(): CacheKey { | ||||
|         const cacheKey = generateCacheKey(this.cacheName) | ||||
|  | ||||
|         core.saveState(this.cacheKeyStateKey, cacheKey.key) | ||||
|         return cacheKey | ||||
|     } | ||||
|  | ||||
|     protected async restoreCache( | ||||
|         cachePath: string[], | ||||
|         cacheKey: string, | ||||
|         cacheRestoreKeys: string[] = [] | ||||
|     ): Promise<string | undefined> { | ||||
|         try { | ||||
|             return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys) | ||||
|         } catch (error) { | ||||
|             if (error instanceof cache.ValidationError) { | ||||
|                 // Validation errors should fail the build action | ||||
|                 throw error | ||||
|             } | ||||
|             // Warn about any other error and continue | ||||
|             core.warning(`Failed to restore ${cacheKey}: ${error}`) | ||||
|             return undefined | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected async afterRestore(_report: CachingReport): Promise<void> {} | ||||
|  | ||||
|     async save(report: CachingReport): Promise<void> { | ||||
|         if (!this.cacheOutputExists()) { | ||||
|             this.debug(`No ${this.cacheDescription} to cache.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         const cacheKey = core.getState(this.cacheKeyStateKey) | ||||
|         const cacheResult = core.getState(this.cacheResultStateKey) | ||||
|  | ||||
|         if (!cacheKey) { | ||||
|             this.debug(`${this.cacheDescription} existed prior to cache restore. Not saving.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (cacheResult && cacheKey === cacheResult) { | ||||
|             core.info(`Cache hit occurred on the cache key ${cacheKey}, not saving cache.`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             await this.beforeSave(report) | ||||
|         } catch (error) { | ||||
|             core.warning(`Save ${this.cacheDescription} failed in 'beforeSave': ${error}`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKey}`) | ||||
|         const cachePath = this.getCachePath() | ||||
|         await this.saveCache(cachePath, cacheKey) | ||||
|  | ||||
|         report.entryReport(this.cacheName).markSaved(cacheKey) | ||||
|  | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     protected async beforeSave(_report: CachingReport): Promise<void> {} | ||||
|  | ||||
|     protected async saveCache(cachePath: string[], cacheKey: string): Promise<void> { | ||||
|         try { | ||||
|             await cache.saveCache(cachePath, cacheKey) | ||||
|         } catch (error) { | ||||
|             if (error instanceof cache.ValidationError) { | ||||
|                 // Validation errors should fail the build action | ||||
|                 throw error | ||||
|             } else if (error instanceof cache.ReserveCacheError) { | ||||
|                 // Reserve cache errors are expected if the artifact has been previously cached | ||||
|                 this.debug(error.message) | ||||
|             } else { | ||||
|                 // Warn about any other error and continue | ||||
|                 core.warning(String(error)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected debug(message: string): void { | ||||
|         if (this.cacheDebuggingEnabled) { | ||||
|             core.info(message) | ||||
|         } else { | ||||
|             core.debug(message) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected abstract cacheOutputExists(): boolean | ||||
|     protected abstract getCachePath(): string[] | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import {GradleUserHomeCache} from './cache-gradle-user-home' | ||||
| import {ProjectDotGradleCache} from './cache-project-dot-gradle' | ||||
| import * as core from '@actions/core' | ||||
| import {CachingReport, isCacheDisabled, isCacheReadOnly} from './cache-utils' | ||||
| import {isCacheDisabled, isCacheReadOnly} from './cache-utils' | ||||
| import {CachingReport} from './cache-base' | ||||
|  | ||||
| const BUILD_ROOT_DIR = 'BUILD_ROOT_DIR' | ||||
| const CACHING_REPORT = 'CACHING_REPORT' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user