GA for granular cache (#1035)
* Add example for Haskell Stack * Revert "Add example for Haskell Stack" * Basic implementation * Updated variable name * Adding wrapper class * Changed logs to warnings * added debug logs * experimenting * Test * test * new try * test * Impl separated * Reverted wrapper changes * Added test cases * Some cleanup * Formatted document * Fixed test cases issues * Slight modification for test cases check * Updated new actions' input descriptions * Reverted custom asks implemented and added wrapper * refactor into a generic outputter * Readme draft for new actions * Generated dist * Fixed breaking test case * Removed return type in promise * Removed commented lines * Calling methods from same file * dist * update save as well * fix merge * Changes for beta release * Update dist folder * Fixed formatting * dist * Add support for gzip fallback for restore of old cache on windows * Fixed test cases * Fixed test cases * Added restore only and save only test cases * Updated new actions dist files * Removed comments * Fixed inputs * Renamed variables and added tests * Fixed breaking test case * Fixed review comments and tests * added stateprovider changes * Deleted stateprovider tests until added * Added stateprovider test cases * Fixed breaking test case * Updated outputs of restore action * Changes for beta release * Update dist folder * Add support for gzip fallback for restore of old cache on windows * update for new beta release * Update save/action.yml Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update restore/action.yml Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update restore/action.yml Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update restore/action.yml Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update restore/action.yml Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Added more assertions as values can't be checked * Removed unused code * Merged beta branch and resolved conflicts * Added save readme * Updates to save readme * Renamed output * Added cache hit info in readme * Update restore/README.md Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update restore/README.md Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update restore/README.md Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update save/README.md Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Update save/README.md Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> * Removed verbose statements * Repositioned new actions introduction * Added test case for restore state * Addressed review comments * nit * nit: added language to code blocks * Updated beta version to 3.2.0-beta.1 * Added stateprovider mock implementations * Linting errors fixed * Save-only warning added * Updated return ID to -2 * Removed -2 error code * Removed comment * Updated cache npm lib version * Updated license version * Updated releases.md * Updated readme with the new actions in what's new Co-authored-by: Malo Bourgon <mbourgon@gmail.com> Co-authored-by: Vipul <vsvipul@github.com> Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com> Co-authored-by: Tanuj Kumar Mishra <tanuj077@users.noreply.github.com> Co-authored-by: Sampark Sharma <phantsure@github.com>
This commit is contained in:
		| @ -1,12 +1,14 @@ | ||||
| export enum Inputs { | ||||
|     Key = "key", | ||||
|     Path = "path", | ||||
|     RestoreKeys = "restore-keys", | ||||
|     UploadChunkSize = "upload-chunk-size" | ||||
|     Key = "key", // Input for cache, restore, save action | ||||
|     Path = "path", // Input for cache, restore, save action | ||||
|     RestoreKeys = "restore-keys", // Input for cache, restore action | ||||
|     UploadChunkSize = "upload-chunk-size" // Input for cache, save action | ||||
| } | ||||
|  | ||||
| export enum Outputs { | ||||
|     CacheHit = "cache-hit" | ||||
|     CacheHit = "cache-hit", // Output from cache, restore action | ||||
|     CachePrimaryKey = "cache-primary-key", // Output from restore action | ||||
|     CacheMatchedKey = "cache-matched-key" // Output from restore action | ||||
| } | ||||
|  | ||||
| export enum State { | ||||
|  | ||||
| @ -1,60 +1,8 @@ | ||||
| import * as cache from "@actions/cache"; | ||||
| import * as core from "@actions/core"; | ||||
|  | ||||
| import { Events, Inputs, State } from "./constants"; | ||||
| import * as utils from "./utils/actionUtils"; | ||||
| import restoreImpl from "./restoreImpl"; | ||||
| import { StateProvider } from "./stateProvider"; | ||||
|  | ||||
| async function run(): Promise<void> { | ||||
|     try { | ||||
|         if (!utils.isCacheFeatureAvailable()) { | ||||
|             utils.setCacheHitOutput(false); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Validate inputs, this can cause task failure | ||||
|         if (!utils.isValidEvent()) { | ||||
|             utils.logWarning( | ||||
|                 `Event Validation Error: The event type ${ | ||||
|                     process.env[Events.Key] | ||||
|                 } is not supported because it's not tied to a branch or tag ref.` | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const primaryKey = core.getInput(Inputs.Key, { required: true }); | ||||
|         core.saveState(State.CachePrimaryKey, primaryKey); | ||||
|  | ||||
|         const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys); | ||||
|         const cachePaths = utils.getInputAsArray(Inputs.Path, { | ||||
|             required: true | ||||
|         }); | ||||
|  | ||||
|         const cacheKey = await cache.restoreCache( | ||||
|             cachePaths, | ||||
|             primaryKey, | ||||
|             restoreKeys | ||||
|         ); | ||||
|  | ||||
|         if (!cacheKey) { | ||||
|             core.info( | ||||
|                 `Cache not found for input keys: ${[ | ||||
|                     primaryKey, | ||||
|                     ...restoreKeys | ||||
|                 ].join(", ")}` | ||||
|             ); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Store the matched cache key | ||||
|         utils.setCacheState(cacheKey); | ||||
|  | ||||
|         const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheKey); | ||||
|         utils.setCacheHitOutput(isExactKeyMatch); | ||||
|         core.info(`Cache restored from key: ${cacheKey}`); | ||||
|     } catch (error: unknown) { | ||||
|         core.setFailed((error as Error).message); | ||||
|     } | ||||
|     await restoreImpl(new StateProvider()); | ||||
| } | ||||
|  | ||||
| run(); | ||||
|  | ||||
							
								
								
									
										69
									
								
								src/restoreImpl.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/restoreImpl.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| import * as cache from "@actions/cache"; | ||||
| import * as core from "@actions/core"; | ||||
|  | ||||
| import { Events, Inputs, Outputs, State } from "./constants"; | ||||
| import { IStateProvider } from "./stateProvider"; | ||||
| import * as utils from "./utils/actionUtils"; | ||||
|  | ||||
| async function restoreImpl( | ||||
|     stateProvider: IStateProvider | ||||
| ): Promise<string | undefined> { | ||||
|     try { | ||||
|         if (!utils.isCacheFeatureAvailable()) { | ||||
|             core.setOutput(Outputs.CacheHit, "false"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Validate inputs, this can cause task failure | ||||
|         if (!utils.isValidEvent()) { | ||||
|             utils.logWarning( | ||||
|                 `Event Validation Error: The event type ${ | ||||
|                     process.env[Events.Key] | ||||
|                 } is not supported because it's not tied to a branch or tag ref.` | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const primaryKey = core.getInput(Inputs.Key, { required: true }); | ||||
|         stateProvider.setState(State.CachePrimaryKey, primaryKey); | ||||
|  | ||||
|         const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys); | ||||
|         const cachePaths = utils.getInputAsArray(Inputs.Path, { | ||||
|             required: true | ||||
|         }); | ||||
|  | ||||
|         const cacheKey = await cache.restoreCache( | ||||
|             cachePaths, | ||||
|             primaryKey, | ||||
|             restoreKeys | ||||
|         ); | ||||
|  | ||||
|         if (!cacheKey) { | ||||
|             core.info( | ||||
|                 `Cache not found for input keys: ${[ | ||||
|                     primaryKey, | ||||
|                     ...restoreKeys | ||||
|                 ].join(", ")}` | ||||
|             ); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Store the matched cache key in states | ||||
|         stateProvider.setState(State.CacheMatchedKey, cacheKey); | ||||
|  | ||||
|         const isExactKeyMatch = utils.isExactKeyMatch( | ||||
|             core.getInput(Inputs.Key, { required: true }), | ||||
|             cacheKey | ||||
|         ); | ||||
|  | ||||
|         core.setOutput(Outputs.CacheHit, isExactKeyMatch.toString()); | ||||
|         core.info(`Cache restored from key: ${cacheKey}`); | ||||
|  | ||||
|         return cacheKey; | ||||
|     } catch (error: unknown) { | ||||
|         core.setFailed((error as Error).message); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default restoreImpl; | ||||
							
								
								
									
										10
									
								
								src/restoreOnly.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/restoreOnly.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import restoreImpl from "./restoreImpl"; | ||||
| import { NullStateProvider } from "./stateProvider"; | ||||
|  | ||||
| async function run(): Promise<void> { | ||||
|     await restoreImpl(new NullStateProvider()); | ||||
| } | ||||
|  | ||||
| run(); | ||||
|  | ||||
| export default run; | ||||
							
								
								
									
										57
									
								
								src/save.ts
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								src/save.ts
									
									
									
									
									
								
							| @ -1,59 +1,8 @@ | ||||
| import * as cache from "@actions/cache"; | ||||
| import * as core from "@actions/core"; | ||||
|  | ||||
| import { Events, Inputs, State } from "./constants"; | ||||
| import * as utils from "./utils/actionUtils"; | ||||
|  | ||||
| // Catch and log any unhandled exceptions.  These exceptions can leak out of the uploadChunk method in | ||||
| // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to | ||||
| // throw an uncaught exception.  Instead of failing this action, just warn. | ||||
| process.on("uncaughtException", e => utils.logWarning(e.message)); | ||||
| import saveImpl from "./saveImpl"; | ||||
| import { StateProvider } from "./stateProvider"; | ||||
|  | ||||
| async function run(): Promise<void> { | ||||
|     try { | ||||
|         if (!utils.isCacheFeatureAvailable()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!utils.isValidEvent()) { | ||||
|             utils.logWarning( | ||||
|                 `Event Validation Error: The event type ${ | ||||
|                     process.env[Events.Key] | ||||
|                 } is not supported because it's not tied to a branch or tag ref.` | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const state = utils.getCacheState(); | ||||
|  | ||||
|         // Inputs are re-evaluted before the post action, so we want the original key used for restore | ||||
|         const primaryKey = core.getState(State.CachePrimaryKey); | ||||
|         if (!primaryKey) { | ||||
|             utils.logWarning(`Error retrieving key from state.`); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (utils.isExactKeyMatch(primaryKey, state)) { | ||||
|             core.info( | ||||
|                 `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const cachePaths = utils.getInputAsArray(Inputs.Path, { | ||||
|             required: true | ||||
|         }); | ||||
|  | ||||
|         const cacheId = await cache.saveCache(cachePaths, primaryKey, { | ||||
|             uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize) | ||||
|         }); | ||||
|  | ||||
|         if (cacheId != -1) { | ||||
|             core.info(`Cache saved with key: ${primaryKey}`); | ||||
|         } | ||||
|     } catch (error: unknown) { | ||||
|         utils.logWarning((error as Error).message); | ||||
|     } | ||||
|     await saveImpl(new StateProvider()); | ||||
| } | ||||
|  | ||||
| run(); | ||||
|  | ||||
							
								
								
									
										68
									
								
								src/saveImpl.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/saveImpl.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| import * as cache from "@actions/cache"; | ||||
| import * as core from "@actions/core"; | ||||
|  | ||||
| import { Events, Inputs, State } from "./constants"; | ||||
| import { IStateProvider } from "./stateProvider"; | ||||
| import * as utils from "./utils/actionUtils"; | ||||
|  | ||||
| // Catch and log any unhandled exceptions.  These exceptions can leak out of the uploadChunk method in | ||||
| // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to | ||||
| // throw an uncaught exception.  Instead of failing this action, just warn. | ||||
| process.on("uncaughtException", e => utils.logWarning(e.message)); | ||||
|  | ||||
| async function saveImpl(stateProvider: IStateProvider): Promise<number | void> { | ||||
|     let cacheId = -1; | ||||
|     try { | ||||
|         if (!utils.isCacheFeatureAvailable()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!utils.isValidEvent()) { | ||||
|             utils.logWarning( | ||||
|                 `Event Validation Error: The event type ${ | ||||
|                     process.env[Events.Key] | ||||
|                 } is not supported because it's not tied to a branch or tag ref.` | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // If restore has stored a primary key in state, reuse that | ||||
|         // Else re-evaluate from inputs | ||||
|         const primaryKey = | ||||
|             stateProvider.getState(State.CachePrimaryKey) || | ||||
|             core.getInput(Inputs.Key); | ||||
|  | ||||
|         if (!primaryKey) { | ||||
|             utils.logWarning(`Key is not specified.`); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // If matched restore key is same as primary key, then do not save cache | ||||
|         // NO-OP in case of SaveOnly action | ||||
|         const restoredKey = stateProvider.getCacheState(); | ||||
|  | ||||
|         if (utils.isExactKeyMatch(primaryKey, restoredKey)) { | ||||
|             core.info( | ||||
|                 `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const cachePaths = utils.getInputAsArray(Inputs.Path, { | ||||
|             required: true | ||||
|         }); | ||||
|  | ||||
|         cacheId = await cache.saveCache(cachePaths, primaryKey, { | ||||
|             uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize) | ||||
|         }); | ||||
|  | ||||
|         if (cacheId != -1) { | ||||
|             core.info(`Cache saved with key: ${primaryKey}`); | ||||
|         } | ||||
|     } catch (error: unknown) { | ||||
|         utils.logWarning((error as Error).message); | ||||
|     } | ||||
|     return cacheId; | ||||
| } | ||||
|  | ||||
| export default saveImpl; | ||||
							
								
								
									
										15
									
								
								src/saveOnly.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/saveOnly.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| import * as core from "@actions/core"; | ||||
|  | ||||
| import saveImpl from "./saveImpl"; | ||||
| import { NullStateProvider } from "./stateProvider"; | ||||
|  | ||||
| async function run(): Promise<void> { | ||||
|     const cacheId = await saveImpl(new NullStateProvider()); | ||||
|     if (cacheId === -1) { | ||||
|         core.warning(`Cache save failed.`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| run(); | ||||
|  | ||||
| export default run; | ||||
							
								
								
									
										46
									
								
								src/stateProvider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/stateProvider.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import * as core from "@actions/core"; | ||||
|  | ||||
| import { Outputs, State } from "./constants"; | ||||
|  | ||||
| export interface IStateProvider { | ||||
|     setState(key: string, value: string): void; | ||||
|     getState(key: string): string; | ||||
|  | ||||
|     getCacheState(): string | undefined; | ||||
| } | ||||
|  | ||||
| class StateProviderBase implements IStateProvider { | ||||
|     getCacheState(): string | undefined { | ||||
|         const cacheKey = this.getState(State.CacheMatchedKey); | ||||
|         if (cacheKey) { | ||||
|             core.debug(`Cache state/key: ${cacheKey}`); | ||||
|             return cacheKey; | ||||
|         } | ||||
|  | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function | ||||
|     setState = (key: string, value: string) => {}; | ||||
|  | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|     getState = (key: string) => ""; | ||||
| } | ||||
|  | ||||
| export class StateProvider extends StateProviderBase { | ||||
|     setState = core.saveState; | ||||
|     getState = core.getState; | ||||
| } | ||||
|  | ||||
| export class NullStateProvider extends StateProviderBase { | ||||
|     stateToOutputMap = new Map<string, string>([ | ||||
|         [State.CacheMatchedKey, Outputs.CacheMatchedKey], | ||||
|         [State.CachePrimaryKey, Outputs.CachePrimaryKey] | ||||
|     ]); | ||||
|  | ||||
|     setState = (key: string, value: string) => { | ||||
|         core.setOutput(this.stateToOutputMap.get(key) as string, value); | ||||
|     }; | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|     getState = (key: string) => ""; | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| import * as cache from "@actions/cache"; | ||||
| import * as core from "@actions/core"; | ||||
|  | ||||
| import { Outputs, RefKey, State } from "../constants"; | ||||
| import { RefKey } from "../constants"; | ||||
|  | ||||
| export function isGhes(): boolean { | ||||
|     const ghUrl = new URL( | ||||
| @ -19,30 +19,6 @@ export function isExactKeyMatch(key: string, cacheKey?: string): boolean { | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export function setCacheState(state: string): void { | ||||
|     core.saveState(State.CacheMatchedKey, state); | ||||
| } | ||||
|  | ||||
| export function setCacheHitOutput(isCacheHit: boolean): void { | ||||
|     core.setOutput(Outputs.CacheHit, isCacheHit.toString()); | ||||
| } | ||||
|  | ||||
| export function setOutputAndState(key: string, cacheKey?: string): void { | ||||
|     setCacheHitOutput(isExactKeyMatch(key, cacheKey)); | ||||
|     // Store the matched cache key if it exists | ||||
|     cacheKey && setCacheState(cacheKey); | ||||
| } | ||||
|  | ||||
| export function getCacheState(): string | undefined { | ||||
|     const cacheKey = core.getState(State.CacheMatchedKey); | ||||
|     if (cacheKey) { | ||||
|         core.debug(`Cache state/key: ${cacheKey}`); | ||||
|         return cacheKey; | ||||
|     } | ||||
|  | ||||
|     return undefined; | ||||
| } | ||||
|  | ||||
| export function logWarning(message: string): void { | ||||
|     const warningPrefix = "[warning]"; | ||||
|     core.info(`${warningPrefix}${message}`); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Sankalp Kotewar
					Sankalp Kotewar