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
This commit is contained in:
Marc Mueller 2023-01-30 12:10:58 +01:00 committed by GitHub
parent 8e3048d0f7
commit 627f0f41f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 174 additions and 15 deletions

View File

@ -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. * 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) * 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. * 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 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). * `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. * `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 * `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 #### Environment Variables

View File

@ -67,3 +67,6 @@
### 3.2.3 ### 3.2.3
- Support cross os caching on Windows as an opt-in feature. - Support cross os caching on Windows as an opt-in feature.
- Fix issue with symlink restoration on Windows for cross-os caches. - Fix issue with symlink restoration on Windows for cross-os caches.
### 3.2.4
- Added option to fail job on cache miss.

View File

@ -205,3 +205,128 @@ test("restore with cache found for restore key", async () => {
); );
expect(failedMock).toHaveBeenCalledTimes(0); 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);
});

View File

@ -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' 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' default: 'false'
required: false required: false
fail-on-cache-miss:
description: 'Fail the workflow if cache entry is not found'
default: 'false'
required: false
outputs: outputs:
cache-hit: cache-hit:
description: 'A boolean value to indicate an exact match was found for the primary key' description: 'A boolean value to indicate an exact match was found for the primary key'

View File

@ -4978,7 +4978,8 @@ var Inputs;
Inputs["Path"] = "path"; Inputs["Path"] = "path";
Inputs["RestoreKeys"] = "restore-keys"; Inputs["RestoreKeys"] = "restore-keys";
Inputs["UploadChunkSize"] = "upload-chunk-size"; 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 = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {
@ -50495,8 +50496,12 @@ function restoreImpl(stateProvider) {
required: true required: true
}); });
const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); 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); const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive);
if (!cacheKey) { 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: ${[ core.info(`Cache not found for input keys: ${[
primaryKey, primaryKey,
...restoreKeys ...restoreKeys

View File

@ -4978,7 +4978,8 @@ var Inputs;
Inputs["Path"] = "path"; Inputs["Path"] = "path";
Inputs["RestoreKeys"] = "restore-keys"; Inputs["RestoreKeys"] = "restore-keys";
Inputs["UploadChunkSize"] = "upload-chunk-size"; 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 = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {
@ -50495,8 +50496,12 @@ function restoreImpl(stateProvider) {
required: true required: true
}); });
const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); 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); const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive);
if (!cacheKey) { 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: ${[ core.info(`Cache not found for input keys: ${[
primaryKey, primaryKey,
...restoreKeys ...restoreKeys

View File

@ -5034,7 +5034,8 @@ var Inputs;
Inputs["Path"] = "path"; Inputs["Path"] = "path";
Inputs["RestoreKeys"] = "restore-keys"; Inputs["RestoreKeys"] = "restore-keys";
Inputs["UploadChunkSize"] = "upload-chunk-size"; 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 = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {

3
dist/save/index.js vendored
View File

@ -4978,7 +4978,8 @@ var Inputs;
Inputs["Path"] = "path"; Inputs["Path"] = "path";
Inputs["RestoreKeys"] = "restore-keys"; Inputs["RestoreKeys"] = "restore-keys";
Inputs["UploadChunkSize"] = "upload-chunk-size"; 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 = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "cache", "name": "cache",
"version": "3.2.3", "version": "3.2.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cache", "name": "cache",
"version": "3.2.3", "version": "3.2.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/cache": "^3.1.2", "@actions/cache": "^3.1.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "cache", "name": "cache",
"version": "3.2.3", "version": "3.2.4",
"private": true, "private": true,
"description": "Cache dependencies and build outputs", "description": "Cache dependencies and build outputs",
"main": "dist/restore/index.js", "main": "dist/restore/index.js",

View File

@ -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. * `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 * `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. * `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 ## Outputs
@ -95,7 +96,7 @@ steps:
### Exit workflow on cache miss ### 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 ```yaml
steps: steps:
@ -106,10 +107,7 @@ steps:
with: with:
path: path/to/dependencies path: path/to/dependencies
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
fail-on-cache-miss: true
- name: Check cache hit
if: steps.cache.outputs.cache-hit != 'true'
run: exit 1
- name: Build - name: Build
run: /build.sh run: /build.sh

View File

@ -15,6 +15,10 @@ inputs:
description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms' description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms'
default: 'false' default: 'false'
required: false required: false
fail-on-cache-miss:
description: 'Fail the workflow if cache entry is not found'
default: 'false'
required: false
outputs: outputs:
cache-hit: cache-hit:
description: 'A boolean value to indicate an exact match was found for the primary key' description: 'A boolean value to indicate an exact match was found for the primary key'

View File

@ -3,7 +3,8 @@ export enum Inputs {
Path = "path", // Input for cache, restore, save action Path = "path", // Input for cache, restore, save action
RestoreKeys = "restore-keys", // Input for cache, restore action RestoreKeys = "restore-keys", // Input for cache, restore action
UploadChunkSize = "upload-chunk-size", // Input for cache, save 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 { export enum Outputs {

View File

@ -34,6 +34,7 @@ async function restoreImpl(
const enableCrossOsArchive = utils.getInputAsBool( const enableCrossOsArchive = utils.getInputAsBool(
Inputs.EnableCrossOsArchive Inputs.EnableCrossOsArchive
); );
const failOnCacheMiss = utils.getInputAsBool(Inputs.FailOnCacheMiss);
const cacheKey = await cache.restoreCache( const cacheKey = await cache.restoreCache(
cachePaths, cachePaths,
@ -44,6 +45,11 @@ async function restoreImpl(
); );
if (!cacheKey) { 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( core.info(
`Cache not found for input keys: ${[ `Cache not found for input keys: ${[
primaryKey, primaryKey,

View File

@ -14,6 +14,7 @@ interface CacheInput {
key: string; key: string;
restoreKeys?: string[]; restoreKeys?: string[];
enableCrossOsArchive?: boolean; enableCrossOsArchive?: boolean;
failOnCacheMiss?: boolean;
} }
export function setInputs(input: CacheInput): void { export function setInputs(input: CacheInput): void {
@ -26,6 +27,8 @@ export function setInputs(input: CacheInput): void {
Inputs.EnableCrossOsArchive, Inputs.EnableCrossOsArchive,
input.enableCrossOsArchive.toString() input.enableCrossOsArchive.toString()
); );
input.failOnCacheMiss !== undefined &&
setInput(Inputs.FailOnCacheMiss, input.failOnCacheMiss.toString());
} }
export function clearInputs(): void { export function clearInputs(): void {
@ -34,4 +37,5 @@ export function clearInputs(): void {
delete process.env[getInputName(Inputs.RestoreKeys)]; delete process.env[getInputName(Inputs.RestoreKeys)];
delete process.env[getInputName(Inputs.UploadChunkSize)]; delete process.env[getInputName(Inputs.UploadChunkSize)];
delete process.env[getInputName(Inputs.EnableCrossOsArchive)]; delete process.env[getInputName(Inputs.EnableCrossOsArchive)];
delete process.env[getInputName(Inputs.FailOnCacheMiss)];
} }