1
0
Fork 0
mirror of https://code.forgejo.org/actions/cache.git synced 2025-04-02 04:57:46 +02:00

Add actions/cache/check action

This commit is contained in:
Marc Mueller 2023-01-30 20:12:01 +01:00
parent 537862ffdb
commit 9dd99b0404
9 changed files with 61502 additions and 11 deletions

217
__tests__/checkOnly.test.ts Normal file
View file

@ -0,0 +1,217 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import run from "../src/checkOnly";
import { Events, RefKey } from "../src/constants";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsArray(name, options);
}
);
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsBool(name, options);
}
);
});
beforeEach(() => {
jest.restoreAllMocks();
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
testUtils.clearInputs();
delete process.env[Events.Key];
delete process.env[RefKey];
});
test("check with no cache found", async () => {
const path = "node_modules";
const key = "node-test";
testUtils.setInputs({
path: path,
key
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = 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,
[],
{
lookupOnly: true
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}`
);
});
test("check with restore keys and no cache found", async () => {
const path = "node_modules";
const key = "node-test";
const restoreKey = "node-";
testUtils.setInputs({
path: path,
key,
restoreKeys: [restoreKey],
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = 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],
{
lookupOnly: true
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
});
test("check with cache found for key", async () => {
const path = "node_modules";
const key = "node-test";
testUtils.setInputs({
path: path,
key
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
await run();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: true
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(outputMock).toHaveBeenCalledWith("cache-matched-key", key);
expect(outputMock).toHaveBeenCalledTimes(3);
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("check with cache found for restore key", async () => {
const path = "node_modules";
const key = "node-test";
const restoreKey = "node-";
testUtils.setInputs({
path: path,
key,
restoreKeys: [restoreKey]
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = 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],
{
lookupOnly: true
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(outputMock).toHaveBeenCalledWith("cache-matched-key", restoreKey);
expect(outputMock).toHaveBeenCalledTimes(3);
expect(infoMock).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
});

72
check/README.md Normal file
View file

@ -0,0 +1,72 @@
# Check action
The check action checks if a cache entry exists without actually downloading it.
## Inputs
* `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
* `cache-hit` - A boolean value to indicate an exact match was found for the key.
* `cache-primary-key` - Cache primary key passed in the input to use in subsequent steps of the workflow.
* `cache-matched-key` - Key of the cache that was restored, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys.
> **Note**
`cache-hit` will be set to `true` only when cache hit occurs for the exact `key` match. For a partial key match via `restore-keys` or a cache miss, it will be set to `false`.
## Use cases
As this is a newly introduced action to give users more control in their workflows, below are some use cases where one can use this action.
### Skip downloading cache if entry exists
Sometimes it's useful to separate build and test jobs. In that case it's not necessary
to restore the cache in the first job if an entry already exists.
#### Step 1 - Build artifact only if cache doesn't exist
```yaml
build:
steps:
- uses: actions/checkout@v3
- users: actions/cache/check@v3
id: cache-check
with:
path: path/to/dependencies
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Build
if: steps.cache-check.outputs.cache-hit != 'true'
run: /build.sh
- uses: actions/cache/save@v3
if: steps.cache-check.outputs.cache-hit != 'true'
with:
path: path/to/dependencies
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
```
#### Step 2 - Restore the built artifact from cache using the same key and path
```yaml
test:
needs: build
matrix:
key: [1, 2, 3]
steps:
- uses: actions/checkout@v3
- uses: actions/cache/restore@v3
with:
path: path/to/dependencies
fail-on-cache-miss: true
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Test
run: /test.sh -key ${{ matrix.key }}
```

34
check/action.yml Normal file
View file

@ -0,0 +1,34 @@
name: 'Check Cache'
description: 'Check if cache artifact exists without downloading it'
author: 'GitHub'
inputs:
path:
description: 'A list of files, directories, and wildcard patterns to restore'
required: true
key:
description: 'An explicit key for restoring the cache'
required: true
restore-keys:
description: 'An ordered list of keys to use for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.'
required: false
enableCrossOsArchive:
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'
cache-primary-key:
description: 'A resolved cache key for which cache match was attempted'
cache-matched-key:
description: 'Key of the cache that was found, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys'
runs:
using: 'node16'
main: '../dist/check-only/index.js'
branding:
icon: 'archive'
color: 'gray-dark'

61155
dist/check-only/index.js vendored

File diff suppressed because one or more lines are too long

View file

@ -50486,7 +50486,7 @@ const cache = __importStar(__webpack_require__(692));
const core = __importStar(__webpack_require__(470));
const constants_1 = __webpack_require__(196);
const utils = __importStar(__webpack_require__(360));
function restoreImpl(stateProvider) {
function restoreImpl(stateProvider, restoreOptions) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (!utils.isCacheFeatureAvailable()) {
@ -50506,7 +50506,7 @@ function restoreImpl(stateProvider) {
});
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, { lookupOnly: restoreOptions === null || restoreOptions === void 0 ? void 0 : restoreOptions.lookupOnly }, enableCrossOsArchive);
if (!cacheKey) {
if (failOnCacheMiss) {
throw new Error(`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`);

View file

@ -50486,7 +50486,7 @@ const cache = __importStar(__webpack_require__(692));
const core = __importStar(__webpack_require__(470));
const constants_1 = __webpack_require__(196);
const utils = __importStar(__webpack_require__(443));
function restoreImpl(stateProvider) {
function restoreImpl(stateProvider, restoreOptions) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (!utils.isCacheFeatureAvailable()) {
@ -50506,7 +50506,7 @@ function restoreImpl(stateProvider) {
});
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, { lookupOnly: restoreOptions === null || restoreOptions === void 0 ? void 0 : restoreOptions.lookupOnly }, enableCrossOsArchive);
if (!cacheKey) {
if (failOnCacheMiss) {
throw new Error(`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`);

View file

@ -0,0 +1,10 @@
import restoreImpl from "./restoreImpl";
import { NullStateProvider } from "./stateProvider";
async function run(): Promise<void> {
await restoreImpl(new NullStateProvider(), { lookupOnly: true });
}
run();
export default run;

11
src/options.ts Normal file
View file

@ -0,0 +1,11 @@
/**
* Options to control cache restore
*/
export interface RestoreOptions {
/**
* Weather to skip downloading the cache entry.
* If lookupOnly is set to true, the restore function will only check if
* a matching cache entry exists.
*/
lookupOnly?: boolean;
}

View file

@ -2,11 +2,13 @@ import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { Events, Inputs, Outputs, State } from "./constants";
import { RestoreOptions } from "./options";
import { IStateProvider } from "./stateProvider";
import * as utils from "./utils/actionUtils";
async function restoreImpl(
stateProvider: IStateProvider
stateProvider: IStateProvider,
restoreOptions?: RestoreOptions
): Promise<string | undefined> {
try {
if (!utils.isCacheFeatureAvailable()) {
@ -40,7 +42,7 @@ async function restoreImpl(
cachePaths,
primaryKey,
restoreKeys,
{},
{ lookupOnly: restoreOptions?.lookupOnly },
enableCrossOsArchive
);