mirror of
https://github.com/actions/cache.git
synced 2025-04-01 19:06:35 +00:00
Add actions/cache/check action
This commit is contained in:
parent
537862ffdb
commit
9dd99b0404
9 changed files with 61502 additions and 11 deletions
217
__tests__/checkOnly.test.ts
Normal file
217
__tests__/checkOnly.test.ts
Normal 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
72
check/README.md
Normal 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
34
check/action.yml
Normal 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
61155
dist/check-only/index.js
vendored
File diff suppressed because one or more lines are too long
4
dist/restore-only/index.js
vendored
4
dist/restore-only/index.js
vendored
|
@ -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}`);
|
||||
|
|
4
dist/restore/index.js
vendored
4
dist/restore/index.js
vendored
|
@ -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}`);
|
||||
|
|
|
@ -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
11
src/options.ts
Normal 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;
|
||||
}
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue