Add fail-on-error input to fail workflow on cache save errors

pull/1749/head
nathan.harris 1 week ago
parent 27d5ce7f10
commit b3c661a9c5
No known key found for this signature in database

@ -90,6 +90,7 @@ If you are using a `self-hosted` Windows runner, `GNU tar` and `zstd` are requir
* `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`
* `lookup-only` - If true, only checks if cache entry exists and skips download. Does not change save cache behavior. Default: `false`
* `fail-on-save-error` - Fail the workflow if the cache save step encounters an error, rather than logging a warning and succeeding. Default: `false`
#### Environment Variables

@ -365,6 +365,84 @@ test("save with server error outputs warning", async () => {
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("save with fail-on-error and save error calls setFailed", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
.mockImplementationOnce(() => savedCacheKey)
.mockImplementationOnce(() => primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.FailOnError, "true");
jest.spyOn(cache, "saveCache").mockImplementationOnce(() => {
throw new Error("HTTP Error Occurred");
});
await saveImpl(new StateProvider());
expect(failedMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(failedMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledTimes(0);
});
test("save with fail-on-save-error (global action input) and save error calls setFailed", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
.mockImplementationOnce(() => savedCacheKey)
.mockImplementationOnce(() => primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.FailOnSaveError, "true");
jest.spyOn(cache, "saveCache").mockImplementationOnce(() => {
throw new Error("HTTP Error Occurred");
});
await saveImpl(new StateProvider());
expect(failedMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(failedMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledTimes(0);
});
test("save with fail-on-error false and save error logs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
.mockImplementationOnce(() => savedCacheKey)
.mockImplementationOnce(() => primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.FailOnError, "false");
jest.spyOn(cache, "saveCache").mockImplementationOnce(() => {
throw new Error("HTTP Error Occurred");
});
await saveImpl(new StateProvider());
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");

@ -26,6 +26,10 @@ inputs:
description: 'Check if a cache entry exists for the given input(s) (key, restore-keys) without downloading the cache'
default: 'false'
required: false
fail-on-save-error:
description: 'Fail the workflow if cache save fails'
default: 'false'
required: false
save-always:
description: 'Run the post step to save the cache even if another step before fails'
default: 'false'

@ -9,6 +9,7 @@ The save action saves a cache. It works similarly to the `cache` action except t
* `key` - An explicit key for a cache entry. See [creating a cache key](../README.md#creating-a-cache-key).
* `path` - A list of files, directories, and wildcard patterns to cache. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns.
* `upload-chunk-size` - The chunk size used to split up large files during upload, in bytes
* `fail-on-error` - Fail the workflow if the cache save encounters an error, rather than logging a warning and succeeding. Default: `false`
### Outputs
@ -68,6 +69,24 @@ with:
key: npm-cache-${{hashfiles(package-lock.json)}}
```
### Fail workflow on save error
By default, errors during cache save (such as exceeding the cache size limit or a network failure) log a warning and allow the workflow to continue. Use `fail-on-error: true` to treat any save error as a workflow failure instead.
```yaml
steps:
- uses: actions/checkout@v6
- name: Install Dependencies
run: /install.sh
- uses: actions/cache/save@v5
with:
path: path/to/dependencies
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
fail-on-error: true
```
### Always save cache
There are instances where some flaky test cases would fail the entire workflow and users would get frustrated because the builds would run for hours and the cache couldn't be saved as the workflow failed in between.

@ -15,6 +15,10 @@ inputs:
description: 'An optional boolean when enabled, allows windows runners to save caches that can be restored on other platforms'
default: 'false'
required: false
fail-on-error:
description: 'Fail the workflow if cache save fails'
default: 'false'
required: false
runs:
using: 'node24'
main: '../dist/save-only/index.js'

@ -5,7 +5,9 @@ export enum Inputs {
UploadChunkSize = "upload-chunk-size", // Input for cache, save action
EnableCrossOsArchive = "enableCrossOsArchive", // Input for cache, restore, save action
FailOnCacheMiss = "fail-on-cache-miss", // Input for cache, restore action
LookupOnly = "lookup-only" // Input for cache, restore action
LookupOnly = "lookup-only", // Input for cache, restore action
FailOnError = "fail-on-error", // Input for save action
FailOnSaveError = "fail-on-save-error" // Input for cache action (combined), alias for FailOnError
}
export enum Outputs {

@ -18,6 +18,9 @@ export async function saveImpl(
stateProvider: IStateProvider
): Promise<number | void> {
let cacheId = -1;
const failOnError =
utils.getInputAsBool(Inputs.FailOnSaveError) ||
utils.getInputAsBool(Inputs.FailOnError);
try {
if (!utils.isCacheFeatureAvailable()) {
return;
@ -73,7 +76,11 @@ export async function saveImpl(
core.info(`Cache saved with key: ${primaryKey}`);
}
} catch (error: unknown) {
utils.logWarning((error as Error).message);
if (failOnError) {
core.setFailed((error as Error).message);
} else {
utils.logWarning((error as Error).message);
}
}
return cacheId;
}

@ -42,4 +42,6 @@ export function clearInputs(): void {
delete process.env[getInputName(Inputs.EnableCrossOsArchive)];
delete process.env[getInputName(Inputs.FailOnCacheMiss)];
delete process.env[getInputName(Inputs.LookupOnly)];
delete process.env[getInputName(Inputs.FailOnError)];
delete process.env[getInputName(Inputs.FailOnSaveError)];
}

Loading…
Cancel
Save