From 627f0f41f6904a5b1efbaed9f96d9eb58e92e920 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 30 Jan 2023 12:10:58 +0100 Subject: [PATCH] Add `fail-on-cache-miss` option (#1036) * Add fail-on-cache-miss option * Small improvements * Changes after rebase * Update description * Only fail if no cache entry is found * Code review * Update readme * Add additional test case * Bump version + changelog * Update package-lock.json * Update Readme --- README.md | 2 + RELEASES.md | 5 +- __tests__/restore.test.ts | 125 +++++++++++++++++++++++++++++++++++++ action.yml | 4 ++ dist/restore-only/index.js | 7 ++- dist/restore/index.js | 7 ++- dist/save-only/index.js | 3 +- dist/save/index.js | 3 +- package-lock.json | 4 +- package.json | 2 +- restore/README.md | 8 +-- restore/action.yml | 6 +- src/constants.ts | 3 +- src/restoreImpl.ts | 6 ++ src/utils/testUtils.ts | 4 ++ 15 files changed, 174 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b39136a..6847616 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ See ["Caching dependencies to speed up workflows"](https://docs.github.com/en/ac * Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 60 minutes. * Two new actions available for granular control over caches - [restore](restore/action.yml) and [save](save/action.yml) * Support cross-os caching as an opt-in feature. See [Cross OS caching](./tips-and-workarounds.md#cross-os-cache) for more info. +* Added option to fail job on cache miss. See [Exit workflow on cache miss](./restore/README.md#exit-workflow-on-cache-miss) for more info. Refer [here](https://github.com/actions/cache/blob/v2/README.md) for previous versions @@ -49,6 +50,7 @@ If you are using a `self-hosted` Windows runner, `GNU tar` and `zstd` are requir * `key` - An explicit key for restoring and saving the cache. Refer [creating a cache key](#creating-a-cache-key). * `restore-keys` - An ordered list of prefix-matched keys to use for restoring stale cache if no cache hit occurred for key. * `enableCrossOsArchive` - An optional boolean when enabled, allows Windows runners to save or restore caches that can be restored or saved respectively on other platforms. Default: false +* `fail-on-cache-miss` - Fail the workflow if cache entry is not found. Default: false #### Environment Variables diff --git a/RELEASES.md b/RELEASES.md index 428d9ec..50cfd91 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -66,4 +66,7 @@ ### 3.2.3 - Support cross os caching on Windows as an opt-in feature. -- Fix issue with symlink restoration on Windows for cross-os caches. \ No newline at end of file +- Fix issue with symlink restoration on Windows for cross-os caches. + +### 3.2.4 +- Added option to fail job on cache miss. diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index ab768ba..5d7eaab 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -205,3 +205,128 @@ test("restore with cache found for restore key", async () => { ); expect(failedMock).toHaveBeenCalledTimes(0); }); + +test("Fail restore when fail on cache miss is enabled and primary + restore keys not found", async () => { + const path = "node_modules"; + const key = "node-test"; + const restoreKey = "node-"; + testUtils.setInputs({ + path: path, + key, + restoreKeys: [restoreKey], + failOnCacheMiss: true + }); + + const failedMock = jest.spyOn(core, "setFailed"); + const stateMock = jest.spyOn(core, "saveState"); + const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(undefined); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith( + [path], + key, + [restoreKey], + {}, + false + ); + + expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(0); + + expect(failedMock).toHaveBeenCalledWith( + `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}` + ); + expect(failedMock).toHaveBeenCalledTimes(1); +}); + +test("restore when fail on cache miss is enabled and primary key doesn't match restored key", async () => { + const path = "node_modules"; + const key = "node-test"; + const restoreKey = "node-"; + testUtils.setInputs({ + path: path, + key, + restoreKeys: [restoreKey], + failOnCacheMiss: true + }); + + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + const stateMock = jest.spyOn(core, "saveState"); + const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(restoreKey); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith( + [path], + key, + [restoreKey], + {}, + false + ); + + expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); + expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey); + expect(stateMock).toHaveBeenCalledTimes(2); + + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + + expect(infoMock).toHaveBeenCalledWith( + `Cache restored from key: ${restoreKey}` + ); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("restore with fail on cache miss disabled and no cache found", async () => { + const path = "node_modules"; + const key = "node-test"; + const restoreKey = "node-"; + testUtils.setInputs({ + path: path, + key, + restoreKeys: [restoreKey], + failOnCacheMiss: false + }); + + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + const stateMock = jest.spyOn(core, "saveState"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(undefined); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith( + [path], + key, + [restoreKey], + {}, + false + ); + + expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); + expect(stateMock).toHaveBeenCalledTimes(1); + + expect(infoMock).toHaveBeenCalledWith( + `Cache not found for input keys: ${key}, ${restoreKey}` + ); + expect(failedMock).toHaveBeenCalledTimes(0); +}); diff --git a/action.yml b/action.yml index 424e191..7afb9c0 100644 --- a/action.yml +++ b/action.yml @@ -18,6 +18,10 @@ inputs: description: 'An optional boolean when enabled, allows windows runners to save or restore caches that can be restored or saved respectively on other platforms' default: 'false' required: false + fail-on-cache-miss: + description: 'Fail the workflow if cache entry is not found' + default: 'false' + required: false outputs: cache-hit: description: 'A boolean value to indicate an exact match was found for the primary key' diff --git a/dist/restore-only/index.js b/dist/restore-only/index.js index f676abb..64766d1 100644 --- a/dist/restore-only/index.js +++ b/dist/restore-only/index.js @@ -4978,7 +4978,8 @@ var Inputs; Inputs["Path"] = "path"; Inputs["RestoreKeys"] = "restore-keys"; Inputs["UploadChunkSize"] = "upload-chunk-size"; - Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action + Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; + Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { @@ -50495,8 +50496,12 @@ function restoreImpl(stateProvider) { required: true }); const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); + const failOnCacheMiss = utils.getInputAsBool(constants_1.Inputs.FailOnCacheMiss); const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive); if (!cacheKey) { + if (failOnCacheMiss) { + throw new Error(`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`); + } core.info(`Cache not found for input keys: ${[ primaryKey, ...restoreKeys diff --git a/dist/restore/index.js b/dist/restore/index.js index 6415478..d15f678 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -4978,7 +4978,8 @@ var Inputs; Inputs["Path"] = "path"; Inputs["RestoreKeys"] = "restore-keys"; Inputs["UploadChunkSize"] = "upload-chunk-size"; - Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action + Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; + Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { @@ -50495,8 +50496,12 @@ function restoreImpl(stateProvider) { required: true }); const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); + const failOnCacheMiss = utils.getInputAsBool(constants_1.Inputs.FailOnCacheMiss); const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive); if (!cacheKey) { + if (failOnCacheMiss) { + throw new Error(`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`); + } core.info(`Cache not found for input keys: ${[ primaryKey, ...restoreKeys diff --git a/dist/save-only/index.js b/dist/save-only/index.js index 0d3295c..39ba9bc 100644 --- a/dist/save-only/index.js +++ b/dist/save-only/index.js @@ -5034,7 +5034,8 @@ var Inputs; Inputs["Path"] = "path"; Inputs["RestoreKeys"] = "restore-keys"; Inputs["UploadChunkSize"] = "upload-chunk-size"; - Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action + Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; + Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { diff --git a/dist/save/index.js b/dist/save/index.js index 1b0a733..c65c686 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -4978,7 +4978,8 @@ var Inputs; Inputs["Path"] = "path"; Inputs["RestoreKeys"] = "restore-keys"; Inputs["UploadChunkSize"] = "upload-chunk-size"; - Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action + Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; + Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { diff --git a/package-lock.json b/package-lock.json index db7f9ef..f806a8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cache", - "version": "3.2.3", + "version": "3.2.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cache", - "version": "3.2.3", + "version": "3.2.4", "license": "MIT", "dependencies": { "@actions/cache": "^3.1.2", diff --git a/package.json b/package.json index 1f8c005..61aa6e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "3.2.3", + "version": "3.2.4", "private": true, "description": "Cache dependencies and build outputs", "main": "dist/restore/index.js", diff --git a/restore/README.md b/restore/README.md index fdfbbd7..9e06bc4 100644 --- a/restore/README.md +++ b/restore/README.md @@ -7,6 +7,7 @@ The restore action, as the name suggest, restores a cache. It acts similar to th * `path` - A list of files, directories, and wildcard patterns to cache and restore. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns. * `key` - String used while saving cache for restoring the cache * `restore-keys` - An ordered list of prefix-matched keys to use for restoring stale cache if no cache hit occurred for key. +* `fail-on-cache-miss` - Fail the workflow if cache entry is not found. Default: false ## Outputs @@ -95,7 +96,7 @@ steps: ### Exit workflow on cache miss -You can use the output of this action to exit the workflow on cache miss. This way you can restrict your workflow to only initiate the build when `cache-hit` occurs, in other words, cache with exact key is found. +You can use `fail-on-cache-miss: true` to exit the workflow on a cache miss. This way you can restrict your workflow to only initiate the build when a cache is matched. Also, if you want to fail if cache did not match primary key, additionally leave `restore-keys` empty! ```yaml steps: @@ -106,10 +107,7 @@ steps: with: path: path/to/dependencies key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - - - name: Check cache hit - if: steps.cache.outputs.cache-hit != 'true' - run: exit 1 + fail-on-cache-miss: true - name: Build run: /build.sh diff --git a/restore/action.yml b/restore/action.yml index 8989197..5de91f2 100644 --- a/restore/action.yml +++ b/restore/action.yml @@ -15,6 +15,10 @@ inputs: description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms' default: 'false' required: false + fail-on-cache-miss: + description: 'Fail the workflow if cache entry is not found' + default: 'false' + required: false outputs: cache-hit: description: 'A boolean value to indicate an exact match was found for the primary key' @@ -27,4 +31,4 @@ runs: main: '../dist/restore-only/index.js' branding: icon: 'archive' - color: 'gray-dark' \ No newline at end of file + color: 'gray-dark' diff --git a/src/constants.ts b/src/constants.ts index 97fa2a0..4de3845 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,7 +3,8 @@ export enum Inputs { 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 - EnableCrossOsArchive = "enableCrossOsArchive" // Input for cache, restore, save action + EnableCrossOsArchive = "enableCrossOsArchive", // Input for cache, restore, save action + FailOnCacheMiss = "fail-on-cache-miss" // Input for cache, restore action } export enum Outputs { diff --git a/src/restoreImpl.ts b/src/restoreImpl.ts index 6214cfd..3ae1dbd 100644 --- a/src/restoreImpl.ts +++ b/src/restoreImpl.ts @@ -34,6 +34,7 @@ async function restoreImpl( const enableCrossOsArchive = utils.getInputAsBool( Inputs.EnableCrossOsArchive ); + const failOnCacheMiss = utils.getInputAsBool(Inputs.FailOnCacheMiss); const cacheKey = await cache.restoreCache( cachePaths, @@ -44,6 +45,11 @@ async function restoreImpl( ); if (!cacheKey) { + if (failOnCacheMiss) { + throw new Error( + `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}` + ); + } core.info( `Cache not found for input keys: ${[ primaryKey, diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts index c0a3f43..18447c0 100644 --- a/src/utils/testUtils.ts +++ b/src/utils/testUtils.ts @@ -14,6 +14,7 @@ interface CacheInput { key: string; restoreKeys?: string[]; enableCrossOsArchive?: boolean; + failOnCacheMiss?: boolean; } export function setInputs(input: CacheInput): void { @@ -26,6 +27,8 @@ export function setInputs(input: CacheInput): void { Inputs.EnableCrossOsArchive, input.enableCrossOsArchive.toString() ); + input.failOnCacheMiss !== undefined && + setInput(Inputs.FailOnCacheMiss, input.failOnCacheMiss.toString()); } export function clearInputs(): void { @@ -34,4 +37,5 @@ export function clearInputs(): void { delete process.env[getInputName(Inputs.RestoreKeys)]; delete process.env[getInputName(Inputs.UploadChunkSize)]; delete process.env[getInputName(Inputs.EnableCrossOsArchive)]; + delete process.env[getInputName(Inputs.FailOnCacheMiss)]; }